Add Coolify REST API server with Scalar docs and UI integration

Express API server on :3100 exposing all Coolify operations:
- CRUD for apps, env vars, servers
- Full upsert pipeline (create/update + env + route + deploy)
- Drift detection, Traefik route management via SSH
- Scalar API docs at /reference, OpenAPI 3.1 spec

UI: New Coolify page with app cards, deploy/delete actions,
env var expansion. Sidebar nav + React Query hooks + fetch client.

Both UI and LLM/CLI use the same HTTP endpoints.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-27 11:02:17 -06:00
parent 2fe49b6725
commit 93d40455d9
16 changed files with 1426 additions and 5 deletions

View File

@@ -0,0 +1,92 @@
import { useState } from 'react';
import { Rocket, Trash2, ChevronDown, ChevronRight, ExternalLink } from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { StatusDot } from '@/components/shared/status-dot';
const buildpackColor = {
dockercompose: 'secondary',
nixpacks: 'outline',
dockerfile: 'outline',
};
function statusColor(status) {
if (!status) return 'gray';
const s = status.toLowerCase();
if (s.includes('running') || s.includes('healthy')) return 'green';
if (s.includes('stopped') || s.includes('exited')) return 'red';
if (s.includes('building') || s.includes('starting')) return 'yellow';
return 'gray';
}
export function CoolifyAppCard({ app, onDeploy, onDelete }) {
const [expanded, setExpanded] = useState(false);
return (
<Card className="overflow-hidden">
<CardHeader className="p-4 pb-2">
<div className="flex items-center justify-between">
<CardTitle className="text-sm font-medium flex items-center gap-2">
<StatusDot color={statusColor(app.status)} />
{app.name}
</CardTitle>
<Badge variant={buildpackColor[app.build_pack] || 'outline'} className="text-[10px]">
{app.build_pack}
</Badge>
</div>
</CardHeader>
<CardContent className="p-4 pt-0 space-y-2">
{/* Port + domain */}
<div className="flex items-center gap-3 text-xs text-muted-foreground">
{app._host_port && <span>:{app._host_port}</span>}
{app.git_branch && <span>{app.git_branch}</span>}
{app.fqdn && (
<a
href={app.fqdn}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 text-primary hover:underline"
onClick={(e) => e.stopPropagation()}
>
{app.fqdn.replace(/^https?:\/\//, '')}
<ExternalLink className="h-3 w-3" />
</a>
)}
</div>
{/* Actions */}
<div className="flex items-center gap-2 pt-1">
<Button variant="ghost" size="sm" className="h-7 text-xs gap-1" onClick={() => onDeploy(app)}>
<Rocket className="h-3 w-3" /> Deploy
</Button>
<Button variant="ghost" size="sm" className="h-7 text-xs gap-1 text-destructive" onClick={() => onDelete(app)}>
<Trash2 className="h-3 w-3" /> Delete
</Button>
<button
className="ml-auto text-xs text-muted-foreground hover:text-foreground flex items-center gap-1"
onClick={() => setExpanded(!expanded)}
>
{expanded ? <ChevronDown className="h-3 w-3" /> : <ChevronRight className="h-3 w-3" />}
Envs
</button>
</div>
{/* Expandable env vars */}
{expanded && app._envs && (
<div className="rounded-md bg-secondary/50 p-2 text-[11px] font-mono space-y-0.5 max-h-40 overflow-auto">
{app._envs.length === 0 && <span className="text-muted-foreground">No env vars</span>}
{app._envs.map((env) => (
<div key={env.id || env.key} className="flex gap-2">
<span className="text-primary">{env.key}</span>
<span className="text-muted-foreground">=</span>
<span className="truncate">{env.value}</span>
</div>
))}
</div>
)}
</CardContent>
</Card>
);
}