Run compose like the terminal does, and follow changes made there (0.67.2)
CI / check (push) Successful in 4m48s
CI / build-and-push (push) Failing after 16m2s

StackPilot called `docker compose -p <folder> -f compose.yaml`, which is
not what `docker compose` typed in the stack's folder does. A
compose.override.yaml or a COMPOSE_FILE in .env applied in a terminal
and was dropped by the next Start in StackPilot, which recreated the
containers without it. A compose file's `name:` or a COMPOSE_PROJECT_NAME
in .env made the terminal's containers invisible: the stack showed as
stopped while running, and Start set up a second copy next to it.

Compose now runs in the stack's folder without -f, so it loads the same
files, and -p is the name compose itself picks there: COMPOSE_PROJECT_NAME
from .env, else the last `name:` among the compose files (${VAR} from
.env), else the folder name. A stack StackPilot already runs under its
folder name keeps that name while it has containers or volumes under it
and none under the declared one: switching would have brought it up
again on empty volumes. Backups record the project, and a restore takes
it from the files it has just put back.

Changes made outside StackPilot now reach every view:

- docker_watch_service reads Docker's events once for the backend. The
  dashboard rollup (cached 25s) and the stats cache are stale as soon as
  the event generation moves; the tiles followed a terminal `compose
  stop` after 15-30s, now within a second.
- Image events re-read the local digests, so a `docker compose pull` in
  a terminal clears the update badge instead of leaving it until the
  next registry check (an hour by default).
- The browser catches up when its event socket reconnects; what changed
  while it was down waited for the 60s poll.
- The stack detail carries a revision of the compose file and .env;
  saving after they changed on disk is refused with 409 and the editor
  offers to overwrite or to load the version on disk. A save used to
  replace a terminal edit without a word.
- A stack whose folder is gone is flagged "folder missing" instead of
  passing for an ordinary stopped stack.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fx7gcuy8d6nPd95g43rGHu
This commit is contained in:
menzeljandClaude Opus 5.5 committed 2026-10-15 15:14:40 +02:00
1 parent d6cebb3371
commit 06234c1067
21 files changed
+811 -45

No files matched your search

+31 -13
View File
@@ -423,6 +423,24 @@ async def stack_updates(stack_id: str, refresh: bool = True) -> dict:
}
def _reconcile_local(image: str, status: UpdateStatus) -> bool:
"""Re-read one image's local digest against the cached remote one (no
registry call). True when that changed what is stored."""
local = _local_digest(image)
available = bool(local and status.remote_digest and local != status.remote_digest)
if local == status.current_digest and available == status.update_available:
return False
status.current_digest = local
status.update_available = available
status.checked_at = time.time()
if not available:
_NOTIFIED.discard(image)
# Persisted too, or a restart brought the cleared badge back until the
# next background check.
_store(status, image in _NOTIFIED)
return True
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
@@ -430,19 +448,19 @@ def refresh_stack_local(stack_id: str) -> None:
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)
# Persisted too, or a restart brought the cleared badge back until the
# next background check.
_store(status, image in _NOTIFIED)
if status is not None and not status.local:
_reconcile_local(image, status)
def refresh_local_digests() -> int:
""":func:`refresh_stack_local` for every cached image — what an image
event triggers (services/docker_watch_service.py), so a pull made in a
terminal clears the badge too. Returns how many changed."""
return sum(
_reconcile_local(image, status)
for image, status in list(_CACHE.items())
if not status.local
)
async def check_all() -> dict[str, dict]: