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 */}
+
+
+ {/* Loading */}
+ {loading &&
}
+
+ {/* Entries by Node */}
+ {bitacora?.nodos?.map((nodoGroup, idx) => (
+
+
+
+ {nodoGroup.nodo}
+ {nodoGroup.ip && ({nodoGroup.ip})}
+ {nodoGroup.entradas.length}
+
+
+
+
+ | I |
+ F |
+ Descripción |
+ E |
+ |
+
+
+
+ {nodoGroup.entradas.map(e => (
+
+ | {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)