/** * Project Dashboard Component * * Main dashboard view for a project showing agents, sprints, issues, and activity. * Integrates real-time updates via SSE. * * @see Issue #40 */ 'use client'; import { useCallback, useMemo, useState } from 'react'; import { useRouter } from 'next/navigation'; import { AlertCircle, RefreshCw } from 'lucide-react'; import { cn } from '@/lib/utils'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Button } from '@/components/ui/button'; import { ConnectionStatus } from '@/components/events/ConnectionStatus'; import { useProjectEvents } from '@/lib/hooks/useProjectEvents'; import { EventType, type ProjectEvent } from '@/lib/types/events'; import { ProjectHeader } from './ProjectHeader'; import { AgentPanel } from './AgentPanel'; import { SprintProgress } from './SprintProgress'; import { IssueSummary } from './IssueSummary'; import { RecentActivity } from './RecentActivity'; import type { Project, AgentInstance, Sprint, BurndownDataPoint, IssueSummary as IssueSummaryType, ActivityItem, } from './types'; // ============================================================================ // Types // ============================================================================ interface ProjectDashboardProps { /** Project ID */ projectId: string; /** Additional CSS classes */ className?: string; } // ============================================================================ // Mock Data (to be replaced with API calls) // ============================================================================ // Mock data for development - will be replaced with TanStack Query hooks const mockProject: Project = { id: 'proj-001', name: 'E-Commerce Platform Redesign', description: 'Complete redesign of the e-commerce platform with modern UI/UX', status: 'in_progress', autonomy_level: 'milestone', current_sprint_id: 'sprint-003', created_at: '2025-01-15T00:00:00Z', owner_id: 'user-001', }; const mockAgents: AgentInstance[] = [ { id: 'agent-001', agent_type_id: 'type-po', project_id: 'proj-001', name: 'Product Owner', role: 'product_owner', status: 'active', current_task: 'Reviewing user story acceptance criteria', last_activity_at: new Date(Date.now() - 2 * 60 * 1000).toISOString(), spawned_at: '2025-01-15T00:00:00Z', avatar: 'PO', }, { id: 'agent-002', agent_type_id: 'type-arch', project_id: 'proj-001', name: 'Architect', role: 'architect', status: 'working', current_task: 'Designing API contract for checkout flow', last_activity_at: new Date(Date.now() - 5 * 60 * 1000).toISOString(), spawned_at: '2025-01-15T00:00:00Z', avatar: 'AR', }, { id: 'agent-003', agent_type_id: 'type-be', project_id: 'proj-001', name: 'Backend Engineer', role: 'backend_engineer', status: 'idle', current_task: 'Waiting for architecture review', last_activity_at: new Date(Date.now() - 15 * 60 * 1000).toISOString(), spawned_at: '2025-01-15T00:00:00Z', avatar: 'BE', }, { id: 'agent-004', agent_type_id: 'type-fe', project_id: 'proj-001', name: 'Frontend Engineer', role: 'frontend_engineer', status: 'active', current_task: 'Implementing product catalog component', last_activity_at: new Date(Date.now() - 1 * 60 * 1000).toISOString(), spawned_at: '2025-01-15T00:00:00Z', avatar: 'FE', }, { id: 'agent-005', agent_type_id: 'type-qa', project_id: 'proj-001', name: 'QA Engineer', role: 'qa_engineer', status: 'pending', current_task: 'Preparing test cases for Sprint 3', last_activity_at: new Date(Date.now() - 30 * 60 * 1000).toISOString(), spawned_at: '2025-01-15T00:00:00Z', avatar: 'QA', }, ]; const mockSprint: Sprint = { id: 'sprint-003', project_id: 'proj-001', name: 'Sprint 3', goal: 'Complete checkout flow', status: 'active', start_date: '2025-01-27', end_date: '2025-02-10', total_issues: 15, completed_issues: 8, in_progress_issues: 4, blocked_issues: 1, todo_issues: 2, }; const mockBurndownData: BurndownDataPoint[] = [ { day: 1, remaining: 45, ideal: 45 }, { day: 2, remaining: 42, ideal: 42 }, { day: 3, remaining: 38, ideal: 39 }, { day: 4, remaining: 35, ideal: 36 }, { day: 5, remaining: 30, ideal: 33 }, { day: 6, remaining: 28, ideal: 30 }, { day: 7, remaining: 25, ideal: 27 }, { day: 8, remaining: 20, ideal: 24 }, ]; const mockIssueSummary: IssueSummaryType = { open: 12, in_progress: 8, in_review: 3, blocked: 2, done: 45, total: 70, }; const mockActivity: ActivityItem[] = [ { id: 'act-001', type: 'agent_message', agent: 'Product Owner', message: 'Approved user story #42: Cart checkout flow', timestamp: new Date(Date.now() - 2 * 60 * 1000).toISOString(), }, { id: 'act-002', type: 'issue_update', agent: 'Backend Engineer', message: 'Moved issue #38 to "In Review"', timestamp: new Date(Date.now() - 8 * 60 * 1000).toISOString(), }, { id: 'act-003', type: 'agent_status', agent: 'Frontend Engineer', message: 'Started working on issue #45', timestamp: new Date(Date.now() - 15 * 60 * 1000).toISOString(), }, { id: 'act-004', type: 'approval_request', agent: 'Architect', message: 'Requesting approval for API design document', timestamp: new Date(Date.now() - 25 * 60 * 1000).toISOString(), requires_action: true, }, { id: 'act-005', type: 'sprint_event', agent: 'System', message: 'Sprint 3 daily standup completed', timestamp: new Date(Date.now() - 60 * 60 * 1000).toISOString(), }, ]; // ============================================================================ // Helper Functions // ============================================================================ /** * Convert SSE events to activity items */ function eventToActivity(event: ProjectEvent): ActivityItem { const getAgentName = (event: ProjectEvent): string | undefined => { if (event.actor_type === 'agent') { const payload = event.payload as Record; return (payload.agent_name as string) || 'Agent'; } if (event.actor_type === 'system') { return 'System'; } return undefined; }; const getMessage = (event: ProjectEvent): string => { const payload = event.payload as Record; switch (event.type) { case EventType.AGENT_SPAWNED: return `spawned as ${payload.role || 'agent'}`; case EventType.AGENT_MESSAGE: return String(payload.message || 'sent a message'); case EventType.AGENT_STATUS_CHANGED: return `status changed to ${payload.new_status}`; case EventType.ISSUE_CREATED: return `created issue: ${payload.title}`; case EventType.ISSUE_UPDATED: return `updated issue #${payload.issue_id}`; case EventType.APPROVAL_REQUESTED: return String(payload.description || 'requested approval'); case EventType.SPRINT_STARTED: return `started sprint: ${payload.sprint_name}`; default: return event.type.replace(/_/g, ' '); } }; const getType = (event: ProjectEvent): ActivityItem['type'] => { if (event.type.startsWith('agent.message')) return 'agent_message'; if (event.type.startsWith('agent.')) return 'agent_status'; if (event.type.startsWith('issue.')) return 'issue_update'; if (event.type.startsWith('approval.')) return 'approval_request'; if (event.type.startsWith('sprint.')) return 'sprint_event'; return 'system'; }; return { id: event.id, type: getType(event), agent: getAgentName(event), message: getMessage(event), timestamp: event.timestamp, requires_action: event.type === EventType.APPROVAL_REQUESTED, }; } // ============================================================================ // Main Component // ============================================================================ export function ProjectDashboard({ projectId, className }: ProjectDashboardProps) { const router = useRouter(); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); // SSE connection for real-time updates const { events: sseEvents, connectionState, error: sseError, retryCount, reconnect, } = useProjectEvents(projectId, { autoConnect: true, onEvent: (event) => { // Handle specific event types for state updates console.log('[Dashboard] Received event:', event.type); }, }); // Convert SSE events to activity items const sseActivities = useMemo(() => { return sseEvents.slice(-10).map(eventToActivity); }, [sseEvents]); // Merge mock activities with SSE activities (SSE takes priority) const allActivities = useMemo(() => { const merged = [...sseActivities, ...mockActivity]; // Sort by timestamp, newest first merged.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()); return merged.slice(0, 10); }, [sseActivities]); // Use mock data for now (will be replaced with TanStack Query) const project = mockProject; const agents = mockAgents; const sprint = mockSprint; const burndownData = mockBurndownData; const issueSummary = mockIssueSummary; // Event handlers const handleStartSprint = useCallback(() => { console.log('Start sprint clicked'); // TODO: Implement start sprint action }, []); const handlePauseProject = useCallback(() => { console.log('Pause project clicked'); // TODO: Implement pause project action }, []); const handleCreateSprint = useCallback(() => { console.log('Create sprint clicked'); // TODO: Navigate to create sprint page }, []); const handleSettings = useCallback(() => { router.push(`/projects/${projectId}/settings`); }, [router, projectId]); const handleManageAgents = useCallback(() => { router.push(`/projects/${projectId}/agents`); }, [router, projectId]); const handleAgentAction = useCallback( (agentId: string, action: 'view' | 'pause' | 'restart' | 'terminate') => { console.log(`Agent action: ${action} on ${agentId}`); // TODO: Implement agent actions }, [] ); const handleViewAllIssues = useCallback(() => { router.push(`/projects/${projectId}/issues`); }, [router, projectId]); const handleViewAllActivity = useCallback(() => { router.push(`/projects/${projectId}/activity`); }, [router, projectId]); const handleActionClick = useCallback((activityId: string) => { console.log(`Action clicked for activity: ${activityId}`); // TODO: Navigate to approval page }, []); // Error state if (error) { return (
Error loading project {error}
); } return (
{/* SSE Connection Status - only show if not connected */} {connectionState !== 'connected' && ( )} {/* Header Section */} {/* Main Content Grid */}
{/* Left Column - Agent Panel & Sprint Overview */}
{/* Agent Panel */} {/* Sprint Overview */}
{/* Right Column - Activity & Issue Summary */}
{/* Issue Summary */} {/* Recent Activity */}
); }