Files
vibn-agent-runner/vibn-frontend/app/[workspace]/project/[projectId]/(home)/logs/page.tsx

285 lines
8.3 KiB
TypeScript

"use client";
import { useState, useEffect } from "react";
import { useParams } from "next/navigation";
import { Activity, Loader2, RefreshCw } from "lucide-react";
import { useAnatomy, type Anatomy } from "@/components/project/use-anatomy";
import {
THEME,
PageHeader,
Card,
EmptyState,
SecondaryButton,
} from "@/components/project/dashboard-ui";
import { Terminal } from "@/components/ui/terminal";
type LiveApp = Anatomy["hosting"]["live"][number];
export default function LogsPage() {
const params = useParams();
const projectId = params.projectId as string;
const { anatomy, loading } = useAnatomy(projectId, { pollMs: 8000 });
const live = anatomy?.hosting.live ?? [];
const [activeUuid, setActiveUuid] = useState<string | null>(null);
const [logs, setLogs] = useState<string | null>(null);
const [logsLoading, setLogsLoading] = useState(false);
// Auto-select first app if none selected
useEffect(() => {
if (live.length > 0 && !activeUuid) {
setActiveUuid(live[0].uuid);
}
}, [live, activeUuid]);
const fetchLogs = async (uuid: string) => {
setLogsLoading(true);
try {
const r = await fetch(`/api/mcp`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
action: "apps.logs",
params: { uuid, lines: 100 },
}),
});
const d = await r.json();
let out = "";
let obj = d.result;
if (typeof obj === "string") {
try {
obj = JSON.parse(obj);
} catch {}
}
if (typeof obj === "object" && obj !== null) {
if (obj.services) {
out = Object.values(obj.services)
.map((s: any) => s.logs)
.join("\n\n");
} else if (obj.log) {
out = obj.log;
} else if (obj.logs) {
out = obj.logs;
} else {
out = JSON.stringify(obj, null, 2);
}
} else {
out = String(obj || d.error || "No logs available.");
}
setLogs(out || "No logs available.");
} catch {
setLogs("Failed to load logs. Is the container running?");
} finally {
setLogsLoading(false);
}
};
// Fetch when active app changes
useEffect(() => {
if (activeUuid) fetchLogs(activeUuid);
}, [activeUuid]);
return (
<div
style={{
flex: 1,
minHeight: 0,
boxSizing: "border-box",
background: THEME.canvasGradient,
fontFamily: THEME.font,
padding: "16px",
display: "flex",
flexDirection: "column",
}}
>
<Card
padding={0}
style={{
maxWidth: "100%",
margin: "0 auto",
width: "100%",
display: "flex",
flexDirection: "column",
flex: 1,
minHeight: 0,
}}
>
{loading && !anatomy ? (
<div
style={{
display: "flex",
alignItems: "center",
gap: 8,
color: THEME.mid,
fontSize: "0.875rem",
padding: "24px",
}}
>
<Loader2 size={15} className="animate-spin" /> Loading
</div>
) : live.length === 0 ? (
<div style={{ padding: 24 }}>
<EmptyState
icon={<Activity size={22} />}
title="No apps running"
hint="Once you deploy an app, its runtime logs will appear here."
/>
</div>
) : (
<div
style={{
display: "flex",
flex: 1,
minHeight: 0,
}}
>
{/* App Picker Column */}
<div
style={{
width: 300,
flexShrink: 0,
display: "flex",
flexDirection: "column",
borderRight: `1px solid ${THEME.borderSoft}`,
overflowY: "auto",
}}
>
<header
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "10px 20px",
position: "sticky",
top: 0,
background: THEME.cardBg,
zIndex: 10,
height: "41px",
boxSizing: "border-box",
borderTopLeftRadius: THEME.radius,
}}
>
<h3
style={{
fontSize: "0.95rem",
fontWeight: 600,
color: THEME.ink,
margin: 0,
}}
>
Apps
</h3>
</header>
<div
style={{
display: "flex",
flexDirection: "column",
gap: 4,
padding: "0 20px 20px 20px",
}}
>
{live.map((app) => (
<button
key={app.uuid}
onClick={() => setActiveUuid(app.uuid)}
style={{
textAlign: "left",
padding: "10px 14px",
background:
activeUuid === app.uuid
? THEME.subtleBg
: "transparent",
border: "1px solid transparent",
borderRadius: THEME.radiusSm,
cursor: "pointer",
fontSize: "0.875rem",
fontWeight: activeUuid === app.uuid ? 500 : 400,
color: activeUuid === app.uuid ? THEME.ink : THEME.mid,
transition: "all 0.1s ease",
}}
>
{app.name}
</button>
))}
</div>
</div>
{/* Log Viewer Column */}
<div
style={{
flex: 1,
minHeight: 0,
display: "flex",
flexDirection: "column",
position: "relative",
}}
>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: "10px 16px",
background: THEME.subtleBg,
borderBottom: `1px solid ${THEME.borderSoft}`,
height: "41px",
boxSizing: "border-box",
}}
>
<span
style={{
fontSize: "0.85rem",
fontWeight: 500,
color: THEME.mid,
}}
>
{live.find((a) => a.uuid === activeUuid)?.name ?? "Logs"}
</span>
<SecondaryButton
icon={
logsLoading ? (
<Loader2 size={13} className="animate-spin" />
) : (
<RefreshCw size={13} />
)
}
onClick={() => activeUuid && fetchLogs(activeUuid)}
disabled={logsLoading}
style={{ padding: "4px 8px", fontSize: "0.75rem" }}
>
Refresh
</SecondaryButton>
</div>
<Terminal
sequence={false}
className="w-full max-w-none shadow-none border-none rounded-none flex-1"
>
<div
style={{
color: THEME.mid,
fontFamily:
"ui-monospace, SFMono-Regular, Menlo, monospace",
fontSize: "13px",
lineHeight: "24px",
padding: "16px 24px",
whiteSpace: "pre-wrap",
wordBreak: "break-all",
}}
>
{logsLoading && !logs
? "Loading..."
: logs || "No logs available."}
</div>
</Terminal>
</div>
</div>
)}
</Card>
</div>
);
}