fix(app): drive program activate toggle off active, not installed
The Programs table's install/uninstall icon keyed off `installed` (shutil.which(name)) — a PATH lookup that only makes sense for python CLI tools. Static frontends have no PATH binary so they were always `installed=false` → a green 'Install' icon even while actively served; container daemons were `installed=null` → same. And for tools the value reflected the castle-api service's PATH, which isn't guaranteed to include ~/.local/bin across reboots, so they could all flip to 'not installed'. Switch ProgramActions/ProgramTable/ProgramDetail to the uniform `active` state (Phase 2's lifecycle.is_active: PATH-independent ~/.local/bin check for tools, systemctl for services/jobs, served-assets check for static frontends). Now every program reflects real state: served frontends show Uninstall, container daemons (no install verb) show no icon. App builds clean.
This commit is contained in:
@@ -41,32 +41,34 @@ export interface ActionOutput {
|
|||||||
interface ProgramActionsProps {
|
interface ProgramActionsProps {
|
||||||
name: string
|
name: string
|
||||||
actions: string[]
|
actions: string[]
|
||||||
installed?: boolean | null
|
active?: boolean | null
|
||||||
compact?: boolean
|
compact?: boolean
|
||||||
onOutput?: (output: ActionOutput) => void
|
onOutput?: (output: ActionOutput) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function visibleActions(
|
function visibleActions(
|
||||||
actions: string[],
|
actions: string[],
|
||||||
installed: boolean | null | undefined,
|
active: boolean | null | undefined,
|
||||||
compact: boolean,
|
compact: boolean,
|
||||||
): string[] {
|
): string[] {
|
||||||
|
// `active` is the uniform lifecycle state (on PATH / running / served), not a
|
||||||
|
// PATH lookup — so it's correct for tools, services, jobs, and static frontends.
|
||||||
if (compact) {
|
if (compact) {
|
||||||
// Table: only install/uninstall based on state
|
// Table: only the activate/deactivate toggle based on state
|
||||||
if (installed === true) return actions.includes("uninstall") ? ["uninstall"] : []
|
if (active === true) return actions.includes("uninstall") ? ["uninstall"] : []
|
||||||
if (installed === false) return actions.includes("install") ? ["install"] : []
|
if (active === false) return actions.includes("install") ? ["install"] : []
|
||||||
// null — show install if available (frontends, etc.)
|
// null — show install if available
|
||||||
return actions.includes("install") ? ["install"] : []
|
return actions.includes("install") ? ["install"] : []
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detail page: dev actions always, install/uninstall based on state
|
// Detail page: dev actions always, activate/deactivate based on state
|
||||||
const visible: string[] = []
|
const visible: string[] = []
|
||||||
for (const a of DEV_ACTIONS) {
|
for (const a of DEV_ACTIONS) {
|
||||||
if (actions.includes(a)) visible.push(a)
|
if (actions.includes(a)) visible.push(a)
|
||||||
}
|
}
|
||||||
if (installed === true) {
|
if (active === true) {
|
||||||
if (actions.includes("uninstall")) visible.push("uninstall")
|
if (actions.includes("uninstall")) visible.push("uninstall")
|
||||||
} else if (installed === false) {
|
} else if (active === false) {
|
||||||
if (actions.includes("install")) visible.push("install")
|
if (actions.includes("install")) visible.push("install")
|
||||||
} else {
|
} else {
|
||||||
// null — show both if available
|
// null — show both if available
|
||||||
@@ -76,11 +78,11 @@ function visibleActions(
|
|||||||
return visible
|
return visible
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ProgramActions({ name, actions, installed, compact, onOutput }: ProgramActionsProps) {
|
export function ProgramActions({ name, actions, active, compact, onOutput }: ProgramActionsProps) {
|
||||||
const { mutate, isPending } = useProgramAction()
|
const { mutate, isPending } = useProgramAction()
|
||||||
const [runningAction, setRunningAction] = useState<string | null>(null)
|
const [runningAction, setRunningAction] = useState<string | null>(null)
|
||||||
|
|
||||||
const visible = visibleActions(actions, installed, !!compact)
|
const visible = visibleActions(actions, active, !!compact)
|
||||||
|
|
||||||
const handleAction = (action: string) => {
|
const handleAction = (action: string) => {
|
||||||
setRunningAction(action)
|
setRunningAction(action)
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ function ProgramRow({ program }: { program: ProgramSummary }) {
|
|||||||
<BehaviorBadge behavior={program.behavior} />
|
<BehaviorBadge behavior={program.behavior} />
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-2.5">
|
<td className="px-3 py-2.5">
|
||||||
<ProgramActions name={program.id} actions={program.actions} installed={program.installed} compact />
|
<ProgramActions name={program.id} actions={program.actions} active={program.active} compact />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export function ProgramDetailPage() {
|
|||||||
stack={deployment.stack}
|
stack={deployment.stack}
|
||||||
source={deployment.source}
|
source={deployment.source}
|
||||||
>
|
>
|
||||||
<ProgramActions name={deployment.id} actions={deployment.actions} installed={deployment.installed} onOutput={setActionOutput} />
|
<ProgramActions name={deployment.id} actions={deployment.actions} active={deployment.active} onOutput={setActionOutput} />
|
||||||
</DetailHeader>
|
</DetailHeader>
|
||||||
|
|
||||||
{actionOutput && actionOutput.action && (
|
{actionOutput && actionOutput.action && (
|
||||||
|
|||||||
Reference in New Issue
Block a user