feat(FN-1850): aggregate projects across connected nodes

- Add /api/projects/across-nodes to merge local projects with online remote node project lists
- Expose fetchProjectsAcrossNodes and ProjectInfoWithSource, and switch useProjects to consume cross-node data
- Update ProjectOverview and ProjectCard with node badges, node count stats, and a node filter dropdown plus responsive styles
- Add server and dashboard test coverage for cross-node aggregation, filtering, and hook behavior changes
- Include a changeset for @gsxdsm/fusion minor release and preserve stale-channel SSE reconnect guards during merge
This commit is contained in:
Fusion
2026-04-18 03:08:00 -07:00
committed by gsxdsm
parent 76123c9538
commit 53babb3524
14 changed files with 1674 additions and 55 deletions

View File

@@ -11,6 +11,8 @@ export interface ProjectCardProps {
onResume: (project: RegisteredProject) => void;
onRemove: (project: RegisteredProject) => void;
node?: NodeInfo;
/** Fallback node name when the node object is not available (e.g., for remote projects) */
nodeNameFallback?: string;
isLoading?: boolean;
}
@@ -71,7 +73,11 @@ function areProjectCardPropsEqual(previous: ProjectCardProps, next: ProjectCardP
const prevNode = previous.node;
const nextNode = next.node;
if (!prevNode && !nextNode) return true;
if (!prevNode && !nextNode) {
// Compare fallback names
if (previous.nodeNameFallback !== next.nodeNameFallback) return false;
return true;
}
if (!prevNode || !nextNode) return false;
return (
@@ -90,6 +96,7 @@ function ProjectCardInner({
onResume,
onRemove,
node,
nodeNameFallback,
isLoading = false,
}: ProjectCardProps) {
const [removeArmed, setRemoveArmed] = useState(false);
@@ -147,9 +154,9 @@ function ProjectCardInner({
<h3 className="project-card-name" title={project.name}>
{project.name}
</h3>
{node && (
<span className="node-badge" title={`Assigned node: ${node.name}`}>
on: {node.name}
{(node || nodeNameFallback) && (
<span className="node-badge" title={`Assigned node: ${node?.name ?? nodeNameFallback}`}>
on: {node?.name ?? nodeNameFallback}
</span>
)}
<span className="project-card-path" title={project.path}>