Controladores rutas y vistas del dashboard
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
25
src/index.ts
25
src/index.ts
@@ -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;
|
||||
|
||||
@@ -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
33
src/views/add-project.ejs
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user