Stage 3: App — program/deployment model & controls

- ProgramActions: install/uninstall (activate) shown only for tools and
  no-service frontends. Daemons activate via a service/job, so the program
  page no longer offers a misleading Install/Uninstall (which secretly
  enabled/disabled the service).
- Program page: new Deployments section listing the services/jobs that run
  the program, as links (a program → 0-N services/jobs).
- Service page: 'Runs' row from the run_target summary field (the old code
  dug into manifest.run, which was empty pre-Stage-1), a Host row for
  proxy_host, and a convenience link to the referenced program.
- lifecycle.activate on a daemon with no service now errors toward
  'castle expose' instead of silently installing its binary to PATH.

core 94 green; ruff + app build clean.
This commit is contained in:
2026-06-14 15:37:20 -07:00
parent 73698fafe7
commit dea585b15b
6 changed files with 132 additions and 13 deletions

View File

@@ -1,5 +1,5 @@
import { useRef } from "react"
import { useParams } from "react-router-dom"
import { useParams, Link } from "react-router-dom"
import { Server, ExternalLink, Terminal, Trash2 } from "lucide-react"
import { useService, useStatus, useEventStream, useCaddyfile } from "@/services/api/hooks"
import { runnerLabel } from "@/lib/labels"
@@ -35,8 +35,6 @@ export function ServiceDetailPage() {
)
}
const runner = (deployment.manifest.run as Record<string, unknown>)?.runner as string | undefined
return (
<div className="max-w-3xl mx-auto px-6 py-8">
<DetailHeader
@@ -83,18 +81,30 @@ export function ServiceDetailPage() {
</a>
</>
)}
{runner && (
{deployment.proxy_host && (
<>
<span className="text-[var(--muted)]">Runner</span>
<span className="text-[var(--muted)]">Host</span>
<span className="font-mono">{deployment.proxy_host}</span>
</>
)}
{deployment.runner && (
<>
<span className="text-[var(--muted)]">Runs</span>
<span className="flex items-center gap-1">
<Terminal size={12} />
{runnerLabel(runner)}
{(deployment.manifest.run as Record<string, string>)?.program && (
<> &middot; {(deployment.manifest.run as Record<string, string>).program}</>
)}
{runnerLabel(deployment.runner)}
{deployment.run_target && <> &middot; <span className="font-mono">{deployment.run_target}</span></>}
</span>
</>
)}
{deployment.program && (
<>
<span className="text-[var(--muted)]">Program</span>
<Link to={`/programs/${deployment.program}`} className="text-[var(--primary)] hover:underline">
{deployment.program}
</Link>
</>
)}
{deployment.port && (
<>
<span className="text-[var(--muted)]">Docs</span>