Dashboard: kind filters on Programs, tools off Services page, in-app confirm modals

- /services was returning every non-job deployment (tools, statics, remotes);
  filter it to kind==service so the Services page shows only services.
- Programs page gains kind-filter chips (All / Service / Job / Tool / Static /
  Reference) with per-kind counts, colored to match KindBadge.
- Replace window.confirm with a reusable ConfirmModal (styled, danger variant,
  Esc/Enter, preserves bulleted bodies); FormFooter's delete uses it, so program/
  service/job deletion now confirms in-app with the enumerated cascade list.

castle-api 58 passed; /services live drops from 37 to 15 (services only).
This commit is contained in:
2026-07-01 11:59:14 -07:00
parent 38074806c1
commit 876b424722
7 changed files with 200 additions and 22 deletions

View File

@@ -405,9 +405,9 @@ def list_services(include_remote: bool = False) -> list[ServiceSummary]:
summaries: list[ServiceSummary] = []
seen: set[str] = set()
# Deployed services (non-scheduled)
# Deployed services only — not jobs, tools (path), statics (caddy), or remotes.
for name, deployed in registry.deployed.items():
if deployed.schedule:
if deployed.kind != "service":
continue
s = _service_from_deployed(name, deployed)
s.node = hostname

View File

@@ -124,6 +124,13 @@ def registry_path(tmp_path: Path, castle_root: Path) -> Generator[Path, None, No
subdomain="test-svc",
managed=True,
),
# A deployed tool (path) — must NOT leak into the /services list.
"test-tool": Deployment(
manager="path",
run_cmd=[],
description="Test tool",
kind="tool",
),
},
)
save_registry(registry, reg_path)

View File

@@ -113,6 +113,13 @@ class TestServicesList:
names = [s["id"] for s in data]
assert "test-job" not in names
def test_excludes_tools(self, client: TestClient) -> None:
"""Tools (path deployments) are not in the services list."""
response = client.get("/services")
data = response.json()
names = [s["id"] for s in data]
assert "test-tool" not in names
class TestServiceDetail:
"""GET /services/{name} endpoint tests."""
@@ -246,8 +253,9 @@ class TestGateway:
data = response.json()
assert data["port"] == 9000
assert data["hostname"] == "test-node"
# Registry has 1 deployed component (test-svc)
assert data["deployment_count"] == 1
# Registry has 2 deployed components (test-svc + test-tool); only the
# service is a managed systemd deployment.
assert data["deployment_count"] == 2
assert data["service_count"] == 1
assert data["managed_count"] == 1

View File

@@ -28,8 +28,8 @@ class TestNodesList:
response = client.get("/nodes")
data = response.json()
local = data[0]
assert local["deployed_count"] == 1 # test-svc
assert local["service_count"] == 1
assert local["deployed_count"] == 2 # test-svc + test-tool
assert local["service_count"] == 1 # only test-svc is a service
def test_includes_remote_nodes(self, client: TestClient, registry_path: Path) -> None:
"""Remote nodes from mesh state are included."""
@@ -81,9 +81,9 @@ class TestNodeDetail:
data = response.json()
assert data["hostname"] == "test-node"
assert data["is_local"] is True
assert len(data["deployed"]) == 1
assert data["deployed"][0]["id"] == "test-svc"
assert data["deployed"][0]["node"] == "test-node"
assert len(data["deployed"]) == 2 # test-svc + test-tool
svc = next(d for d in data["deployed"] if d["id"] == "test-svc")
assert svc["node"] == "test-node"
def test_unknown_node_returns_404(self, client: TestClient) -> None:
"""Returns 404 for unknown hostname."""