285 lines
8.3 KiB
TypeScript
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>
|
|
);
|
|
}
|