fix(rls): add account_id to AISessionStep creations, fix boards toast
Some checks failed
CI / backend (push) Failing after 16m37s
CI / frontend (push) Failing after 45s
CI / e2e (push) Has been skipped
Mirror to GitHub / mirror (push) Successful in 3s

- flowpilot_engine: pass account_id at all 5 AISessionStep instantiation
  sites (_create_step_from_parsed x3, briefing step, status update step).
  Phase 4 RLS blocked every INSERT with NULL account_id — this broke all
  new FlowPilot sessions since the Phase 4 migration was applied.
- integrations: list_boards returns [] on PSAError instead of 502, stopping
  the spurious 'Server error' toast on dashboard load (boards are optional).
- client.ts: 5xx global toast now shows backend detail when available.
- useFlowPilotSession: startSession extracts backend detail for error state;
  suppresses duplicate toast for 5xx (global interceptor already handles it).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-15 04:41:14 +00:00
parent 567985402f
commit 0d9babb986
4 changed files with 19 additions and 6 deletions

View File

@@ -362,8 +362,9 @@ async def list_boards(
provider = await get_provider_for_account(current_user.account_id, db) provider = await get_provider_for_account(current_user.account_id, db)
boards = await provider.list_boards() boards = await provider.list_boards()
return [PSABoardResponse(id=b.id, name=b.name) for b in boards] return [PSABoardResponse(id=b.id, name=b.name) for b in boards]
except PSAError as e: except PSAError:
raise HTTPException(status_code=502, detail=str(e)) # Boards are optional UI chrome — degrade gracefully rather than surfacing a toast
return []
@router.get("/tickets/search", response_model=list[PSATicketSearchResult]) @router.get("/tickets/search", response_model=list[PSATicketSearchResult])

View File

@@ -330,6 +330,7 @@ async def start_session(
# 7. Create first step # 7. Create first step
step = _create_step_from_parsed( step = _create_step_from_parsed(
session_id=session.id, session_id=session.id,
account_id=session.account_id,
step_order=0, step_order=0,
parsed=parsed, parsed=parsed,
input_tokens=input_tokens, input_tokens=input_tokens,
@@ -433,6 +434,7 @@ async def process_response(
# Create new step # Create new step
step = _create_step_from_parsed( step = _create_step_from_parsed(
session_id=session.id, session_id=session.id,
account_id=session.account_id,
step_order=session.step_count - 1, step_order=session.step_count - 1,
parsed=parsed, parsed=parsed,
input_tokens=input_tokens, input_tokens=input_tokens,
@@ -694,6 +696,7 @@ async def pickup_session(
briefing_step = AISessionStep( briefing_step = AISessionStep(
id=uuid.uuid4(), id=uuid.uuid4(),
session_id=session.id, session_id=session.id,
account_id=session.account_id,
branch_id=session.active_branch_id if session.is_branching else None, branch_id=session.active_branch_id if session.is_branching else None,
step_order=session.step_count, step_order=session.step_count,
step_type="action", step_type="action",
@@ -765,6 +768,7 @@ async def pickup_session(
next_step = _create_step_from_parsed( next_step = _create_step_from_parsed(
session_id=session.id, session_id=session.id,
account_id=session.account_id,
step_order=session.step_count - 1, step_order=session.step_count - 1,
parsed=parsed, parsed=parsed,
input_tokens=input_tokens, input_tokens=input_tokens,
@@ -997,6 +1001,7 @@ async def generate_status_update(
step = AISessionStep( step = AISessionStep(
id=uuid.uuid4(), id=uuid.uuid4(),
session_id=session.id, session_id=session.id,
account_id=session.account_id,
branch_id=session.active_branch_id if session.is_branching else None, branch_id=session.active_branch_id if session.is_branching else None,
step_order=session.step_count, step_order=session.step_count,
step_type="status_update", step_type="status_update",
@@ -1440,6 +1445,7 @@ def _format_engineer_response(request: StepResponseRequest) -> str:
def _create_step_from_parsed( def _create_step_from_parsed(
session_id: UUID, session_id: UUID,
account_id: UUID,
step_order: int, step_order: int,
parsed: dict[str, Any], parsed: dict[str, Any],
input_tokens: int, input_tokens: int,
@@ -1487,6 +1493,7 @@ def _create_step_from_parsed(
return AISessionStep( return AISessionStep(
id=uuid.uuid4(), id=uuid.uuid4(),
session_id=session_id, session_id=session_id,
account_id=account_id,
branch_id=branch_id, branch_id=branch_id,
step_order=step_order, step_order=step_order,
step_type=step_type if parsed["type"] != "resolution_suggestion" else "action", step_type=step_type if parsed["type"] != "resolution_suggestion" else "action",

View File

@@ -49,9 +49,9 @@ function handleGlobalError(error: AxiosError) {
return return
} }
// Server errors (5xx) // Server errors (5xx) — show backend detail when available, else generic message
if (status >= 500) { if (status >= 500) {
toast.error('Server error - please try again later') toast.error(detail || 'Server error - please try again later')
return return
} }
} }

View File

@@ -99,9 +99,14 @@ export function useFlowPilotSession(): UseFlowPilotSession {
setAllSteps([firstStep]) setAllSteps([firstStep])
setCurrentStep(firstStep) setCurrentStep(firstStep)
} catch (e: unknown) { } catch (e: unknown) {
const message = e instanceof Error ? e.message : 'Failed to start session' // Prefer the backend's detail message over the generic axios status string
const detail = (e as any)?.response?.data?.detail
const message = typeof detail === 'string' ? detail : (e instanceof Error ? e.message : 'Failed to start session')
setError(message) setError(message)
toast.error(message) // Global axios interceptor already shows a toast for 5xx — skip duplicate
if (!(e as any)?.response?.status || (e as any)?.response?.status < 500) {
toast.error(message)
}
} finally { } finally {
setIsLoading(false) setIsLoading(false)
} }