From c8b68ad26d565f8c27a330278f2826801ee296c4 Mon Sep 17 00:00:00 2001 From: Michael Chihlas Date: Thu, 16 Apr 2026 01:49:39 +0000 Subject: [PATCH] =?UTF-8?q?docs:=20fix=20PSA=20ticket=20management=20spec?= =?UTF-8?q?=20=E2=80=94=20pagination=20source,=20widget,=20linked=20ticket?= =?UTF-8?q?=20IDs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Define PaginatedTicketResult provider type + parallel count fetch via CW /count endpoint - Fix dashboard widget: updates existing TicketQueue (not new), uses searchTicketsQueue - Fix NewTicketModal prefill: expand PSATicketInfo with company_id/board_id fields - Correct Dashboard section description: not collapsible, TicketQueue already exists Co-Authored-By: Claude Sonnet 4.6 --- ...2026-04-16-psa-ticket-management-design.md | 63 ++++++++++++++++--- 1 file changed, 54 insertions(+), 9 deletions(-) diff --git a/docs/superpowers/specs/2026-04-16-psa-ticket-management-design.md b/docs/superpowers/specs/2026-04-16-psa-ticket-management-design.md index 9b709e79..16b9b4ef 100644 --- a/docs/superpowers/specs/2026-04-16-psa-ticket-management-design.md +++ b/docs/superpowers/specs/2026-04-16-psa-ticket-management-design.md @@ -63,10 +63,32 @@ Methods: - `update_status(account_id, ticket_id, status_id) → PSATicketStatusUpdate` - `list_resources(account_id, ticket_id) → list[PSAResource]` -### PSA Provider — New Abstract Methods +### PSA Provider — New Abstract Methods and Paginated Result Type -Four explicit methods added to `PSAProvider` base class and `ConnectWiseProvider`: +**New type in `backend/app/services/psa/types.py`:** +```python +@dataclass +class PaginatedTicketResult: + items: list[PSATicket] + total: int + page: int + page_size: int +``` +**`search_tickets` signature change** — updated on both the abstract base and `ConnectWiseProvider` to return `PaginatedTicketResult` instead of `list[PSATicket]`: +```python +# base.py +@abstractmethod +async def search_tickets(self, query: str, **filters) -> PaginatedTicketResult: ... +``` + +**How `total` is fetched** — ConnectWise provides `GET /service/tickets/count?conditions=...` which accepts the same conditions string as the page fetch. The `ConnectWiseProvider.search_tickets()` implementation fires two parallel requests: +1. `GET /service/tickets?conditions=...&pageSize=N&page=N` — the current page +2. `GET /service/tickets/count?conditions=...` — returns `{ "count": 142 }` + +Both use the same built conditions string. `asyncio.gather()` runs them in parallel. The count result is used to populate `PaginatedTicketResult.total`. + +**New abstract methods** added to `PSAProvider` base and `ConnectWiseProvider`: ```python async def list_resources(self, ticket_id: int) -> list[PSAResource]: ... async def add_resource(self, ticket_id: int, member_id: int) -> PSAResource: ... @@ -312,10 +334,28 @@ A "New Ticket" button in the ResolutionAssist session header. Always visible reg ### Both Paths — NewTicketModal Pre-population -When a session has a linked ticket, the modal receives: +**The linked ticket IDs problem:** The current `PSATicketInfo` type in `frontend/src/types/integrations.ts` only exposes `company_name` and `board_name` — not `company_id` or `board_id`. The modal needs the numeric IDs to pre-populate the form selects. + +**Fix:** Expand `PSATicketInfo` in `types/integrations.ts` to add the optional ID fields: +```typescript +export interface PSATicketInfo { + id: string + summary: string + company_name: string | null + board_name: string | null + status_name: string | null + priority_name: string | null + company_id: number | null // add + board_id: number | null // add +} +``` + +These fields are already returned by the CW API in `get_ticket()` — update `_map_ticket()` in `ConnectWiseProvider` and the `PSATicketInfo` Pydantic schema to pass them through. + +`AssistantChatPage` already stores the linked ticket as `linkedTicket: PSATicketInfo | null` in local state (populated when a ticket is linked via `TicketLinkIndicator`). Once `PSATicketInfo` includes the IDs, the modal receives: ```typescript initialValues: { - company_id: linkedTicket.company_id, // from session's linked ticket + company_id: linkedTicket.company_id, board_id: linkedTicket.board_id, } ``` @@ -348,11 +388,13 @@ No new backend endpoints — the modal reuses `POST /integrations/psa/tickets` a ### Placement -In the collapsible Dashboard section of `QuickStartPage`, alongside `PendingEscalations` and `ActiveFlowPilotSessions`. Component: `MyQueueWidget`. +`TicketQueue` **already exists** in `QuickStartPage` (line 64, below `ActiveFlowPilotSessions`, above the Dashboard section). It currently auto-hides if no PSA connection exists. This spec updates the existing `TicketQueue` component — it is **not** a new widget and does not need to be added to `QuickStartPage`. The Dashboard section below it is not collapsible. ### Data Fetching -On mount: `GET /integrations/psa/member-mappings` first to detect mapping state, then `searchTickets({ assigned_to_me: true, include_closed: false, page_size: 5 })` if a mapping exists for the current user. +On mount: `GET /integrations/psa/member-mappings` first to detect mapping state, then `integrationsApi.searchTicketsQueue({ assigned_to_me: true, include_closed: false, page_size: 5 })` if a mapping exists for the current user. + +`searchTicketsQueue` is used (not `searchTickets`) because it already accepts `assigned_to_me` and `page_size` params. Its return type will be updated to `TicketListResponse` as part of the search endpoint migration, so the widget reads `.items` after that change. Member mapping detection is explicit — the widget checks the mappings response, not the ticket result. "No mapping" and "no tickets" are distinct states. @@ -391,8 +433,10 @@ Backend `search_tickets()` adds `orderBy=priority desc,dateEntered desc` to the ### Modified Backend Files - `backend/app/api/endpoints/integrations.py` — 6 new endpoints, update search to return `TicketListResponse` -- `backend/app/services/psa/base.py` — 4 new abstract methods -- `backend/app/services/psa/connectwise/provider.py` — implement 4 new methods +- `backend/app/services/psa/types.py` — add `PaginatedTicketResult` dataclass +- `backend/app/services/psa/base.py` — 4 new abstract methods; update `search_tickets` return type to `PaginatedTicketResult` +- `backend/app/services/psa/connectwise/provider.py` — implement 4 new methods; update `search_tickets` to fire parallel count request and return `PaginatedTicketResult`; update `_map_ticket()` to pass through `company_id` and `board_id` +- `backend/app/schemas/psa_connection.py` — add `company_id` and `board_id` to `PSATicketInfo` Pydantic schema - `backend/app/services/assistant_chat_service.py` — add spin-off ticket rule to `ASSISTANT_SYSTEM_PROMPT` - ~~`backend/app/services/flowpilot_engine.py`~~ — no changes (FlowPilot out of scope for this feature) - ~~`backend/app/services/unified_chat_service.py`~~ — no changes (existing `[ACTIONS]` parser handles the format) @@ -419,7 +463,8 @@ Backend `search_tickets()` adds `orderBy=priority desc,dateEntered desc` to the - `frontend/src/pages/AssistantChatPage.tsx` — handle `create_spin_off_ticket` command in action renderer + add "New Ticket" button to session header - `frontend/src/pages/QuickStartPage.tsx` — MyQueueWidget - `frontend/src/api/integrations.ts` — update `searchTickets()` and `searchTicketsQueue()` return types to `TicketListResponse` -- `frontend/src/components/dashboard/TicketQueue.tsx` — read `.items` from paginated response +- `frontend/src/types/integrations.ts` — add `company_id: number | null` and `board_id: number | null` to `PSATicketInfo` +- `frontend/src/components/dashboard/TicketQueue.tsx` — update existing component: read `.items`, add mapping-state detection, member-mapping check, and 5-item cap - `frontend/src/components/session/TicketPickerModal.tsx` — read `.items` from paginated response ---