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);