/** * PromptDeployments (#120) — environment deploy lifecycle for a prompt template: * live version per environment, deploy / rollback actions (protected envs are * AgentGate-gated server-side), a tamper-evident deploy-history timeline, and a * ledger-verify button. Append-only: rollback is a new ledger row, never a delete. */ import { useState } from 'react'; import { useApi } from '../hooks/useApi'; import { getPromptEnvironments, getPromptDeployments, deployPromptVersion, rollbackPromptVersion, verifyDeployLedger, } from '../api/prompts'; import type { PromptVersion, PromptDeployment, DeployLedgerVerifyResult } from '../api/prompts'; interface Props { templateId: string; versions: PromptVersion[]; liveVersions: Record; /** Called after a successful deploy/rollback so the parent can refetch live versions. */ onChange: () => void; } const ENV_TONE: Record = { prod: 'bg-red-100 text-red-800 border-red-300', production: 'bg-red-100 text-red-800 border-red-300', staging: 'bg-yellow-100 text-yellow-800 border-yellow-300', }; const envTone = (env: string): string => ENV_TONE[env] ?? 'bg-gray-100 text-gray-700 border-gray-300'; const errMsg = (e: unknown): string => (e instanceof Error ? e.message : String(e)); function StatusBadge({ d }: { d: PromptDeployment }) { const denied = d.status === 'denied'; return ( {denied ? '✗ denied' : d.action === 'rollback' ? '↩ rolled back' : '✓ deployed'} ); } export function PromptDeployments({ templateId, versions, liveVersions, onChange }: Props) { const { data: environments } = useApi(() => getPromptEnvironments(), []); const { data: deployments, refetch } = useApi(() => getPromptDeployments(templateId), [templateId]); const [environment, setEnvironment] = useState(''); const [versionId, setVersionId] = useState(''); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const [verifyResult, setVerifyResult] = useState(null); const verNum = (id?: string): number | undefined => versions.find((v) => v.id === id)?.versionNumber; const env = environment || environments?.[0]?.name || ''; const ver = versionId || versions[0]?.id || ''; async function run(action: 'deploy' | 'rollback') { if (!env || !ver) return; setBusy(true); setError(null); try { if (action === 'deploy') await deployPromptVersion(templateId, { environment: env, versionId: ver }); else await rollbackPromptVersion(templateId, { environment: env, toVersionId: ver }); refetch(); onChange(); } catch (e) { setError(errMsg(e)); } finally { setBusy(false); } } async function verify() { if (!env) return; setError(null); try { setVerifyResult(await verifyDeployLedger(env)); } catch (e) { setError(errMsg(e)); } } return (

Deployments

{/* Live version per environment */}
{(environments ?? []).map((e) => { const live = liveVersions[e.name]; return ( {e.protected && 🔒} {e.name}: {live ? `v${verNum(live) ?? '?'}` : '—'} ); })}
{/* Deploy / rollback */}
{error &&
{error}
} {verifyResult && (
Ledger “{verifyResult.environment}”:{' '} {verifyResult.valid ? `✓ verified (${verifyResult.count} record${verifyResult.count !== 1 ? 's' : ''})` : `✗ broken at seq ${verifyResult.brokenAtSeq} — ${verifyResult.reason}`}
)} {/* Deploy history (newest first) */}

History

{deployments && deployments.length === 0 &&

No deployments yet.

} {(deployments ?? []).map((d) => (
{d.environment} v{verNum(d.versionId) ?? '?'} by {d.actorId ?? 'unknown'} {d.approverId && · approved by {d.approverId}} {new Date(d.createdAt).toLocaleString()}
))}
); }