From d8f220d825a17c7b411c1ffadf8862aab9c59151 Mon Sep 17 00:00:00 2001 From: Alex Blank Date: Sun, 24 May 2026 19:33:38 +0000 Subject: [PATCH 1/2] fix: unify session loading states across pages - Add per-item busy overlay to SessionCard component - Remove full-screen loading overlay from sessions page - Remove loadingAction state, use per-item busy state only - Add handleStart to sessions page for consistency - Add session-card CSS for busy overlay positioning - Both home and sessions pages now use same per-item loading pattern --- apps/web/src/components/session-card.tsx | 7 ++++- apps/web/src/pages/sessions.tsx | 33 +++++++++++------------- apps/web/src/styles.css | 19 ++++++++++++++ 3 files changed, 40 insertions(+), 19 deletions(-) diff --git a/apps/web/src/components/session-card.tsx b/apps/web/src/components/session-card.tsx index 00aa0c3..b2ac9dc 100644 --- a/apps/web/src/components/session-card.tsx +++ b/apps/web/src/components/session-card.tsx @@ -75,7 +75,12 @@ export function SessionCard({ const isActive = ["running", "building", "starting", "probing", "pending", "unhealthy"].includes(session.status); return ( -
+
+ {isBusy && ( +
+ +
+ )}
diff --git a/apps/web/src/pages/sessions.tsx b/apps/web/src/pages/sessions.tsx index 11dba2a..3933fae 100644 --- a/apps/web/src/pages/sessions.tsx +++ b/apps/web/src/pages/sessions.tsx @@ -9,6 +9,7 @@ import { type Session, deleteInstance, stopInstance, + startInstance, checkInstanceHealth, recreateInstanceTunnel, } from "../api/sessions"; @@ -38,7 +39,6 @@ export const SessionsPage = () => { const [tunnelHealth, setTunnelHealth] = useState>({}); const [loadingSessionId, setLoadingSessionId] = useState(null); - const [loadingAction, setLoadingAction] = useState(""); const loadSessions = useCallback(async () => { setStatus("loading"); @@ -156,7 +156,6 @@ export const SessionsPage = () => { const handleStop = async (session: Session) => { setLoadingSessionId(session.id); - setLoadingAction("Stopping..."); try { await stopInstance(session.project_id, session.repository_id, session.id); await loadSessions(); @@ -164,13 +163,11 @@ export const SessionsPage = () => { // ignore } finally { setLoadingSessionId(null); - setLoadingAction(""); } }; const handleDelete = async (session: Session) => { setLoadingSessionId(session.id); - setLoadingAction("Deleting..."); try { await deleteInstance(session.project_id, session.repository_id, session.id); setDirtyDeleteSession(null); @@ -189,13 +186,11 @@ export const SessionsPage = () => { } } finally { setLoadingSessionId(null); - setLoadingAction(""); } }; const handleForceDelete = async (session: Session) => { setLoadingSessionId(session.id); - setLoadingAction("Force deleting..."); try { await deleteInstance(session.project_id, session.repository_id, session.id, true); setDirtyDeleteSession(null); @@ -205,13 +200,11 @@ export const SessionsPage = () => { // ignore } finally { setLoadingSessionId(null); - setLoadingAction(""); } }; const handleRecreateTunnel = async (session: Session) => { setLoadingSessionId(session.id); - setLoadingAction("Recreating tunnel..."); try { await recreateInstanceTunnel( session.project_id, @@ -224,7 +217,18 @@ export const SessionsPage = () => { // ignore } finally { setLoadingSessionId(null); - setLoadingAction(""); + } + }; + + const handleStart = async (session: Session) => { + setLoadingSessionId(session.id); + try { + await startInstance(session.project_id, session.repository_id, session.id); + await loadSessions(); + } catch { + // ignore + } finally { + setLoadingSessionId(null); } }; @@ -273,18 +277,11 @@ export const SessionsPage = () => { )} {/* Session List */} -
- {loadingSessionId && ( -
-
- -

{loadingAction}

-
-
- )} +
Date: Sun, 24 May 2026 19:40:42 +0000 Subject: [PATCH 2/2] fix: prevent duplicate session action requests causing 404s - Add early return guards in all session action handlers (start, stop, delete, recreate tunnel) - Prevents race conditions where double-clicks or rapid clicks fire duplicate API calls - First delete succeeds, second would 404 because instance is already deleted - Applied to both sessions page and dashboard/home page --- apps/web/src/pages/dashboard.tsx | 4 ++++ apps/web/src/pages/sessions.tsx | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/apps/web/src/pages/dashboard.tsx b/apps/web/src/pages/dashboard.tsx index 3071d1f..1b6d99e 100644 --- a/apps/web/src/pages/dashboard.tsx +++ b/apps/web/src/pages/dashboard.tsx @@ -143,6 +143,7 @@ export const HomePage = () => { }; const handleStop = async (session: SessionView) => { + if (actionBusy === session.id) return; setActionBusy(session.id); try { await stopInstance(session.project_id, session.repository_id, session.id); @@ -153,6 +154,7 @@ export const HomePage = () => { }; const handleDelete = async (session: SessionView) => { + if (actionBusy === session.id) return; setActionBusy(session.id); try { await deleteInstance(session.project_id, session.repository_id, session.id); @@ -165,6 +167,7 @@ export const HomePage = () => { }; const handleRecreateTunnel = async (session: SessionView) => { + if (actionBusy === session.id) return; setActionBusy(session.id); try { await recreateInstanceTunnel(session.project_id, session.repository_id, session.id); @@ -175,6 +178,7 @@ export const HomePage = () => { }; const handleStart = async (session: SessionView) => { + if (actionBusy === session.id) return; setActionBusy(session.id); try { await startInstance(session.project_id, session.repository_id, session.id); diff --git a/apps/web/src/pages/sessions.tsx b/apps/web/src/pages/sessions.tsx index 3933fae..22ac603 100644 --- a/apps/web/src/pages/sessions.tsx +++ b/apps/web/src/pages/sessions.tsx @@ -155,6 +155,7 @@ export const SessionsPage = () => { }; const handleStop = async (session: Session) => { + if (loadingSessionId === session.id) return; setLoadingSessionId(session.id); try { await stopInstance(session.project_id, session.repository_id, session.id); @@ -167,6 +168,7 @@ export const SessionsPage = () => { }; const handleDelete = async (session: Session) => { + if (loadingSessionId === session.id) return; setLoadingSessionId(session.id); try { await deleteInstance(session.project_id, session.repository_id, session.id); @@ -190,6 +192,7 @@ export const SessionsPage = () => { }; const handleForceDelete = async (session: Session) => { + if (loadingSessionId === session.id) return; setLoadingSessionId(session.id); try { await deleteInstance(session.project_id, session.repository_id, session.id, true); @@ -204,6 +207,7 @@ export const SessionsPage = () => { }; const handleRecreateTunnel = async (session: Session) => { + if (loadingSessionId === session.id) return; setLoadingSessionId(session.id); try { await recreateInstanceTunnel( @@ -221,6 +225,7 @@ export const SessionsPage = () => { }; const handleStart = async (session: Session) => { + if (loadingSessionId === session.id) return; setLoadingSessionId(session.id); try { await startInstance(session.project_id, session.repository_id, session.id);