feat(logs): add Database logs and Build logs to the Server Logs dashboard page
This commit is contained in:
@@ -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<string | null>(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<string, any> = { 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() {
|
||||
>
|
||||
<Loader2 size={15} className="animate-spin" /> Loading…
|
||||
</div>
|
||||
) : live.length === 0 && previews.length === 0 ? (
|
||||
) : live.length === 0 &&
|
||||
previews.length === 0 &&
|
||||
databases.length === 0 &&
|
||||
deployments.length === 0 ? (
|
||||
<div style={{ padding: 24 }}>
|
||||
<EmptyState
|
||||
icon={<Activity size={22} />}
|
||||
@@ -189,7 +237,13 @@ export default function LogsPage() {
|
||||
{live.map((app) => (
|
||||
<button
|
||||
key={app.uuid}
|
||||
onClick={() => setActiveItem({ type: "app", id: app.uuid })}
|
||||
onClick={() =>
|
||||
setActiveItem({
|
||||
type: "app",
|
||||
id: app.uuid,
|
||||
name: app.name,
|
||||
})
|
||||
}
|
||||
style={{
|
||||
textAlign: "left",
|
||||
padding: "8px 12px",
|
||||
@@ -240,7 +294,11 @@ export default function LogsPage() {
|
||||
<button
|
||||
key={preview.id}
|
||||
onClick={() =>
|
||||
setActiveItem({ type: "preview", id: preview.id })
|
||||
setActiveItem({
|
||||
type: "preview",
|
||||
id: preview.id,
|
||||
name: preview.name || `Port ${preview.port}`,
|
||||
})
|
||||
}
|
||||
style={{
|
||||
textAlign: "left",
|
||||
@@ -263,6 +321,117 @@ export default function LogsPage() {
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{databases.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,
|
||||
}}
|
||||
>
|
||||
Databases
|
||||
</h3>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 4,
|
||||
padding: "0 20px",
|
||||
}}
|
||||
>
|
||||
{databases.map((db) => (
|
||||
<button
|
||||
key={db.uuid}
|
||||
onClick={() =>
|
||||
setActiveItem({
|
||||
type: "database",
|
||||
id: db.uuid,
|
||||
name: db.name,
|
||||
})
|
||||
}
|
||||
style={{
|
||||
textAlign: "left",
|
||||
padding: "8px 12px",
|
||||
background:
|
||||
activeItem?.id === db.uuid
|
||||
? THEME.subtleBg
|
||||
: "transparent",
|
||||
border: "1px solid transparent",
|
||||
borderRadius: THEME.radiusSm,
|
||||
cursor: "pointer",
|
||||
fontSize: "0.875rem",
|
||||
fontWeight: activeItem?.id === db.uuid ? 500 : 400,
|
||||
color: activeItem?.id === db.uuid ? THEME.ink : THEME.mid,
|
||||
transition: "all 0.1s ease",
|
||||
}}
|
||||
>
|
||||
{db.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{deployments.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,
|
||||
}}
|
||||
>
|
||||
Build Logs
|
||||
</h3>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 4,
|
||||
padding: "0 20px",
|
||||
}}
|
||||
>
|
||||
{deployments.map((dep) => (
|
||||
<button
|
||||
key={dep.uuid}
|
||||
onClick={() =>
|
||||
setActiveItem({
|
||||
type: "deployment",
|
||||
id: dep.uuid,
|
||||
name: dep.name,
|
||||
})
|
||||
}
|
||||
style={{
|
||||
textAlign: "left",
|
||||
padding: "8px 12px",
|
||||
background:
|
||||
activeItem?.id === dep.uuid
|
||||
? THEME.subtleBg
|
||||
: "transparent",
|
||||
border: "1px solid transparent",
|
||||
borderRadius: THEME.radiusSm,
|
||||
cursor: "pointer",
|
||||
fontSize: "0.875rem",
|
||||
fontWeight: activeItem?.id === dep.uuid ? 500 : 400,
|
||||
color:
|
||||
activeItem?.id === dep.uuid ? THEME.ink : THEME.mid,
|
||||
transition: "all 0.1s ease",
|
||||
}}
|
||||
>
|
||||
{dep.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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"}
|
||||
</span>
|
||||
<SecondaryButton
|
||||
icon={
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
getWorkspaceGcsHmacCredentials,
|
||||
} from "@/lib/workspace-gcs";
|
||||
import { VIBN_GCS_LOCATION } from "@/lib/gcp/storage";
|
||||
import { coolifyFetch } from "@/lib/coolify";
|
||||
import { getApplicationRuntimeLogs } from "@/lib/coolify-logs";
|
||||
import { callVibnChat } from "@/lib/ai/vibn-chat-model";
|
||||
import { execInCoolifyApp } from "@/lib/coolify-exec";
|
||||
@@ -375,6 +376,10 @@ export async function POST(request: Request) {
|
||||
return await toolDatabasesUpdate(principal, params);
|
||||
case "databases.delete":
|
||||
return await toolDatabasesDelete(principal, params);
|
||||
case "databases.logs":
|
||||
return await toolDatabasesLogs(principal, params);
|
||||
case "deployments.logs":
|
||||
return await toolDeploymentsLogs(principal, params);
|
||||
|
||||
case "auth.list":
|
||||
return await toolAuthList(principal);
|
||||
@@ -5270,6 +5275,67 @@ async function toolFsGrep(principal: Principal, params: Record<string, any>) {
|
||||
});
|
||||
}
|
||||
|
||||
async function toolDatabasesLogs(
|
||||
principal: Principal,
|
||||
params: Record<string, any>,
|
||||
) {
|
||||
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<string, any>,
|
||||
) {
|
||||
// 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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user