diff --git a/backend/agent_app.py b/backend/agent_app.py index cbf5a13..9a0ff9f 100644 --- a/backend/agent_app.py +++ b/backend/agent_app.py @@ -374,6 +374,8 @@ async def lifecycle(stack_id: str, action: str) -> dict: "detail": result.get("stderr", "").strip()[-2000:], }, ) + if action in ("pull", "update"): + update_service.refresh_stack_local(stack_id) return result diff --git a/backend/routers/stacks.py b/backend/routers/stacks.py index 164f654..019b720 100644 --- a/backend/routers/stacks.py +++ b/backend/routers/stacks.py @@ -329,12 +329,16 @@ async def restart_stack(stack_id: str, request: Request, session: Session = Depe @router.post("/{stack_id}/pull") async def pull_stack(stack_id: str, request: Request, session: Session = Depends(get_session), user: User = Depends(require_admin)): - return await _lifecycle(compose_service.pull, "pull", stack_id, request, session, user) + result = await _lifecycle(compose_service.pull, "pull", stack_id, request, session, user) + update_service.refresh_stack_local(stack_id) + return result @router.post("/{stack_id}/update") async def update_stack_images(stack_id: str, request: Request, session: Session = Depends(get_session), user: User = Depends(require_admin)): - return await _lifecycle(compose_service.update, "update", stack_id, request, session, user) + result = await _lifecycle(compose_service.update, "update", stack_id, request, session, user) + update_service.refresh_stack_local(stack_id) + return result @router.post("/{stack_id}/down") diff --git a/backend/services/auto_update_service.py b/backend/services/auto_update_service.py index a67fd0b..a00032e 100644 --- a/backend/services/auto_update_service.py +++ b/backend/services/auto_update_service.py @@ -110,6 +110,7 @@ async def _run_local(session: Session, policy: AutoUpdate) -> None: _record(session, policy, "error", str(exc)) await _safe_notify(EVENT_PULL_FAILED, f"Auto-update of '{stack_id}' failed", str(exc), session) return + update_service.refresh_stack_local(stack_id) _record(session, policy, "updated", stale) await _safe_notify( EVENT_STACK_AUTO_UPDATED, f"Stack '{stack_id}' auto-updated", diff --git a/backend/services/update_service.py b/backend/services/update_service.py index 447f85b..a4fc952 100644 --- a/backend/services/update_service.py +++ b/backend/services/update_service.py @@ -269,6 +269,25 @@ async def stack_updates(stack_id: str, refresh: bool = True) -> dict: } +def refresh_stack_local(stack_id: str) -> None: + """Re-read the local digests of one stack's images and reconcile them with + the cached remote digests (no registry calls). Called right after a manual + pull/update so the amber indicator clears immediately instead of lingering + until the next background pass.""" + for image in stack_images(stack_id): + status = _CACHE.get(image) + if status is None: + continue + local = _local_digest(image) + status.current_digest = local + status.update_available = bool( + local and status.remote_digest and local != status.remote_digest + ) + status.checked_at = time.time() + if not status.update_available: + _NOTIFIED.discard(image) + + async def check_all() -> dict[str, dict]: images = _all_running_images() for image in images: diff --git a/backend/version.py b/backend/version.py index 68f2105..df9ec5b 100644 --- a/backend/version.py +++ b/backend/version.py @@ -1,3 +1,3 @@ """Single source of truth for the StackPilot release version.""" -APP_VERSION = "0.38.3" +APP_VERSION = "0.38.4" diff --git a/frontend/src/components/stacks/AgentStacksSection.tsx b/frontend/src/components/stacks/AgentStacksSection.tsx index 3c2c5e2..74801c6 100644 --- a/frontend/src/components/stacks/AgentStacksSection.tsx +++ b/frontend/src/components/stacks/AgentStacksSection.tsx @@ -47,6 +47,7 @@ export function AgentStacksSection({ agent, isAdmin }: { agent: Agent; isAdmin: await agentsApi.action(agent.id, id, action); toast.success(`${label} ${id} ✓`, { id: t }); qc.invalidateQueries({ queryKey: ["agent-stacks", agent.id] }); + qc.invalidateQueries({ queryKey: ["agent-stack-updates", agent.id] }); } catch (e) { toast.error(apiErrorMessage(e), { id: t }); } finally { diff --git a/frontend/src/hooks/useStackActions.ts b/frontend/src/hooks/useStackActions.ts index 0e6b377..070a868 100644 --- a/frontend/src/hooks/useStackActions.ts +++ b/frontend/src/hooks/useStackActions.ts @@ -20,6 +20,7 @@ export function useStackActions() { toast.success(`${label} ${id} ✓`, { id: t }); qc.invalidateQueries({ queryKey: ["stacks"] }); qc.invalidateQueries({ queryKey: ["stack", id] }); + qc.invalidateQueries({ queryKey: ["stack-updates"] }); } catch (err) { toast.error(apiErrorMessage(err), { id: t }); } finally {