diff --git a/web/src/lib/date-formatting.ts b/web/src/lib/date-formatting.ts new file mode 100644 index 0000000..acbb70f --- /dev/null +++ b/web/src/lib/date-formatting.ts @@ -0,0 +1,21 @@ +/** + * Date formatting utility functions. + */ + +/** + * Format a date string into human-readable format. + * @param dateStr - ISO date string or any valid Date input + * @returns Formatted date string (e.g., "Feb 11, 2026") or original string if parsing fails + */ +export function formatDate(dateStr: string): string { + try { + const date = new Date(dateStr); + return date.toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + }); + } catch { + return dateStr; + } +} diff --git a/web/src/views/Dashboard.svelte b/web/src/views/Dashboard.svelte index b65cf6e..75275cf 100644 --- a/web/src/views/Dashboard.svelte +++ b/web/src/views/Dashboard.svelte @@ -14,7 +14,7 @@ import ErrorMessage from '../components/ErrorMessage.svelte'; import StatusBadge from '../components/StatusBadge.svelte'; import PriorityBadge from '../components/PriorityBadge.svelte'; - import type { GraphNode, ActiveAgent } from '../types'; + import type { GraphNode } from '../types'; type DashboardData = { projectGoals: Array<{ projectName: string; goalCount: number; projectId: string }>; @@ -33,24 +33,40 @@ /** * Load dashboard data on mount. + * Read reactive dependencies synchronously, then call async function. */ - $effect(async () => { + $effect(() => { + // Read reactive deps synchronously so Svelte tracks them + const projects = projectsState.projects; + loadDashboardData(projects); + }); + + /** + * Async function to load all dashboard data. + * Parallelizes goal fetches per-project, then parallelizes agent fetches. + */ + async function loadDashboardData(projects: typeof projectsState.projects): Promise { try { loading = true; error = null; - // Load all projects + // Load all projects first await loadProjects(); - // For each project, load its goals + // Fetch goals for all projects in parallel using Promise.allSettled + const goalResults = await Promise.allSettled( + projects.map((project) => apiClient.listGoals(project.id)) + ); + const projectGoals: DashboardData['projectGoals'] = []; const allActiveGoals: DashboardData['activeGoals'] = []; - let totalActiveAgents = 0; + const agentFetchPromises: Promise[] = []; - for (const project of projectsState.projects) { - try { - // Get goals for this project - const goals = await apiClient.listGoals(project.id); + // Process goal results + goalResults.forEach((result, idx) => { + const project = projects[idx]; + if (result.status === 'fulfilled') { + const goals = result.value; projectGoals.push({ projectName: project.name, goalCount: goals.length, @@ -59,25 +75,29 @@ // Collect active goals const activeGoalsForProject = goals.filter((g) => g.status === 'active'); - for (const goal of activeGoalsForProject) { + activeGoalsForProject.forEach((goal) => { allActiveGoals.push({ goal, projectName: project.name, }); - // Count active agents for this goal - try { - const agents = await apiClient.listAgents(goal.id); - totalActiveAgents += agents.length; - } catch { - // Ignore errors fetching agents for individual goals - } - } - } catch { - // Ignore errors fetching goals for individual projects - // Continue with next project + // Queue agent fetch for this goal + agentFetchPromises.push( + apiClient.listAgents(goal.id).catch(() => []) // Return empty array on error + ); + }); } - } + }); + + // Fetch all agents in parallel using Promise.allSettled + const agentResults = await Promise.allSettled(agentFetchPromises); + + let totalActiveAgents = 0; + agentResults.forEach((result) => { + if (result.status === 'fulfilled') { + totalActiveAgents += result.value.length; + } + }); dashboardData = { projectGoals, @@ -89,7 +109,7 @@ } finally { loading = false; } - }); + } /** * Handle project click - navigate to project detail. diff --git a/web/src/views/ProjectDetail.svelte b/web/src/views/ProjectDetail.svelte index 56803f9..4096821 100644 --- a/web/src/views/ProjectDetail.svelte +++ b/web/src/views/ProjectDetail.svelte @@ -8,6 +8,7 @@ import { apiClient } from '../api'; import { getCurrentRoute } from '../router.svelte'; import { navigate } from '../router.svelte'; + import { formatDate } from '../lib/date-formatting'; import LoadingSpinner from '../components/LoadingSpinner.svelte'; import ErrorMessage from '../components/ErrorMessage.svelte'; import StatusBadge from '../components/StatusBadge.svelte'; @@ -40,22 +41,31 @@ /** * Load project details, goals, and decisions on mount or when projectId changes. + * Read reactive dependencies synchronously, then call async function. */ - $effect(async () => { - if (!projectId) { + $effect(() => { + // Read reactive deps synchronously so Svelte tracks them + const id = projectId; + if (!id) { error = 'No project ID provided'; return; } + loadProjectDetail(id); + }); + /** + * Async function to load project details. + */ + async function loadProjectDetail(id: string): Promise { try { loading = true; error = null; // Load project - const project = await apiClient.getProject(projectId); + const project = await apiClient.getProject(id); // Load goals - const goals = await apiClient.listGoals(projectId); + const goals = await apiClient.listGoals(id); // For each goal, load its tree to count tasks const goalsWithTasks: Array = []; @@ -80,7 +90,7 @@ } // Load decisions - const decisions = await apiClient.listDecisions(projectId); + const decisions = await apiClient.listDecisions(id); projectData = { project, @@ -92,22 +102,6 @@ } finally { loading = false; } - }); - - /** - * Format a date string into human-readable format. - */ - function formatDate(dateStr: string): string { - try { - const date = new Date(dateStr); - return date.toLocaleDateString('en-US', { - year: 'numeric', - month: 'short', - day: 'numeric', - }); - } catch { - return dateStr; - } } /** @@ -138,7 +132,9 @@ } function handleDecisionClick(decisionId: string): void { - navigate(`/goals/${decisionId}/decisions`); + // Extract goal ID from hierarchical node ID (format: ra-XXXX.N) + const goalId = decisionId.split('.')[0]; + navigate(`/goals/${goalId}/decisions`); } diff --git a/web/src/views/ProjectList.svelte b/web/src/views/ProjectList.svelte index ba7fbf7..c779efb 100644 --- a/web/src/views/ProjectList.svelte +++ b/web/src/views/ProjectList.svelte @@ -6,6 +6,7 @@ import { loadProjects, projectsState } from '../stores/projects.svelte'; import { navigate } from '../router.svelte'; + import { formatDate } from '../lib/date-formatting'; import LoadingSpinner from '../components/LoadingSpinner.svelte'; import ErrorMessage from '../components/ErrorMessage.svelte'; @@ -14,8 +15,17 @@ /** * Load projects on mount. + * Read reactive dependencies synchronously, then call async function. */ - $effect(async () => { + $effect(() => { + // Synchronously trigger load on mount + loadProjectsData(); + }); + + /** + * Async function to load projects. + */ + async function loadProjectsData(): Promise { try { loading = true; error = null; @@ -25,22 +35,6 @@ } finally { loading = false; } - }); - - /** - * Format a date string into human-readable format. - */ - function formatDate(dateStr: string): string { - try { - const date = new Date(dateStr); - return date.toLocaleDateString('en-US', { - year: 'numeric', - month: 'short', - day: 'numeric', - }); - } catch { - return dateStr; - } } /**