From 0175a995b3ce3f578aeadb67f99ffb7b7e5a1c69 Mon Sep 17 00:00:00 2001 From: ErickHub192 Date: Wed, 5 Aug 2026 13:08:55 -0600 Subject: [PATCH] El spinner se quedaba pegado encima del preview listo Bug que introduje con el spinner de etapas. El server decia "preview listo en 8.6s" en el log, pero la Sala seguia girando para siempre. La causa: el join lee previewArrancando y ponia el spinner sin mirar si YA habia preview. La sala se despierta sola al primer request, asi que el arranque puede terminar ANTES de que entres: para entonces el preview:ready ya paso y no vuelve, y el spinner se quedaba encima de un preview que si existia. Ahora solo se pone si no hay previewUrl. Segundo hueco del mismo cambio: si el arranque terminaba sin preview (sala vacia, o algo fallo) no se emitia nada, asi que quien hubiera visto una etapa esperaba un ready que nunca llegaba. Se agrega preview:sin-arranque, que quita el spinner y devuelve el mensaje de "pidele a un agente que arranque el proyecto", que es lo accionable. Y queda anotado por que bootPreview no emite: corre al despertar la sala, cuando no hay nadie conectado a quien avisar. Para ese caso el canal es el previewUrl del joined. --- server/src/index.ts | 10 +++++++++- server/src/rooms.ts | 4 ++++ web/src/App.tsx | 11 ++++++++++- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/server/src/index.ts b/server/src/index.ts index 3ccaa70..0f4bfde 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -840,7 +840,15 @@ async function notifyPreviewWhenReady(room: Room): Promise { // como una pantalla muerta. io.to(room.id).emit("preview:arrancando", { etapa }); }); - if (url) io.to(room.id).emit("preview:ready", { previewUrl: url }); + if (url) { + io.to(room.id).emit("preview:ready", { previewUrl: url }); + return; + } + + // Sin URL: o la sala sigue vacía (normal, no hay nada que levantar) o el + // arranque falló. En ambos casos hay que avisar, porque quien vio una etapa se + // quedaría con el spinner girando para siempre esperando un ready que no llega. + io.to(room.id).emit("preview:sin-arranque"); } // ── Arranque ──────────────────────────────────────────────────────────────── diff --git a/server/src/rooms.ts b/server/src/rooms.ts index dfaf2da..cd2806f 100644 --- a/server/src/rooms.ts +++ b/server/src/rooms.ts @@ -168,6 +168,10 @@ export async function loadRoomIndex(): Promise { * Una sala recién creada está vacía: no hay nada que levantar, y eso es normal * (el agente todavía no scaffoldea). No es error ni espera indefinida — el * preview arranca solo cuando aparezca algo, vía `maybeStartPreview`. + * + * Esto NO emite nada por el socket, a propósito: corre al despertar la sala, + * cuando todavía no hay nadie conectado a quien avisar. Quien entre después + * recibe la URL en el `joined`; ese es el canal para este caso. */ async function bootPreview(room: Room): Promise { try { diff --git a/web/src/App.tsx b/web/src/App.tsx index 122515b..8bb0e3e 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -265,7 +265,12 @@ function Sala({ roomId, name }: { roomId: string; name: string }) { // El chat que ya existía en la sala (sobrevivió al reinicio). if (p.messages?.length) setMessages(p.messages); // Llegaste mientras se levantaba: el evento de etapa ya pasó. - if (p.previewArrancando) setArrancando("servidor"); + // + // Solo si NO hay preview todavía. El arranque puede haber terminado antes + // de que entraras (la sala se despierta sola al primer request), y entonces + // el `preview:ready` ya pasó y no vuelve: sin esta condición el spinner se + // quedaba girando encima de un preview que sí existía. + if (p.previewArrancando && !p.previewUrl) setArrancando("servidor"); }); socket.on("presence", ({ members }: { members: Member[] }) => setMembers(members)); socket.on("preview:ready", () => { @@ -275,6 +280,10 @@ function Sala({ roomId, name }: { roomId: string; name: string }) { socket.on("preview:arrancando", ({ etapa }: { etapa: "contenedor" | "dependencias" | "servidor" }) => setArrancando(etapa), ); + // El arranque terminó sin preview: la sala sigue vacía o algo falló. Se quita + // el spinner y vuelve el mensaje de "pídele a un agente que arranque el + // proyecto", que es lo accionable. + socket.on("preview:sin-arranque", () => setArrancando(null)); socket.on("agents", ({ agents }: { agents: Agent[] }) => setAgents(agents)); // Hay un punto nuevo en la línea de tiempo (commit, revert o bookmark). socket.on("history:new", () => setHistVersion((v) => v + 1));