-
+
{filtered.length === 0 ? (
@@ -43,3 +84,29 @@ export function ProgramList({ programs }: ProgramListProps) {
)
}
+
+function Chip({
+ label,
+ active,
+ activeClass,
+ onClick,
+}: {
+ label: string
+ active: boolean
+ activeClass: string
+ onClick: () => void
+}) {
+ return (
+
+ )
+}
diff --git a/app/src/components/detail/fields.tsx b/app/src/components/detail/fields.tsx
index a534cbd..1382ab9 100644
--- a/app/src/components/detail/fields.tsx
+++ b/app/src/components/detail/fields.tsx
@@ -1,6 +1,7 @@
import { useMemo, useState } from "react"
import { Trash2 } from "lucide-react"
import { SecretsEditor } from "@/components/SecretsEditor"
+import { ConfirmModal } from "@/components/ConfirmModal"
const INPUT =
"bg-black/30 border border-[var(--border)] rounded px-3 py-1.5 text-sm focus:outline-none focus:border-[var(--primary)]"
@@ -185,15 +186,14 @@ export function FormFooter({
/** When set, removal is disallowed and this reason is shown instead of the button. */
deleteBlocked?: string
}) {
+ const [confirmOpen, setConfirmOpen] = useState(false)
return (
{deleteBlocked ? (
{deleteBlocked}
) : onDelete ? (
+ {onDelete && (
+ {
+ setConfirmOpen(false)
+ onDelete()
+ }}
+ onCancel={() => setConfirmOpen(false)}
+ />
+ )}
)
}
diff --git a/castle-api/src/castle_api/routes.py b/castle-api/src/castle_api/routes.py
index 4d6bb7b..fc9ad31 100644
--- a/castle-api/src/castle_api/routes.py
+++ b/castle-api/src/castle_api/routes.py
@@ -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
diff --git a/castle-api/tests/conftest.py b/castle-api/tests/conftest.py
index 4c34a70..907ca96 100644
--- a/castle-api/tests/conftest.py
+++ b/castle-api/tests/conftest.py
@@ -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)
diff --git a/castle-api/tests/test_health.py b/castle-api/tests/test_health.py
index 2b67ccc..230bb5f 100644
--- a/castle-api/tests/test_health.py
+++ b/castle-api/tests/test_health.py
@@ -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
diff --git a/castle-api/tests/test_nodes.py b/castle-api/tests/test_nodes.py
index 24ad5e8..adb8f99 100644
--- a/castle-api/tests/test_nodes.py
+++ b/castle-api/tests/test_nodes.py
@@ -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."""