Dashboard: programs list deployments; Tools page; statics on Services; editable launcher

- Programs page: each card lists the program's deployments (name · kind) instead
  of a single kind badge; kind-filter chips removed (nav is the kind lens now).
  protonmail honestly shows tool + job.
- New Tools nav page (Wrench) — programs with a tool deployment (/programs?kind=tool).
- Services page now shows statics too: /services returns service + static, and a
  static renders as a caddy-served card (KindBadge, served URL, no start/stop).
- Program detail: DeploymentsSection + deleteConfirm rebuilt over program.deployments;
  header drops the (now nonexistent) program kind.
- ServiceFields / JobFields: launcher is now an editable select (python|command|
  container|compose|node), persisted into run.launcher via the normal save.

clean pnpm build + tsc --noEmit.
This commit is contained in:
2026-07-01 12:32:09 -07:00
parent 10a86d0b6f
commit 01505328ad
13 changed files with 199 additions and 148 deletions

View File

@@ -11,8 +11,7 @@ import { CreateDeploymentForm, type CreatePrefill } from "./CreateDeploymentForm
* lifecycle is shown inline here; service/job deployments link to their own pages
* where start/stop lives. This is the single home for "how this program runs". */
export function DeploymentsSection({ program }: { program: ProgramDetail }) {
const { services, jobs, kind } = program
const none = services.length === 0 && jobs.length === 0
const { deployments } = program
const [creating, setCreating] = useState(false)
const { data: allServices } = useServices()
@@ -29,6 +28,11 @@ export function DeploymentsSection({ program }: { program: ProgramDetail }) {
launcher: program.stack?.startsWith("python") || !program.stack ? "python" : "command",
}
// Tool/static deployments are 1:1 with the program (same name) — their
// lifecycle is shown inline; service/job deployments link to their own pages.
const inline = deployments.filter((d) => d.kind === "tool" || d.kind === "static")
const linked = deployments.filter((d) => d.kind === "service" || d.kind === "job")
return (
<div className="bg-[var(--card)] border border-[var(--border)] rounded-lg p-5 mb-6">
<div className="flex items-center justify-between mb-1">
@@ -47,8 +51,13 @@ export function DeploymentsSection({ program }: { program: ProgramDetail }) {
</p>
{/* The program's own path/caddy deployment — its lifecycle, inline. */}
{kind === "tool" && <PathLifecycle name={program.id} active={program.active} />}
{kind === "static" && <StaticStatus name={program.id} active={program.active} />}
{inline.map((d) =>
d.kind === "tool" ? (
<PathLifecycle key={d.name} name={program.id} active={program.active} />
) : (
<StaticStatus key={d.name} name={program.id} active={program.active} />
),
)}
{creating && (
<CreateDeploymentForm
@@ -59,40 +68,27 @@ export function DeploymentsSection({ program }: { program: ProgramDetail }) {
)}
{/* Service/job deployments — managed on their own detail pages. */}
{none ? (
(kind === "tool" || kind === "static") ? null : (
<p className="text-sm text-[var(--muted)]">
{kind === "service"
? "No service yet — this program isn't deployed."
: "No deployment yet."}
</p>
)
) : (
{deployments.length === 0 && !creating ? (
<p className="text-sm text-[var(--muted)]">No deployment yet.</p>
) : linked.length > 0 ? (
<div className="space-y-1.5 mt-1">
{services.map((s) => (
{linked.map((d) => (
<Link
key={s}
to={`/services/${s}`}
key={d.name}
to={d.kind === "job" ? `/jobs/${d.name}` : `/services/${d.name}`}
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>
{d.kind === "job" ? (
<Clock size={14} className="text-[var(--muted)]" />
) : (
<Server size={14} className="text-[var(--muted)]" />
)}
<span className="font-medium">{d.name}</span>
<span className="text-xs text-[var(--muted)]">{d.kind}</span>
</Link>
))}
</div>
)}
) : null}
</div>
)
}