diff --git a/src/application/sim-controller.ts b/src/application/sim-controller.ts new file mode 100644 index 0000000..c942f9c --- /dev/null +++ b/src/application/sim-controller.ts @@ -0,0 +1,21 @@ +import { type Request, type Response, type NextFunction } from 'express'; +import { SimUsecases } from './sim-usecases.js'; +import ejs from 'ejs'; +import path from 'path'; + +export class SimController { + constructor(private usecases: SimUsecases) {} + + renderDashboard = async (req: Request, res: Response, next: NextFunction) => { + try { + const proyectos = await this.usecases.getDashboardData(); + const filePath = path.join(process.cwd(), 'src/views/index.ejs'); + ejs.renderFile(filePath, { proyectos }, (err, str) => { + if (err) return next(err); + res.send(str); + }); + } catch (err) { + next(err); + } + } +} diff --git a/src/application/sim-usecases.ts b/src/application/sim-usecases.ts new file mode 100644 index 0000000..936cd0f --- /dev/null +++ b/src/application/sim-usecases.ts @@ -0,0 +1,57 @@ +import axios from 'axios'; +import { type Pool } from 'pg'; +import { type Proyecto, RepositoryError } from '../domain/common.js'; + +export class SimUsecases { + constructor(private db: Pool) {} + + async checkAllProjectsHealth(): Promise { + try { + const { rows: projects } = await this.db.query('SELECT * FROM proyectos'); + + const checks = projects.map(async (p) => { + let status: Proyecto['estado_codigo'] = 'OK'; + try { + // Solo 200 es válido: cualquier otro código (301, 403, 5xx) indica un problema real del servicio. + await axios.get(p.url_health, { timeout: 5000, validateStatus: (s) => s === 200 }); + } catch (e) { + const axiosError = e as { code?: string }; + status = axiosError.code === 'ECONNABORTED' ? 'TIMEOUT' : 'ERROR'; + } + + return this.db.query( + 'INSERT INTO estados (proyecto_id, estado_codigo, fecha) VALUES ($1, $2, NOW())', + [p.id, status] + ); + }); + + // allSettled en lugar de all: un fallo al insertar en BD no debe cancelar el resto de checks. + await Promise.allSettled(checks); + } catch (err) { + throw new RepositoryError('Fallo masivo en el chequeo de salud', { cause: err instanceof Error ? err.message : String(err) }); + } + } + + async getDashboardData(): Promise { + // LEFT JOIN LATERAL nos deja calcular "la última fila de estados" para cada proyecto. + // Usamos LEFT JOIN para no perder proyectos que todavía no tienen estados registrados. + const query = ` + SELECT p.id, p.nombre, p.url_health, e.estado_codigo, e.fecha + FROM proyectos p + LEFT JOIN LATERAL ( + SELECT estado_codigo, fecha + FROM estados + WHERE proyecto_id = p.id + ORDER BY fecha DESC + LIMIT 1 + ) e ON true + ORDER BY p.nombre ASC; + `; + try { + const { rows } = await this.db.query(query); + return rows; + } catch (err) { + throw new RepositoryError('Error al recuperar datos del dashboard', { cause: err instanceof Error ? err.message : String(err) }); + } + } +} diff --git a/src/config/postgreConfig.ts b/src/config/postgreConfig.ts index 43eb035..b71db16 100644 --- a/src/config/postgreConfig.ts +++ b/src/config/postgreConfig.ts @@ -4,9 +4,9 @@ import dotenv from 'dotenv'; dotenv.config(); export const pool = new pg.Pool({ - host: process.env.DB_HOST || 'localhost', - port: Number(process.env.DB_PORT) || 5432, - user: process.env.DB_USER || 'postgres', - password: process.env.DB_PASSWORD || 'password', - database: process.env.DB_NAME || 'monitor_db' -}); \ No newline at end of file + host: process.env.DB_HOST, + port: Number(process.env.DB_PORT), + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME +}); diff --git a/src/domain/common.ts b/src/domain/common.ts index 0262007..4d5330e 100644 --- a/src/domain/common.ts +++ b/src/domain/common.ts @@ -4,8 +4,6 @@ export class AppError extends Error { this.name = 'AppError'; } } - -export class ValidationError extends AppError { constructor(msg: string, details = {}) { super(msg, 400, details); this.name = 'ValidationError'; } } export class RepositoryError extends AppError { constructor(msg: string, details = {}) { super(msg, 500, details); this.name = 'RepositoryError'; } } export interface Proyecto { diff --git a/src/infrastructure/sim-routes-http.ts b/src/infrastructure/sim-routes-http.ts new file mode 100644 index 0000000..1764b80 --- /dev/null +++ b/src/infrastructure/sim-routes-http.ts @@ -0,0 +1,10 @@ +import { Router } from 'express'; +import { SimController } from '../application/sim-controller.js'; +import { SimUsecases } from '../application/sim-usecases.js'; + +export function createRouter(usecases: SimUsecases): Router { + const router = Router(); + const controller = new SimController(usecases); + router.get('/', controller.renderDashboard); + return router; +} \ No newline at end of file