fix(tickets): fix statuses endpoint, members auth gate, and graceful error handling
All checks were successful
Mirror to GitHub / mirror (push) Successful in 3s
All checks were successful
Mirror to GitHub / mirror (push) Successful in 3s
- Add GET /boards/{board_id}/statuses endpoint — direct board-to-statuses lookup
without ticket roundabout; used by filter bar and new ticket form
- Fix TicketsPage and NewTicketModal to call getBoardStatuses(board_id) instead
of misusing getTicketStatuses(ticket_id) with a board_id value
- Fix list_members auth: was require_account_owner (owner/super_admin only) —
changed to require_engineer_or_admin so engineers can see member list for
ticket assignment
- list_members: return [] on PSAError instead of 502 (Lesson 111 pattern)
- get_ticket_statuses: return [] on PSAError instead of 502
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -788,7 +788,30 @@ async def get_ticket_statuses(
|
|||||||
except PSANotFoundError:
|
except PSANotFoundError:
|
||||||
raise HTTPException(status_code=404, detail="Ticket not found")
|
raise HTTPException(status_code=404, detail="Ticket not found")
|
||||||
except PSAError as e:
|
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 ─────────────────────────────────────────
|
# ── member mapping endpoints ─────────────────────────────────────────
|
||||||
@@ -796,7 +819,7 @@ async def get_ticket_statuses(
|
|||||||
|
|
||||||
@router.get("/members", response_model=list[PsaMemberResponse])
|
@router.get("/members", response_model=list[PsaMemberResponse])
|
||||||
async def list_members(
|
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)],
|
db: Annotated[AsyncSession, Depends(get_db)],
|
||||||
):
|
):
|
||||||
"""List CW members (from CW API)."""
|
"""List CW members (from CW API)."""
|
||||||
@@ -814,7 +837,9 @@ async def list_members(
|
|||||||
for m in members
|
for m in members
|
||||||
]
|
]
|
||||||
except PSAError as e:
|
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])
|
@router.get("/member-mappings", response_model=list[PsaMemberMappingResponse])
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ export const integrationsApi = {
|
|||||||
apiClient.get<PSATicketInfo>(`/integrations/psa/tickets/${id}`).then(r => r.data),
|
apiClient.get<PSATicketInfo>(`/integrations/psa/tickets/${id}`).then(r => r.data),
|
||||||
getTicketStatuses: (ticketId: string) =>
|
getTicketStatuses: (ticketId: string) =>
|
||||||
apiClient.get<PSATicketStatusItem[]>(`/integrations/psa/tickets/${ticketId}/statuses`).then(r => r.data),
|
apiClient.get<PSATicketStatusItem[]>(`/integrations/psa/tickets/${ticketId}/statuses`).then(r => r.data),
|
||||||
|
getBoardStatuses: (boardId: number | string) =>
|
||||||
|
apiClient.get<PSATicketStatusItem[]>(`/integrations/psa/boards/${boardId}/statuses`).then(r => r.data),
|
||||||
listMembers: () =>
|
listMembers: () =>
|
||||||
apiClient.get<PsaMemberResponse[]>('/integrations/psa/members').then(r => r.data),
|
apiClient.get<PsaMemberResponse[]>('/integrations/psa/members').then(r => r.data),
|
||||||
getMemberMappings: () =>
|
getMemberMappings: () =>
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export function NewTicketModal({ defaultTab = 'quick', initialValues, summaryHin
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (draft.board_id) {
|
if (draft.board_id) {
|
||||||
integrationsApi.getTicketStatuses(String(draft.board_id))
|
integrationsApi.getBoardStatuses(draft.board_id)
|
||||||
.then(setStatuses).catch(() => {})
|
.then(setStatuses).catch(() => {})
|
||||||
} else {
|
} else {
|
||||||
setStatuses([])
|
setStatuses([])
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export default function TicketsPage() {
|
|||||||
// Load statuses when board changes
|
// Load statuses when board changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (filters.board_id) {
|
if (filters.board_id) {
|
||||||
integrationsApi.getTicketStatuses(String(filters.board_id))
|
integrationsApi.getBoardStatuses(filters.board_id)
|
||||||
.then(setStatuses).catch(() => {})
|
.then(setStatuses).catch(() => {})
|
||||||
} else {
|
} else {
|
||||||
setStatuses([])
|
setStatuses([])
|
||||||
|
|||||||
Reference in New Issue
Block a user