diff --git a/adn/07_proyectos.md b/adn/07_proyectos.md index 12daf8e0..09b17d14 100644 --- a/adn/07_proyectos.md +++ b/adn/07_proyectos.md @@ -58,3 +58,11 @@ Cada proyecto debe tener un archivo manifiesto con las siguientes secciones: ## 5. Relación con el ADN Los proyectos son el **tejido conectivo** que une nodos y bitácoras en un propósito común. El dashboard web de un proyecto es la interfaz de comunicación asíncrona con los stakeholders, permitiendo visibilidad sin interrumpir la operación. + +## 6. Proyectos Activos + +| Código | Nombre | Estado | Manifiesto | +| :--- | :--- | :---: | :--- | +| P2601 | Sistema DASUTEN | ⏳ | [P2601_dasuten.md](../proyectos/P2601_dasuten.md) | +| P2602 | Backups DTIC | ⏳ | [P2602_backups.md](../proyectos/P2602_backups.md) | +| P2603 | Sistema de Bitácoras | ⏳ | [P2603_bitacoras.md](../proyectos/P2603_bitacoras.md) | diff --git a/bitacoras/2026-03-03.md b/bitacoras/2026-03-03.md index 072e5a3f..deda7add 100644 --- a/bitacoras/2026-03-03.md +++ b/bitacoras/2026-03-03.md @@ -12,13 +12,13 @@ ### ⏳ En Proceso | ID | NODO | DETALLE | | :--- | :--- | :--- | +| [⏳ - P2603 - **Sistema de Bitácoras**](#srv-ns8) | srv-ns8 | Nuevo proyecto: Aplicación web `dtic-BITACORAs` con arquitectura frontend/backend para registro de operaciones. | | [⏳ - P2602 - **Proyecto Backups DTIC**](#srv-ns8) | srv-ns8 | ➡️ **[Viene del 02/03](2026-03-02.md)**
Fase 1: Reconocimiento. SSH 12/15 ✅. Pendiente: pcv-dasu1/2, pcv-serviio (Windows pmox2). | -| [⏳ - NOTA01 - **Sincronización Notas**](#srv-ns8) | srv-ns8 | Configuración de `rclone` para sincronizar `dtic-Notas` de Google Drive en `notas/dtic-Notas`. | ### 📝 Resumen de Actividades | NODO | RESUMEN INTEGRAL | | :--- | :--- | -| srv-ns8 | Inicio de jornada presencial (08:00). Reorganización de archivos en `dtic-DIGI` y directorio de notas. Configuración de rclone para sincronización con Google Drive. | +| srv-ns8 | Inicio de jornada presencial (08:00). Reorganización de archivos en `dtic-DIGI` y directorio de notas. Configuración de rclone para sincronización con Google Drive. Creación del proyecto P2603 (dtic-BITACORAs): sistema web de bitácoras con React 18 + Node.js + PostgreSQL. | --- @@ -31,9 +31,10 @@ | I | F | Descripción | E | | :--- | :--- | :--- | :--- | +| 12:35 | 12:50 | (IA) **P2603 - Sistema de Bitácoras**: Creación completa de `dtic-BITACORAs/`. Backend API REST (3 rutas CRUD), esquema SQL I-F-D-E, frontend React 18 dark mode, Docker Compose. 3 contenedores operativos. | [P] ✅ | | 11:15 | 11:37 | (IA) **Categorización de Nodos**: Separación de nodos exclusivos vs. soporte en P2601 y P2602. ADN 07 actualizado con el nuevo estándar. | [P] ✅ | -| 10:20 | 11:15 | **Sincronización Notas & Dashboard**: Vinculación de GDrive y actualización de `Estadoproyecto.tsx` con hitos reales. | [P] ✅ | -| 10:50 | 11:00 | **Unificación BKPs**: Estandarización completa a `dtic-BKPs`. Referencias internas actualizadas y `ns8-bkps` jubilado. | [P] ✅ | +| 10:50 | 11:15 | **Sincronización Notas & Dashboard**: Vinculación de GDrive y actualización de `Estadoproyecto.tsx`. | [P] ✅ | +| 10:20 | 10:50 | **Unificación BKPs**: Estandarización completa a `dtic-BKPs`. Referencias internas actualizadas y `ns8-bkps` jubilado. | [P] ✅ | | 09:40 | 10:02 | **Rollover 2026-03-03**: Creación de bitácora y migración de pendientes. | [P] ✅ | | 08:00 | 09:40 | Inicio sesión presencial. Tareas de organización en `dtic-DIGI` y `notas/`. | [P] 👁️ | @@ -44,11 +45,13 @@ | I | F | Descripción | E | | :--- | :--- | :--- | :--- | -| 11:37 | 11:46 | (IA) **Diagnóstico de Red**: Se intentó retomar P02. Fallo de `ping` y `ssh` a **10.0.10.205** (srv-dasu): *No route to host*. | [P] ⚠️ | -| 11:46 | 12:00 | (IA) **Restauración de Servicio**: Conectividad con srv-dasu restaurada tras intervención en red física por el usuario. | [P] ✅ | +| 12:05 | 12:08 | (IA) **Preparación**: Verificación de estado en srv-dasu. VMs 100 y 101 se encuentran en estado *stopped*. | [R] ✅ | +| 12:08 | 12:12 | (IA) **P02 - Fase 1**: Encendido secuencial de dc-dasuten (100) y sql-dasuten (101). Restauración de regla MASQUERADE en iptables (faltante tras reinicio). | [P] ✅ | +| 12:12 | 12:30 | (IA) **P02 - Fase 2**: Verificación de conectividad SSH. VMs arrancando, Windows Server Core en proceso de boot. | [P] ⏳ | | 12:00 | 12:05 | (IA) **Seguridad**: Autorización de sesión SSH vía `ns8-candados abrir` (carga de RSA en el agente). | [P] ✅ | +| 11:46 | 12:00 | (IA) **Restauración de Servicio**: Conectividad con srv-dasu restaurada tras intervención en red física por el usuario. | [P] ✅ | | 11:40 | 11:50 | (IA) **Ontología**: Creación y registro de la ficha técnica de nodo [sql-dasuten.md](../nodos/sql-dasuten.md). | [R] ✅ | -| 12:05 | - | (IA) **Preparación**: Verificación de estado de la VM 101 y motor SQL Server 2019 Core. | [P] ⏳ | +| 11:37 | 11:46 | (IA) **Diagnóstico de Red**: Se intentó retomar P02. Fallo de `ping` y `ssh` a **10.0.10.205** (srv-dasu): *No route to host*. | [P] ⚠️ | --- diff --git a/dtic-BITACORAs/backend/Dockerfile b/dtic-BITACORAs/backend/Dockerfile new file mode 100644 index 00000000..c8db6f09 --- /dev/null +++ b/dtic-BITACORAs/backend/Dockerfile @@ -0,0 +1,7 @@ +FROM node:18-alpine +WORKDIR /app +COPY package*.json ./ +RUN npm install +COPY . . +EXPOSE 3001 +CMD ["npm", "run", "dev"] diff --git a/dtic-BITACORAs/backend/package.json b/dtic-BITACORAs/backend/package.json new file mode 100644 index 00000000..81ddf41b --- /dev/null +++ b/dtic-BITACORAs/backend/package.json @@ -0,0 +1,19 @@ +{ + "name": "dtic-bitacoras-api", + "version": "1.0.0", + "description": "API REST para dtic-BITACORAs", + "main": "src/server.js", + "scripts": { + "start": "node src/server.js", + "dev": "nodemon src/server.js" + }, + "dependencies": { + "cors": "^2.8.5", + "express": "^4.18.2", + "helmet": "^7.1.0", + "pg": "^8.11.3" + }, + "devDependencies": { + "nodemon": "^3.0.2" + } +} \ No newline at end of file diff --git a/dtic-BITACORAs/backend/src/routes/bitacoras.js b/dtic-BITACORAs/backend/src/routes/bitacoras.js new file mode 100644 index 00000000..0378ca3c --- /dev/null +++ b/dtic-BITACORAs/backend/src/routes/bitacoras.js @@ -0,0 +1,102 @@ +module.exports = (pool) => { + const router = require('express').Router(); + + // GET /api/bitacoras — Listar bitácoras (opcionalmente por fecha) + router.get('/', async (req, res) => { + const { fecha } = req.query; + if (fecha) { + const { rows } = await pool.query('SELECT * FROM bitacoras.bitacoras WHERE fecha = $1', [fecha]); + if (rows.length === 0) { + // Auto-crear bitácora del día + const insert = await pool.query( + 'INSERT INTO bitacoras.bitacoras (fecha) VALUES ($1) RETURNING *', [fecha] + ); + return res.json(insert.rows[0]); + } + return res.json(rows[0]); + } + const { rows } = await pool.query('SELECT * FROM bitacoras.bitacoras ORDER BY fecha DESC LIMIT 30'); + res.json(rows); + }); + + // GET /api/bitacoras/:fecha/completa — Bitácora completa con entradas agrupadas por nodo + router.get('/:fecha/completa', async (req, res) => { + const { fecha } = req.params; + + // Obtener o crear bitácora + let bitacora; + const bResult = await pool.query('SELECT * FROM bitacoras.bitacoras WHERE fecha = $1', [fecha]); + if (bResult.rows.length === 0) { + const insert = await pool.query('INSERT INTO bitacoras.bitacoras (fecha) VALUES ($1) RETURNING *', [fecha]); + bitacora = insert.rows[0]; + } else { + bitacora = bResult.rows[0]; + } + + // Obtener entradas con datos del nodo + const { rows: entradas } = await pool.query(` + SELECT e.*, n.nombre as nodo_nombre, n.ip as nodo_ip + FROM bitacoras.entradas e + LEFT JOIN bitacoras.nodos n ON e.nodo_id = n.id + WHERE e.bitacora_id = $1 + ORDER BY e.inicio DESC + `, [bitacora.id]); + + // Agrupar por nodo + const porNodo = {}; + entradas.forEach(e => { + const nodo = e.nodo_nombre || 'sin-nodo'; + if (!porNodo[nodo]) porNodo[nodo] = { nodo: nodo, ip: e.nodo_ip, entradas: [] }; + porNodo[nodo].entradas.push(e); + }); + + res.json({ ...bitacora, nodos: Object.values(porNodo) }); + }); + + // GET /api/bitacoras/:fecha/export — Exportar a Markdown + router.get('/:fecha/export', async (req, res) => { + const { fecha } = req.params; + const bResult = await pool.query('SELECT * FROM bitacoras.bitacoras WHERE fecha = $1', [fecha]); + if (bResult.rows.length === 0) return res.status(404).json({ error: 'Bitácora no encontrada' }); + + const bitacora = bResult.rows[0]; + const { rows: entradas } = await pool.query(` + SELECT e.*, n.nombre as nodo_nombre + FROM bitacoras.entradas e + LEFT JOIN bitacoras.nodos n ON e.nodo_id = n.id + WHERE e.bitacora_id = $1 + ORDER BY n.nombre, e.inicio DESC + `, [bitacora.id]); + + // Generar Markdown compatible con ADN + const [y, m, d] = fecha.split('-'); + let md = `# Bitácora de Operaciones - ${d}/${m}/${y}\n\n`; + md += `## 📂 Actividades Detalladas\n\n`; + + const porNodo = {}; + entradas.forEach(e => { + const nodo = e.nodo_nombre || 'sin-nodo'; + if (!porNodo[nodo]) porNodo[nodo] = []; + porNodo[nodo].push(e); + }); + + Object.entries(porNodo).forEach(([nodo, entries]) => { + md += `### ${nodo}\n\n`; + md += `| I | F | Descripción | E |\n`; + md += `| :--- | :--- | :--- | :--- |\n`; + entries.forEach(e => { + const inicio = e.inicio ? e.inicio.substring(0, 5) : '-'; + const fin = e.fin ? e.fin.substring(0, 5) : '-'; + const ia = e.es_ia ? '(IA) ' : ''; + const estado = `[${e.modo}] ${e.estado}`; + md += `| ${inicio} | ${fin} | ${ia}${e.descripcion} | ${estado} |\n`; + }); + md += '\n'; + }); + + res.set('Content-Type', 'text/markdown'); + res.send(md); + }); + + return router; +}; diff --git a/dtic-BITACORAs/backend/src/routes/entradas.js b/dtic-BITACORAs/backend/src/routes/entradas.js new file mode 100644 index 00000000..f05cc4c5 --- /dev/null +++ b/dtic-BITACORAs/backend/src/routes/entradas.js @@ -0,0 +1,46 @@ +module.exports = (pool) => { + const router = require('express').Router(); + + // GET /api/entradas — Listar entradas (filtro por bitacora_id o nodo_id) + router.get('/', async (req, res) => { + const { bitacora_id, nodo_id } = req.query; + let query = 'SELECT e.*, n.nombre as nodo_nombre FROM bitacoras.entradas e LEFT JOIN bitacoras.nodos n ON e.nodo_id = n.id WHERE 1=1'; + const params = []; + if (bitacora_id) { params.push(bitacora_id); query += ` AND e.bitacora_id = $${params.length}`; } + if (nodo_id) { params.push(nodo_id); query += ` AND e.nodo_id = $${params.length}`; } + query += ' ORDER BY e.inicio DESC'; + const { rows } = await pool.query(query, params); + res.json(rows); + }); + + // POST /api/entradas — Crear entrada + router.post('/', async (req, res) => { + const { inicio, fin, descripcion, estado, modo, es_ia, bitacora_id, nodo_id } = req.body; + const { rows } = await pool.query(` + INSERT INTO bitacoras.entradas (inicio, fin, descripcion, estado, modo, es_ia, bitacora_id, nodo_id) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING * + `, [inicio, fin || null, descripcion, estado || '⏳', modo || 'P', es_ia || false, bitacora_id, nodo_id]); + res.status(201).json(rows[0]); + }); + + // PUT /api/entradas/:id — Actualizar entrada + router.put('/:id', async (req, res) => { + const { id } = req.params; + const { inicio, fin, descripcion, estado, modo, es_ia } = req.body; + const { rows } = await pool.query(` + UPDATE bitacoras.entradas SET inicio=$1, fin=$2, descripcion=$3, estado=$4, modo=$5, es_ia=$6 + WHERE id=$7 RETURNING * + `, [inicio, fin, descripcion, estado, modo, es_ia, id]); + if (rows.length === 0) return res.status(404).json({ error: 'Entrada no encontrada' }); + res.json(rows[0]); + }); + + // DELETE /api/entradas/:id — Eliminar entrada + router.delete('/:id', async (req, res) => { + const { rowCount } = await pool.query('DELETE FROM bitacoras.entradas WHERE id = $1', [req.params.id]); + if (rowCount === 0) return res.status(404).json({ error: 'Entrada no encontrada' }); + res.json({ message: 'Entrada eliminada' }); + }); + + return router; +}; diff --git a/dtic-BITACORAs/backend/src/routes/nodos.js b/dtic-BITACORAs/backend/src/routes/nodos.js new file mode 100644 index 00000000..fab96f6a --- /dev/null +++ b/dtic-BITACORAs/backend/src/routes/nodos.js @@ -0,0 +1,21 @@ +module.exports = (pool) => { + const router = require('express').Router(); + + // GET /api/nodos — Listar todos los nodos + router.get('/', async (req, res) => { + const { rows } = await pool.query('SELECT * FROM bitacoras.nodos WHERE activo = true ORDER BY nombre'); + res.json(rows); + }); + + // POST /api/nodos — Crear nodo + router.post('/', async (req, res) => { + const { nombre, ip, tipo, descripcion } = req.body; + const { rows } = await pool.query( + 'INSERT INTO bitacoras.nodos (nombre, ip, tipo, descripcion) VALUES ($1, $2, $3, $4) RETURNING *', + [nombre, ip, tipo, descripcion] + ); + res.status(201).json(rows[0]); + }); + + return router; +}; diff --git a/dtic-BITACORAs/backend/src/server.js b/dtic-BITACORAs/backend/src/server.js new file mode 100644 index 00000000..9fcf9678 --- /dev/null +++ b/dtic-BITACORAs/backend/src/server.js @@ -0,0 +1,40 @@ +const express = require('express'); +const cors = require('cors'); +const helmet = require('helmet'); +const { Pool } = require('pg'); + +const app = express(); +const PORT = process.env.PORT || 3001; + +// Middleware +app.use(helmet()); +app.use(cors({ origin: process.env.CORS_ORIGIN || '*' })); +app.use(express.json()); + +// Database pool +const pool = new Pool({ connectionString: process.env.DATABASE_URL }); + +// Health check +app.get('/health', async (req, res) => { + try { + await pool.query('SELECT 1'); + res.json({ status: 'ok', timestamp: new Date().toISOString() }); + } catch (err) { + res.status(500).json({ status: 'error', message: err.message }); + } +}); + +// Routes +app.use('/api/nodos', require('./routes/nodos')(pool)); +app.use('/api/bitacoras', require('./routes/bitacoras')(pool)); +app.use('/api/entradas', require('./routes/entradas')(pool)); + +// Error handler +app.use((err, req, res, next) => { + console.error('Error:', err.message); + res.status(500).json({ error: err.message }); +}); + +app.listen(PORT, () => { + console.log(`🔐 dtic-BITACORAs API corriendo en puerto ${PORT}`); +}); diff --git a/dtic-BITACORAs/docker-compose.yml b/dtic-BITACORAs/docker-compose.yml new file mode 100644 index 00000000..ae70fdf1 --- /dev/null +++ b/dtic-BITACORAs/docker-compose.yml @@ -0,0 +1,47 @@ +services: + postgres: + image: postgres:15-alpine + environment: + POSTGRES_USER: dtic_user + POSTGRES_PASSWORD: dtic_pass_2026 + POSTGRES_DB: dtic_bitacoras + ports: + - "5433:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + - ./docker/init.sql:/docker-entrypoint-initdb.d/init.sql + healthcheck: + test: ["CMD-SHELL", "pg_isready -U dtic_user -d dtic_bitacoras"] + interval: 5s + timeout: 5s + retries: 5 + + api: + build: ./backend + ports: + - "3002:3001" + environment: + NODE_ENV: development + PORT: 3001 + DATABASE_URL: postgres://dtic_user:dtic_pass_2026@postgres:5432/dtic_bitacoras + JWT_SECRET: dtic_bitacoras_secret_2026 + CORS_ORIGIN: "*" + depends_on: + postgres: + condition: service_healthy + volumes: + - ./backend/src:/app/src + + frontend: + build: ./frontend + ports: + - "5174:5173" + environment: + VITE_API_URL: http://localhost:3002 + depends_on: + - api + volumes: + - ./frontend/src:/app/src + +volumes: + postgres_data: diff --git a/dtic-BITACORAs/docker/init.sql b/dtic-BITACORAs/docker/init.sql new file mode 100644 index 00000000..b438c06b --- /dev/null +++ b/dtic-BITACORAs/docker/init.sql @@ -0,0 +1,91 @@ +-- dtic-BITACORAs - Esquema de Base de Datos +-- Formato I-F-D-E (Inicio, Fin, Descripción, Estado) + +CREATE SCHEMA IF NOT EXISTS bitacoras; + +-- Nodos del sistema (servidores, VMs, PCs) +CREATE TABLE bitacoras.nodos ( + id SERIAL PRIMARY KEY, + nombre VARCHAR(50) NOT NULL UNIQUE, + ip VARCHAR(45), + tipo VARCHAR(20) DEFAULT 'servidor', + descripcion TEXT, + activo BOOLEAN DEFAULT true, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Bitácoras diarias +CREATE TABLE bitacoras.bitacoras ( + id SERIAL PRIMARY KEY, + fecha DATE NOT NULL UNIQUE, + resumen TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Temas (agrupadores de entradas por nodo) +CREATE TABLE bitacoras.temas ( + id SERIAL PRIMARY KEY, + id_tema VARCHAR(20) NOT NULL, + titulo VARCHAR(200) NOT NULL, + estado VARCHAR(10) DEFAULT '⏳', + descripcion TEXT, + bitacora_id INTEGER REFERENCES bitacoras.bitacoras(id) ON DELETE CASCADE, + nodo_id INTEGER REFERENCES bitacoras.nodos(id), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Entradas cronológicas (formato I-F-D-E) +CREATE TABLE bitacoras.entradas ( + id SERIAL PRIMARY KEY, + inicio TIME NOT NULL, + fin TIME, + descripcion TEXT NOT NULL, + estado VARCHAR(10) DEFAULT '⏳', + modo CHAR(1) DEFAULT 'P' CHECK (modo IN ('P', 'R')), + es_ia BOOLEAN DEFAULT false, + tema_id INTEGER REFERENCES bitacoras.temas(id) ON DELETE CASCADE, + bitacora_id INTEGER REFERENCES bitacoras.bitacoras(id) ON DELETE CASCADE, + nodo_id INTEGER REFERENCES bitacoras.nodos(id), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Índices +CREATE INDEX idx_bitacoras_fecha ON bitacoras.bitacoras(fecha); +CREATE INDEX idx_entradas_bitacora ON bitacoras.entradas(bitacora_id); +CREATE INDEX idx_entradas_nodo ON bitacoras.entradas(nodo_id); +CREATE INDEX idx_temas_bitacora ON bitacoras.temas(bitacora_id); + +-- Trigger para updated_at +CREATE OR REPLACE FUNCTION bitacoras.update_timestamp() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER trg_nodos_updated BEFORE UPDATE ON bitacoras.nodos + FOR EACH ROW EXECUTE FUNCTION bitacoras.update_timestamp(); +CREATE TRIGGER trg_bitacoras_updated BEFORE UPDATE ON bitacoras.bitacoras + FOR EACH ROW EXECUTE FUNCTION bitacoras.update_timestamp(); +CREATE TRIGGER trg_temas_updated BEFORE UPDATE ON bitacoras.temas + FOR EACH ROW EXECUTE FUNCTION bitacoras.update_timestamp(); +CREATE TRIGGER trg_entradas_updated BEFORE UPDATE ON bitacoras.entradas + FOR EACH ROW EXECUTE FUNCTION bitacoras.update_timestamp(); + +-- Datos semilla: Nodos del proyecto +INSERT INTO bitacoras.nodos (nombre, ip, tipo, descripcion) VALUES + ('srv-ns8', '10.0.10.8', 'servidor', 'Nodo central de gestión, GIT y Dashboard'), + ('srv-dasu', '10.0.10.205', 'hipervisor', 'Hipervisor Proxmox VE (DASUTEN)'), + ('dc-dasuten', '10.0.100.10', 'vm', 'Controlador de Dominio AD DS + DNS'), + ('sql-dasuten', '10.0.100.11', 'vm', 'Motor SQL Server 2019 Core'), + ('pcv-dasu0', '10.0.100.12', 'vm', 'VM de pruebas Win 10 LTSC'), + ('pc-dasu0', NULL, 'pc', 'PC física cliente oficina DASUTeN'), + ('srv-pmox1', '10.0.10.201', 'hipervisor', 'Pool master Proxmox'), + ('srv-pmox2', '10.0.10.202', 'hipervisor', 'Nodo Proxmox secundario'), + ('srv-pmox3', '10.0.10.203', 'hipervisor', 'Nodo Proxmox terciario (64GB RAM)'), + ('srv-xen1', '10.0.10.23', 'hipervisor', 'Citrix XenServer 7.0 (Legacy)'); diff --git a/dtic-BITACORAs/frontend/Dockerfile b/dtic-BITACORAs/frontend/Dockerfile new file mode 100644 index 00000000..1e1006dd --- /dev/null +++ b/dtic-BITACORAs/frontend/Dockerfile @@ -0,0 +1,7 @@ +FROM node:18-alpine +WORKDIR /app +COPY package*.json ./ +RUN npm install +COPY . . +EXPOSE 5173 +CMD ["npm", "run", "dev"] diff --git a/dtic-BITACORAs/frontend/index.html b/dtic-BITACORAs/frontend/index.html new file mode 100644 index 00000000..9ec4e7d4 --- /dev/null +++ b/dtic-BITACORAs/frontend/index.html @@ -0,0 +1,16 @@ + + + + + + + dtic-BITACORAs | DTIC UTNLR + + + + + +
+ + + diff --git a/dtic-BITACORAs/frontend/package.json b/dtic-BITACORAs/frontend/package.json new file mode 100644 index 00000000..9ff67a0b --- /dev/null +++ b/dtic-BITACORAs/frontend/package.json @@ -0,0 +1,21 @@ +{ + "name": "dtic-bitacoras-frontend", + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite --host", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@types/react": "^18.2.43", + "@types/react-dom": "^18.2.17", + "@vitejs/plugin-react": "^4.2.1", + "typescript": "^5.2.2", + "vite": "^5.1.0" + } +} \ No newline at end of file diff --git a/dtic-BITACORAs/frontend/src/App.tsx b/dtic-BITACORAs/frontend/src/App.tsx new file mode 100644 index 00000000..50b48080 --- /dev/null +++ b/dtic-BITACORAs/frontend/src/App.tsx @@ -0,0 +1,223 @@ +import React, { useState, useEffect, useCallback } from 'react'; + +const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3002'; + +interface Entrada { + id: number; + inicio: string; + fin: string | null; + descripcion: string; + estado: string; + modo: string; + es_ia: boolean; + nodo_nombre: string; + nodo_ip: string; +} + +interface NodoGroup { + nodo: string; + ip: string; + entradas: Entrada[]; +} + +interface Bitacora { + id: number; + fecha: string; + resumen: string; + nodos: NodoGroup[]; +} + +interface Nodo { + id: number; + nombre: string; + ip: string; +} + +function App() { + const [fecha, setFecha] = useState(new Date().toISOString().split('T')[0]); + const [bitacora, setBitacora] = useState(null); + const [nodos, setNodos] = useState([]); + const [loading, setLoading] = useState(true); + + // Form state + const [newInicio, setNewInicio] = useState(''); + const [newFin, setNewFin] = useState(''); + const [newDesc, setNewDesc] = useState(''); + const [newEstado, setNewEstado] = useState('⏳'); + const [newModo, setNewModo] = useState('P'); + const [newNodoId, setNewNodoId] = useState(''); + const [newEsIa, setNewEsIa] = useState(false); + + const fetchBitacora = useCallback(async () => { + setLoading(true); + try { + const res = await fetch(`${API_URL}/api/bitacoras/${fecha}/completa`); + const data = await res.json(); + setBitacora(data); + } catch (err) { console.error('Error:', err); } + setLoading(false); + }, [fecha]); + + const fetchNodos = async () => { + try { + const res = await fetch(`${API_URL}/api/nodos`); + setNodos(await res.json()); + } catch (err) { console.error('Error:', err); } + }; + + useEffect(() => { fetchBitacora(); }, [fetchBitacora]); + useEffect(() => { fetchNodos(); }, []); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!bitacora || !newDesc || !newInicio || !newNodoId) return; + try { + await fetch(`${API_URL}/api/entradas`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + inicio: newInicio, fin: newFin || null, descripcion: newDesc, + estado: newEstado, modo: newModo, es_ia: newEsIa, + bitacora_id: bitacora.id, nodo_id: parseInt(newNodoId) + }) + }); + setNewDesc(''); setNewInicio(''); setNewFin(''); + fetchBitacora(); + } catch (err) { console.error('Error:', err); } + }; + + const handleDelete = async (id: number) => { + await fetch(`${API_URL}/api/entradas/${id}`, { method: 'DELETE' }); + fetchBitacora(); + }; + + const totalEntradas = bitacora?.nodos?.reduce((sum, n) => sum + n.entradas.length, 0) || 0; + const totalNodos = bitacora?.nodos?.length || 0; + + return ( +
+ + +
+ {/* Date Selector */} +
+ + setFecha(e.target.value)} /> + +
+ + {/* Stats */} +
+
+
{totalEntradas}
+
Entradas
+
+
+
{totalNodos}
+
Nodos Activos
+
+
+
{bitacora?.nodos?.reduce((s, n) => s + n.entradas.filter(e => e.estado === '✅').length, 0) || 0}
+
Completadas
+
+
+
{bitacora?.nodos?.reduce((s, n) => s + n.entradas.filter(e => e.estado === '⏳').length, 0) || 0}
+
En Proceso
+
+
+ + {/* New Entry Form */} +
+
Nueva Entrada
+
+ setNewInicio(e.target.value)} placeholder="I" required /> + setNewFin(e.target.value)} placeholder="F" /> + setNewDesc(e.target.value)} placeholder="Descripción..." required /> + + + + + +
+
+ + {/* Loading */} + {loading &&
} + + {/* Entries by Node */} + {bitacora?.nodos?.map((nodoGroup, idx) => ( +
+
+ + {nodoGroup.nodo} + {nodoGroup.ip && ({nodoGroup.ip})} + {nodoGroup.entradas.length} +
+ + + + + + + + + + + + {nodoGroup.entradas.map(e => ( + + + + + + + + ))} + +
IFDescripciónE
{e.inicio?.substring(0, 5)}{e.fin ? e.fin.substring(0, 5) : '—'} + {e.es_ia && IA} + {e.descripcion} + [{e.modo}] {e.estado} + +
+
+ ))} + + {!loading && totalEntradas === 0 && ( +
+ + Sin entradas para esta fecha. ¡Creá la primera! +
+ )} +
+
+ ); +} + +export default App; diff --git a/dtic-BITACORAs/frontend/src/index.css b/dtic-BITACORAs/frontend/src/index.css new file mode 100644 index 00000000..a3761cea --- /dev/null +++ b/dtic-BITACORAs/frontend/src/index.css @@ -0,0 +1,242 @@ +/* dtic-BITACORAs — Estética del Dashboard P2601 */ + +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); + +:root { + --primary: #667eea; + --secondary: #764ba2; + --gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + --success: #10b981; + --warning: #f59e0b; + --danger: #ef4444; + --info: #3b82f6; + --bg: #0f172a; + --bg-card: #1e293b; + --bg-card-hover: #334155; + --bg-input: #1e293b; + --text: #f1f5f9; + --text-secondary: #94a3b8; + --text-muted: #64748b; + --border: #334155; + --radius: 12px; + --shadow: 0 4px 20px rgba(0, 0, 0, 0.3); + --transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +* { box-sizing: border-box; margin: 0; padding: 0; } + +body { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; + background: var(--bg); + color: var(--text); + min-height: 100vh; +} + +/* Layout */ +.app { min-height: 100vh; display: flex; flex-direction: column; } + +.navbar { + background: rgba(15, 23, 42, 0.8); + backdrop-filter: blur(20px); + border-bottom: 1px solid var(--border); + padding: 1rem 2rem; + display: flex; + align-items: center; + justify-content: space-between; + position: sticky; + top: 0; + z-index: 100; +} + +.navbar-brand { + font-size: 1.4rem; + font-weight: 700; + background: var(--gradient); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.navbar-brand i { -webkit-text-fill-color: initial; color: var(--primary); } + +.badge { + display: inline-block; + padding: 0.2rem 0.6rem; + border-radius: 20px; + font-size: 0.7rem; + font-weight: 600; + letter-spacing: 0.05em; +} + +.badge-gradient { background: var(--gradient); color: white; } + +/* Main content */ +.main { flex: 1; padding: 2rem; max-width: 1400px; margin: 0 auto; width: 100%; } + +/* Cards */ +.card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 1.5rem; + transition: var(--transition); + animation: fadeIn 0.5s ease-out; +} + +.card:hover { transform: translateY(-2px); box-shadow: var(--shadow); border-color: var(--primary); } + +.card-header { + background: var(--gradient); + color: white; + padding: 1rem 1.5rem; + border-radius: var(--radius) var(--radius) 0 0; + margin: -1.5rem -1.5rem 1.5rem; + font-weight: 600; + display: flex; + align-items: center; + gap: 0.5rem; +} + +/* Table I-F-D-E */ +.table-ifde { width: 100%; border-collapse: collapse; font-size: 0.9rem; } +.table-ifde th { + background: rgba(102, 126, 234, 0.15); + color: var(--primary); + padding: 0.75rem 1rem; + text-align: left; + font-weight: 600; + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 0.05em; + border-bottom: 2px solid var(--border); +} +.table-ifde td { + padding: 0.75rem 1rem; + border-bottom: 1px solid var(--border); + vertical-align: top; +} +.table-ifde tr:hover td { background: rgba(102, 126, 234, 0.05); } +.table-ifde .col-time { width: 60px; font-family: 'JetBrains Mono', monospace; color: var(--text-secondary); font-size: 0.85rem; } +.table-ifde .col-status { width: 80px; text-align: center; } +.table-ifde .col-desc { color: var(--text); } +.table-ifde .ia-tag { + background: rgba(102, 126, 234, 0.2); + color: var(--primary); + padding: 0.1rem 0.4rem; + border-radius: 4px; + font-size: 0.75rem; + font-weight: 600; + margin-right: 0.3rem; +} + +/* Nodo section */ +.nodo-section { margin-bottom: 2rem; } +.nodo-title { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 1.1rem; + font-weight: 600; + margin-bottom: 1rem; + padding-bottom: 0.5rem; + border-bottom: 2px solid var(--border); +} +.nodo-title i { color: var(--primary); } +.nodo-ip { color: var(--text-muted); font-size: 0.8rem; font-weight: 400; } + +/* Form inline */ +.form-inline { display: flex; gap: 0.5rem; align-items: center; padding: 1rem; background: rgba(102, 126, 234, 0.05); border-radius: var(--radius); margin-top: 1rem; flex-wrap: wrap; } +.form-inline input, .form-inline select { + background: var(--bg-input); + border: 1px solid var(--border); + color: var(--text); + padding: 0.5rem 0.75rem; + border-radius: 8px; + font-size: 0.85rem; + font-family: inherit; + transition: var(--transition); +} +.form-inline input:focus, .form-inline select:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2); +} +.form-inline .input-time { width: 80px; } +.form-inline .input-desc { flex: 1; min-width: 200px; } + +/* Buttons */ +.btn { + padding: 0.5rem 1rem; + border-radius: 8px; + border: none; + font-weight: 500; + cursor: pointer; + transition: var(--transition); + font-size: 0.85rem; + display: inline-flex; + align-items: center; + gap: 0.3rem; +} +.btn:hover { transform: translateY(-1px); } +.btn-primary { background: var(--gradient); color: white; } +.btn-primary:hover { box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4); } +.btn-sm { padding: 0.3rem 0.6rem; font-size: 0.75rem; } +.btn-danger { background: var(--danger); color: white; } +.btn-success { background: var(--success); color: white; } + +/* Date selector */ +.date-selector { + display: flex; + align-items: center; + gap: 1rem; + margin-bottom: 2rem; +} +.date-selector input[type="date"] { + background: var(--bg-card); + border: 1px solid var(--border); + color: var(--text); + padding: 0.6rem 1rem; + border-radius: 8px; + font-size: 1rem; + font-family: inherit; +} +.date-selector input[type="date"]:focus { + outline: none; + border-color: var(--primary); +} + +/* Stats bar */ +.stats-bar { display: flex; gap: 1rem; margin-bottom: 2rem; flex-wrap: wrap; } +.stat-card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 1rem 1.5rem; + flex: 1; + min-width: 150px; + text-align: center; + transition: var(--transition); +} +.stat-card:hover { border-color: var(--primary); } +.stat-value { font-size: 2rem; font-weight: 700; background: var(--gradient); -webkit-background-clip: text; -webkit-text-fill-color: transparent; } +.stat-label { font-size: 0.8rem; color: var(--text-muted); margin-top: 0.3rem; text-transform: uppercase; letter-spacing: 0.05em; } + +/* Animations */ +@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } + +/* Scrollbar */ +::-webkit-scrollbar { width: 8px; } +::-webkit-scrollbar-track { background: var(--bg); } +::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; } +::-webkit-scrollbar-thumb:hover { background: var(--text-muted); } + +/* Responsive */ +@media (max-width: 768px) { + .main { padding: 1rem; } + .navbar { padding: 0.75rem 1rem; } + .stats-bar { flex-direction: column; } + .form-inline { flex-direction: column; } + .form-inline .input-desc { min-width: 100%; } +} diff --git a/dtic-BITACORAs/frontend/src/main.tsx b/dtic-BITACORAs/frontend/src/main.tsx new file mode 100644 index 00000000..9dbd1478 --- /dev/null +++ b/dtic-BITACORAs/frontend/src/main.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; +import './index.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/dtic-BITACORAs/frontend/tsconfig.json b/dtic-BITACORAs/frontend/tsconfig.json new file mode 100644 index 00000000..3153f70a --- /dev/null +++ b/dtic-BITACORAs/frontend/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": [ + "ES2020", + "DOM", + "DOM.Iterable" + ], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noFallthroughCasesInSwitch": true + }, + "include": [ + "src" + ], + "references": [ + { + "path": "./tsconfig.node.json" + } + ] +} \ No newline at end of file diff --git a/dtic-BITACORAs/frontend/tsconfig.node.json b/dtic-BITACORAs/frontend/tsconfig.node.json new file mode 100644 index 00000000..b85dd47b --- /dev/null +++ b/dtic-BITACORAs/frontend/tsconfig.node.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": [ + "vite.config.ts" + ] +} \ No newline at end of file diff --git a/dtic-BITACORAs/frontend/vite.config.ts b/dtic-BITACORAs/frontend/vite.config.ts new file mode 100644 index 00000000..d78a55e4 --- /dev/null +++ b/dtic-BITACORAs/frontend/vite.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + server: { host: true, port: 5173 }, + define: { 'import.meta.env.VERSION': JSON.stringify(require('./package.json').version) } +}); diff --git a/dtic-BKPs_v5.5.1/CHANGELOG.md b/dtic-BKPs/dtic-BKPs_v5.5.1/CHANGELOG.md similarity index 100% rename from dtic-BKPs_v5.5.1/CHANGELOG.md rename to dtic-BKPs/dtic-BKPs_v5.5.1/CHANGELOG.md diff --git a/dtic-BKPs_v5.5.1/PROYECT_dtic-BKPs.md b/dtic-BKPs/dtic-BKPs_v5.5.1/PROYECT_dtic-BKPs.md similarity index 100% rename from dtic-BKPs_v5.5.1/PROYECT_dtic-BKPs.md rename to dtic-BKPs/dtic-BKPs_v5.5.1/PROYECT_dtic-BKPs.md diff --git a/dtic-BKPs_v5.5.1/README.md b/dtic-BKPs/dtic-BKPs_v5.5.1/README.md similarity index 100% rename from dtic-BKPs_v5.5.1/README.md rename to dtic-BKPs/dtic-BKPs_v5.5.1/README.md diff --git a/dtic-BKPs_v5.5.1/app-run-sh b/dtic-BKPs/dtic-BKPs_v5.5.1/app-run-sh similarity index 100% rename from dtic-BKPs_v5.5.1/app-run-sh rename to dtic-BKPs/dtic-BKPs_v5.5.1/app-run-sh diff --git a/dtic-BKPs_v5.5.1/dtic-BKPs_app.rb b/dtic-BKPs/dtic-BKPs_v5.5.1/dtic-BKPs_app.rb similarity index 100% rename from dtic-BKPs_v5.5.1/dtic-BKPs_app.rb rename to dtic-BKPs/dtic-BKPs_v5.5.1/dtic-BKPs_app.rb diff --git a/dtic-BKPs_v5.5.1/dtic-BKPs_tasks.rb b/dtic-BKPs/dtic-BKPs_v5.5.1/dtic-BKPs_tasks.rb similarity index 100% rename from dtic-BKPs_v5.5.1/dtic-BKPs_tasks.rb rename to dtic-BKPs/dtic-BKPs_v5.5.1/dtic-BKPs_tasks.rb diff --git a/dtic-BKPs_v5.5.1/fxs/config_handler.rb b/dtic-BKPs/dtic-BKPs_v5.5.1/fxs/config_handler.rb similarity index 100% rename from dtic-BKPs_v5.5.1/fxs/config_handler.rb rename to dtic-BKPs/dtic-BKPs_v5.5.1/fxs/config_handler.rb diff --git a/dtic-BKPs_v5.5.1/fxs/proc_syncPmox.rb b/dtic-BKPs/dtic-BKPs_v5.5.1/fxs/proc_syncPmox.rb similarity index 100% rename from dtic-BKPs_v5.5.1/fxs/proc_syncPmox.rb rename to dtic-BKPs/dtic-BKPs_v5.5.1/fxs/proc_syncPmox.rb diff --git a/dtic-BKPs_v5.5.1/fxs/proc_syncXEN.rb b/dtic-BKPs/dtic-BKPs_v5.5.1/fxs/proc_syncXEN.rb similarity index 100% rename from dtic-BKPs_v5.5.1/fxs/proc_syncXEN.rb rename to dtic-BKPs/dtic-BKPs_v5.5.1/fxs/proc_syncXEN.rb diff --git a/dtic-BKPs_v5.5.1/fxs/sync_rclone.rb b/dtic-BKPs/dtic-BKPs_v5.5.1/fxs/sync_rclone.rb similarity index 100% rename from dtic-BKPs_v5.5.1/fxs/sync_rclone.rb rename to dtic-BKPs/dtic-BKPs_v5.5.1/fxs/sync_rclone.rb diff --git a/dtic-BKPs_v5.5.1/fxs/ui_handlers.rb b/dtic-BKPs/dtic-BKPs_v5.5.1/fxs/ui_handlers.rb similarity index 100% rename from dtic-BKPs_v5.5.1/fxs/ui_handlers.rb rename to dtic-BKPs/dtic-BKPs_v5.5.1/fxs/ui_handlers.rb diff --git a/dtic-BKPs_v5.5.1/src/core/globals.rb b/dtic-BKPs/dtic-BKPs_v5.5.1/src/core/globals.rb similarity index 100% rename from dtic-BKPs_v5.5.1/src/core/globals.rb rename to dtic-BKPs/dtic-BKPs_v5.5.1/src/core/globals.rb diff --git a/dtic-BKPs_v5.5.1/src/install.sh b/dtic-BKPs/dtic-BKPs_v5.5.1/src/install.sh similarity index 100% rename from dtic-BKPs_v5.5.1/src/install.sh rename to dtic-BKPs/dtic-BKPs_v5.5.1/src/install.sh diff --git a/proyectos/P2603_bitacoras.md b/proyectos/P2603_bitacoras.md new file mode 100644 index 00000000..16fedf2b --- /dev/null +++ b/proyectos/P2603_bitacoras.md @@ -0,0 +1,75 @@ +# P2603 - Sistema de Bitácoras (dtic-BITACORAs) + +> Manifiesto de Proyecto | Referencia: [`adn/07_proyectos.md`](../adn/07_proyectos.md) + +## Identificación + +| Campo | Valor | +| :--- | :--- | +| **Código** | P2603 | +| **Nombre** | Sistema de Bitácoras Web | +| **Estado** | ⏳ En Ejecución | +| **Inicio** | 03/03/2026 | +| **Fin Estimado** | Por definir | + +## Objetivo + +Evolucionar el registro manual de operaciones (archivos `.md`) hacia una **aplicación web** con arquitectura frontend/backend que permita: + +1. **Registro en tiempo real** de entradas de bitácora mediante API REST. +2. **Consulta visual** de la actividad diaria agrupada por nodo y proyecto. +3. **Compatibilidad** con el formato I-F-D-E del ADN (exportación a Markdown). +4. **Estética unificada** con el dashboard del proyecto P2601. + +## Stakeholders + +| Rol | Persona | Visibilidad | +| :--- | :--- | :--- | +| **Responsable Técnico** | Lic. Ricardo MONLA | Operador directo | +| **Dirección de TIC** | (Jerárquico superior) | Dashboard asíncrono | + +## Nodos Involucrados + +### Nodos Exclusivos (Core) +| Nodo | IP | Rol en el Proyecto | +| :--- | :--- | :--- | +| [**srv-ns8**](../nodos/srv-ns8.md) | `10.0.10.8` | Servidor de la aplicación (Docker) | + +## Herramientas + +| Herramienta | Uso | +| :--- | :--- | +| **React 18 + TypeScript** | Frontend SPA | +| **Vite** | Build tool y dev server | +| **Node.js + Express** | Backend API REST | +| **PostgreSQL 15** | Base de datos relacional | +| **Docker Compose** | Orquestación de servicios | + +## Hitos Clave + +| Fecha | ID | Estado | Hito | +| :--- | :--- | :---: | :--- | +| 03/03 | INIT | ⏳ | Inicialización del proyecto. Creación de `dtic-BITACORAs/` con esquema DB y APIs. | +| — | UI01 | 📍 | Frontend: Vista de bitácora diaria con entradas I-F-D-E agrupadas por nodo. | +| — | API01 | 📍 | Backend: CRUD completo de entradas, nodos y bitácoras. | +| — | EXP01 | 📍 | Exportador a Markdown compatible con el ADN. | +| — | PROD | 📍 | Despliegue en producción (srv-ns8, Nginx reverse proxy). | + +## Arquitectura + +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ Frontend │ │ Backend │ │ Database │ +│ React/Vite │◄──►│ Node.js/Express │◄──►│ PostgreSQL 15 │ +│ Puerto 5173 │ │ Puerto 3001 │ │ Puerto 5432 │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ + │ │ + └──────────────────────┘ + Docker Compose +``` + +## Referencias + +- **Bitácoras**: [03/03](../bitacoras/2026-03-03.md) +- **ADN**: [Ontología](../adn/01_ontologia.md) | [Proyectos](../adn/07_proyectos.md) | [Bitácora](../adn/02_bitacora.md) +- **Base Legacy**: [servicios/bitacoras/](../servicios/bitacoras/README.md)