diff --git a/client/src/components/ProjectsSidebar.tsx b/client/src/components/ProjectsSidebar.tsx index 7e3d10b..b002a12 100644 --- a/client/src/components/ProjectsSidebar.tsx +++ b/client/src/components/ProjectsSidebar.tsx @@ -15,6 +15,7 @@ import { Wrench, RefreshCw, ClipboardList, + AlertCircle, } from 'lucide-react'; import { useApp } from '../hooks/useApp'; import { useNotifications } from '../context/NotificationContext'; @@ -38,7 +39,8 @@ export function ProjectsSidebar({ isCollapsed = false, onExpand, }: ProjectsSidebarProps) { - const { projects, repositories, handleDeleteRepository } = useApp(); + const { projects, repositories, handleDeleteRepository, loading, error, refreshData } = + useApp(); const { showNotification } = useNotifications(); const navigate = useNavigate(); const router = useRouterState(); @@ -386,6 +388,23 @@ export function ProjectsSidebar({ + {error && !loading && ( +
+ +
+

Couldn't reach the server to load your projects.

+ +
+
+ )} +
{projects.map((project) => { const projectRepos = projectRepoMap.get(project.id) || []; diff --git a/client/src/context/AppContext.tsx b/client/src/context/AppContext.tsx index db49f22..63879e6 100644 --- a/client/src/context/AppContext.tsx +++ b/client/src/context/AppContext.tsx @@ -12,29 +12,55 @@ import { } from '../api'; import { AppContext } from './context'; +// The client and server start as separate containers with no readiness +// handshake between them - on a stack restart the client's first request can +// beat the server to accepting connections. Retry the initial load a few +// times before surfacing an error, so a normal restart doesn't strand the +// user on an empty project list. +const INITIAL_LOAD_MAX_ATTEMPTS = 4; +const INITIAL_LOAD_RETRY_DELAY_MS = 1000; + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + export function AppProvider({ children }: { children: ReactNode }) { const [projects, setProjects] = useState([]); const [repositories, setRepositories] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const refreshData = async () => { + const loadData = async (): Promise => { try { - setLoading(true); setError(null); const [projectList, repoList] = await Promise.all([getProjects(), getRepositories()]); setProjects(projectList); setRepositories(repoList); + return true; } catch (err) { setError('Failed to load data'); console.error('Failed to load data:', err); - } finally { - setLoading(false); + return false; } }; + const refreshData = async () => { + setLoading(true); + await loadData(); + setLoading(false); + }; + useEffect(() => { - refreshData(); + const loadWithRetry = async () => { + setLoading(true); + for (let attempt = 1; attempt <= INITIAL_LOAD_MAX_ATTEMPTS; attempt++) { + const succeeded = await loadData(); + if (succeeded) break; + if (attempt < INITIAL_LOAD_MAX_ATTEMPTS) { + await sleep(INITIAL_LOAD_RETRY_DELAY_MS * attempt); + } + } + setLoading(false); + }; + loadWithRetry(); }, []); const handleAddProject = async (name: string, description?: string) => {