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

@@ -0,0 +1,57 @@
import { Link } from "react-router-dom"
import { Server, Clock } from "lucide-react"
import type { ProgramDetail } from "@/types"
/** The services and jobs that deploy a program. A program → 0-N services and
* 0-N jobs; these are convenience links, not ownership (a deployment can run
* anything, program-backed or not). */
export function DeploymentsSection({ program }: { program: ProgramDetail }) {
const { services, jobs, behavior } = program
const none = services.length === 0 && jobs.length === 0
return (
<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">
Deployments
</h2>
<p className="text-xs text-[var(--muted)] mb-4">
Services and jobs that run this program.
</p>
{none ? (
<p className="text-sm text-[var(--muted)]">
{behavior === "daemon"
? "No service yet — this daemon isn't deployed."
: behavior === "tool"
? "Not scheduled — add a job to run it on a timer."
: "None."}
</p>
) : (
<div className="space-y-1.5">
{services.map((s) => (
<Link
key={s}
to={`/services/${s}`}
className="flex items-center gap-2 text-sm hover:text-[var(--primary)] transition-colors"
>
<Server size={14} className="text-[var(--muted)]" />
<span className="font-medium">{s}</span>
<span className="text-xs text-[var(--muted)]">service</span>
</Link>
))}
{jobs.map((j) => (
<Link
key={j}
to={`/jobs/${j}`}
className="flex items-center gap-2 text-sm hover:text-[var(--primary)] transition-colors"
>
<Clock size={14} className="text-[var(--muted)]" />
<span className="font-medium">{j}</span>
<span className="text-xs text-[var(--muted)]">job</span>
</Link>
))}
</div>
)}
</div>
)
}