From 0632e5f21a86c5b79453ef19e91161b67662818d Mon Sep 17 00:00:00 2001 From: Trabajo Date: Thu, 7 May 2026 09:30:11 +0200 Subject: [PATCH 1/2] Implementado pausado y soft delete de proyectos --- deployment/database/migrations/1.0.0_init.sql | 3 +- src/application/monitor-controller.ts | 28 +++++++++ src/application/monitor-usecases.ts | 22 +++++++ src/domain/common.ts | 1 + src/infrastructure/monitor-routes-http.ts | 5 +- src/repository/monitor-repository.ts | 22 ++++++- src/views/index.ejs | 59 ++++++++++++++++--- 7 files changed, 128 insertions(+), 12 deletions(-) diff --git a/deployment/database/migrations/1.0.0_init.sql b/deployment/database/migrations/1.0.0_init.sql index ede6375..cfe9f9b 100644 --- a/deployment/database/migrations/1.0.0_init.sql +++ b/deployment/database/migrations/1.0.0_init.sql @@ -13,7 +13,8 @@ $$; CREATE TABLE proyectos ( id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, nombre VARCHAR(100) NOT NULL, - url_health TEXT NOT NULL + url_health TEXT NOT NULL, + estado_monitoreo VARCHAR(20) NOT NULL DEFAULT 'ACTIVO' ); -- 3. Tabla de estados (con BIGINT y ENUM) diff --git a/src/application/monitor-controller.ts b/src/application/monitor-controller.ts index cc7de57..b49172e 100644 --- a/src/application/monitor-controller.ts +++ b/src/application/monitor-controller.ts @@ -71,4 +71,32 @@ export class MonitorController { next(err); } } + + toggleProjectStatus = 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.toggleProjectStatus(projectId); + res.sendStatus(204); + } catch (err) { + next(err); + } + } + + deleteProject = 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.softDeleteProject(projectId); + res.sendStatus(204); + } catch (err) { + next(err); + } + } } diff --git a/src/application/monitor-usecases.ts b/src/application/monitor-usecases.ts index c424546..569de2a 100644 --- a/src/application/monitor-usecases.ts +++ b/src/application/monitor-usecases.ts @@ -73,5 +73,27 @@ export class MonitorUsecases { await this.checkProjectAndSaveStatus(project); } + + async toggleProjectStatus(projectId: number): Promise { + const project = await this.repository.getProjectById(projectId); + + if (!project) { + throw new AppError('Proyecto no encontrado', 404); + } + + const nuevoEstado = project.estado_monitoreo === 'ACTIVO' ? 'PAUSADO' : 'ACTIVO'; + + await this.repository.updateProjectStatus(project.id, nuevoEstado); + } + + async softDeleteProject(projectId: number): Promise { + const project = await this.repository.getProjectById(projectId); + + if (!project) { + throw new AppError('Proyecto no encontrado', 404); + } + + await this.repository.updateProjectStatus(projectId, 'ELIMINADO'); + } } diff --git a/src/domain/common.ts b/src/domain/common.ts index 4d5330e..7e0e5f0 100644 --- a/src/domain/common.ts +++ b/src/domain/common.ts @@ -10,6 +10,7 @@ export interface Proyecto { id: number; nombre: string; url_health: string; + estado_monitoreo: 'ACTIVO' | 'PAUSADO' | 'ELIMINADO'; estado_codigo?: 'OK' | 'ERROR' | 'TIMEOUT' | 'PENDIENTE'; fecha?: Date; } \ No newline at end of file diff --git a/src/infrastructure/monitor-routes-http.ts b/src/infrastructure/monitor-routes-http.ts index bfdc44c..bbddd07 100644 --- a/src/infrastructure/monitor-routes-http.ts +++ b/src/infrastructure/monitor-routes-http.ts @@ -1,4 +1,4 @@ -import { Router } from 'express'; +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'; @@ -13,6 +13,9 @@ export function createRouter(usecases: MonitorUsecases, scheduler: MonitorSchedu router.post('/check-all', controller.checkAllProjects); router.post('/check/:id', controller.checkProject); router.post('/monitor-interval', controller.updateMonitorInterval); + router.post('/proyectos/:id/toggle-status', controller.toggleProjectStatus); + router.post('/proyectos/:id/eliminar', controller.deleteProject); + return router; } diff --git a/src/repository/monitor-repository.ts b/src/repository/monitor-repository.ts index b5d7f29..d4cc17a 100644 --- a/src/repository/monitor-repository.ts +++ b/src/repository/monitor-repository.ts @@ -6,7 +6,7 @@ export class MonitorRepository { async getProjects(): Promise { try { - const { rows } = await this.db.query('SELECT * FROM proyectos'); + const { rows } = await this.db.query("SELECT * FROM proyectos WHERE estado_monitoreo = 'ACTIVO'"); return rows; } catch (err) { throw new RepositoryError('Error al recuperar proyectos', { @@ -46,7 +46,7 @@ export class MonitorRepository { async getDashboardData(): Promise { const query = ` - SELECT p.id, p.nombre, p.url_health, e.estado_codigo, e.fecha + SELECT p.id, p.nombre, p.url_health, p.estado_monitoreo, e.estado_codigo, e.fecha FROM proyectos p LEFT JOIN LATERAL ( SELECT estado_codigo, fecha @@ -55,6 +55,7 @@ export class MonitorRepository { ORDER BY fecha DESC LIMIT 1 ) e ON true + WHERE p.estado_monitoreo != 'ELIMINADO' ORDER BY p.nombre ASC; `; @@ -71,7 +72,7 @@ export class MonitorRepository { async addProject(nombre: string, url_health: string): Promise { try { await this.db.query( - 'INSERT INTO proyectos (nombre, url_health) VALUES ($1, $2)', + 'INSERT INTO proyectos (nombre, url_health) VALUES ($1, $2);', [nombre.trim(), url_health.trim()] ); } catch (err) { @@ -80,4 +81,19 @@ export class MonitorRepository { }); } } + + async updateProjectStatus(id: number, nuevoEstado: Proyecto['estado_monitoreo']): Promise { + try { + await this.db.query( + 'UPDATE proyectos SET estado_monitoreo = $1 WHERE id = $2;', + [nuevoEstado, id] + ); + } catch (err) { + throw new RepositoryError('Error al actualizar el estado del proyecto', { + id, + nuevoEstado, + cause: err instanceof Error ? err.message : String(err) + }) + } + } } diff --git a/src/views/index.ejs b/src/views/index.ejs index af1db89..ea026cc 100644 --- a/src/views/index.ejs +++ b/src/views/index.ejs @@ -1,4 +1,4 @@ - + @@ -12,10 +12,12 @@ th, td { border: 1px solid #ddd; padding: 12px; text-align: left; } th { background: #f4f4f4; font-weight: bold; } tr:hover { background: #fafafa; } + tr.status-PAUSADO { opacity: 0.6; background: #eee; } .OK { color: #2e7d32; font-weight: bold; } .ERROR { color: #c62828; font-weight: bold; } .TIMEOUT { color: #e65100; font-weight: bold; } + .PAUSADO { color: #757575; font-weight: bold; } .toolbar { display: flex; @@ -58,6 +60,16 @@ .btn-refresh { background: #0277bd; color: white; } .btn-mini { padding: 4px 8px; font-size: 16px; } .btn:disabled { background: #ccc; cursor: not-allowed; } + .actions .btn-mini { + background: white; + color: #111; + border: 1px solid #111; + } + .actions .btn-mini:disabled { + background: #eee; + color: #777; + border-color: #999; + } .refresh-group { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; } .status-message { color: #2e7d32; min-width: 70px; } @@ -102,21 +114,32 @@ <% proyectos.forEach(function(p) { %> - + <%= p.nombre %> - + <%= p.url_health %> - - <%= p.estado_codigo || 'PENDIENTE' %> + + <%= p.estado_monitoreo === 'PAUSADO' ? 'PAUSADO' : (p.estado_codigo || 'PENDIENTE') %> <%= p.fecha ? new Date(p.fecha).toLocaleString('es-ES') : '-' %> - - + + + + + + + + + <% }) %> @@ -186,6 +209,28 @@ btn.innerText = originalText; } } + + async function runAction(url, btn) { + btn.disabled = true; + try { + const response = await fetch(url, { method: 'POST' }); + if (response.ok) { + location.reload(); + } else { + alert("Error al realizar la acción"); + btn.disabled = false; + } + } catch (error) { + console.error("Fallo en la peticion:", error); + btn.disabled = false; + } + } + + function confirmDelete(url, btn) { + if (confirm("¿Estas seguro de que quieres dejar de monitorizar este proyecto? El historial no se borrara.")) { + runAction(url, btn); + } + } schedulePageRefresh(monitorIntervalSeconds); From 1bd87c3d69224e1ab53d3eb63aa7f351c56b9579 Mon Sep 17 00:00:00 2001 From: Trabajo Date: Thu, 7 May 2026 12:27:59 +0200 Subject: [PATCH 2/2] =?UTF-8?q?Implementado=20actualizaci=C3=B3n=20de=20no?= =?UTF-8?q?mbres=20y=20endpoints?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/application/monitor-controller.ts | 15 +++++++++++++++ src/application/monitor-usecases.ts | 21 ++++++++++++++++++--- src/config/postgres-config.ts | 10 +++++----- src/infrastructure/monitor-routes-http.ts | 1 + src/repository/monitor-repository.ts | 13 +++++++++++++ src/views/index.ejs | 11 +++++++---- 6 files changed, 59 insertions(+), 12 deletions(-) diff --git a/src/application/monitor-controller.ts b/src/application/monitor-controller.ts index b49172e..a66959b 100644 --- a/src/application/monitor-controller.ts +++ b/src/application/monitor-controller.ts @@ -99,4 +99,19 @@ export class MonitorController { next(err); } } + + updateProject = 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); + } + + const { nombre, url_health } = req.body; + await this.usecases.updateProject(projectId, nombre, url_health); + res.redirect('/'); + } catch (err) { + next(err); + } + } } diff --git a/src/application/monitor-usecases.ts b/src/application/monitor-usecases.ts index 569de2a..9831f08 100644 --- a/src/application/monitor-usecases.ts +++ b/src/application/monitor-usecases.ts @@ -39,8 +39,7 @@ export class MonitorUsecases { }); } } - - async createNewProject(nombre: string, urlHealth: string): Promise { + private validateProjectInput(nombre: string, urlHealth: string): { nombre: string; urlHealth: string } { const trimmedNombre = typeof nombre === 'string' ? nombre.trim() : ''; const trimmedUrl = typeof urlHealth === 'string' ? urlHealth.trim() : ''; @@ -62,7 +61,23 @@ export class MonitorUsecases { throw new AppError('La URL de health check no es valida', 400); } - await this.repository.addProject(trimmedNombre, trimmedUrl); + return { nombre: trimmedNombre, urlHealth: trimmedUrl }; + } + + async createNewProject(nombre: string, urlHealth: string): Promise { + const validated = this.validateProjectInput(nombre, urlHealth); + await this.repository.addProject(validated.nombre, validated.urlHealth); + } + + async updateProject(projectId: number, nombre: string, urlHealth: string): Promise { + const project = await this.repository.getProjectById(projectId); + + if (!project || project.estado_monitoreo === 'ELIMINADO') { + throw new AppError('Proyecto no encontrado', 404); + } + + const validated = this.validateProjectInput(nombre, urlHealth); + await this.repository.updateProject(projectId, validated.nombre, validated.urlHealth); } async checkProjectHealth(projectId: number): Promise { diff --git a/src/config/postgres-config.ts b/src/config/postgres-config.ts index 1a65a36..65d0916 100644 --- a/src/config/postgres-config.ts +++ b/src/config/postgres-config.ts @@ -4,9 +4,9 @@ import dotenv from 'dotenv'; dotenv.config(); export const pool = new pg.Pool({ - host: process.env.POSTGRES_HOST, - port: Number(process.env.POSTGRES_PORT), - user: process.env.POSTGRES_USER, - password: process.env.POSTGRES_PASSWORD, - database: process.env.POSTGRES_DB + host: process.env.POSTGRES_HOST ?? process.env.DB_HOST, + port: Number(process.env.POSTGRES_PORT ?? process.env.DB_PORT), + user: process.env.POSTGRES_USER ?? process.env.DB_USER, + password: process.env.POSTGRES_PASSWORD ?? process.env.DB_PASSWORD, + database: process.env.POSTGRES_DB ?? process.env.DB_NAME }); diff --git a/src/infrastructure/monitor-routes-http.ts b/src/infrastructure/monitor-routes-http.ts index bbddd07..861abe7 100644 --- a/src/infrastructure/monitor-routes-http.ts +++ b/src/infrastructure/monitor-routes-http.ts @@ -15,6 +15,7 @@ export function createRouter(usecases: MonitorUsecases, scheduler: MonitorSchedu router.post('/monitor-interval', controller.updateMonitorInterval); router.post('/proyectos/:id/toggle-status', controller.toggleProjectStatus); router.post('/proyectos/:id/eliminar', controller.deleteProject); + router.post('/proyectos/:id/editar', controller.updateProject); return router; diff --git a/src/repository/monitor-repository.ts b/src/repository/monitor-repository.ts index d4cc17a..921e7c1 100644 --- a/src/repository/monitor-repository.ts +++ b/src/repository/monitor-repository.ts @@ -96,4 +96,17 @@ export class MonitorRepository { }) } } + async updateProject(id: number, nombre: string, urlHealth: string): Promise { + try { + await this.db.query( + 'UPDATE proyectos SET nombre = $1, url_health = $2 WHERE id = $3;', + [nombre.trim(), urlHealth.trim(), id] + ); + } catch (err) { + throw new RepositoryError('Error al actualizar el proyecto', { + id, + cause: err instanceof Error ? err.message : String(err) + }); + } + } } diff --git a/src/views/index.ejs b/src/views/index.ejs index ea026cc..4f708d3 100644 --- a/src/views/index.ejs +++ b/src/views/index.ejs @@ -115,11 +115,13 @@ <% proyectos.forEach(function(p) { %> - <%= p.nombre %> - - <%= p.url_health %> - +
+ +
+ + + <%= p.estado_monitoreo === 'PAUSADO' ? 'PAUSADO' : (p.estado_codigo || 'PENDIENTE') %> @@ -130,6 +132,7 @@ +