diff --git a/agent-node/agent_node/node.py b/agent-node/agent_node/node.py index dc2446c..b1cb589 100644 --- a/agent-node/agent_node/node.py +++ b/agent-node/agent_node/node.py @@ -279,7 +279,15 @@ r_path = os.path.relpath(abs_path, watch_path) files.append(agent_pb2.FileInfo(path=r_path, size=0, hash="", is_dir=True)) except Exception as e: - logger.error(f"Manifest generation error: {e}") + print(f" [❌] Manifest generation failed for {rel_path}: {e}") + self.task_queue.put(agent_pb2.ClientTaskMessage( + file_sync=agent_pb2.FileSyncMessage( + session_id=session_id, + task_id=task_id, + status=agent_pb2.SyncStatus(code=agent_pb2.SyncStatus.ERROR, message=str(e)) + ) + )) + return self.task_queue.put(agent_pb2.ClientTaskMessage( file_sync=agent_pb2.FileSyncMessage( diff --git a/ui/client-app/src/components/FileSystemNavigator.js b/ui/client-app/src/components/FileSystemNavigator.js index 8677aa0..9097692 100644 --- a/ui/client-app/src/components/FileSystemNavigator.js +++ b/ui/client-app/src/components/FileSystemNavigator.js @@ -20,6 +20,7 @@ const [newItemModal, setNewItemModal] = useState(null); // { parentPath, isDir } const [deleteModal, setDeleteModal] = useState(null); // path const [operationLoading, setOperationLoading] = useState(false); + const [folderLoading, setFolderLoading] = useState({}); // { [path]: boolean } const fetchLevel = useCallback(async (path) => { const data = await nodeFsList(nodeId, path); @@ -43,38 +44,53 @@ if (nodeId) { loadRoot(); - // Only poll if sync status is enabled and we have pending files + // Only poll if sync status is enabled if (showSyncStatus) { const interval = setInterval(() => { - const hasUnsynced = tree.some(f => !f.is_dir && !f.is_synced); - if (hasUnsynced || tree.length === 0) { - loadRoot(); - } - }, 8000); + setTree(prev => { + const hasUnsynced = prev.some(f => !f.is_dir && !f.is_synced); + if (hasUnsynced) { + loadRoot(); + } + return prev; + }); + }, 10000); return () => clearInterval(interval); } } - }, [nodeId, loadRoot, tree, showSyncStatus]); + }, [nodeId, loadRoot, showSyncStatus]); // Fixed infinite loop by removing tree const toggleFolder = async (path) => { const isExpanded = expanded[path]; if (!isExpanded) { setExpanded(prev => ({ ...prev, [path]: true })); - // Only fetch if we don't already have children for this path in our flat tree - // A child would have a path like "path/filename" or "/path/filename" - const prefix = path === "/" ? "/" : (path.endsWith("/") ? path : path + "/"); - const hasChildren = tree.some(node => node.path.startsWith(prefix) && node.path !== path); + + // Normalize path for prefix check: e.g. "etc" -> "etc/" + const target = (path === "/" || path === ".") ? "" : (path.startsWith("/") ? path.slice(1) : path); + const prefix = target === "" ? "" : (target.endsWith("/") ? target : target + "/"); + + const hasChildren = tree.some(node => { + const nodeRel = node.path.startsWith("/") ? node.path.slice(1) : node.path; + return nodeRel.startsWith(prefix) && nodeRel !== target; + }); if (!hasChildren) { + setFolderLoading(prev => ({ ...prev, [path]: true })); try { - const children = await fetchLevel(path === "/" ? "." : path); + // Node expects relative path for subdirs, or "." for root + const fetchPath = (path === "/" || path === ".") ? "." : (path.startsWith("/") ? path.slice(1) : path); + const children = await fetchLevel(fetchPath); + setTree(prev => { const existingPaths = new Set(prev.map(f => f.path)); const newOnes = children.filter(c => !existingPaths.has(c.path)); return [...prev, ...newOnes]; }); } catch (err) { + console.error("Folder expansion failed:", err); setError(`Failed to open folder: ${err.message}`); + } finally { + setFolderLoading(prev => ({ ...prev, [path]: false })); } } } else { @@ -86,9 +102,8 @@ if (!name || !newItemModal) return; const { parentPath, isDir } = newItemModal; - // Ensure parentPath is correctly handled for the root let fullPath; - if (parentPath === "." || parentPath === "" || !parentPath) { + if (!parentPath || parentPath === "." || parentPath === "" || parentPath === "/") { fullPath = name; } else { fullPath = `${parentPath}/${name}`; @@ -99,7 +114,7 @@ try { await nodeFsTouch(nodeId, fullPath, "", isDir); setNewItemModal(null); - setTimeout(loadRoot, 500); // Small delay to let gRPC propagate + setTimeout(loadRoot, 500); } catch (err) { setError(`Failed to create: ${err.message}`); } finally { @@ -151,29 +166,32 @@ }; // Helper to render tree recursively - // Helper to render tree recursively const renderSubTree = (currentPath, depth = 0) => { // Normalize currentPath for filtering + const normCurrent = (currentPath === "/" || currentPath === "." || currentPath === "") + ? "" + : (currentPath.startsWith("/") ? currentPath.slice(1) : currentPath); + const children = tree.filter(node => { - // Special handling for root "/" - if (currentPath === "/" || currentPath === initialPath) { - // If it's a top-level node, it shouldn't contain more slashes than the root path - const rel = node.path.startsWith("/") ? node.path.slice(1) : node.path; - return !rel.includes("/"); + const nodePath = node.path.startsWith("/") ? node.path.slice(1) : node.path; + + if (normCurrent === "") { + // Root level: paths without slashes + return !nodePath.includes("/"); } - // For nested folders, a child is anything that starts with "currentPath/" and has no further slashes - const prefix = currentPath.endsWith("/") ? currentPath : currentPath + "/"; - if (!node.path.startsWith(prefix)) return false; - if (node.path === currentPath) return false; + // Nested level: starts with "parent/" and has no further slashes + const prefix = normCurrent.endsWith("/") ? normCurrent : normCurrent + "/"; + if (!nodePath.startsWith(prefix)) return false; + if (nodePath === normCurrent) return false; - const sub = node.path.slice(prefix.length); + const sub = nodePath.slice(prefix.length); return !sub.includes("/"); }); // Sort: Folders first, then Alphabetical const sorted = [...children].sort((a, b) => { - if (a.is_dir !== b.is_dir) return b.is_dir ? -1 : 1; + if (a.is_dir !== b.is_dir) return a.is_dir ? -1 : 1; return a.name.localeCompare(b.name); }); @@ -184,7 +202,9 @@ className="mr-2 text-gray-400" onClick={() => node.is_dir ? toggleFolder(node.path) : handleView(node.path)} > - {node.is_dir ? ( + {folderLoading[node.path] ? ( +
+ ) : node.is_dir ? ( ) : (