Replace polling with real-time SSE stream and eliminate Python API dependency
- Add single /api/events SSE endpoint replacing 5 separate polling intervals - Query Prometheus directly for system stats (replaces Python API on port 9876) - Query Coolify PostgreSQL directly for deployments (replaces SSH/tinker approach) - Add EventManager singleton for server-side polling + client broadcast - Add useEventStream hook with exponential backoff reconnection - Add live deployment log streaming via SSE for in-progress builds - Add redeploy button and live duration counter in deployments table - Add SSE connection indicator in header (green=live, red=offline) - Externalize all hardcoded 192.168.1.3 references to env vars via config.ts - Reduce API route code by ~400 lines through shared library modules Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
128
src/lib/coolify-db.ts
Normal file
128
src/lib/coolify-db.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import pg from 'pg';
|
||||
import { serverConfig } from './config';
|
||||
import type { Deployment, DeploymentStatus } from './deployments';
|
||||
|
||||
const { Pool } = pg;
|
||||
|
||||
let pool: pg.Pool | null = null;
|
||||
|
||||
function getPool(): pg.Pool {
|
||||
if (!pool) {
|
||||
pool = new Pool({
|
||||
connectionString: 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<string, unknown>): 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,
|
||||
logs: (row.logs as string) || undefined,
|
||||
duration,
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchDeployments(limit = 50): Promise<Deployment[]> {
|
||||
const db = getPool();
|
||||
const { rows } = await db.query(`
|
||||
SELECT
|
||||
q.deployment_uuid,
|
||||
a.uuid AS application_uuid,
|
||||
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
|
||||
FROM application_deployment_queues q
|
||||
LEFT JOIN applications a ON a.id = q.application_id
|
||||
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<string, string>();
|
||||
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<Deployment | null> {
|
||||
const db = getPool();
|
||||
const { rows } = await db.query(`
|
||||
SELECT
|
||||
q.deployment_uuid,
|
||||
a.uuid AS application_uuid,
|
||||
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.logs
|
||||
FROM application_deployment_queues q
|
||||
LEFT JOIN applications a ON a.id = q.application_id
|
||||
WHERE q.deployment_uuid = $1
|
||||
`, [uuid]);
|
||||
|
||||
if (rows.length === 0) return null;
|
||||
return rowToDeployment(rows[0]);
|
||||
}
|
||||
|
||||
export async function fetchActiveDeploymentLogs(): Promise<Array<{ uuid: string; logs: string; status: string }>> {
|
||||
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 }>;
|
||||
}
|
||||
Reference in New Issue
Block a user