refactor(dashboard): discover via docker compose ls
test / unit (pull_request) Successful in 17s
test / integration (pull_request) Successful in 1m8s

PRD 0018 chunk 5. The dashboard's operator-edit verbs
(`routes edit`, `pipelock edit`) enumerated running sidecars
via `docker ps --filter name=...` prefix scans. Switch to
`docker compose ls`-based discovery so the dashboard, cleanup
CLI, and launch step all agree on what's running.

Mechanics:

  - `claude_bottle/backend/docker/compose.py` grows three shared
    helpers: `list_compose_projects` (the JSON parse moved out
    of cleanup), `slug_from_compose_project` (inverse of
    `compose_project_name`), and `list_active_slugs` (sugar over
    the first two for the common "what's running?" question).
  - cleanup.py drops its private `_list_compose_projects` +
    `_PROJECT_PREFIX` in favor of the shared ones; `list_active`
    simplifies (one compose-ls call, not two).
  - dashboard.py's `_discover_sidecar_slugs` becomes
    `_discover_active_with_service`: cross-references the active
    slug list with a label-filtered `docker ps` so only bottles
    whose given service container is actually up surface in the
    edit menu. Bottles without an egress sidecar (no
    bottle.egress.routes) no longer appear for `routes edit`.

3 new unit tests cover the slug ↔ compose-project naming
contract; manual probe with a fake compose project confirms
both `discover_egress_slugs` and `discover_pipelock_slugs`
return the expected slug.
This commit is contained in:
2026-05-26 00:14:16 -04:00
parent 0ae544d2a6
commit 1fa3745832
4 changed files with 126 additions and 65 deletions
+25 -10
View File
@@ -27,6 +27,10 @@ from ..backend.docker.capability_apply import (
CapabilityApplyError,
apply_capability_change,
)
from ..backend.docker.compose import (
COMPOSE_PROJECT_PREFIX,
list_active_slugs,
)
from ..backend.docker.egress_apply import (
EgressApplyError,
add_route,
@@ -79,15 +83,23 @@ class QueuedProposal:
queue_dir: Path
def _discover_sidecar_slugs(name_prefix: str) -> list[str]:
"""Slugs of bottles whose sidecar container names start with
`name_prefix`. Empty list if docker isn't reachable or not
installed."""
def _discover_active_with_service(service: str) -> list[str]:
"""Slugs of bottles whose compose project is up AND has the
named service container running. PRD 0018 chunk 5 grounded the
discovery on `docker compose ls` so all the dashboard verbs
agree with the cleanup CLI about what's running. A second
`docker ps` filter narrows by service label — a bottle without
egress routes has no egress service, and the operator-edit
flow shouldn't offer it for routes editing."""
slugs = list_active_slugs()
if not slugs:
return []
try:
r = subprocess.run(
[
"docker", "ps",
"--filter", f"name=^{name_prefix}",
"--filter", f"label=com.docker.compose.service={service}",
"--filter", f"name=^{COMPOSE_PROJECT_PREFIX}{service}-",
"--format", "{{.Names}}",
],
capture_output=True, text=True, check=False,
@@ -96,24 +108,27 @@ def _discover_sidecar_slugs(name_prefix: str) -> list[str]:
return []
if r.returncode != 0:
return []
prefix = f"{COMPOSE_PROJECT_PREFIX}{service}-"
out: list[str] = []
for line in (r.stdout or "").splitlines():
line = line.strip()
if line.startswith(name_prefix):
out.append(line[len(name_prefix):])
return sorted(out)
if line.startswith(prefix):
slug = line[len(prefix):]
if slug in slugs:
out.append(slug)
return sorted(set(out))
def discover_egress_slugs() -> list[str]:
"""Slugs of bottles with a running egress sidecar. Used by
the operator-initiated `routes edit` verb."""
return _discover_sidecar_slugs("claude-bottle-egress-")
return _discover_active_with_service("egress")
def discover_pipelock_slugs() -> list[str]:
"""Slugs of bottles with a running pipelock sidecar. Used by
the operator-initiated `pipelock edit` verb."""
return _discover_sidecar_slugs("claude-bottle-pipelock-")
return _discover_active_with_service("pipelock")
def _approval_status(qp: QueuedProposal, verb: str) -> str: