diff --git a/vibn-frontend/app/[workspace]/project/[projectId]/(home)/logs/page.tsx b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/logs/page.tsx index 386c43d3..60adeb0a 100644 --- a/vibn-frontend/app/[workspace]/project/[projectId]/(home)/logs/page.tsx +++ b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/logs/page.tsx @@ -23,9 +23,28 @@ export default function LogsPage() { const previews = anatomy?.hosting.previews ?? []; + const databases = anatomy?.infrastructure.databases ?? []; + + const deployments: { uuid: string; name: string }[] = []; + live.forEach((app) => { + if (app.inFlightBuild) { + deployments.push({ + uuid: app.inFlightBuild.uuid, + name: `Build (Active): ${app.name}`, + }); + } + if (app.lastBuild) { + deployments.push({ + uuid: app.lastBuild.uuid, + name: `Build (Latest): ${app.name}`, + }); + } + }); + const [activeItem, setActiveItem] = useState<{ - type: "app" | "preview"; + type: "app" | "preview" | "database" | "deployment"; id: string; + name: string; } | null>(null); const [logs, setLogs] = useState(null); const [logsLoading, setLogsLoading] = useState(false); @@ -34,20 +53,46 @@ export default function LogsPage() { useEffect(() => { if (activeItem) return; if (live.length > 0) { - setActiveItem({ type: "app", id: live[0].uuid }); + setActiveItem({ type: "app", id: live[0].uuid, name: live[0].name }); } else if (previews.length > 0) { - setActiveItem({ type: "preview", id: previews[0].id }); + setActiveItem({ + type: "preview", + id: previews[0].id, + name: previews[0].name || `Port ${previews[0].port}`, + }); + } else if (databases.length > 0) { + setActiveItem({ + type: "database", + id: databases[0].uuid, + name: databases[0].name, + }); + } else if (deployments.length > 0) { + setActiveItem({ + type: "deployment", + id: deployments[0].uuid, + name: deployments[0].name, + }); } - }, [live, previews, activeItem]); + }, [live, previews, databases, deployments, activeItem]); - const fetchLogs = async (item: { type: "app" | "preview"; id: string }) => { + const fetchLogs = async (item: { + type: "app" | "preview" | "database" | "deployment"; + id: string; + name: string; + }) => { setLogsLoading(true); try { - const action = item.type === "app" ? "apps.logs" : "dev_server.logs"; - const payloadParams = - item.type === "app" - ? { uuid: item.id, lines: 100 } - : { id: item.id, lines: 100, projectId }; + let action = "apps.logs"; + let payloadParams: Record = { uuid: item.id, lines: 100 }; + + if (item.type === "preview") { + action = "dev_server.logs"; + payloadParams = { id: item.id, lines: 100, projectId }; + } else if (item.type === "database") { + action = "databases.logs"; + } else if (item.type === "deployment") { + action = "deployments.logs"; + } const r = await fetch(`/api/mcp`, { method: "POST", @@ -134,7 +179,10 @@ export default function LogsPage() { > Loading… - ) : live.length === 0 && previews.length === 0 ? ( + ) : live.length === 0 && + previews.length === 0 && + databases.length === 0 && + deployments.length === 0 ? (
} @@ -189,7 +237,13 @@ export default function LogsPage() { {live.map((app) => ( ))}
+ + {databases.length > 0 && ( +
+

+ Databases +

+
+ )} +
+ {databases.map((db) => ( + + ))} +
+ + {deployments.length > 0 && ( +
+

+ Build Logs +

+
+ )} +
+ {deployments.map((dep) => ( + + ))} +
{/* Log Viewer Column */} @@ -294,11 +463,7 @@ export default function LogsPage() { color: THEME.mid, }} > - {activeItem?.type === "app" - ? (live.find((a) => a.uuid === activeItem.id)?.name ?? - "Logs") - : previews.find((p) => p.id === activeItem?.id)?.name || - "Dev Server Logs"} + {activeItem?.name ?? "Logs"} ) { }); } +async function toolDatabasesLogs( + principal: Principal, + params: Record, +) { + const projectUuid = requireCoolifyProject(principal); + if (projectUuid instanceof NextResponse) return projectUuid; + const ownedUuids = await getOwnedCoolifyProjectUuids(principal.workspace); + const uuid = String(params.uuid ?? "").trim(); + if (!uuid) + return NextResponse.json( + { error: 'Param "uuid" is required' }, + { status: 400 }, + ); + await getDatabaseInWorkspace(uuid, ownedUuids); + + const linesRaw = Number(params.lines ?? 200); + const lines = Number.isFinite(linesRaw) ? linesRaw : 200; + + try { + const cmd = `cid=$(docker ps -a --filter name=${uuid} --format '{{.Names}}' | head -1); if [ -z "$cid" ]; then echo "NO_CONTAINER"; exit 0; fi; docker logs --tail ${lines} "$cid" 2>&1`; + const res = await runOnCoolifyHost(cmd, { timeoutMs: 15_000 }); + + if (res.code !== 0) { + throw new Error(`docker logs exited ${res.code}: ${res.stderr.trim()}`); + } + + const raw = + res.stdout.trim() === "NO_CONTAINER" + ? "No running container found for this database." + : res.stdout; + return NextResponse.json({ result: raw }); + } catch (err: any) { + return NextResponse.json({ error: err.message }, { status: 500 }); + } +} + +async function toolDeploymentsLogs( + principal: Principal, + params: Record, +) { + // Validate workspace + const projectUuid = requireCoolifyProject(principal); + if (projectUuid instanceof NextResponse) return projectUuid; + + const uuid = String(params.uuid ?? "").trim(); + if (!uuid) + return NextResponse.json( + { error: 'Param "uuid" is required' }, + { status: 400 }, + ); + + try { + const res = await coolifyFetch(`/deployments/${uuid}/logs`); + return NextResponse.json({ + result: res.logs || "No logs available for this deployment.", + }); + } catch (err: any) { + return NextResponse.json({ error: err.message }, { status: 500 }); + } +} + // ── dev_server.* ───────────────────────────────────────────────────── async function toolDevServerStart( diff --git a/vibn-frontend/lib/coolify.ts b/vibn-frontend/lib/coolify.ts index 8668ad25..7c37e5ff 100644 --- a/vibn-frontend/lib/coolify.ts +++ b/vibn-frontend/lib/coolify.ts @@ -162,7 +162,7 @@ export interface CoolifyDeployment { commit?: string; } -async function coolifyFetch(path: string, options: RequestInit = {}) { +export async function coolifyFetch(path: string, options: RequestInit = {}) { const url = `${COOLIFY_URL}/api/v1${path}`; const res = await fetch(url, { ...options,