Populate the program stack select from castle-api

The dashboard's stack dropdown hardcoded its options (python-cli/fastapi/
react-vite), so a supabase program showed "(none)" — the value had no matching
option. Make the backend the single source of truth instead:

- core: available_stacks() returns sorted(HANDLERS) — the stacks castle has
  handlers for.
- castle-api: GET /stacks exposes it.
- cli: --stack choices derive from available_stacks() (drops the duplicated list).
- app: ProgramFields fetches /stacks (useStacks) and renders options from it,
  always including the current value so it never silently blanks; StackBadge
  gets a Supabase label.

A new backend stack now appears everywhere without a frontend edit.
This commit is contained in:
2026-06-30 21:43:11 -07:00
parent bd9ea76d6a
commit 14145c9f15
6 changed files with 38 additions and 9 deletions

View File

@@ -1,5 +1,6 @@
import { useState } from "react"
import type { ProgramDetail } from "@/types"
import { useStacks } from "@/services/api/hooks"
import { Field, TextField, FormFooter } from "./fields"
const SELECT =
@@ -21,6 +22,7 @@ type Obj = Record<string, unknown>
/** Edit a program's catalog config (its source identity), not how it's deployed. */
export function ProgramFields({ program, onSave, onDelete }: Props) {
const m = program.manifest
const { data: stacks = [] } = useStacks()
const [saving, setSaving] = useState(false)
const [saved, setSaved] = useState(false)
@@ -114,9 +116,13 @@ export function ProgramFields({ program, onSave, onDelete }: Props) {
>
<select value={stack} onChange={(e) => setStack(e.target.value)} className={`w-48 ${SELECT}`}>
<option value="">(none)</option>
<option value="python-cli">python-cli</option>
<option value="python-fastapi">python-fastapi</option>
<option value="react-vite">react-vite</option>
{/* Options come from the backend (GET /stacks); include the current
value even if unknown so it never silently blanks. */}
{Array.from(new Set([...stacks, ...(stack ? [stack] : [])])).map((s) => (
<option key={s} value={s}>
{s}
</option>
))}
</select>
</Field>
<TextField label="Version" value={version} onChange={setVersion} width="w-32" hint="Optional metadata." />