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