Merge pull request 'Función de borrado y actualización' (#4) from feat/setup-and-config into main
Reviewed-on: #4
This commit was merged in pull request #4.
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -71,4 +71,47 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,8 +39,7 @@ export class MonitorUsecases {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async createNewProject(nombre: string, urlHealth: string): Promise<void> {
|
||||
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<void> {
|
||||
const validated = this.validateProjectInput(nombre, urlHealth);
|
||||
await this.repository.addProject(validated.nombre, validated.urlHealth);
|
||||
}
|
||||
|
||||
async updateProject(projectId: number, nombre: string, urlHealth: string): Promise<void> {
|
||||
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<void> {
|
||||
@@ -73,5 +88,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');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,10 @@ 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);
|
||||
router.post('/proyectos/:id/editar', controller.updateProject);
|
||||
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -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,32 @@ 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
async updateProject(id: number, nombre: string, urlHealth: string): Promise<void> {
|
||||
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)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,35 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<% proyectos.forEach(function(p) { %>
|
||||
<tr>
|
||||
<td><%= p.nombre %></td>
|
||||
<tr class="status-<%= p.estado_monitoreo %>">
|
||||
<td>
|
||||
<a href="<%= p.url_health %>" target="_blank" title="<%= p.url_health %>">
|
||||
<%= p.url_health %>
|
||||
</a>
|
||||
<form id="edit-<%= p.id %>" action="/proyectos/<%= p.id %>/editar" method="POST">
|
||||
<input type="text" name="nombre" value="<%= p.nombre %>" required maxlength="100">
|
||||
</form>
|
||||
</td>
|
||||
<td class="<%= p.estado_codigo || 'PENDIENTE' %>">
|
||||
<%= p.estado_codigo || 'PENDIENTE' %>
|
||||
<td>
|
||||
<input form="edit-<%= p.id %>" type="url" name="url_health" value="<%= p.url_health %>" required>
|
||||
</td>
|
||||
<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>
|
||||
<button form="edit-<%= p.id %>" class="btn btn-mini" type="submit" title="Guardar">Guardar</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 +212,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>
|
||||
|
||||
Reference in New Issue
Block a user