import pg from 'pg'; import { serverConfig } from './config'; import type { Deployment, DeploymentStatus } from './deployments'; const { Pool } = pg; let pool: pg.Pool | null = null; function parseDbUrl(url: string): pg.PoolConfig { // Manual parsing to handle special chars in password (/, =, etc.) const match = url.match(/^postgres(?:ql)?:\/\/([^:]+):(.+)@([^:]+):(\d+)\/(.+)$/); if (match) { return { user: match[1], password: match[2], host: match[3], port: parseInt(match[4]), database: match[5] }; } return { connectionString: url }; } function getPool(): pg.Pool { if (!pool) { pool = new Pool({ ...parseDbUrl(serverConfig.coolifyDbUrl), max: 3, idleTimeoutMillis: 30000, connectionTimeoutMillis: 5000, }); pool.on('error', (err) => { console.error('Coolify DB pool error:', err); }); } return pool; } function mapStatus(status: string): DeploymentStatus { if (status === 'failed') return 'error'; if (status === 'cancelled-by-user') return 'cancelled'; return status as DeploymentStatus; } function rowToDeployment(row: Record): Deployment { const createdAt = row.created_at as string; const updatedAt = row.updated_at as string; let duration: number | undefined; if (createdAt && updatedAt) { const start = new Date(createdAt).getTime(); const end = new Date(updatedAt).getTime(); duration = Math.max(0, Math.floor((end - start) / 1000)); } return { deployment_uuid: row.deployment_uuid as string, application_uuid: (row.application_uuid as string) || 'unknown', application_name: (row.application_name as string) || 'Unknown App', application_fqdn: (row.application_fqdn as string) || undefined, status: mapStatus(row.status as string), created_at: createdAt, updated_at: updatedAt, git_branch: (row.git_branch as string) || 'main', git_commit_sha: (row.commit as string) || undefined, commit_message: (row.commit_message as string) || undefined, is_webhook: row.is_webhook as boolean | undefined, is_api: row.is_api as boolean | undefined, logs: (row.logs as string) || undefined, duration, }; } export async function fetchDeployments(limit = 50): Promise { const db = getPool(); const { rows } = await db.query(` SELECT q.deployment_uuid, a.uuid AS application_uuid, COALESCE(q.application_name, a.name) AS application_name, a.fqdn AS application_fqdn, q.status, q.created_at, q.updated_at, a.git_branch, q.commit, q.commit_message, q.is_webhook, q.is_api FROM application_deployment_queues q LEFT JOIN applications a ON a.id = q.application_id::bigint ORDER BY q.created_at DESC LIMIT $1 `, [limit]); const deployments = rows.map(rowToDeployment); // Mark latest finished deployment per app as current const latestByApp = new Map(); for (const d of deployments) { if (d.status === 'finished' && !latestByApp.has(d.application_uuid)) { latestByApp.set(d.application_uuid, d.deployment_uuid); } } for (const d of deployments) { d.is_current = latestByApp.get(d.application_uuid) === d.deployment_uuid; } return deployments; } export async function fetchDeploymentDetail(uuid: string): Promise { const db = getPool(); const { rows } = await db.query(` SELECT q.deployment_uuid, a.uuid AS application_uuid, COALESCE(q.application_name, a.name) AS application_name, a.fqdn AS application_fqdn, q.status, q.created_at, q.updated_at, a.git_branch, q.commit, q.commit_message, q.is_webhook, q.is_api, q.logs FROM application_deployment_queues q LEFT JOIN applications a ON a.id = q.application_id::bigint WHERE q.deployment_uuid = $1 `, [uuid]); if (rows.length === 0) return null; return rowToDeployment(rows[0]); } export async function fetchActiveDeploymentLogs(): Promise> { const db = getPool(); const { rows } = await db.query(` SELECT deployment_uuid AS uuid, logs, status FROM application_deployment_queues WHERE status IN ('in_progress', 'queued') ORDER BY created_at DESC `); return rows as Array<{ uuid: string; logs: string; status: string }>; }