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) => {