refactor: Decouple roles.

This commit is contained in:
2026-02-23 01:49:24 -08:00
parent 72d35f2641
commit eeaa5045d0
55 changed files with 2144 additions and 1276 deletions

View File

@@ -20,7 +20,7 @@ export function ComponentDetailPage() {
const { mutate, isPending } = useServiceAction()
const health = statusResp?.statuses.find((s) => s.id === name)
const isDown = health?.status === "down"
const isTool = component?.roles.includes("tool") ?? false
const isTool = component?.category === "tool"
const { data: toolDetail } = useToolDetail(isTool ? (name ?? "") : "")
const isGateway = name === "castle-gateway"
const { data: caddyfile } = useCaddyfile(isGateway)
@@ -28,10 +28,14 @@ export function ComponentDetailPage() {
const { data: unitData } = useSystemdUnit(name ?? "", showUnit && !!component?.systemd)
const [message, setMessage] = useState<{ type: "ok" | "error"; text: string } | null>(null)
const configSection = component?.category === "service" ? "services"
: component?.category === "job" ? "jobs"
: "components"
const handleSave = async (compName: string, config: Record<string, unknown>) => {
setMessage(null)
try {
await apiClient.put(`/config/components/${compName}`, { config })
await apiClient.put(`/config/${configSection}/${compName}`, { config })
setMessage({ type: "ok", text: "Saved to castle.yaml" })
refetch()
qc.invalidateQueries({ queryKey: ["components"] })
@@ -43,7 +47,7 @@ export function ComponentDetailPage() {
const handleDelete = async (compName: string) => {
try {
await apiClient.delete(`/config/components/${compName}`)
await apiClient.delete(`/config/${configSection}/${compName}`)
qc.invalidateQueries({ queryKey: ["components"] })
navigate("/")
} catch (e: unknown) {
@@ -116,10 +120,11 @@ export function ComponentDetailPage() {
</div>
</div>
<div className="flex gap-1.5 mb-6">
{component.roles.map((role) => (
<RoleBadge key={role} role={role} />
))}
<div className="flex items-center gap-3 mb-6">
<RoleBadge role={component.category} />
{component.source && (
<span className="text-sm text-[var(--muted)] font-mono">{component.source}</span>
)}
</div>
{message && (

View File

@@ -1,5 +1,10 @@
import { ComponentTable } from "@/components/ComponentTable"
import { useMemo } from "react"
import { Link } from "react-router-dom"
import { useComponents, useStatus, useGateway, useEventStream } from "@/services/api/hooks"
import { ServiceSection } from "@/components/ServiceSection"
import { JobSection } from "@/components/JobSection"
import { ToolSection } from "@/components/ToolSection"
import { SectionHeader } from "@/components/SectionHeader"
export function Dashboard() {
useEventStream()
@@ -7,6 +12,20 @@ export function Dashboard() {
const { data: statusResp } = useStatus()
const { data: gateway } = useGateway()
const { services, jobs, tools, frontends, other } = useMemo(() => {
const s = { services: [] as typeof components, jobs: [] as typeof components, tools: [] as typeof components, frontends: [] as typeof components, other: [] as typeof components }
for (const c of components ?? []) {
if (c.category === "service") s.services!.push(c)
else if (c.category === "job") s.jobs!.push(c)
else if (c.category === "tool") s.tools!.push(c)
else if (c.category === "frontend") s.frontends!.push(c)
else s.other!.push(c)
}
return { services: s.services!, jobs: s.jobs!, tools: s.tools!, frontends: s.frontends!, other: s.other! }
}, [components])
const statuses = statusResp?.statuses ?? []
return (
<div className="max-w-6xl mx-auto px-6 py-8">
<div className="mb-8">
@@ -15,21 +34,79 @@ export function Dashboard() {
Personal software platform
{gateway && (
<span className="ml-2 text-sm">
&middot; {gateway.component_count} components &middot; port {gateway.port}
&middot; {services.length} services &middot; {jobs.length} jobs
&middot; {tools.length} tools &middot; port {gateway.port}
</span>
)}
</p>
</div>
{isLoading ? (
<p className="text-[var(--muted)]">Loading components...</p>
) : components ? (
<ComponentTable
components={components}
statuses={statusResp?.statuses ?? []}
/>
<p className="text-[var(--muted)]">Loading...</p>
) : (
<p className="text-red-400">Failed to load components</p>
<div className="space-y-10">
{services.length > 0 && (
<ServiceSection services={services} statuses={statuses} />
)}
{jobs.length > 0 && (
<JobSection jobs={jobs} />
)}
{tools.length > 0 && (
<ToolSection tools={tools} />
)}
{frontends.length > 0 && (
<section>
<SectionHeader category="frontend" />
<div className="border border-[var(--border)] rounded-lg overflow-hidden">
<table className="w-full text-sm">
<tbody>
{frontends.map((fe) => (
<tr key={fe.id} className="border-b border-[var(--border)] last:border-b-0 hover:bg-[var(--card)]/50 transition-colors">
<td className="px-3 py-2.5">
<Link
to={`/component/${fe.id}`}
className="font-medium hover:text-[var(--primary)] transition-colors"
>
{fe.id}
</Link>
</td>
<td className="px-3 py-2.5 text-[var(--muted)]">
{fe.description ?? "—"}
</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
)}
{other.length > 0 && (
<section>
<SectionHeader category="component" />
<div className="border border-[var(--border)] rounded-lg overflow-hidden">
<table className="w-full text-sm">
<tbody>
{other.map((c) => (
<tr key={c.id} className="border-b border-[var(--border)] last:border-b-0 hover:bg-[var(--card)]/50 transition-colors">
<td className="px-3 py-2.5">
<Link
to={`/component/${c.id}`}
className="font-medium hover:text-[var(--primary)] transition-colors"
>
{c.id}
</Link>
</td>
<td className="px-3 py-2.5 text-[var(--muted)]">
{c.description ?? "—"}
</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
)}
</div>
)}
</div>
)