diff --git a/app/src/components/Layout.tsx b/app/src/components/Layout.tsx
index e369642..e5d29e6 100644
--- a/app/src/components/Layout.tsx
+++ b/app/src/components/Layout.tsx
@@ -1,6 +1,8 @@
import { useEffect, useState } from "react"
import { Link, NavLink, Outlet } from "react-router-dom"
import {
+ Boxes,
+ ChevronDown,
ChevronLeft,
ChevronRight,
Clock,
@@ -12,46 +14,121 @@ import {
Share2,
Wrench,
X,
+ type LucideIcon,
} from "lucide-react"
import { cn } from "@/lib/utils"
import { useEventStream } from "@/services/api/hooks"
-const NAV = [
+type NavLeaf = { to: string; label: string; icon: LucideIcon; end?: boolean }
+type NavGroup = { label: string; icon: LucideIcon; children: NavLeaf[] }
+
+// Services, Scheduled, and Tools are all deployment lenses — grouped under a
+// "Deployments" parent. Programs (the catalog) stays top-level.
+const NAV: (NavLeaf | NavGroup)[] = [
{ to: "/", label: "Overview", icon: LayoutDashboard, end: true },
- { to: "/gateway", label: "Gateway", icon: Globe, end: false },
- { to: "/services", label: "Services", icon: Server, end: false },
- { to: "/scheduled", label: "Scheduled", icon: Clock, end: false },
- { to: "/tools", label: "Tools", icon: Wrench, end: false },
- { to: "/programs", label: "Programs", icon: Package, end: false },
- { to: "/mesh", label: "Mesh", icon: Share2, end: false },
+ { to: "/gateway", label: "Gateway", icon: Globe },
+ {
+ label: "Deployments",
+ icon: Boxes,
+ children: [
+ { to: "/services", label: "Services", icon: Server },
+ { to: "/scheduled", label: "Scheduled", icon: Clock },
+ { to: "/tools", label: "Tools", icon: Wrench },
+ ],
+ },
+ { to: "/programs", label: "Programs", icon: Package },
+ { to: "/mesh", label: "Mesh", icon: Share2 },
]
const COLLAPSE_KEY = "castle-nav-collapsed"
+function NavLeafLink({
+ leaf,
+ collapsed,
+ indent,
+ onNavigate,
+}: {
+ leaf: NavLeaf
+ collapsed: boolean
+ indent?: boolean
+ onNavigate?: () => void
+}) {
+ const Icon = leaf.icon
+ return (
+
+ cn(
+ "flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors",
+ collapsed && "justify-center px-0",
+ indent && !collapsed && "pl-9",
+ isActive
+ ? "bg-[var(--primary)]/10 text-[var(--foreground)] font-medium"
+ : "text-[var(--muted)] hover:text-[var(--foreground)] hover:bg-[var(--card)]",
+ )
+ }
+ >
+
+ {!collapsed && {leaf.label}}
+
+ )
+}
+
+function NavGroupItem({
+ group,
+ collapsed,
+ onNavigate,
+}: {
+ group: NavGroup
+ collapsed: boolean
+ onNavigate?: () => void
+}) {
+ const [open, setOpen] = useState(true)
+ // On the icon rail there's no room for a group header — show the children flat.
+ if (collapsed) {
+ return (
+ <>
+ {group.children.map((c) => (
+
+ ))}
+ >
+ )
+ }
+ const Icon = group.icon
+ return (
+
+
+ {open && (
+
+ {group.children.map((c) => (
+
+ ))}
+
+ )}
+
+ )
+}
+
function NavItems({ collapsed, onNavigate }: { collapsed: boolean; onNavigate?: () => void }) {
return (
)
}
diff --git a/app/src/components/detail/ConfigPanel.tsx b/app/src/components/detail/ConfigPanel.tsx
index 19ab21a..26ea2a2 100644
--- a/app/src/components/detail/ConfigPanel.tsx
+++ b/app/src/components/detail/ConfigPanel.tsx
@@ -3,14 +3,22 @@ import { useNavigate } from "react-router-dom"
import { useQueryClient } from "@tanstack/react-query"
import { Check, RefreshCw } from "lucide-react"
import { apiClient } from "@/services/api/client"
-import type { AnyDetail, ProgramDetail, ServiceDetail, JobDetail } from "@/types"
+import type {
+ AnyDetail,
+ DeploymentDetail,
+ ProgramDetail,
+ ServiceDetail,
+ JobDetail,
+} from "@/types"
import { ProgramFields } from "./ProgramFields"
import { ServiceFields } from "./ServiceFields"
import { JobFields } from "./JobFields"
+import { ToolFields } from "./ToolFields"
+import { StaticFields } from "./StaticFields"
interface ConfigPanelProps {
- deployment: AnyDetail
- configSection: "services" | "jobs" | "programs"
+ deployment: AnyDetail | DeploymentDetail
+ configSection: "services" | "jobs" | "programs" | "tools" | "static"
onRefetch: () => void
}
@@ -139,6 +147,18 @@ export function ConfigPanel({ deployment, configSection, onRefetch }: ConfigPane
onSave={handleSave}
onDelete={handleDelete}
/>
+ ) : configSection === "tools" ? (
+
+ ) : configSection === "static" ? (
+
) : (
d.kind === "tool" || d.kind === "static")
- const linked = deployments.filter((d) => d.kind === "service" || d.kind === "job")
+ const detailPath = (name: string, kind: string) =>
+ kind === "tool" ? `/tools/${name}` : kind === "job" ? `/jobs/${name}` : `/services/${name}`
return (
- Deployment
+ Deployments