feat: Enhance gateway management and add Caddyfile generation support

This commit is contained in:
2026-02-21 01:08:11 -08:00
parent 54ba2ccc62
commit f99c2dad86
16 changed files with 711 additions and 230 deletions

View File

@@ -25,7 +25,7 @@ export function ComponentCard({ component, health }: ComponentCardProps) {
<div className="bg-[var(--card)] border border-[var(--border)] rounded-lg p-5">
<div className="flex items-start justify-between mb-2">
<Link
to={`/${component.id}`}
to={`/component/${component.id}`}
className="text-base font-semibold hover:text-[var(--primary)] transition-colors"
>
{component.id}

View File

@@ -220,7 +220,7 @@ function ComponentRow({
<tr 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.id}`}
to={`/component/${component.id}`}
className="font-medium hover:text-[var(--primary)] transition-colors"
title={component.systemd?.unit_path ?? undefined}
>

View File

@@ -12,7 +12,7 @@ export function ToolCard({ tool }: ToolCardProps) {
<div className="bg-[var(--card)] border border-[var(--border)] rounded-lg p-5">
<div className="flex items-start justify-between mb-2">
<Link
to={`/${tool.id}`}
to={`/component/${tool.id}`}
className="text-base font-semibold hover:text-[var(--primary)] transition-colors"
>
{tool.id}

View File

@@ -3,7 +3,7 @@ import { useParams, Link, useNavigate } from "react-router-dom"
import { ArrowLeft, Check, Play, RefreshCw, Square } from "lucide-react"
import { useQueryClient } from "@tanstack/react-query"
import { apiClient } from "@/services/api/client"
import { useComponent, useStatus, useServiceAction, useEventStream, useToolDetail } from "@/services/api/hooks"
import { useComponent, useStatus, useServiceAction, useEventStream, useToolDetail, useCaddyfile, useSystemdUnit } from "@/services/api/hooks"
import { runnerLabel } from "@/lib/labels"
import { ComponentFields } from "@/components/ComponentFields"
import { HealthBadge } from "@/components/HealthBadge"
@@ -22,6 +22,10 @@ export function ComponentDetailPage() {
const isDown = health?.status === "down"
const isTool = component?.roles.includes("tool") ?? false
const { data: toolDetail } = useToolDetail(isTool ? (name ?? "") : "")
const isGateway = name === "gateway"
const { data: caddyfile } = useCaddyfile(isGateway)
const [showUnit, setShowUnit] = useState(false)
const { data: unitData } = useSystemdUnit(name ?? "", showUnit && !!component?.systemd)
const [message, setMessage] = useState<{ type: "ok" | "error"; text: string } | null>(null)
const handleSave = async (compName: string, config: Record<string, unknown>) => {
@@ -189,9 +193,17 @@ export function ComponentDetailPage() {
{component.systemd && (
<div className="bg-[var(--card)] border border-[var(--border)] rounded-lg p-5 mb-6">
<h2 className="text-sm font-semibold text-[var(--muted)] uppercase tracking-wider mb-1">
Systemd
</h2>
<div className="flex items-center justify-between mb-1">
<h2 className="text-sm font-semibold text-[var(--muted)] uppercase tracking-wider">
Systemd
</h2>
<button
onClick={() => setShowUnit((v) => !v)}
className="text-xs text-[var(--primary)] hover:underline"
>
{showUnit ? "Hide unit file" : "View unit file"}
</button>
</div>
<div className="grid grid-cols-2 gap-x-6 gap-y-2 text-sm mt-3">
<span className="text-[var(--muted)]">Unit</span>
<span className="font-mono">{component.systemd.unit_name}</span>
@@ -204,6 +216,40 @@ export function ComponentDetailPage() {
</>
)}
</div>
{showUnit && unitData && (
<div className="mt-4 space-y-3">
<div>
<span className="text-xs text-[var(--muted)] block mb-1">{component.systemd.unit_name}</span>
<pre className="text-sm whitespace-pre-wrap bg-[var(--background)] rounded p-3 border border-[var(--border)] font-mono overflow-x-auto">
{unitData.service}
</pre>
</div>
{unitData.timer && (
<div>
<span className="text-xs text-[var(--muted)] block mb-1">
{component.systemd.unit_name.replace(".service", ".timer")}
</span>
<pre className="text-sm whitespace-pre-wrap bg-[var(--background)] rounded p-3 border border-[var(--border)] font-mono overflow-x-auto">
{unitData.timer}
</pre>
</div>
)}
</div>
)}
</div>
)}
{isGateway && caddyfile?.content && (
<div className="bg-[var(--card)] border border-[var(--border)] rounded-lg p-5 mb-6">
<h2 className="text-sm font-semibold text-[var(--muted)] uppercase tracking-wider mb-1">
Caddyfile
</h2>
<p className="text-xs text-[var(--muted)] mb-3">
Generated reverse proxy configuration served by the gateway.
</p>
<pre className="text-sm whitespace-pre-wrap bg-[var(--background)] rounded p-3 border border-[var(--border)] font-mono overflow-x-auto">
{caddyfile.content}
</pre>
</div>
)}

View File

@@ -8,7 +8,7 @@ export const router = createBrowserRouter([
element: <Dashboard />,
},
{
path: "/:name",
path: "/component/:name",
element: <ComponentDetailPage />,
},
])

View File

@@ -43,6 +43,14 @@ export function useGateway() {
})
}
export function useCaddyfile(enabled = true) {
return useQuery({
queryKey: ["gateway", "caddyfile"],
queryFn: () => apiClient.get<{ content: string }>("/gateway/caddyfile"),
enabled,
})
}
async function waitForApi(attempts = 20, interval = 1000): Promise<void> {
for (let i = 0; i < attempts; i++) {
try {
@@ -54,6 +62,14 @@ async function waitForApi(attempts = 20, interval = 1000): Promise<void> {
}
}
export function useSystemdUnit(name: string, enabled = true) {
return useQuery({
queryKey: ["services", name, "unit"],
queryFn: () => apiClient.get<{ service: string; timer: string | null }>(`/services/${name}/unit`),
enabled: enabled && !!name,
})
}
export function useServiceAction() {
const qc = useQueryClient()
return useMutation({