feat: relationship model (requires/repos/predicates) + git sync
A derived, mostly-computed model of how programs, deployments, and repos relate,
plus the git-sync surfaces that motivated it. See docs/relationships.md.
Core:
- `requires: [{kind, ref, version?, bind?}]` on programs + deployments — one
precondition relation; `system_dependencies` is its `{kind: system}` alias.
kind fixes meaning + check (system=installed, deployment=exists).
- relations.py: derives repos (git toplevel / monorepo), fan-in, and the
predicates functional?/fresh?/deployed? — nothing stored.
- env is generated FROM a `{kind: deployment, bind}` requirement (target URL →
consumer env), never scraped back into one; explicit defaults.env still wins.
- git.py: working-copy status/pull, repo toplevel + remote url.
Surfaces:
- `castle graph` + GET /graph — the relationship diagnostic.
- GET /repos, /repos/{key}/git|sync — repo-scoped sync (a repo is the sync unit;
a monorepo backs several programs). GET /programs/{name}/git|sync + repo context.
- Dashboard: Graph screen, program-page git status + repo-aware Sync, and a
monorepo banner on Programs.
Governing principle: predicates are derived; encode only the non-derivable, as a
node or edge property. Pull-only sync — converge stays a separate step.
This commit is contained in:
@@ -1,17 +1,43 @@
|
||||
import { useState } from "react"
|
||||
import { useParams } from "react-router-dom"
|
||||
import { useProgram } from "@/services/api/hooks"
|
||||
import { useProgram, useProgramGit, useProgramSync } from "@/services/api/hooks"
|
||||
import { subdomainUrl } from "@/lib/labels"
|
||||
import { DetailHeader } from "@/components/detail/DetailHeader"
|
||||
import { ConfigPanel } from "@/components/detail/ConfigPanel"
|
||||
import { DeploymentsSection } from "@/components/detail/DeploymentsSection"
|
||||
import { ProgramActions, ActionOutputPanel, type ActionOutput } from "@/components/ProgramActions"
|
||||
import { GitSyncRow } from "@/components/detail/GitSyncRow"
|
||||
|
||||
export function ProgramDetailPage() {
|
||||
const { name } = useParams<{ name: string }>()
|
||||
const { data: deployment, isLoading, error, refetch } = useProgram(name ?? "")
|
||||
const git = useProgramGit(name ?? "", !!deployment?.repo)
|
||||
const sync = useProgramSync()
|
||||
const [actionOutput, setActionOutput] = useState<ActionOutput | null>(null)
|
||||
|
||||
const handleSync = () => {
|
||||
if (!deployment) return
|
||||
setActionOutput(null)
|
||||
sync.mutate(deployment.id, {
|
||||
onSuccess: (data) => {
|
||||
const lines = [data.output || (data.pulled ? "Pulled." : "Already up to date.")]
|
||||
if (data.pulled && data.deployments.length) {
|
||||
lines.push("", `May need a restart/apply: ${data.deployments.join(", ")}`)
|
||||
}
|
||||
setActionOutput({ action: "sync", text: lines.join("\n"), ok: true })
|
||||
},
|
||||
onError: (err) => {
|
||||
let text = String(err)
|
||||
try {
|
||||
text = JSON.parse((err as Error).message).detail ?? text
|
||||
} catch {
|
||||
text = (err as Error).message ?? text
|
||||
}
|
||||
setActionOutput({ action: "sync", text, ok: false })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto px-6 py-8 text-[var(--muted)]">Loading...</div>
|
||||
@@ -85,6 +111,18 @@ export function ProgramDetailPage() {
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{deployment.repo && git.data?.is_repo && (
|
||||
<>
|
||||
<span className="text-[var(--muted)]">Git</span>
|
||||
<GitSyncRow
|
||||
status={git.data}
|
||||
program={deployment.id}
|
||||
loading={git.isFetching}
|
||||
syncing={sync.isPending}
|
||||
onSync={handleSync}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{deployment.version && (
|
||||
<>
|
||||
<span className="text-[var(--muted)]">Version</span>
|
||||
|
||||
Reference in New Issue
Block a user