Controladores rutas y vistas del dashboard

This commit is contained in:
Trabajo
2026-05-05 10:05:21 +02:00
parent 818a1899b0
commit d8522b49af
6 changed files with 286 additions and 60 deletions

View File

@@ -1,21 +1,74 @@
import { type Request, type Response, type NextFunction } from 'express';
import { MonitorScheduler } from './monitor-scheduler.js';
import { MonitorUsecases } from './monitor-usecases.js';
import ejs from 'ejs';
import path from 'path';
import { AppError } from '../domain/common.js';
export class MonitorController {
constructor(private usecases: MonitorUsecases) {}
constructor(
private usecases: MonitorUsecases,
private scheduler: MonitorScheduler
) {}
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);
res.render('index', {
proyectos,
monitorIntervalSeconds: this.scheduler.getIntervalSeconds()
});
} catch (err) {
next(err);
}
}
renderAddProject = async (_req: Request, res: Response, next: NextFunction) => {
try {
res.render('add-project');
} catch (err) {
next(err);
}
}
saveProject = async (req: Request, res: Response, next: NextFunction) => {
try {
const { nombre, url_health } = req.body;
await this.usecases.createNewProject(nombre, url_health);
res.redirect('/');
} catch (err) {
next(err);
}
}
checkAllProjects = async (_req: Request, res: Response, next: NextFunction) => {
try {
await this.usecases.checkAllProjectsHealth();
res.sendStatus(204);
} catch (err) {
next(err);
}
}
checkProject = async (req: Request, res: Response, next: NextFunction) => {
try {
const projectId = Number(req.params['id']);
if (!Number.isInteger(projectId) || projectId <= 0) {
throw new AppError('Id de proyecto invalido', 400);
}
await this.usecases.checkProjectHealth(projectId);
res.sendStatus(204);
} catch (err) {
next(err);
}
}
updateMonitorInterval = async (req: Request, res: Response, next: NextFunction) => {
try {
const seconds = Number(req.body.seconds);
this.scheduler.setIntervalSeconds(seconds);
res.json({ seconds: this.scheduler.getIntervalSeconds() });
} catch (err) {
next(err);
}
}
}

View File

