feat: environment model + deployment tracking (phase 3a)

- Environment/Deployment XORM models + migration 010
- Full CRUD API: GET/POST/PATCH/DELETE /environments + /deployments
- Deployment status update endpoint, publishes deployment.* NATS events
- EnvironmentsPage with deploy cards, history accordion, deploy modal
- Sidebar Environments nav item between Pipelines and Settings
- Repo page deployment status badges (env name + SHA pill per
  environment)
- Environment/Deployment types in types/api.ts + environments.ts query
  hooks
This commit is contained in:
2026-05-11 21:20:12 +02:00
parent 4f2fb846dd
commit 24bf4706e1
14 changed files with 1168 additions and 31 deletions
+33
View File
@@ -3,6 +3,7 @@ import { useParams, useSearchParams, Link } from 'react-router-dom'
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import { useRepo, useRepoTree, useRepoBlob, useRepoBranches } from '../api/queries/repos'
import { useEnvironments } from '../api/queries/environments'
import { TreeBrowser } from '../components/repos/TreeBrowser'
import { RepoListSkeleton } from '../ui/Skeleton'
import { RepoAvatar } from '../ui/RepoAvatar'
@@ -21,6 +22,7 @@ export default function RepoPage() {
const { data: repo, isLoading, isError } = useRepo(owner, repoName)
const { data: branches } = useRepoBranches(owner, repoName)
const { data: environments } = useEnvironments(owner, repoName)
const { track } = useRecentRepos()
useEffect(() => { if (owner && repoName) track(owner, repoName) }, [owner, repoName])
@@ -65,6 +67,37 @@ export default function RepoPage() {
{repo.description && (
<p className="text-sm text-[var(--c-muted)]">{repo.description}</p>
)}
{/* Deployment status badges */}
{environments && environments.length > 0 && (
<div className="flex items-center gap-1.5 flex-wrap mt-2">
{environments.map(env => {
const status = env.latestDeployment?.status
const dot: Record<string, string> = {
success: 'bg-[var(--c-success)]',
in_progress: 'bg-[var(--c-brand)] animate-pulse',
failure: 'bg-[var(--c-danger)]',
pending: 'bg-[var(--c-subtle)]',
cancelled: 'bg-[var(--c-subtle)]',
}
return (
<Link
key={env.id}
to={`/repos/${owner}/${repoName}/environments`}
className="flex items-center gap-1.5 px-2 py-0.5 rounded-full border border-[var(--c-border)] bg-[var(--c-surface-muted)] hover:border-[var(--c-brand-focus)] transition-colors"
>
<span className={`w-1.5 h-1.5 rounded-full shrink-0 ${status ? (dot[status] ?? 'bg-[var(--c-subtle)]') : 'bg-[var(--c-subtle)]'}`} />
<span className="text-[10px] font-medium text-[var(--c-muted)]">{env.name}</span>
{env.latestDeployment?.sha && (
<span className="text-[10px] font-mono text-[var(--c-subtle)]">
{env.latestDeployment.sha.slice(0, 7)}
</span>
)}
</Link>
)
})}
</div>
)}
</div>
<div className="flex items-center gap-2 shrink-0">