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

@@ -172,12 +172,13 @@ export function ProgramFields({ program, onSave, onDelete }: Props) {
* confirm enumerates exactly what will happen. Source on disk is always kept. */
function deleteConfirm(program: ProgramDetail): string {
const actions: string[] = []
// The program's own 1:1 deployment (tool/static) isn't listed in services/jobs.
if (program.kind === "tool") actions.push(`uninstall ${program.id} from your PATH`)
else if (program.kind === "static")
actions.push(`stop serving ${program.id} (drop its gateway route)`)
// Named service/job deployments that reference this program.
const named = [...program.services, ...program.jobs]
// Enumerate the cascade — one line per deployment, keyed on its kind.
const named: string[] = []
for (const d of program.deployments) {
if (d.kind === "tool") actions.push(`uninstall ${d.name} from your PATH`)
else if (d.kind === "static") actions.push(`stop serving ${d.name}`)
else named.push(d.name)
}
if (named.length) actions.push(`stop, disable, and remove: ${named.join(", ")}`)
// Always: the catalog entry itself.
actions.push(`remove "${program.id}" from the catalog`)