Files
fusion/packages/dashboard/app/hooks/useProjectHealth.ts
gsxdsm f88611b071 feat(KB-502): add dashboard multi-project hooks and project API routes
- Add project API routes for multi-project backend integration
- Create useProjects hook for listing and managing registered projects
- Create useCurrentProject hook for current project selection and switching
- Create useProjectHealth hook for real-time project health metrics
- Create useActivityLog hook with ActivityLogModal integration
- Add comprehensive test coverage for all hooks (useProjects, useCurrentProject, useActivityLog)
- Add test setup utilities for React Query mocking
2026-04-01 01:15:44 -07:00

98 lines
2.5 KiB
TypeScript

import { useState, useEffect, useRef, useCallback } from "react";
import type { ProjectHealth } from "../api";
import { fetchProjectHealth } from "../api";
export interface UseProjectHealthResult {
/** Current health metrics */
health: ProjectHealth | null;
/** Project status derived from health */
status: "active" | "paused" | "errored" | "initializing" | null;
/** Number of active tasks */
activeTasks: number;
/** Last activity timestamp */
lastActivityAt: string | null;
/** Loading state */
loading: boolean;
/** Manually refresh health */
refresh: () => Promise<void>;
}
const POLL_INTERVAL_MS = 10000; // 10 seconds
/**
* Hook for polling project health metrics.
* Automatically polls every 10 seconds when the project is active.
* Stops polling when component unmounts.
*/
export function useProjectHealth(projectId: string | null): UseProjectHealthResult {
const [health, setHealth] = useState<ProjectHealth | null>(null);
const [loading, setLoading] = useState(false);
const intervalRef = useRef<NodeJS.Timeout | null>(null);
const refresh = useCallback(async () => {
if (!projectId) return;
try {
setLoading(true);
const data = await fetchProjectHealth(projectId);
setHealth(data);
} catch (err) {
// Silently fail - don't clear health on error
console.error("Failed to fetch project health:", err);
} finally {
setLoading(false);
}
}, [projectId]);
// Initial fetch
useEffect(() => {
if (!projectId) {
setHealth(null);
return;
}
refresh();
}, [projectId, refresh]);
// Polling when project is active
useEffect(() => {
if (!projectId) return;
// Stop any existing interval
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
// Only poll when project is active
const shouldPoll = !health || health.status === "active" || health.status === "initializing";
if (shouldPoll) {
intervalRef.current = setInterval(() => {
refresh();
}, POLL_INTERVAL_MS);
}
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
}, [projectId, health?.status, refresh]);
// Derived values
const status = health?.status ?? null;
const activeTasks = health?.activeTaskCount ?? 0;
const lastActivityAt = health?.lastActivityAt ?? null;
return {
health,
status,
activeTasks,
lastActivityAt,
loading,
refresh,
};
}