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:
@@ -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 });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user