Use HTTP API for production deployment data

- Production fetches from local coolify-api.py at port 9876
- Development continues using SSH to query Coolify database
- Avoids need for docker socket access in nuc-portal container

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-02-02 01:56:06 +00:00
parent 58308c9c62
commit 73ac2ddc21
2 changed files with 94 additions and 74 deletions

View File

@@ -1,6 +1,8 @@
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
const IS_PRODUCTION = process.env.NODE_ENV === 'production'; const IS_PRODUCTION = process.env.NODE_ENV === 'production';
// Internal API endpoint for production (served by Python script on NUC host)
const DEPLOYMENTS_API_URL = 'http://192.168.1.3:9876/deployments';
export async function GET( export async function GET(
request: Request, request: Request,
@@ -9,12 +11,28 @@ export async function GET(
const { uuid } = await params; const { uuid } = await params;
try { try {
const { exec } = await import('child_process'); let deployment: Record<string, unknown>;
const { promisify } = await import('util');
const execAsync = promisify(exec);
// PHP code to fetch single deployment with logs if (IS_PRODUCTION) {
const phpCode = ` // In production, use internal HTTP API served by coolify-api.py on NUC host
const response = await fetch(`${DEPLOYMENTS_API_URL}/${uuid}`, {
cache: 'no-store',
signal: AbortSignal.timeout(30000),
});
if (!response.ok) {
throw new Error(`Deployments API error: ${response.status}`);
}
deployment = await response.json();
} else {
// In development, use SSH to call docker exec on NUC
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') $d = \\App\\Models\\ApplicationDeploymentQueue::with('application')
->where('deployment_uuid', '${uuid}') ->where('deployment_uuid', '${uuid}')
->first(); ->first();
@@ -39,45 +57,40 @@ echo json_encode([
]); ]);
`; `;
const base64Code = Buffer.from(phpCode).toString('base64'); const base64Code = Buffer.from(phpCode).toString('base64');
const command = `ssh nuc "echo '${base64Code}' | base64 -d | docker exec -i coolify php artisan tinker"`;
let command: string; const { stdout } = await execAsync(command, {
if (IS_PRODUCTION) { maxBuffer: 10 * 1024 * 1024,
command = `echo '${base64Code}' | base64 -d | docker exec -i coolify php artisan tinker`; timeout: 30000,
} else { });
command = `ssh nuc "echo '${base64Code}' | base64 -d | docker exec -i coolify php artisan tinker"`;
}
const { stdout } = await execAsync(command, { // Parse output - find JSON object in tinker output
maxBuffer: 10 * 1024 * 1024, const lines = stdout.split('\n');
timeout: 30000, let jsonStr = '';
});
// Parse output - find JSON object in tinker output for (const line of lines) {
const lines = stdout.split('\n'); let cleaned = line;
let jsonStr = ''; if (cleaned.startsWith('. ')) {
cleaned = cleaned.substring(2);
} else if (cleaned.startsWith('> ')) {
continue;
}
for (const line of lines) { const trimmed = cleaned.trim();
let cleaned = line; if (trimmed.startsWith('{')) {
if (cleaned.startsWith('. ')) { jsonStr = trimmed;
cleaned = cleaned.substring(2); break;
} else if (cleaned.startsWith('> ')) { }
continue;
} }
const trimmed = cleaned.trim(); if (!jsonStr) {
if (trimmed.startsWith('{')) { throw new Error('No JSON output found');
jsonStr = trimmed;
break;
} }
}
if (!jsonStr) { deployment = JSON.parse(jsonStr);
throw new Error('No JSON output found');
} }
const deployment = JSON.parse(jsonStr);
if (deployment.error) { if (deployment.error) {
return NextResponse.json({ error: deployment.error }, { status: 404 }); return NextResponse.json({ error: deployment.error }, { status: 404 });
} }

View File

@@ -2,14 +2,31 @@ import { NextResponse } from 'next/server';
import type { Deployment, DeploymentStatus } from '@/lib/deployments'; import type { Deployment, DeploymentStatus } from '@/lib/deployments';
const IS_PRODUCTION = process.env.NODE_ENV === 'production'; const IS_PRODUCTION = process.env.NODE_ENV === 'production';
// Internal API endpoint for production (served by Python script on NUC host)
const DEPLOYMENTS_API_URL = 'http://192.168.1.3:9876/deployments';
async function fetchDeploymentsFromCoolify(): Promise<Deployment[]> { async function fetchDeploymentsFromCoolify(): Promise<Deployment[]> {
const { exec } = await import('child_process'); let rawDeployments: Array<Record<string, unknown>>;
const { promisify } = await import('util');
const execAsync = promisify(exec);
// Base64 encode the PHP code to avoid escaping issues if (IS_PRODUCTION) {
const phpCode = ` // In production, use internal HTTP API served by coolify-api.py on NUC host
const response = await fetch(DEPLOYMENTS_API_URL, {
cache: 'no-store',
signal: AbortSignal.timeout(30000),
});
if (!response.ok) {
throw new Error(`Deployments API error: ${response.status}`);
}
rawDeployments = await response.json();
} else {
// In development, use SSH to call docker exec on NUC
const { exec } = await import('child_process');
const { promisify } = await import('util');
const execAsync = promisify(exec);
const phpCode = `
$deployments = \\App\\Models\\ApplicationDeploymentQueue::with('application') $deployments = \\App\\Models\\ApplicationDeploymentQueue::with('application')
->orderBy('created_at', 'desc') ->orderBy('created_at', 'desc')
->limit(50) ->limit(50)
@@ -33,51 +50,41 @@ $result = $deployments->map(function($d) {
echo json_encode($result->toArray()); echo json_encode($result->toArray());
`; `;
const base64Code = Buffer.from(phpCode).toString('base64'); const base64Code = Buffer.from(phpCode).toString('base64');
const command = `ssh nuc "echo '${base64Code}' | base64 -d | docker exec -i coolify php artisan tinker"`;
let command: string; const { stdout } = await execAsync(command, {
if (IS_PRODUCTION) { maxBuffer: 10 * 1024 * 1024,
// Running on NUC - direct docker exec timeout: 30000,
command = `echo '${base64Code}' | base64 -d | docker exec -i coolify php artisan tinker`; });
} else {
// Running locally - SSH to NUC
command = `ssh nuc "echo '${base64Code}' | base64 -d | docker exec -i coolify php artisan tinker"`;
}
const { stdout } = await execAsync(command, { // Parse tinker output to find JSON
maxBuffer: 10 * 1024 * 1024, // 10MB buffer const lines = stdout.split('\n');
timeout: 30000, // 30 second timeout let jsonStr = '';
});
// The output contains tinker prompts (lines starting with > or .) followed by JSON for (const line of lines) {
// Tinker outputs ". " prefix on continuation lines and the result let cleaned = line;
const lines = stdout.split('\n'); if (cleaned.startsWith('. ')) {
let jsonStr = ''; cleaned = cleaned.substring(2);
} else if (cleaned.startsWith('> ')) {
continue;
}
for (const line of lines) { const trimmed = cleaned.trim();
// Remove tinker prompt prefixes if (trimmed.startsWith('[{') || trimmed.startsWith('[{"') || trimmed === '[]') {
let cleaned = line; jsonStr = trimmed;
if (cleaned.startsWith('. ')) { break;
cleaned = cleaned.substring(2); }
} else if (cleaned.startsWith('> ')) {
continue; // Skip command echo lines
} }
const trimmed = cleaned.trim(); if (!jsonStr) {
// Look for line starting with [{ which indicates JSON array of objects console.error('Raw output:', stdout.substring(0, 1000));
if (trimmed.startsWith('[{') || trimmed.startsWith('[{"') || trimmed === '[]') { throw new Error('No JSON output found in tinker response');
jsonStr = trimmed;
break;
} }
}
if (!jsonStr) { rawDeployments = JSON.parse(jsonStr);
console.error('Raw output:', stdout.substring(0, 1000));
throw new Error('No JSON output found in tinker response');
} }
const rawDeployments = JSON.parse(jsonStr);
// Track latest deployment per application for "Current" badge // Track latest deployment per application for "Current" badge
const latestByApp = new Map<string, string>(); const latestByApp = new Map<string, string>();