diff --git a/RELEASE_NOTES_v2.2.42.md b/RELEASE_NOTES_v2.2.42.md new file mode 100644 index 0000000..6e3f1c8 --- /dev/null +++ b/RELEASE_NOTES_v2.2.42.md @@ -0,0 +1,18 @@ +# Release Notes v2.2.42 + +Dato: 3. marts 2026 + +## Fix: Yealink webhook compatibility + deploy robusthed +- Tilføjet `GET` support på Mission Control telefoni-webhooks, så Yealink callback-URLs ikke returnerer `405 Method Not Allowed`. +- Webhook-endpoints understøtter nu query-parametre for `call_id`, `caller_number`, `queue_name` og valgfri `timestamp`. +- `updateto.sh` er hærdet med tydelig fail-fast ved portkonflikter og mislykket container-opstart, så scriptet ikke melder succes ved delvis fejl. + +## Ændrede filer +- `app/dashboard/backend/mission_router.py` +- `updateto.sh` +- `VERSION` + +## Påvirkede endpoints +- `/api/v1/mission/webhook/telefoni/ringing` (`POST` + `GET`) +- `/api/v1/mission/webhook/telefoni/answered` (`POST` + `GET`) +- `/api/v1/mission/webhook/telefoni/hangup` (`POST` + `GET`) diff --git a/VERSION b/VERSION index 4a8398d..4543a20 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.2.41 +2.2.42 diff --git a/app/dashboard/backend/mission_router.py b/app/dashboard/backend/mission_router.py index 3479172..fe73725 100644 --- a/app/dashboard/backend/mission_router.py +++ b/app/dashboard/backend/mission_router.py @@ -32,6 +32,37 @@ class MissionUptimeWebhook(BaseModel): payload: Dict[str, Any] = Field(default_factory=dict) +def _first_query_param(request: Request, *names: str) -> Optional[str]: + for name in names: + value = request.query_params.get(name) + if value and str(value).strip(): + return str(value).strip() + return None + + +def _parse_query_timestamp(request: Request) -> Optional[datetime]: + raw = _first_query_param(request, "timestamp", "time", "event_time") + if not raw: + return None + try: + return datetime.fromisoformat(raw.replace("Z", "+00:00")) + except Exception: + return None + + +def _event_from_query(request: Request) -> MissionCallEvent: + call_id = _first_query_param(request, "call_id", "callid", "id", "session_id", "uuid") + if not call_id: + raise HTTPException(status_code=400, detail="Missing call_id query parameter") + + return MissionCallEvent( + call_id=call_id, + caller_number=_first_query_param(request, "caller_number", "caller", "from", "number", "phone"), + queue_name=_first_query_param(request, "queue_name", "queue", "group", "line"), + timestamp=_parse_query_timestamp(request), + ) + + def _get_webhook_token() -> str: db_token = MissionService.get_setting_value("mission_webhook_token", "") or "" env_token = (getattr(settings, "MISSION_WEBHOOK_TOKEN", "") or "").strip() @@ -175,6 +206,12 @@ async def mission_telefoni_ringing(event: MissionCallEvent, request: Request, to return {"status": "ok"} +@router.get("/mission/webhook/telefoni/ringing") +async def mission_telefoni_ringing_get(request: Request, token: Optional[str] = Query(None)): + event = _event_from_query(request) + return await mission_telefoni_ringing(event, request, token) + + @router.post("/mission/webhook/telefoni/answered") async def mission_telefoni_answered(event: MissionCallEvent, request: Request, token: Optional[str] = Query(None)): _validate_mission_webhook_token(request, token) @@ -204,6 +241,12 @@ async def mission_telefoni_answered(event: MissionCallEvent, request: Request, t return {"status": "ok"} +@router.get("/mission/webhook/telefoni/answered") +async def mission_telefoni_answered_get(request: Request, token: Optional[str] = Query(None)): + event = _event_from_query(request) + return await mission_telefoni_answered(event, request, token) + + @router.post("/mission/webhook/telefoni/hangup") async def mission_telefoni_hangup(event: MissionCallEvent, request: Request, token: Optional[str] = Query(None)): _validate_mission_webhook_token(request, token) @@ -233,6 +276,12 @@ async def mission_telefoni_hangup(event: MissionCallEvent, request: Request, tok return {"status": "ok"} +@router.get("/mission/webhook/telefoni/hangup") +async def mission_telefoni_hangup_get(request: Request, token: Optional[str] = Query(None)): + event = _event_from_query(request) + return await mission_telefoni_hangup(event, request, token) + + @router.post("/mission/webhook/uptime") async def mission_uptime_webhook(payload: MissionUptimeWebhook, request: Request, token: Optional[str] = Query(None)): _validate_mission_webhook_token(request, token) diff --git a/updateto.sh b/updateto.sh index 84e942f..b0d954e 100644 --- a/updateto.sh +++ b/updateto.sh @@ -74,6 +74,16 @@ fi echo "✅ .env opdateret" +# Guard against host port conflicts before attempting startup +POSTGRES_BIND_ADDR="${POSTGRES_BIND_ADDR:-127.0.0.1}" +POSTGRES_PORT="${POSTGRES_PORT:-5432}" +if podman ps --format '{{.Names}} {{.Ports}}' | grep -E "${POSTGRES_BIND_ADDR}:${POSTGRES_PORT}->5432/tcp" | grep -v "bmc-hub-postgres-prod" >/dev/null 2>&1; then + echo "❌ Fejl: Portkonflikt på ${POSTGRES_BIND_ADDR}:${POSTGRES_PORT} (Postgres host-port)" + echo " Sæt en ledig port i .env, fx POSTGRES_PORT=5433" + podman ps --format 'table {{.Names}}\t{{.Ports}}' + exit 1 +fi + # Stop containers echo "" echo "⏹️ Stopper containere..." @@ -82,7 +92,24 @@ podman-compose -f "$PODMAN_COMPOSE_FILE" down # Pull/rebuild with new version echo "" echo "🔨 Bygger nyt image med version $VERSION..." -podman-compose -f "$PODMAN_COMPOSE_FILE" up -d --build +if ! podman-compose -f "$PODMAN_COMPOSE_FILE" up -d --build; then + echo "❌ Fejl: podman-compose up fejlede" + echo " Tjek logs med: podman-compose -f $PODMAN_COMPOSE_FILE logs --tail=200" + exit 1 +fi + +# Validate that key containers are actually running after startup +if ! podman ps --format '{{.Names}}' | grep -q '^bmc-hub-postgres-prod$'; then + echo "❌ Fejl: bmc-hub-postgres-prod kører ikke efter startup" + podman ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' + exit 1 +fi + +if ! podman ps --format '{{.Names}}' | grep -q '^bmc-hub-api-prod$'; then + echo "❌ Fejl: bmc-hub-api-prod kører ikke efter startup" + podman ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' + exit 1 +fi # Sync migrations from container to host echo ""