diff --git a/backend/app/api/endpoints/integrations.py b/backend/app/api/endpoints/integrations.py index 78abf7a2..3f8b28d4 100644 --- a/backend/app/api/endpoints/integrations.py +++ b/backend/app/api/endpoints/integrations.py @@ -788,7 +788,30 @@ async def get_ticket_statuses( except PSANotFoundError: raise HTTPException(status_code=404, detail="Ticket not found") except PSAError as e: - raise HTTPException(status_code=502, detail=str(e)) + logger.warning("get_ticket_statuses(%s) failed: %s", ticket_id, e) + return [] + + +@router.get("/boards/{board_id}/statuses", response_model=list[PSATicketStatusItem]) +async def get_board_statuses( + board_id: int, + current_user: Annotated[User, Depends(require_engineer_or_admin)], + db: Annotated[AsyncSession, Depends(get_db)], +): + """Get available statuses for a service board directly (no ticket lookup required).""" + if not current_user.account_id: + raise HTTPException(status_code=400, detail="User has no account") + + from app.services.psa.registry import get_provider_for_account + from app.services.psa.exceptions import PSAError + + try: + provider = await get_provider_for_account(current_user.account_id, db) + statuses = await provider.get_ticket_statuses(board_id) + return [PSATicketStatusItem(id=s.id, name=s.name, is_closed=s.is_closed) for s in statuses] + except PSAError as e: + logger.warning("get_board_statuses(%s) failed: %s", board_id, e) + return [] # ── member mapping endpoints ───────────────────────────────────────── @@ -796,7 +819,7 @@ async def get_ticket_statuses( @router.get("/members", response_model=list[PsaMemberResponse]) async def list_members( - current_user: Annotated[User, Depends(require_account_owner)], + current_user: Annotated[User, Depends(require_engineer_or_admin)], db: Annotated[AsyncSession, Depends(get_db)], ): """List CW members (from CW API).""" @@ -814,7 +837,9 @@ async def list_members( for m in members ] except PSAError as e: - raise HTTPException(status_code=502, detail=str(e)) + # Members are optional display data — degrade gracefully + logger.warning("list_members failed: %s", e) + return [] @router.get("/member-mappings", response_model=list[PsaMemberMappingResponse]) diff --git a/frontend/src/api/integrations.ts b/frontend/src/api/integrations.ts index aace4a3e..7694a135 100644 --- a/frontend/src/api/integrations.ts +++ b/frontend/src/api/integrations.ts @@ -30,6 +30,8 @@ export const integrationsApi = { apiClient.get(`/integrations/psa/tickets/${id}`).then(r => r.data), getTicketStatuses: (ticketId: string) => apiClient.get(`/integrations/psa/tickets/${ticketId}/statuses`).then(r => r.data), + getBoardStatuses: (boardId: number | string) => + apiClient.get(`/integrations/psa/boards/${boardId}/statuses`).then(r => r.data), listMembers: () => apiClient.get('/integrations/psa/members').then(r => r.data), getMemberMappings: () => diff --git a/frontend/src/components/tickets/NewTicketModal.tsx b/frontend/src/components/tickets/NewTicketModal.tsx index 17d1fe68..69a1ce11 100644 --- a/frontend/src/components/tickets/NewTicketModal.tsx +++ b/frontend/src/components/tickets/NewTicketModal.tsx @@ -44,7 +44,7 @@ export function NewTicketModal({ defaultTab = 'quick', initialValues, summaryHin useEffect(() => { if (draft.board_id) { - integrationsApi.getTicketStatuses(String(draft.board_id)) + integrationsApi.getBoardStatuses(draft.board_id) .then(setStatuses).catch(() => {}) } else { setStatuses([]) diff --git a/frontend/src/pages/TicketsPage.tsx b/frontend/src/pages/TicketsPage.tsx index befc7b32..bfb28f52 100644 --- a/frontend/src/pages/TicketsPage.tsx +++ b/frontend/src/pages/TicketsPage.tsx @@ -56,7 +56,7 @@ export default function TicketsPage() { // Load statuses when board changes useEffect(() => { if (filters.board_id) { - integrationsApi.getTicketStatuses(String(filters.board_id)) + integrationsApi.getBoardStatuses(filters.board_id) .then(setStatuses).catch(() => {}) } else { setStatuses([])