Complete Phase 2: Frontend implementation with React + TypeScript

Frontend Features:
- React 18 + Vite + TypeScript + Tailwind CSS + Zustand
- JWT authentication with automatic token refresh
- Tree library with search and category filtering
- Full tree navigation (decision/action/solution nodes)
- Session management with notes and completion
- Session history with export (Markdown/Text/HTML)
- ErrorBoundary for graceful error handling

Backend Fixes:
- CORS: Added port 5174 to allowed origins
- Sessions: Fixed JSONB datetime serialization (mode='json')

Documentation:
- Updated PROGRESS.md with Phase 2 completion
- Updated 03-DEVELOPMENT-ROADMAP.md with checked items
- Added PHASE-2.5-PERSONAL-BRANCHING.md spec

Seed Data:
- Added backend/scripts/seed_data.py with Password Reset tree

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Michael Chihlas
2026-01-27 22:42:22 -05:00
parent 7d96807fb1
commit cd10ecd42c
51 changed files with 9014 additions and 111 deletions

View File

@@ -0,0 +1,152 @@
import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { sessionsApi } from '@/api'
import type { Session } from '@/types'
import { cn } from '@/lib/utils'
export function SessionHistoryPage() {
const navigate = useNavigate()
const [sessions, setSessions] = useState<Session[]>([])
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [filter, setFilter] = useState<'all' | 'completed' | 'active'>('all')
useEffect(() => {
loadSessions()
}, [filter])
const loadSessions = async () => {
setIsLoading(true)
setError(null)
try {
const params = filter === 'all' ? {} : { completed: filter === 'completed' }
const sessionsData = await sessionsApi.list(params)
setSessions(sessionsData)
} catch (err) {
setError('Failed to load sessions')
console.error(err)
} finally {
setIsLoading(false)
}
}
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleString()
}
return (
<div className="container mx-auto px-4 py-8">
<div className="mb-8">
<h1 className="text-3xl font-bold text-foreground">Session History</h1>
<p className="mt-2 text-muted-foreground">
View and manage your troubleshooting sessions
</p>
</div>
{/* Filter Tabs */}
<div className="mb-6 flex gap-2 border-b border-border">
{(['all', 'active', 'completed'] as const).map((tab) => (
<button
key={tab}
onClick={() => setFilter(tab)}
className={cn(
'px-4 py-2 text-sm font-medium transition-colors',
filter === tab
? 'border-b-2 border-primary text-primary'
: 'text-muted-foreground hover:text-foreground'
)}
>
{tab.charAt(0).toUpperCase() + tab.slice(1)}
</button>
))}
</div>
{/* Error State */}
{error && (
<div className="mb-6 rounded-md bg-destructive/10 p-4 text-destructive">
{error}
</div>
)}
{/* Loading State */}
{isLoading ? (
<div className="flex justify-center py-12">
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
</div>
) : sessions.length === 0 ? (
<div className="py-12 text-center text-muted-foreground">
No sessions found.{' '}
<button
onClick={() => navigate('/trees')}
className="text-primary hover:underline"
>
Start a new session
</button>
</div>
) : (
<div className="space-y-4">
{sessions.map((session) => (
<div
key={session.id}
className="rounded-lg border border-border bg-card p-4 shadow-sm transition-shadow hover:shadow-md"
>
<div className="flex items-start justify-between">
<div>
<div className="flex items-center gap-2">
<span
className={cn(
'inline-block h-2 w-2 rounded-full',
session.completed_at ? 'bg-green-500' : 'bg-yellow-500'
)}
/>
<span className="font-medium text-card-foreground">
{session.ticket_number || 'No ticket'}
</span>
{session.client_name && (
<span className="text-muted-foreground">
· {session.client_name}
</span>
)}
</div>
<p className="mt-1 text-sm text-muted-foreground">
Started: {formatDate(session.started_at)}
{session.completed_at && (
<> · Completed: {formatDate(session.completed_at)}</>
)}
</p>
<p className="mt-1 text-sm text-muted-foreground">
{session.decisions.length} decisions recorded
</p>
</div>
<div className="flex gap-2">
<button
onClick={() => navigate(`/sessions/${session.id}`)}
className={cn(
'rounded-md border border-input px-3 py-1.5 text-sm font-medium',
'hover:bg-accent hover:text-accent-foreground'
)}
>
View Details
</button>
{!session.completed_at && (
<button
onClick={() => navigate(`/trees/${session.tree_id}/navigate`, { state: { sessionId: session.id } })}
className={cn(
'rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground',
'hover:bg-primary/90'
)}
>
Resume
</button>
)}
</div>
</div>
</div>
))}
</div>
)}
</div>
)
}
export default SessionHistoryPage