@@ -2,33 +2,28 @@ import express from "express";
import type { Request, Response, NextFunction } from "express";
import path from "path";
import { createRouter } from "./infrastructure/monitor-routes-http.js";
import { MonitorScheduler } from "./application/monitor-scheduler.js";
import { MonitorUsecases } from "./application/monitor-usecases.js";
import { pool } from "./config/postgreConfig.js";
import { MonitorRepository } from "./repository/monitor-repository.js";
import { pool } from "./config/postgres-config.js";
import { AppError } from "./domain/common.js";
const app = express();
const port = 3000;
const port = Number(process.env.PORT) || 3000;
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.set("view engine", "ejs");
app.set("views", path.join(process.cwd(), "src/views"));
const monitorJob = new MonitorUsecases(pool);
const monitorRepository = new MonitorRepository(pool);
const monitorJob = new MonitorUsecases(monitorRepository);
const monitorScheduler = new MonitorScheduler(monitorJob, 300000);
app.use("/", createRouter(monitorJob));
app.use("/", createRouter(monitorJob, monitorScheduler));
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();
monitorScheduler.start();
app.use((err: Error, req: Request, res: Response, _next: NextFunction) => {
const status = err instanceof AppError ? err.status : 500;

View File

@@ -1,10 +1,18 @@
import { Router } from 'express';
import { MonitorController } from '../application/monitor-controller.js';
import { MonitorScheduler } from '../application/monitor-scheduler.js';
import { MonitorUsecases } from '../application/monitor-usecases.js';
export function createRouter(usecases: MonitorUsecases): Router {
export function createRouter(usecases: MonitorUsecases, scheduler: MonitorScheduler): Router {
const router = Router();
const controller = new MonitorController(usecases);
const controller = new MonitorController(usecases, scheduler);
router.get('/', controller.renderDashboard);
router.get('/nuevo', controller.renderAddProject);
router.post('/proyectos', controller.saveProject);
router.post('/check-all', controller.checkAllProjects);
router.post('/check/:id', controller.checkProject);
router.post('/monitor-interval', controller.updateMonitorInterval);
return router;
}

33
src/views/add-project.ejs Normal file
View File

@@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Nuevo Proyecto</title>
<style>
body { font-family: Arial, sans-serif; max-width: 600px; margin: 40px auto; padding: 20px; background: #f9f9f9; }
.card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
label { display: block; margin-bottom: 8px; font-weight: bold; }
input { width: 100%; padding: 10px; margin-bottom: 20px; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box; }
.actions { display: flex; justify-content: space-between; align-items: center; }
button { background: #2e7d32; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; }
a { color: #666; text-decoration: none; }
</style>
</head>
<body>
<div class="card">
<h1>Añadir Proyecto</h1>
<form action="/proyectos" method="POST">
<label>Nombre</label>
<input type="text" name="nombre" required placeholder="Nombre del servicio">
<label>URL de Health Check</label>
<input type="url" name="url_health" required placeholder="https://mi-api.com/health">
<div class="actions">
<a href="/">Volver</a>
<button type="submit">Guardar</button>
</div>
</form>
</div>
</body>
</html>

View File

@@ -11,7 +11,7 @@
</style>
</head>
<body>
<%# Vista genérica para captura de rrores mediante el middleware de Express %>
<%# Vista genérica para captura de errores mediante el middleware de Express %>
<h1>Error</h1>
<p><%= message %></p>
<a href="/">Volver al inicio</a>

View File

@@ -5,52 +5,189 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Monitor de Salud</title>
<style>
/* Estilos base para una interfaz limpia y centrada */
body { font-family: Arial, sans-serif; max-width: 1200px; margin: 0 auto; padding: 20px; background: #f9f9f9; }
h1 { color: #333; }
table { width: 100%; border-collapse: collapse; background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
td { max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
td { max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
th, td { border: 1px solid #ddd; padding: 12px; text-align: left; }
th { background: #f4f4f4; font-weight: bold; }
tr:hover { background: #fafafa; }
/* Estados visuales basados en la respuesta del monitor */
.OK { color: #2e7d32; font-weight: bold; }
.ERROR { color: #c62828; font-weight: bold; }
.TIMEOUT { color: #e65100; font-weight: bold; }
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
background: #fff;
padding: 15px;
border-radius: 4px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
gap: 16px;
}
.toolbar-actions {
display: flex;
align-items: center;
gap: 10px;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
font: inherit;
line-height: 1;
text-decoration: none;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
height: 36px;
box-sizing: border-box;
}
.btn svg {
width: 16px;
height: 16px;
flex: 0 0 auto;
}
.btn-add { background: #2e7d32; color: white; }
.btn-refresh { background: #0277bd; color: white; }
.btn-mini { padding: 4px 8px; font-size: 16px; }
.btn:disabled { background: #ccc; cursor: not-allowed; }
.refresh-group { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.status-message { color: #2e7d32; min-width: 70px; }
input { padding: 6px; border-radius: 4px; border: 1px solid #ddd; }
input[type="number"] { text-align: right; }
</style>
</head>
<body>
<h1>Monitor de Salud</h1>
<table>
<thead>
<tr>
<th>Proyecto</th>
<th>URL</th>
<th>Estado</th>
<th>Última comprobación</th>
</tr>
</thead>
<tbody>
<%# Iteración de la lista de proyectos inyectada de desde el controlador. %>
<% proyectos.forEach(function(p) { %>
<tr>
<td><%= p.nombre %></td>
<td>
<a href="<%= p.url_health %>" target="_blank" title="<%= p.url_health %>">
<%= p.url_health %>
</a>
</td>
<%# La clase CSS coincide con el estado_codigo para aplicar el color correspondiente %>
<td class="<%= p.estado_codigo || 'PENDIENTE' %>"><%= p.estado_codigo || 'PENDIENTE' %>
</td>
<td>
<%# Formateo de fecha en servidor antes de renderizar %>
<%= p.fecha ? new Date(p.fecha).toLocaleString('es-ES') : '—' %>
</td>
</tr>
<% }) %>
</tbody>
</table>
<h1>Monitor de Salud</h1>
<div class="toolbar">
<div class="toolbar-actions">
<a href="/nuevo" class="btn btn-add">
<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="9"></circle>
<path d="M12 8v8"></path>
<path d="M8 12h8"></path>
</svg>
Añadir Proyecto
</a>
<button class="btn btn-refresh" onclick="runCheck('/check-all', this)">Actualizar Todo</button>
</div>
<div class="refresh-group">
<label for="refreshSeconds">Comprobar estados cada:</label>
<input type="number" id="refreshSeconds" min="10" max="3600" value="<%= monitorIntervalSeconds %>" style="width: 60px;">
<span>segundos</span>
<button class="btn btn-mini" onclick="updateMonitorInterval(this)">Aplicar</button>
<span id="intervalStatus" class="status-message"></span>
</div>
</div>
<table>
<thead>
<tr>
<th>Proyecto</th>
<th>URL</th>
<th>Estado</th>
<th>Ultima comprobacion</th>
<th>Acciones</th>
</tr>
</thead>
<tbody>
<% proyectos.forEach(function(p) { %>
<tr>
<td><%= p.nombre %></td>
<td>
<a href="<%= p.url_health %>" target="_blank" title="<%= p.url_health %>">
<%= p.url_health %>
</a>
</td>
<td class="<%= p.estado_codigo || 'PENDIENTE' %>">
<%= p.estado_codigo || 'PENDIENTE' %>
</td>
<td>
<%= p.fecha ? new Date(p.fecha).toLocaleString('es-ES') : '-' %>
</td>
<td>
<button class="btn btn-refresh btn-mini" onclick="runCheck('/check/<%= p.id %>', this)">Refrescar</button>
</td>
</tr>
<% }) %>
</tbody>
</table>
<script>
const monitorIntervalSeconds = <%= monitorIntervalSeconds %>;
function schedulePageRefresh(seconds) {
setTimeout(() => {
location.reload();
}, seconds * 1000);
}
async function runCheck(url, btn) {
const originalText = btn.innerText;
btn.disabled = true;
btn.innerText = "...";
try {
const response = await fetch(url, { method: 'POST' });
if (response.ok) {
location.reload();
} else {
alert("Error al actualizar");
btn.disabled = false;
btn.innerText = originalText;
}
} catch (error) {
console.error("Fallo en la peticion:", error);
btn.disabled = false;
btn.innerText = originalText;
}
}
async function updateMonitorInterval(btn) {
const seconds = parseInt(document.getElementById('refreshSeconds').value, 10);
const status = document.getElementById('intervalStatus');
const originalText = btn.innerText;
if (!Number.isInteger(seconds) || seconds < 10 || seconds > 3600) {
alert("El intervalo debe estar entre 10 y 3600 segundos.");
return;
}
btn.disabled = true;
btn.innerText = "...";
status.innerText = "";
try {
const response = await fetch('/monitor-interval', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ seconds })
});
if (response.ok) {
location.reload();
} else {
alert("Error al guardar el intervalo");
}
} catch (error) {
console.error("Fallo guardando el intervalo:", error);
} finally {
btn.disabled = false;
btn.innerText = originalText;
}
}
schedulePageRefresh(monitorIntervalSeconds);
</script>
</body>
</html>