feat(ai-session): add Phase 2 PSA integration, escalation handoff, and session management

Phase 2 of the FlowPilot-First Pivot connecting AI sessions to ConnectWise PSA:

Slice 1 — PSA Ticket Intake:
- FlowPilotEngine accepts psa_ticket intake with graceful CW API fallback
- Ticket picker on intake screen (refactored TicketPickerModal for dual-mode)
- Ticket context card in session sidebar

Slice 2 — Auto Documentation Push:
- PSA documentation service with resolution/escalation note formatting
- Time entry creation via new ConnectWise provider method
- Automatic retry scheduler (APScheduler, 5min interval, 3 retries)
- PSA push status indicators in frontend with manual retry button
- Member mapping warning when CW member not mapped

Slice 3 — Session Pause/Resume & Escalation Handoff:
- Pause/resume endpoints for same-engineer session bookmarking
- Escalation flow: requesting_escalation status, self-escalation blocked
- Enhanced escalation package with LLM-generated hypotheses/suggestions
- Pickup endpoint with continue/fresh resume modes and briefing step
- Escalation queue (sidebar nav + dedicated page)
- SessionBriefing component with continue/fresh choice UI
- EscalateModal with PSA-aware button text

Slice 4 — Mid-Session Ticket Linking:
- Link ticket retroactively with context injection into system prompt
- Link Ticket button in session sidebar

Slice 5 — FlowPilot PSA Settings:
- Settings tab on IntegrationsPage with 7 configurable options
- Stored as flowpilot_settings JSONB on PsaConnection

Database: 2 migrations (flowpilot_settings, psa_post_log changes, status constraint)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-19 01:30:05 +00:00
parent 2063a799b0
commit bbe590bfec
37 changed files with 3698 additions and 121 deletions

View File

@@ -0,0 +1,82 @@
import { useState } from 'react'
import { AlertTriangle, Loader2 } from 'lucide-react'
import { Modal } from '@/components/common/Modal'
import type { EscalateSessionRequest } from '@/types/ai-session'
interface EscalateModalProps {
open: boolean
onClose: () => void
onEscalate: (data: EscalateSessionRequest) => Promise<unknown>
isProcessing: boolean
hasPsaTicket: boolean
}
export function EscalateModal({ open, onClose, onEscalate, isProcessing, hasPsaTicket }: EscalateModalProps) {
const [reason, setReason] = useState('')
const handleSubmit = async () => {
if (!reason.trim() || reason.trim().length < 5) return
await onEscalate({ escalation_reason: reason.trim() })
setReason('')
onClose()
}
const handleClose = () => {
if (!isProcessing) {
setReason('')
onClose()
}
}
return (
<Modal isOpen={open} onClose={handleClose} title="Escalate Session" size="sm">
<div className="space-y-4">
<div className="flex items-start gap-3 rounded-xl border border-amber-400/20 bg-amber-400/5 p-3">
<AlertTriangle size={16} className="text-amber-400 shrink-0 mt-0.5" />
<p className="text-sm text-amber-400">
This will mark the session as requesting escalation. Team members will see it in their escalation queue and can pick it up with full context.
</p>
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-foreground">
Why are you escalating?
</label>
<textarea
value={reason}
onChange={(e) => setReason(e.target.value)}
placeholder="e.g. I've exhausted all networking diagnostics and suspect this is a firewall policy issue that requires senior admin access..."
className="w-full rounded-lg border border-border bg-card px-4 py-3 text-sm text-foreground placeholder:text-muted-foreground focus:border-[rgba(6,182,212,0.3)] focus:outline-none resize-none"
rows={4}
autoFocus
/>
<p className="mt-1 text-[0.625rem] text-[#5a6170]">
Minimum 5 characters. This will be shown to the engineer who picks up.
</p>
</div>
<div className="flex gap-2">
<button
onClick={handleClose}
disabled={isProcessing}
className="flex-1 rounded-lg bg-[rgba(255,255,255,0.04)] border border-[rgba(255,255,255,0.06)] px-4 py-2.5 text-sm font-medium text-foreground hover:border-[rgba(255,255,255,0.12)] transition-colors disabled:opacity-50"
>
Cancel
</button>
<button
onClick={handleSubmit}
disabled={!reason.trim() || reason.trim().length < 5 || isProcessing}
className="flex-1 flex items-center justify-center gap-2 rounded-lg bg-amber-500/90 px-4 py-2.5 text-sm font-semibold text-[#101114] hover:bg-amber-500 active:scale-[0.97] disabled:opacity-40 transition-all"
>
{isProcessing ? (
<Loader2 size={14} className="animate-spin" />
) : (
<AlertTriangle size={14} />
)}
{hasPsaTicket ? 'Escalate & Update Ticket' : 'Escalate'}
</button>
</div>
</div>
</Modal>
)
}