Implementado pausado y soft delete de proyectos

This commit is contained in:
Trabajo
2026-05-07 09:30:11 +02:00
parent d8522b49af
commit 0632e5f21a
7 changed files with 128 additions and 12 deletions

View File

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

View File

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

View File

@@ -73,5 +73,27 @@ export class MonitorUsecases {
await this.checkProjectAndSaveStatus(project);
}
async toggleProjectStatus(projectId: number): Promise<void> {
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<void> {
const project = await this.repository.getProjectById(projectId);
if (!project) {
throw new AppError('Proyecto no encontrado', 404);
}
await this.repository.updateProjectStatus(projectId, 'ELIMINADO');
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -6,7 +6,7 @@ export class MonitorRepository {
async getProjects(): Promise<Proyecto[]> {
try {
const { rows } = await this.db.query<Proyecto>('SELECT * FROM proyectos');
const { rows } = await this.db.query<Proyecto>("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<Proyecto[]> {
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<void> {
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<void> {
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)
})
}
}
}

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
@@ -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 @@
</thead>
<tbody>
<% proyectos.forEach(function(p) { %>
<tr>
<tr class="status-<%= p.estado_monitoreo %>">
<td><%= p.nombre %></td>
<td>
<a href="<%= p.url_health %>" target="_blank" title="<%= p.url_health %>">
<a href="<%= p.url_health %>" target="_blank">
<%= p.url_health %>
</a>
</td>
<td class="<%= p.estado_codigo || 'PENDIENTE' %>">
<%= p.estado_codigo || 'PENDIENTE' %>
<td class="<%= p.estado_monitoreo === 'PAUSADO' ? 'PAUSADO' : (p.estado_codigo || 'PENDIENTE') %>">
<%= p.estado_monitoreo === 'PAUSADO' ? 'PAUSADO' : (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 class="actions">
<!-- Botón refrescar -->
<button class="btn btn-refresh btn-mini" onclick="runCheck('/check/<%= p.id %>', this)" title="Refrescar">↻</button>
<!-- Botón pausar -->
<button class="btn btn-pause btn-mini" onclick="runAction('/proyectos/<%= p.id%>/toggle-status', this)">
<%= p.estado_monitoreo === 'ACTIVO' ? '⏸' : '▶' %>
</button>
<!-- Botón eliminar -->
<button class="btn btn-delete btn-mini" onclick="confirmDelete('/proyectos/<%= p.id %>/eliminar', this)">
🗑
</button>
</td>
</tr>
<% }) %>
@@ -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);
</script>