feat(logs): add Dev Previews to server logs page

This commit is contained in:
2026-06-14 14:52:16 -07:00
parent d3b3bc11d9
commit f8cc4b32b0

View File

@@ -21,26 +21,40 @@ export default function LogsPage() {
const { anatomy, loading } = useAnatomy(projectId, { pollMs: 8000 });
const live = anatomy?.hosting.live ?? [];
const [activeUuid, setActiveUuid] = useState<string | null>(null);
const previews = anatomy?.hosting.previews ?? [];
const [activeItem, setActiveItem] = useState<{
type: "app" | "preview";
id: string;
} | null>(null);
const [logs, setLogs] = useState<string | null>(null);
const [logsLoading, setLogsLoading] = useState(false);
// Auto-select first app if none selected
// Auto-select first available item
useEffect(() => {
if (live.length > 0 && !activeUuid) {
setActiveUuid(live[0].uuid);
if (activeItem) return;
if (live.length > 0) {
setActiveItem({ type: "app", id: live[0].uuid });
} else if (previews.length > 0) {
setActiveItem({ type: "preview", id: previews[0].id });
}
}, [live, activeUuid]);
}, [live, previews, activeItem]);
const fetchLogs = async (uuid: string) => {
const fetchLogs = async (item: { type: "app" | "preview"; id: 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 };
const r = await fetch(`/api/mcp`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
action: "apps.logs",
params: { uuid, lines: 100 },
action,
params: payloadParams,
}),
});
const d = await r.json();
@@ -77,10 +91,10 @@ export default function LogsPage() {
}
};
// Fetch when active app changes
// Fetch when active item changes
useEffect(() => {
if (activeUuid) fetchLogs(activeUuid);
}, [activeUuid]);
if (activeItem) fetchLogs(activeItem);
}, [activeItem]);
return (
<div
@@ -120,12 +134,12 @@ export default function LogsPage() {
>
<Loader2 size={15} className="animate-spin" /> Loading
</div>
) : live.length === 0 ? (
) : live.length === 0 && previews.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."
title="No active servers"
hint="Once you deploy an app or start a dev server, its runtime logs will appear here."
/>
</div>
) : (
@@ -145,59 +159,51 @@ export default function LogsPage() {
flexDirection: "column",
borderRight: `1px solid ${THEME.borderSoft}`,
overflowY: "auto",
paddingBottom: 24,
}}
>
<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,
}}
>
{live.length > 0 && (
<div style={{ padding: "16px 20px 8px" }}>
<h3
style={{
fontSize: "0.95rem",
fontSize: "0.7rem",
fontWeight: 600,
color: THEME.ink,
color: THEME.muted,
textTransform: "uppercase",
letterSpacing: "0.06em",
margin: 0,
}}
>
Apps
Live Apps
</h3>
</header>
</div>
)}
<div
style={{
display: "flex",
flexDirection: "column",
gap: 4,
padding: "0 20px 20px 20px",
padding: "0 20px",
}}
>
{live.map((app) => (
<button
key={app.uuid}
onClick={() => setActiveUuid(app.uuid)}
onClick={() => setActiveItem({ type: "app", id: app.uuid })}
style={{
textAlign: "left",
padding: "10px 14px",
padding: "8px 12px",
background:
activeUuid === app.uuid
activeItem?.id === 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,
fontWeight: activeItem?.id === app.uuid ? 500 : 400,
color:
activeItem?.id === app.uuid ? THEME.ink : THEME.mid,
transition: "all 0.1s ease",
}}
>
@@ -205,6 +211,58 @@ export default function LogsPage() {
</button>
))}
</div>
{previews.length > 0 && (
<div style={{ padding: "24px 20px 8px" }}>
<h3
style={{
fontSize: "0.7rem",
fontWeight: 600,
color: THEME.muted,
textTransform: "uppercase",
letterSpacing: "0.06em",
margin: 0,
}}
>
Dev Previews
</h3>
</div>
)}
<div
style={{
display: "flex",
flexDirection: "column",
gap: 4,
padding: "0 20px",
}}
>
{previews.map((preview) => (
<button
key={preview.id}
onClick={() =>
setActiveItem({ type: "preview", id: preview.id })
}
style={{
textAlign: "left",
padding: "8px 12px",
background:
activeItem?.id === preview.id
? THEME.subtleBg
: "transparent",
border: "1px solid transparent",
borderRadius: THEME.radiusSm,
cursor: "pointer",
fontSize: "0.875rem",
fontWeight: activeItem?.id === preview.id ? 500 : 400,
color:
activeItem?.id === preview.id ? THEME.ink : THEME.mid,
transition: "all 0.1s ease",
}}
>
{preview.name || `Port ${preview.port}`}
</button>
))}
</div>
</div>
{/* Log Viewer Column */}
@@ -236,7 +294,11 @@ export default function LogsPage() {
color: THEME.mid,
}}
>
{live.find((a) => a.uuid === activeUuid)?.name ?? "Logs"}
{activeItem?.type === "app"
? (live.find((a) => a.uuid === activeItem.id)?.name ??
"Logs")
: previews.find((p) => p.id === activeItem?.id)?.name ||
"Dev Server Logs"}
</span>
<SecondaryButton
icon={
@@ -246,7 +308,7 @@ export default function LogsPage() {
<RefreshCw size={13} />
)
}
onClick={() => activeUuid && fetchLogs(activeUuid)}
onClick={() => activeItem && fetchLogs(activeItem)}
disabled={logsLoading}
style={{ padding: "4px 8px", fontSize: "0.75rem" }}
>