Add global Deployments dashboard with expandable logs
- New Deployments tab showing all Coolify deployments - TanStack Table with sorting, filtering, pagination - Status badges (Ready/Building/Error/Queued/Cancelled) - Application and status filter dropdowns - Expandable rows showing build logs in real-time - Auto-refresh every 10 seconds when tab is active - Log polling every 2 seconds for in-progress deployments - API routes that query Coolify database directly via docker exec Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
97
src/app/api/deployments/[uuid]/route.ts
Normal file
97
src/app/api/deployments/[uuid]/route.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
const IS_PRODUCTION = process.env.NODE_ENV === 'production';
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ uuid: string }> }
|
||||
) {
|
||||
const { uuid } = await params;
|
||||
|
||||
try {
|
||||
const { exec } = await import('child_process');
|
||||
const { promisify } = await import('util');
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
// PHP code to fetch single deployment with logs
|
||||
const phpCode = `
|
||||
$d = \\App\\Models\\ApplicationDeploymentQueue::with('application')
|
||||
->where('deployment_uuid', '${uuid}')
|
||||
->first();
|
||||
|
||||
if (!$d) {
|
||||
echo json_encode(['error' => 'Not found']);
|
||||
exit;
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'deployment_uuid' => $d->deployment_uuid,
|
||||
'application_uuid' => $d->application?->uuid ?? 'unknown',
|
||||
'application_name' => $d->application?->name ?? 'Unknown',
|
||||
'status' => $d->status,
|
||||
'created_at' => $d->created_at->toIso8601String(),
|
||||
'updated_at' => $d->updated_at->toIso8601String(),
|
||||
'git_branch' => $d->application?->git_branch ?? 'main',
|
||||
'git_commit_sha' => $d->commit ?? null,
|
||||
'commit_message' => $d->commit_message ?? null,
|
||||
'is_webhook' => $d->is_webhook ?? false,
|
||||
'logs' => $d->logs ?? null,
|
||||
]);
|
||||
`;
|
||||
|
||||
const base64Code = Buffer.from(phpCode).toString('base64');
|
||||
|
||||
let command: string;
|
||||
if (IS_PRODUCTION) {
|
||||
command = `echo '${base64Code}' | base64 -d | docker exec -i coolify php artisan tinker`;
|
||||
} else {
|
||||
command = `ssh nuc "echo '${base64Code}' | base64 -d | docker exec -i coolify php artisan tinker"`;
|
||||
}
|
||||
|
||||
const { stdout } = await execAsync(command, {
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
// Parse output - find JSON object in tinker output
|
||||
const lines = stdout.split('\n');
|
||||
let jsonStr = '';
|
||||
|
||||
for (const line of lines) {
|
||||
let cleaned = line;
|
||||
if (cleaned.startsWith('. ')) {
|
||||
cleaned = cleaned.substring(2);
|
||||
} else if (cleaned.startsWith('> ')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const trimmed = cleaned.trim();
|
||||
if (trimmed.startsWith('{')) {
|
||||
jsonStr = trimmed;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!jsonStr) {
|
||||
throw new Error('No JSON output found');
|
||||
}
|
||||
|
||||
const deployment = JSON.parse(jsonStr);
|
||||
|
||||
if (deployment.error) {
|
||||
return NextResponse.json({ error: deployment.error }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json(deployment, {
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching deployment:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch deployment', details: String(error) },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user