Runtime half: convergent deploy + unified active lifecycle

Convergent deploy (prefix-based prune): castle deploy now removes orphaned
castle-* systemd units/timers no longer in the registry. Full deploy only;
partial (--name) deploys preserve siblings. Fixes the orphaned-timer / stale-
path drift class.

Container runner: derive --name from the service name (castle-<name>) instead
of the image name; skip the <PREFIX>_DATA_DIR env injection for containers
(avoids colliding with image env namespaces like NEO4J_*).

Unified active lifecycle (core/lifecycle.py): install/uninstall keep their
names but become activate/deactivate, dispatching by behavior — tool→PATH,
daemon/self-serving-frontend/job→systemd, static frontend→served. is_active
reports a uniform state (PATH-independent for tools). The CLI install/uninstall
and service enable/disable route through the extracted core.

API + dashboard: surface a uniform `active` state on programs alongside
`installed`; ProgramDetail shows active/inactive.

Tests: test_deploy_prune (6), test_lifecycle (5). 168 tests pass; lint clean.
This commit is contained in:
2026-06-13 18:08:54 -07:00
parent 400e0b253b
commit 2953aea548
10 changed files with 414 additions and 101 deletions

View File

@@ -85,10 +85,12 @@ export function ProgramDetailPage() {
<span>{runnerLabel(component.runner)}</span>
</>
)}
{component.installed !== null && (
{component.active !== null && (
<>
<span className="text-[var(--muted)]">Installed</span>
<span>{component.installed ? "Yes" : "No"}</span>
<span className="text-[var(--muted)]">Active</span>
<span className={component.active ? "text-green-400" : "text-[var(--muted)]"}>
{component.active ? "● active" : "○ inactive"}
</span>
</>
)}
</div>

View File

@@ -51,6 +51,7 @@ export interface ProgramSummary {
commands: Record<string, string[][]> | null
system_dependencies: string[]
installed: boolean | null
active: boolean | null
actions: string[]
node: string | null
}
@@ -83,6 +84,7 @@ export interface ComponentSummary {
system_dependencies: string[]
schedule: string | null
installed: boolean | null
active: boolean | null
node: string | null
}