diff --git a/.gitignore b/.gitignore index 2309cc8..9fe585b 100644 --- a/.gitignore +++ b/.gitignore @@ -136,3 +136,9 @@ dist .yarn/install-state.gz .pnp.* +# Cursor (AI editor) local state +.cursor/ + +# JetBrains IDE local state +.idea/ + diff --git a/README.md b/README.md index 67f8277..16b7d6d 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,86 @@ # sf-monitorizacion-health +Aplicación Node.js/Express para monitorizar el endpoint de salud (`url_health`) de un conjunto de proyectos y mostrar un dashboard con el **último estado** registrado en PostgreSQL. + +## Qué hace + +1. Lee todos los registros de `proyectos`. +2. Para cada proyecto hace un `GET` a `url_health` con un `timeout` de 5s. +3. Guarda un registro en `estados` con: + - `OK` si la respuesta HTTP es `200` + - `ERROR` si la respuesta no es `200` (o cualquier error distinto) + - `TIMEOUT` si el error es `ECONNABORTED` +4. El dashboard (`GET /`) muestra, por proyecto, el **último** registro de `estados`. + +El job se ejecuta de forma periódica (cada 60s) en bucle recursivo para evitar solapamientos. + +## Requisitos + +- Node.js 18+ (recomendado) +- PostgreSQL + +## Configuración + +La app lee estas variables de entorno (consultadas desde `src/config/postgreConfig.ts`): + +- `DB_HOST` +- `DB_PORT` +- `DB_USER` +- `DB_PASSWORD` +- `DB_NAME` + +Ejemplo `.env`: + +```env +DB_HOST=localhost +DB_PORT=5432 +DB_USER=postgres +DB_PASSWORD=postgres +DB_NAME=sf_monitorizacion_health +``` + +## Base de datos + +La migración `deployment/database/migrations/001_init.sql` crea: + +- `proyectos` +- tipo ENUM `tipo_estado` con `OK`, `ERROR`, `TIMEOUT` +- `estados` +- índice para consultar rápido el último estado por proyecto + +> Nota: la migración hace `DROP TABLE IF EXISTS`, así que destruye los datos actuales. + +## Cómo ejecutar + +1. Instala dependencias: + - `npm install` +2. Asegúrate de tener creada la BD (ejecuta `deployment/database/migrations/001_init.sql`). +3. Ejecuta en modo desarrollo: + - `npm run dev` + +La app levanta el servidor en `http://localhost:3000`. + +## Dashboard + +Ruta: +- `GET /` : renderiza `src/views/index.ejs` + +En la tabla se muestra: +- Nombre del proyecto (`proyectos.nombre`) +- URL (`proyectos.url_health`) +- Estado (`estados.estado_codigo` del último registro) +- “Última comprobación” (`estados.fecha`) + +Si un proyecto todavía no tiene estados registrados, el dashboard muestra `PENDIENTE`. + +## Añadir proyectos + +Inserta en `proyectos`: + +```sql +INSERT INTO proyectos (nombre, url_health) +VALUES ('Mi proyecto', 'https://ejemplo.com/health'); +``` + +Los estados empezarán a generarse automáticamente cuando arranque el servidor y empiece el ciclo de checks. + diff --git a/deployment/database/migrations/001_init.sql b/deployment/database/migrations/001_init.sql new file mode 100644 index 0000000..ede6375 --- /dev/null +++ b/deployment/database/migrations/001_init.sql @@ -0,0 +1,28 @@ +-- 1. Comprobando nombres de tablas, y creando tipo de forma seguro +DROP TABLE IF EXISTS estados; +DROP TABLE IF EXISTS proyectos; +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'tipo_estado') THEN + CREATE TYPE tipo_estado AS ENUM ('OK', 'ERROR', 'TIMEOUT'); + END IF; +END +$$; + +-- 2. Tabla de proyectos +CREATE TABLE proyectos ( + id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + nombre VARCHAR(100) NOT NULL, + url_health TEXT NOT NULL +); + +-- 3. Tabla de estados (con BIGINT y ENUM) +CREATE TABLE estados ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + proyecto_id INT REFERENCES proyectos(id), + estado_codigo tipo_estado NOT NULL, + fecha TIMESTAMPTZ NOT NULL DEFAULT now() +); + +-- 4. Índice para agilizar la búsqueda de los últimos estados +CREATE INDEX idx_estados_proyecto_fecha ON estados (proyecto_id, fecha DESC); diff --git a/src/application/sim-controller.ts b/src/application/sim-controller.ts index c942f9c..343da21 100644 --- a/src/application/sim-controller.ts +++ b/src/application/sim-controller.ts @@ -6,7 +6,7 @@ import path from 'path'; export class SimController { constructor(private usecases: SimUsecases) {} - renderDashboard = async (req: Request, res: Response, next: NextFunction) => { + 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'); diff --git a/src/index.ts b/src/index.ts index 39dff5b..55fd278 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,15 +1,52 @@ -import express, { Request, Response, NextFunction } from "express"; +import express from "express"; +import type { Request, Response, NextFunction } from "express"; +import path from "path"; +import { createRouter } from "./infrastructure/sim-routes-http.js"; +import { SimUsecases } from "./application/sim-usecases.js"; +import { pool } from "./config/postgreConfig.js"; +import { AppError } from "./domain/common.js"; + const app = express(); const port = 3000; -app.use(express.json()); -app.get("/", (req: Request, res: Response) => { - res.send({ message: "Hello World" }); -}); -app.use((err: Error, req: Request, res: Response, next: NextFunction) => { - res.status(500).send("Algo salió mal"); +app.use(express.json()); + +app.set("view engine", "ejs"); +app.set("views", path.join(process.cwd(), "src/views")); + +const monitorJob = new SimUsecases(pool); + +app.use("/", createRouter(monitorJob)); + +const poll = async () => { + try { + await monitorJob.checkAllProjectsHealth(); + } catch (err) { + console.error(`[Background Job Error]: ${err instanceof Error ? err.message : String(err)}`); + } finally { + setTimeout(poll, 300000); + } +}; + +void poll(); + +app.use((err: Error, req: Request, res: Response, _next: NextFunction) => { + const status = err instanceof AppError ? err.status : 500; + const details = err instanceof AppError ? err.details : {}; + + console.error(`[Error ${status}][${err.name}]: ${err.message}`, details); + if (req.accepts("html")) { + return res.status(status).render("error", { + status, + message: err.message + }); + } + res.status(status).json({ + error: err.message, + status + }); }); app.listen(port, () => { console.log(`Server is running on port ${port}`); -}); +}); \ No newline at end of file diff --git a/src/views/error.ejs b/src/views/error.ejs new file mode 100644 index 0000000..3a180a1 --- /dev/null +++ b/src/views/error.ejs @@ -0,0 +1,19 @@ + + + + + + Error + + + + <%# Vista genérica para captura de rrores mediante el middleware de Express %> +

Error

+

<%= message %>

+ Volver al inicio + + diff --git a/src/views/index.ejs b/src/views/index.ejs new file mode 100644 index 0000000..028e84b --- /dev/null +++ b/src/views/index.ejs @@ -0,0 +1,56 @@ + + + + + + Monitor de Salud + + + +

Monitor de Salud

+ + + + + + + + + + + <%# Iteración de la lista de proyectos inyectada de desde el controlador. %> + <% proyectos.forEach(function(p) { %> + + + + <%# La clase CSS coincide con el estado_codigo para aplicar el color correspondiente %> + + + + <% }) %> + +
ProyectoURLEstadoÚltima comprobación
<%= p.nombre %> + + <%= p.url_health %> + + <%= p.estado_codigo || 'PENDIENTE' %> + + <%# Formateo de fecha en servidor antes de renderizar %> + <%= p.fecha ? new Date(p.fecha).toLocaleString('es-ES') : '—' %> +
+ +