import { useState, type ReactElement } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { MissionTimeline } from '@/components/projects/mission-timeline'; import { PrdViewer } from '@/components/projects/prd-viewer'; import { TaskDetailModal } from '@/components/tasks/task-detail-modal'; import { TaskListView } from '@/components/tasks/task-list-view'; import { TaskStatusSummary } from '@/components/tasks/task-status-summary'; import { PartialDataNotice, StaleDataNotice, UnavailableDataNotice, } from '@/components/freshness/freshness-notices'; import { api } from '@/lib/api'; import { cn } from '@/lib/cn'; import type { Mission, Project, Task, TaskStatus } from '@/lib/types'; import { combineFreshness, UNKNOWN_VERDICT, verdictValue, type FreshSnapshot, } from '@/lib/freshness/model'; import { describeFailure, useFreshCollection } from '@/lib/freshness/use-fresh-collection'; import { validateMissionCollection, validateProjectEntity, validateTaskCollection, } from '@/lib/freshness/validators'; type Tab = 'overview' | 'tasks' | 'missions' | 'prd'; const projectStatusColors: Record = { active: 'bg-success/20 text-success', paused: 'bg-warning/20 text-warning', completed: 'bg-blue-600/20 text-blue-400', archived: 'bg-gray-600/20 text-gray-400', }; const taskStatusColors: Record = { 'not-started': 'bg-gray-600/20 text-gray-300', 'in-progress': 'bg-blue-600/20 text-blue-400', blocked: 'bg-error/20 text-error', done: 'bg-success/20 text-success', cancelled: 'bg-gray-600/20 text-gray-500', }; interface TabButtonProps { id: Tab; label: string; activeTab: Tab; onClick: (tab: Tab) => void; } function TabButton({ id, label, activeTab, onClick }: TabButtonProps): ReactElement { return ( ); } /** Remounts per project id so no state from one project renders for another. */ export function ProjectDetailPage(): ReactElement { const { id = '' } = useParams(); return ; } function ProjectDetail({ id }: { id: string }): ReactElement { const navigate = useNavigate(); const enabled = id.length > 0; // Primary collection gates the surface; missions and tasks are secondaries // whose failures degrade the surface to `partial` instead of rendering // empty healthy lists. const project = useFreshCollection({ source: `gateway:/api/projects/${id}`, fetcher: (signal) => api(`/api/projects/${id}`, { signal }), validate: validateProjectEntity, // No last-known restore: the entity carries workspace identity that // cannot be scope-checked before display (see ProjectsPage note). enabled, }); const missions = useFreshCollection({ source: 'gateway:/api/missions', fetcher: (signal) => api('/api/missions', { signal }), validate: validateMissionCollection, cacheKey: enabled ? 'missions' : null, enabled, }); const tasks = useFreshCollection({ source: `gateway:/api/tasks?projectId=${id}`, fetcher: (signal) => api(`/api/tasks?projectId=${id}`, { signal }), validate: validateTaskCollection, cacheKey: enabled ? `project-tasks:${id}` : null, enabled, }); const [activeTab, setActiveTab] = useState('overview'); const [taskFilter, setTaskFilter] = useState('all'); const [selectedTask, setSelectedTask] = useState(null); const surface = combineFreshness(project.freshness, [missions.freshness, tasks.freshness]); const tasksVerified = tasks.freshness === 'current'; const projectMissions = missions.data?.filter((mission) => mission.projectId === id) ?? null; const retryAll = (): void => { void Promise.all([project.revalidate(), missions.revalidate(), tasks.revalidate()]); }; if (!enabled) { return (

Project

Project id is missing.
); } if (project.freshness === 'unknown') { return (

Project

Loading project...

); } if (project.freshness === 'unavailable' || project.data === null) { return (

Project

); } const projectTasks = tasks.data ?? null; const filteredTasks = projectTasks === null ? [] : taskFilter === 'all' ? projectTasks : projectTasks.filter((task) => task.status === taskFilter); // Derived completion verdicts: unknown (never green) unless the task // collection is verified current. const doneCount = projectTasks?.filter((task) => task.status === 'done').length ?? 0; const inProgressCount = projectTasks?.filter((task) => task.status === 'in-progress').length ?? 0; const blockedCount = projectTasks?.filter((task) => task.status === 'blocked').length ?? 0; const prdContent = getPrdContent(project.data); const tabs: Array<{ id: Tab; label: string }> = [ { id: 'overview', label: 'Overview' }, { id: 'tasks', label: `Tasks (${projectTasks === null ? UNKNOWN_VERDICT : projectTasks.length})`, }, { id: 'missions', label: `Missions (${projectMissions === null ? UNKNOWN_VERDICT : projectMissions.length})`, }, ...(prdContent ? [{ id: 'prd' as const, label: 'PRD' }] : []), ]; const staleSnapshot: FreshSnapshot | null = project.freshness === 'stale' ? project.snapshot : missions.freshness === 'stale' ? missions.snapshot : tasks.freshness === 'stale' ? tasks.snapshot : null; const missingSections: string[] = []; if (missions.freshness === 'unavailable') missingSections.push('Missions'); if (tasks.freshness === 'unavailable') missingSections.push('Tasks'); return (

{project.data.name}

{project.data.status}
{project.data.description ? (

{project.data.description}

) : null}

Created {new Date(project.data.createdAt).toLocaleDateString()} ยท Updated{' '} {new Date(project.data.updatedAt).toLocaleDateString()}

{staleSnapshot !== null ? (
) : null} {missingSections.length > 0 ? (
) : null}
0 ? 'text-error' : undefined} />
{tabs.map((tab) => ( ))}
{activeTab === 'overview' ? ( ) : null} {activeTab === 'tasks' ? (
{projectTasks === null ? ( ) : ( <>
)}
) : null} {activeTab === 'missions' ? ( projectMissions === null ? ( ) : ( ) ) : null} {activeTab === 'prd' && prdContent ? (
) : null} {selectedTask ? ( setSelectedTask(null)} /> ) : null}
); } function OverviewTab({ project, missions, tasks, }: { project: Project; missions: Mission[] | null; tasks: Task[] | null; }): ReactElement { const recentTasks = tasks === null ? null : [...tasks] .sort( (left, right) => new Date(right.updatedAt).getTime() - new Date(left.updatedAt).getTime(), ) .slice(0, 5); return (

Recent Tasks

{recentTasks === null ? ( ) : recentTasks.length === 0 ? (

No tasks yet

) : (
{recentTasks.map((task) => (
{task.title} {task.status}
))}
)}

Missions

{missions === null ? ( ) : missions.length === 0 ? (

No missions yet

) : ( )}
{project.metadata && Object.keys(project.metadata).length > 0 ? (

Project Metadata

              {JSON.stringify(project.metadata, null, 2)}
            
) : null}
); } function StatCard({ label, value, valueClass, }: { label: string; value: string; valueClass?: string; }): ReactElement { return (

{label}

{value}

); } function getPrdContent(project: Project): string | null { if (!project.metadata) return null; const prd = project.metadata['prd']; if (typeof prd === 'string' && prd.trim().length > 0) { return prd; } const prdContent = project.metadata['prdContent']; if (typeof prdContent === 'string' && prdContent.trim().length > 0) { return prdContent; } return null; }