From 160da47f969bb7ad9b08e0eafffc267a1beb6693 Mon Sep 17 00:00:00 2001 From: Ricardo Monla Date: Sat, 28 Mar 2026 12:18:42 -0300 Subject: [PATCH] [P2606] WebServer NGINX: plan actualizado, ficha nodo, HTTPS operativo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - P2606: Plan v1.1 EN EJECUCIÓN. Fases 1-3 completadas. - Ficha nodo srvv-nginx-rm.md. P2606 en adn/07_proyectos.md. - Incluye cambios acumulados de sesiones anteriores. --- adn/01_ontologia.md | 59 +- adn/02_bitacora.md | 18 +- adn/05_ia.md | 47 +- adn/06_gobernanza.md | 2 +- adn/07_proyectos.md | 1 + adn/README.md | 23 +- adn/tools/candados/.boveda.json | 4 +- adn/tools/candados/.session | 2 +- adn/tools/candados/README.md | 4 +- adn/tools/candados/candados.rb | 51 +- adn/tools/cli/ambitos.rb | 256 +++++ adn/tools/cli/db.rb | 144 +-- adn/tools/cli/db/ambito.rb | 158 +++ adn/tools/cli/db/bitacora.rb | 21 +- adn/tools/cli/db/evento.rb | 166 +++- adn/tools/cli/db/misc.rb | 42 +- adn/tools/cli/db/nodo.rb | 126 ++- adn/tools/cli/generar.rb | 36 +- adn/tools/cli/jornada.rb | 31 + adn/tools/cli/nodos.rb | 81 +- adn/tools/cli/salud.rb | 123 ++- adn/tools/core/constants.rb | 4 +- adn/tools/db/core/ambito_db.rb | 103 ++ adn/tools/db/core/bitacora_db.rb | 157 ++- adn/tools/db/core/bitacora_db.rb.backup | 918 ++++++++++++++++++ adn/tools/db/core/mejoras_sensor.rb | 227 +++++ .../db/migrations/001_create_ambitos.sql | 54 ++ .../001_create_mejoras_metricas_table.rb | 64 ++ .../migrations/002_migrate_nodos_ambitos.sql | 55 ++ adn/tools/run | 3 + adn/tools/templates/plantillas_eventos.rb | 179 ++++ adn/tools/w-zombi/data/cmd.json | 2 +- docs/bitacoras/{ => _hist}/2026-03-16.md | 3 + docs/bitacoras/_hist/2026-03-19.md | 1 + docs/prompt/260316-0812_Continua.md | 6 +- docs/prompt/260325-0809_Contexto.md | 28 + docs/prompt/260325-0900_Contexto-IA.md | 340 +++++++ .../proy/P2601_Dasuten/P2601.06.02_AD-Join.md | 18 +- .../P2601.07.01_Integracion-DASU-PC-0.md | 49 - .../P2601.07.01_Integracion-DASU-PC.md | 19 + .../P2601.08_Unificacion-Redes-ISP.md | 166 ++++ .../P2601_Dasuten/P2601.09_DASUTEN-sin-DC.md | 137 +++ docs/proy/P2601_Dasuten/P2601_dasuten.md | 37 +- .../P2604.05.01_Estudio-Friccion-IA.md | 8 +- .../P2604.06.05_Herencia-Modo-Jornada.md | 78 ++ .../P2604.06.06_Acceso-Remoto-Bitacora-Web.md | 112 +++ ...-Mejora-Continua-para-el-Ecosistema-ADN.md | 87 +- .../P2604.07_Plantillas-Escritura-Eventos.md | 101 ++ .../P2604_Mejoras-ADN/P2604.08_Ambito-Nodo.md | 419 ++++++++ .../P2604.09_Referencia-Planes-Bitacora.md | 80 ++ .../P2604_Mejoras-ADN/P2604_Mejoras-ADN.md | 25 +- .../P2606.01_Plan-WebServer-NGINX.md | 70 ++ .../P2606_WebServer-NGINX.md | 137 +++ nodos/{dasu-pc-0.md => dasu-pc.md} | 7 +- nodos/{dasu-pcv-0.md => dasu-pcv.md} | 29 +- nodos/dasu-sql2.md | 51 + nodos/dasu-srvv-dc.md | 21 +- nodos/dasu-srvv-sql.md | 26 +- nodos/srv-dasu.md | 89 +- nodos/srvv-nginx-rm.md | 62 ++ servicios/nginx/conf.d/ns8.conf | 4 +- .../backend/src/routes/ambitos.js | 143 +++ .../backend/src/routes/bitacoras.js | 48 +- .../dtic-bitacoras/backend/src/routes/docs.js | 66 ++ .../backend/src/routes/entradas.js | 76 +- .../backend/src/routes/gestion.js | 19 +- .../backend/src/routes/nodos.js | 56 +- .../dtic-bitacoras/backend/src/server.js | 2 + .../nginx/dtic-bitacoras/docker-compose.yml | 2 + .../nginx/dtic-bitacoras/docker/init.sql | 1 + .../nginx/dtic-bitacoras/frontend/src/App.tsx | 551 ++++++++--- .../dtic-bitacoras/frontend/src/index.css | 170 +++- .../frontend/src/pages/AmbitoDashboard.tsx | 162 ++++ .../dtic-bitacoras/frontend/vite.config.ts | 2 +- tmp_dmidecode.txt | 54 ++ tmp_eventos.txt | 127 +++ tmp_eventos_left.txt | 187 ++++ tmp_pci_slots.txt | 48 + 78 files changed, 6461 insertions(+), 624 deletions(-) create mode 100644 adn/tools/cli/ambitos.rb create mode 100644 adn/tools/cli/db/ambito.rb create mode 100644 adn/tools/db/core/ambito_db.rb create mode 100644 adn/tools/db/core/bitacora_db.rb.backup create mode 100644 adn/tools/db/core/mejoras_sensor.rb create mode 100644 adn/tools/db/migrations/001_create_ambitos.sql create mode 100644 adn/tools/db/migrations/001_create_mejoras_metricas_table.rb create mode 100644 adn/tools/db/migrations/002_migrate_nodos_ambitos.sql create mode 100644 adn/tools/templates/plantillas_eventos.rb rename docs/bitacoras/{ => _hist}/2026-03-16.md (88%) create mode 100644 docs/bitacoras/_hist/2026-03-19.md create mode 100644 docs/prompt/260325-0809_Contexto.md create mode 100644 docs/prompt/260325-0900_Contexto-IA.md delete mode 100644 docs/proy/P2601_Dasuten/P2601.07.01_Integracion-DASU-PC-0.md create mode 100644 docs/proy/P2601_Dasuten/P2601.07.01_Integracion-DASU-PC.md create mode 100644 docs/proy/P2601_Dasuten/P2601.08_Unificacion-Redes-ISP.md create mode 100644 docs/proy/P2601_Dasuten/P2601.09_DASUTEN-sin-DC.md create mode 100644 docs/proy/P2604_Mejoras-ADN/P2604.06.05_Herencia-Modo-Jornada.md create mode 100644 docs/proy/P2604_Mejoras-ADN/P2604.06.06_Acceso-Remoto-Bitacora-Web.md create mode 100644 docs/proy/P2604_Mejoras-ADN/P2604.07_Plantillas-Escritura-Eventos.md create mode 100644 docs/proy/P2604_Mejoras-ADN/P2604.08_Ambito-Nodo.md create mode 100644 docs/proy/P2604_Mejoras-ADN/P2604.09_Referencia-Planes-Bitacora.md create mode 100644 docs/proy/P2606_WebServer-NGINX/P2606.01_Plan-WebServer-NGINX.md create mode 100644 docs/proy/P2606_WebServer-NGINX/P2606_WebServer-NGINX.md rename nodos/{dasu-pc-0.md => dasu-pc.md} (85%) rename nodos/{dasu-pcv-0.md => dasu-pcv.md} (61%) create mode 100644 nodos/dasu-sql2.md create mode 100644 nodos/srvv-nginx-rm.md create mode 100644 servicios/nginx/dtic-bitacoras/backend/src/routes/ambitos.js create mode 100644 servicios/nginx/dtic-bitacoras/backend/src/routes/docs.js create mode 100644 servicios/nginx/dtic-bitacoras/frontend/src/pages/AmbitoDashboard.tsx create mode 100644 tmp_dmidecode.txt create mode 100644 tmp_eventos.txt create mode 100644 tmp_eventos_left.txt create mode 100644 tmp_pci_slots.txt diff --git a/adn/01_ontologia.md b/adn/01_ontologia.md index 9f041093..3f7e1c14 100644 --- a/adn/01_ontologia.md +++ b/adn/01_ontologia.md @@ -20,6 +20,40 @@ Facultad Regional La Rioja --- +## Modelo Ámbito/Nodo + +El ecosistema ADN implementa una jerarquía **Ámbito → Nodos** que agrupa lógicamente los recursos por área organizativa. + +### Jerarquía de Ámbitos + +| Ámbito | Descripción | Nodos | +|--------|-------------|-------| +| `dtic-DIIAA` | Ámbito raíz: Infraestructura y servicios propios | srv-ns8, srv-pmox*, srv-xen1, srvv-*, dtic-BITACORAS | +| `dtic-DASUTEN` | Sub-ámbito: Departamento de Servicios UTN (gestionado) | srv-dasu, sql-dasuten, dc-dasuten, dasu-pc, dasu-pcv* | +| `dtic-BITACORAS` | Sistema de registro ADN | dtic-BITACORAS | + +### Relaciones +- **Ámbito → Nodos**: Uno a muchos (1:N) +- **Nodos → Entradas**: Uno a muchos (1:N) +- **Entradas → Ámbito**: Deriva automáticamente del nodo asociado + +### Consultas por Ámbito + +```bash +# CLI: Listar nodos de un ámbito +./adn/tools/run db nodo:listar --ambito dtic-DASUTEN + +# CLI: Listar eventos de un ámbito +./adn/tools/run db evento:listar --ambito dtic-DIIAA + +# API: Endpoints REST +GET /api/ambitos +GET /api/ambitos/:id/nodos +GET /api/entradas?ambito_id=2 +``` + +--- + ## Nodos de la Red (Árbol de Dependencia) ### 🖥️ srv-ns8 (Physical / Hybrid) @@ -70,12 +104,29 @@ Facultad Regional La Rioja | - | **[srvv-sysacadweb](../nodos/srvv-sysacadweb.md)** | *Interna* | Web SysACAD | | - | **[srvv-maurik](../nodos/srvv-maurik.md)** | *Desconocida* | Domain Controller | -### 🛰️ Nodos Externos / Satélites -| Nodo | Ubicación | Descripción | +### 🛰️ Nodos Externos / Satélites (DASUTEN) + +#### **[srv-dasu](../nodos/srv-dasu.md)** - Hipervisor Principal +| Nodo | IP Local | IP Tailscale | Rol | Estado | +| :--- | :--- | :--- | :--- | :--- | +| **[srv-dasu](../nodos/srv-dasu.md)** | `10.0.100.1` (vmbr0) | `100.112.46.104` | Proxmox VE + Subnet Router | ✅ ONLINE | + +#### VMs en srv-dasu (Red 10.0.100.0/24) +| VM | IP Local | IP Tailscale | Rol | Estado | +| :--- | :--- | :--- | :--- | :--- | +| **dasu-srvv-dc** | `10.0.100.10` | `100.85.117.101` | AD DS + DNS | ✅ ONLINE | +| **dasu-srvv-sql** | `10.0.100.11` | `100.107.24.124` | SQL Server 2019 | ✅ ONLINE | +| **dasu-pcv** | `10.0.100.12` | `100.118.129.44` | VM Pruebas | ⚠️ Offline (7d) | + +#### Nodo Físico DASUTEN +| Nodo | IP Tailscale | Rol | Estado | +| :--- | :--- | :--- | :--- | +| **[dasu-pc](../nodos/dasu-pc.md)** | `100.100.145.51` | PC Cliente (Dominio) | ✅ ONLINE | + +#### Otros Nodos Externos +| Nodo | IP | Descripción | | :--- | :--- | :--- | | **[cam-hikvision-24](../nodos/cam-hikvision-24.md)** | `10.0.10.24` | Cámara / DVR | -| **[dasu-pc-0](../nodos/dasu-pc-0.md)** | Oficina DASUTEN | Win10 Físico (Ryzen 5) | -| **[srv-dasu](../nodos/srv-dasu.md)** | Oficina DASUTEN | Proxmox VE Físico (Standalone) | --- diff --git a/adn/02_bitacora.md b/adn/02_bitacora.md index 74bb5dc8..a2cea38b 100644 --- a/adn/02_bitacora.md +++ b/adn/02_bitacora.md @@ -21,6 +21,7 @@ Esta hebra define el formato, estructura y reglas de sincronización de las bit ## 3. Actividades Detalladas - **Subdivisión**: Encabezados por Nodo (`### srv-ns8`). +- **Referencia de Ámbito**: Cada nodo tiene un ámbito asociado (ej: `srv-ns8` → `dtic-DIIAA`). En la vista web, los eventos se agrupan por **ámbito** y la columna **Nodo** muestra el origen de cada evento. - **Formato Jerárquico**: 1. Título H4 del Tema: `#### [Estado] - [ID] - [Nombre Tarea]` (ej. `#### ✅ - A09 - dtic-BKPs C4`). 2. Párrafo Narrativo: Descripción rica que incluye referencias de migración (ej. `➡️ **[Viene del 23/02]...**`) y que explica el "qué" y el "por qué" de la tarea. @@ -35,9 +36,24 @@ Esta hebra define el formato, estructura y reglas de sincronización de las bit - **Semántica (General)**: Uso de una taxonomía visual rica para escaneo visual ultrarrápido (ver hebra [04_iconografia.md](04_iconografia.md)). - **Trazabilidad de Proyectos (Prefijos)**: Cuando una actividad forme parte de un plan formalizado en `docs/proy/`, la descripción del evento en la bitácora DEBE comenzar con el código del plan entre corchetes. - **Formato**: `[PROY.FASE.ITEM] Descripción de la actividad` - - **Ejemplo**: `[P2601.07.01] Configuración de DNS en dasu-pc-0` + - **Ejemplo**: `[P2601.07.01] Configuración de DNS en dasu-pc` - **Auto-Registro (IA)**: Cualquier acción técnica autónoma de la IA **DEBE** registrarse explícitamente en las tablas cronológicas indicando "(IA)" y el respectivo emoji semántico. +### 3.3 Plantillas de Detalle para Eventos +Para enriquecer los detalles de eventos, usar plantillas predefinidas: + +**Herramienta**: `./adn/tools/run generar evento ` + +**Tipos disponibles**: +- `implementacion` - Implementación técnica +- `investigacion` - Investigación/exploración +- `resolucion` - Resolución de incidentes +- `documentacion` - Documentación/corrección +- `reunion` - Reuniones/comunicaciones +- `test` - Pruebas/validación +- `debug` - Depuración +- `planificacion` - Planificación + ### 3.1 Ejemplo Estructural ```markdown diff --git a/adn/05_ia.md b/adn/05_ia.md index b7488eea..8fc1ae71 100644 --- a/adn/05_ia.md +++ b/adn/05_ia.md @@ -78,4 +78,49 @@ El sistema ADN implementa DB Hooks en la capa de datos para automatizar sincroni - **Ventaja**: Elimina la necesidad de editar dos archivos distintos para la misma acción, reduciendo la dependencia de IA para tareas de sincronización. -- **Implementación**: Los hooks están en `adn/tools/db/core/bitacora_db.rb` y se ejecutan después de operaciones `create_entrada` y `update_entrada`. \ No newline at end of file +- **Implementación**: Los hooks están en `adn/tools/db/core/bitacora_db.rb` y se ejecutan después de operaciones `create_entrada` y `update_entrada`. + +### 17. Sensor de Mejora Continua +**OBLIGATORIO.** El sistema implementa detección automática de fricción operativa: + +**Ritual de Inicio**: Al inicio de cada sesión o cambio de contexto mayor, consultar: +```bash +./adn/tools/run salud --mejoras --json +``` + +**Patrones Detectados**: +1. **Conexiones DB excesivas**: > 5 conexiones/minuto → Sugiere connection pooling +2. **Comandos repetitivos**: > 5 comandos similares → Sugiere automatización +3. **Inconsistencias documento-DB**: Entradas `[P2604]` sin actualización en proyectos + +**Acción**: Si se detectan patrones, exponer al operador y proponer mejora antes de continuar. + +**Implementación**: `adn/tools/db/core/mejoras_sensor.rb` + +### 18. Plantillas de Escritura para Eventos +Para enriquecer los detalles de eventos con estructura consistente: + +**Uso**: `./adn/tools/run generar evento ` + +**Tipos con iconos**: +| Ícono | Tipo | Uso | +|-------|------|-----| +| 🔧 | `implementacion` | Implementación técnica | +| 🔍 | `investigacion` | Investigación/exploración | +| 🛠️ | `resolucion` | Resolución de incidentes | +| 📝 | `documentacion` | Documentación/corrección | +| ✅ | `test` | Pruebas/validación | +| 🐛 | `debug` | Depuración | +| 📋 | `planificacion` | Planificación | +| 👥 | `reunion` | Reuniones/comunicaciones | + +**Objetivo**: Mejorar trazabilidad y escaneo visual en bitácoras. + +**Formato estándar para eventos**: +``` +[PROY.COD] Título ✅ + +**Tipo de evento** +**Campo 1**: ... +**Campo 2**: ... +``` diff --git a/adn/06_gobernanza.md b/adn/06_gobernanza.md index 3796009b..5b7a47f6 100644 --- a/adn/06_gobernanza.md +++ b/adn/06_gobernanza.md @@ -7,7 +7,7 @@ Esta hebra establece las reglas fundamentales de idioma, nomenclatura, coherenci ## 1. Idioma y Nomenclatura - **Directiva**: **Todo el contenido debe estar en español.** -- **Nomenclatura de Nodos**: Estrictamente **minúsculas** en nombres de archivo y referencias (ej: `srv-ns8`, `dasu-pc-0`, `srvv-maurik`). +- **Nomenclatura de Nodos**: Estrictamente **minúsculas** en nombres de archivo y referencias (ej: `srv-ns8`, `dasu-pc`, `srvv-maurik`). - **Alcance**: - Interacciones con el asistente (chat). - Documentación (archivos `.md`). diff --git a/adn/07_proyectos.md b/adn/07_proyectos.md index d32133b5..012e1d51 100644 --- a/adn/07_proyectos.md +++ b/adn/07_proyectos.md @@ -111,3 +111,4 @@ Los proyectos son el **tejido conectivo** que une nodos y bitácoras en un prop | P2603 | Sistema de Bitácoras | ⏳ | [P2603_Bitacoras](../docs/proy/P2603_Bitacoras/P2603_bitacoras.md) | | P2604 | Mejoras ADN | ⏳ | [P2604_Mejoras-ADN](../docs/proy/P2604_Mejoras-ADN/P2604_mejoras_ADN.md) | | P2605 | Bitácoras Web | ⏳ | [P2605_Bitacoras-Web](../docs/proy/P2605_Bitacoras-Web/P2605_Migracion_Solo_Bitacoras_Web.md) | +| P2606 | WebServer NGINX | ⏳ | [P2606_WebServer-NGINX](../docs/proy/P2606_WebServer-NGINX/P2606_WebServer-NGINX.md) | diff --git a/adn/README.md b/adn/README.md index ffa3febf..034e2726 100644 --- a/adn/README.md +++ b/adn/README.md @@ -59,25 +59,22 @@ # Salud de la base de datos ./adn/tools/run db salud:bd -# Gestión de eventos (entradas cronológicas) -# Estado y modo se calculan automáticamente: -# - Estado: si tiene --fin → ✅, si no → ⏳ -# - Modo: si no se especifica, se obtiene desde la última jornada del día -./adn/tools/run db evento:crear --nodo --descripcion "Texto" --inicio HH:MM [--fin HH:MM] [--modo P|R] -./adn/tools/run db evento:listar [--desde FECHA] [--hasta FECHA] [--nodo NODO] -./adn/tools/run db evento:actualizar [--fin HH:MM] # Estado se calcula automáticamente +./adn/tools/run db evento:crear --nodo --descripcion "..." [--plantilla ] +./adn/tools/run db evento:listar [--desde FECHA] [--hasta FECHA] [--nodo NODO] [--ambito AMBITO] [--detalle] +./adn/tools/run db evento:actualizar [--fin HH:MM] [--descripcion "..."] # Estado ✅ se calcula automáticamente con --fin ./adn/tools/run db evento:eliminar -# Gestión de bitácoras -./adn/tools/run db bitacora:crear [FECHA] -./adn/tools/run db bitacora:listar [--limit N] +# Gestión de ámbitos (agrupadores de nodos) +./adn/tools/run db ámbito:crear --nombre [--descripcion "texto"] [--parent-id ] +./adn/tools/run db ámbito:listar +./adn/tools/run db ámbito:nodos # Gestión de nodos -./adn/tools/run db nodo:crear --nombre --tipo -./adn/tools/run db nodo:listar [--tipo TIPO] +./adn/tools/run db nodo:crear --nombre --tipo [--ambito ] +./adn/tools/run db nodo:listar [--tipo TIPO] [--ambito AMBITO] # Estadísticas -./adn/tools/run db estadisticas +./adn/tools/run db estadisticas [--ambito AMBITO] ``` --- diff --git a/adn/tools/candados/.boveda.json b/adn/tools/candados/.boveda.json index 09ee09ef..e0a5811b 100644 --- a/adn/tools/candados/.boveda.json +++ b/adn/tools/candados/.boveda.json @@ -8,5 +8,7 @@ "dominio-utnlarioja.intranet:monlaricardo": "eyJpdiI6InFRQ3VNYWlVSHlKMURNdzciLCJ0YWciOiJ1a3hESUZlaEt6enV3UXZxWUFrWXhRPT0iLCJkYXRhIjoiNVNaekdxVyt6dE5WQmhPaXF3PT0ifQ==", "ns8:rmonla:sudo": "eyJpdiI6IkRqbTBhVlZSYUpVdno3TFkiLCJ0YWciOiJJSnE2ei8zZHIrV2hFQks1VjNpVGN3PT0iLCJkYXRhIjoiT3JxTjRhTmlQK2R3TlJZZk9BPT0ifQ==", "srv-dasu:root": "eyJpdiI6Ik5LY09JR1FvaFVPRXd3bjUiLCJ0YWciOiJwb0NLZ3A4N3VOQUZMQWhMS1hUVWZnPT0iLCJkYXRhIjoicEgvWTFmd0RhSFQ0YWRHRUxyZ1lBQT09In0=", - "srv-ns8:rmonla:sudo": "eyJpdiI6InJDOUp2STdYR1crb2NkMTgiLCJ0YWciOiJHSUZVbXM5VnVmZ3d1NXQ3SFVwOUdRPT0iLCJkYXRhIjoib1ZVTzdaLzk1WGdwZDhKREM3aWpnV3c9In0=" + "srv-ns8:rmonla:sudo": "eyJpdiI6InJDOUp2STdYR1crb2NkMTgiLCJ0YWciOiJHSUZVbXM5VnVmZ3d1NXQ3SFVwOUdRPT0iLCJkYXRhIjoib1ZVTzdaLzk1WGdwZDhKREM3aWpnV3c9In0=", + "dasu-sql2:Administrator": "eyJpdiI6IlJ0VWJzblVGM2NhWEtvNlgiLCJ0YWciOiJtM0llb2luUmRUZng0NnBsL1JtKzl3PT0iLCJkYXRhIjoiSHVhaklOTFlsSm1PbDJkTjNoYlF0UT09In0=", + "srvv-nginx-rm:root": "eyJpdiI6Ijc0STVkTk92Tk5LZEZZWWciLCJ0YWciOiJlOTAwcGpTa21PZFVhN2FJTzIxU1VBPT0iLCJkYXRhIjoiYUJRR3hRakFub1F1OCtyTVdWcW8ifQ==" } diff --git a/adn/tools/candados/.session b/adn/tools/candados/.session index cb4e10bf..4483884c 100644 --- a/adn/tools/candados/.session +++ b/adn/tools/candados/.session @@ -1 +1 @@ -1773874654 \ No newline at end of file +1774708556 \ No newline at end of file diff --git a/adn/tools/candados/README.md b/adn/tools/candados/README.md index 1891d70a..0251f733 100644 --- a/adn/tools/candados/README.md +++ b/adn/tools/candados/README.md @@ -58,7 +58,9 @@ eval $(ruby candados.rb load srv-dasu:rmonla) # USR y PASS están disponibles en el shell ``` -Internamente escribe los exports a un archivo temporal (`/tmp/.candados_env_`) con permisos `600`, y a stdout solo emite el comando `source` + autoborrado. +Internamente escribe los exports a un archivo temporal (`tmp/.candados_env_`) con permisos `600`, y a stdout solo emite el comando `source` + autoborrado. + +**Nota:** Los temporales se guardan en `tmp/` del repositorio (no en `/tmp/`) para cumplir con el principio 11 de `05_ia.md` (Workspace Bounds). ## 🔑 Resolución Automática de Pares diff --git a/adn/tools/candados/candados.rb b/adn/tools/candados/candados.rb index 25f5bc7b..373bb663 100755 --- a/adn/tools/candados/candados.rb +++ b/adn/tools/candados/candados.rb @@ -38,11 +38,13 @@ require 'fileutils' # ─── Rutas ─────────────────────────────────────────────────────────── TOOL_DIR = File.expand_path(__dir__) +PROJECT_ROOT = File.expand_path('../../..', TOOL_DIR) # Desde adn/tools/candados -> adn/tools -> adn -> root MASTER_KEY = File.join(TOOL_DIR, '.master.key') OLD_KEY = File.expand_path('../../.master.key', TOOL_DIR) BOVEDA = File.join(TOOL_DIR, '.boveda.json') SESION = File.join(TOOL_DIR, '.session') ACCESS_LOG = File.join(TOOL_DIR, 'access.log') +TMP_DIR = File.join(PROJECT_ROOT, 'tmp') # Directorio para temporales (principio 11 - 05_ia.md) # ─── Transición de Seguridad ───────────────────────────────────────── if File.exist?(OLD_KEY) && !File.exist?(MASTER_KEY) @@ -175,20 +177,41 @@ end # ─── Comandos ──────────────────────────────────────────────────────── -def cmd_abrir - puts "#{C::CYAN}🔐 candados: Autorización SSH#{C::RESET}" - puts "#{C::DIM}Limpiando identidades previas...#{C::RESET}" +def cmd_abrir(cripto, boveda) + verificar_sesion! + token = boveda.get('rsa') + abort "#{C::RED}✗ Clave 'rsa' no encontrada en la bóveda. Guardala con: ruby candados.rb set rsa#{C::RESET}" if token.nil? + + passphrase = cripto.decrypt(token) + registrar_acceso('ABRIR', 'SSH key load via ssh-add') + + $stderr.puts "#{C::CYAN}🔐 candados: Cargando clave SSH...#{C::RESET}" system('ssh-add -D > /dev/null 2>&1') - puts "#{C::YELLOW}🔑 Ingresá tu passphrase:#{C::RESET}" - system('ssh-add') + # Crear script temporal para SSH_ASKPASS (no interactivo) + require 'securerandom' + FileUtils.mkdir_p(TMP_DIR) unless File.exist?(TMP_DIR) + askpass_path = File.join(TMP_DIR, ".ssh_askpass_#{SecureRandom.hex(4)}") + safe_pass = passphrase.gsub("'", "'\\''") + File.write(askpass_path, "#!/bin/sh\necho '#{safe_pass}'\n") + File.chmod(0700, askpass_path) - system('ssh-add -l > /dev/null 2>&1') - unless $?.success? - abort "#{C::RED}✗ Sin identidades SSH cargadas. Abortando.#{C::RESET}" + # Cargar clave via SSH_ASKPASS + ENV['SSH_ASKPASS'] = askpass_path + ENV['SSH_ASKPASS_REQUIRE'] = 'force' + ENV['DISPLAY'] = ENV['DISPLAY'] || ':0' + system('ssh-add < /dev/null') + + # Limpiar + FileUtils.rm_f(askpass_path) + ENV.delete('SSH_ASKPASS') + ENV.delete('SSH_ASKPASS_REQUIRE') + + if system('ssh-add -l > /dev/null 2>&1') + $stderr.puts "#{C::GREEN}🔓 SSH autorizado. Identidades cargadas en el agente.#{C::RESET}" + else + abort "#{C::RED}✗ Sin identidades SSH cargadas. Verificá la passphrase en 'rsa'.#{C::RESET}" end - - puts "#{C::GREEN}🔓 SSH autorizado. Las identidades permanecen activas mientras dure la sesión del agente.#{C::RESET}" end def cmd_encrypt(cripto, texto) @@ -326,8 +349,10 @@ def cmd_load(cripto, boveda, clave) # Seguridad: NUNCA imprimir secretos a stdout/stderr. # Escribir exports en un archivo temporal con permisos estrictos (600). # A stdout solo emitimos el comando `source` + autoborrado. + # NOTA: Usamos tmp/ del proyecto (no /tmp/) para cumplir principio 11 de 05_ia.md require 'securerandom' - tmpfile_path = "/tmp/.candados_env_#{SecureRandom.hex(8)}" + FileUtils.mkdir_p(TMP_DIR) unless File.exist?(TMP_DIR) + tmpfile_path = File.join(TMP_DIR, ".candados_env_#{SecureRandom.hex(8)}") safe_user = user_value.gsub("'", "'\\''") safe_pass = pass_value.gsub("'", "'\\''") @@ -463,6 +488,10 @@ if __FILE__ == $0 autorizar! when 'cerrar' cerrar_sesion! + when 'abrir' + cripto = Cripto.new(cargar_master_key) + boveda = Boveda.new(BOVEDA) + cmd_abrir(cripto, boveda) when 'sudo' cripto = Cripto.new(cargar_master_key) boveda = Boveda.new(BOVEDA) diff --git a/adn/tools/cli/ambitos.rb b/adn/tools/cli/ambitos.rb new file mode 100644 index 00000000..76dfac6d --- /dev/null +++ b/adn/tools/cli/ambitos.rb @@ -0,0 +1,256 @@ +# frozen_string_literal: true + +require 'optparse' +require_relative '../core/colores' +require_relative '../core/help_formatter' +require_relative '../db/core/bitacora_db' + +module ADN + class SubcomandoAmbitos + def initialize(args, logger) + @args = args + @logger = logger + end + + def ejecutar + if @args.empty? || ['help', '-h', '--help'].include?(@args[0]) + mostrar_ayuda + return + end + + subcomando = @args[0] + sub_args = @args[1..-1] || [] + + case subcomando + when 'listar' + comando_listar(sub_args) + when 'crear' + comando_crear(sub_args) + when 'info' + comando_info(sub_args) + when 'nodos' + comando_nodos(sub_args) + else + puts "#{Color::RED}✗ Subcomando desconocido: '#{subcomando}'#{Color::RESET}" + puts "\n" + mostrar_ayuda + exit 1 + end + rescue Interrupt + puts "\n#{Color::YELLOW}⚠ Operación interrumpida por el usuario#{Color::RESET}" + exit 130 + rescue => e + @logger.error("Error en operación de ámbitos: #{e.message}") + puts "#{Color::RED}✗ Error: #{e.message}#{Color::RESET}" + exit 1 + end + + private + + def comando_listar(args) + opts = { activo: nil } + + parser = OptionParser.new do |opt| + opt.on("--activo", "Mostrar solo ámbitos activos") { opts[:activo] = true } + opt.on("--inactivo", "Mostrar solo ámbitos inactivos") { opts[:activo] = false } + opt.on("-h", "--help", "Mostrar ayuda") do + puts HelpFormatter.generar_help( + titulo: "Listar Ámbitos", + descripcion: "Muestra la lista de ámbitos (agrupadores de nodos).", + uso: HelpFormatter.format_usage("adn ambitos listar", "[OPCIONES]"), + opciones: [ + { names: ["activo"], desc: "Mostrar solo ámbitos activos" }, + { names: ["inactivo"], desc: "Mostrar solo ámbitos inactivos" }, + { names: ["h", "help"], desc: "Mostrar esta ayuda" } + ], + ejemplos: [{ cmd: "./adn/tools/run ambitos listar", desc: "Listar todos los ámbitos" }] + ) + exit 0 + end + end + + parsear_argumentos!(parser, args) + + puts "#{Color::CYAN}📋 Listando ámbitos...#{Color::RESET}\n\n" + + BitacorasDB::BitacoraDB.with_connection do |db| + ambitos = db.list_ambitos(activo: opts[:activo]) + + if ambitos.empty? + puts "#{Color::YELLOW}⚠ No se encontraron ámbitos.#{Color::RESET}" + return + end + + puts "#{Color::BOLD}#{"ID".ljust(5)} #{"Nombre".ljust(20)} #{"Nodos".ljust(7)} #{"Activo".ljust(6)} #{"Padre".ljust(20)}#{Color::RESET}" + puts "#{"─" * 5} #{"─" * 20} #{"─" * 7} #{"─" * 6} #{"─" * 20}" + + ambitos.each do |a| + id = a['id'].to_s.ljust(5) + nombre = a['nombre'].to_s.ljust(20) + count = a['count_nodos'].to_s.ljust(7) + activo = (a['activo'] ? '✅' : '❌').ljust(6) + padre = (a['parent_nombre'] || '-').ljust(20) + + puts "#{id} #{nombre} #{count} #{activo} #{padre}" + end + puts "\nTotal: #{ambitos.length} ámbitos." + end + end + + def comando_crear(args) + opts = { nombre: nil, descripcion: nil, activo: true } + + parser = OptionParser.new do |opt| + opt.on("--nombre NOMBRE", "Nombre del ámbito") { |n| opts[:nombre] = n } + opt.on("--descripcion DESC", "Descripción del ámbito") { |d| opts[:descripcion] = d } + opt.on("-h", "--help", "Mostrar ayuda") do + puts HelpFormatter.generar_help( + titulo: "Crear Ámbito", + descripcion: "Genera un nuevo ámbito para agrupar nodos de infraestructura.", + uso: HelpFormatter.format_usage("adn ambitos crear", "--nombre NOMBRE [OPCIONES]"), + opciones: [ + { names: ["nombre NOMBRE"], desc: "Nombre único del ámbito (obligatorio)" }, + { names: ["descripcion DESC"], desc: "Descripción del propósito del ámbito" }, + { names: ["h", "help"], desc: "Mostrar esta ayuda" } + ], + ejemplos: [{ cmd: "./adn/tools/run ambitos crear --nombre dtic-Pruebas", desc: "Crear un ámbito de pruebas" }] + ) + exit 0 + end + end + + parsear_argumentos!(parser, args) + + unless opts[:nombre] + puts "#{Color::RED}✗ Error: El parámetro --nombre es obligatorio.#{Color::RESET}" + exit 1 + end + + puts "#{Color::CYAN}📦 Creando ámbito '#{opts[:nombre]}'...#{Color::RESET}" + + BitacorasDB::BitacoraDB.with_connection do |db| + begin + ambito = db.create_ambito(nombre: opts[:nombre], descripcion: opts[:descripcion]) + puts "#{Color::GREEN}✅ Ámbito creado exitosamente. ID: #{ambito['id']}#{Color::RESET}" + rescue PG::UniqueViolation + puts "#{Color::RED}✗ Error: Ya existe un ámbito con el nombre '#{opts[:nombre]}'.#{Color::RESET}" + exit 1 + end + end + end + + def comando_info(args) + opts = {} + parser = OptionParser.new do |opt| + opt.on("-h", "--help", "Mostrar ayuda") do + puts HelpFormatter.generar_help( + titulo: "Info de Ámbito", + descripcion: "Muestra información y estadísticas detalladas de un ámbito.", + uso: HelpFormatter.format_usage("adn ambitos info", ""), + opciones: [], + ejemplos: [{ cmd: "./adn/tools/run ambitos info dtic-DASUTEN", desc: "Ver detalles de dtic-DASUTEN" }] + ) + exit 0 + end + end + + parsear_argumentos!(parser, args) + objetivo = args.first + + if objetivo.nil? || objetivo.empty? + puts "#{Color::RED}✗ Error: Debes especificar el nombre o ID del ámbito.#{Color::RESET}" + exit 1 + end + + BitacorasDB::BitacoraDB.with_connection do |db| + ambito = db.get_ambito(objetivo) + if ambito.nil? + puts "#{Color::RED}✗ Error: Ámbito '#{objetivo}' no encontrado.#{Color::RESET}" + exit 1 + end + + puts "#{Color::CYAN}ℹ Info del Ámbito: #{ambito['nombre']}#{Color::RESET}\n\n" + puts " #{Color::BOLD}ID:#{Color::RESET} #{ambito['id']}" + puts " #{Color::BOLD}Nombre:#{Color::RESET} #{ambito['nombre']}" + puts " #{Color::BOLD}Descripción:#{Color::RESET} #{ambito['descripcion'] || '(sin descripción)'}" + puts " #{Color::BOLD}Padre:#{Color::RESET} #{ambito['parent_nombre'] || '(raíz)'}" + puts " #{Color::BOLD}Activo:#{Color::RESET} #{ambito['activo'] ? 'Sí' : 'No'}" + puts " #{Color::BOLD}Nodos:#{Color::RESET} #{ambito['count_nodos']}" + puts "" + end + end + + def comando_nodos(args) + opts = {} + parser = OptionParser.new do |opt| + opt.on("-h", "--help", "Mostrar ayuda") do + puts HelpFormatter.generar_help( + titulo: "Nodos de Ámbito", + descripcion: "Lista todos los nodos pertenecientes a un ámbito.", + uso: HelpFormatter.format_usage("adn ambitos nodos", ""), + opciones: [], + ejemplos: [{ cmd: "./adn/tools/run ambitos nodos dtic-DASUTEN", desc: "Ver componentes agrupados bajo dtic-DASUTEN" }] + ) + exit 0 + end + end + + parsear_argumentos!(parser, args) + objetivo = args.first + + if objetivo.nil? || objetivo.empty? + puts "#{Color::RED}✗ Error: Debes especificar el nombre o ID del ámbito.#{Color::RESET}" + exit 1 + end + + BitacorasDB::BitacoraDB.with_connection do |db| + ambito = db.get_ambito(objetivo) + if ambito.nil? + puts "#{Color::RED}✗ Error: Ámbito '#{objetivo}' no encontrado.#{Color::RESET}" + exit 1 + end + + puts "#{Color::CYAN}🖥️ Nodos del ámbito '#{ambito['nombre']}'...#{Color::RESET}\n\n" + nodos = db.get_nodos_by_ambito(ambito['id']) + + if nodos.empty? + puts "#{Color::YELLOW}⚠ Este ámbito no tiene nodos asignados.#{Color::RESET}" + return + end + + nodos.each do |n| + tipo = n['tipo'].to_s.ljust(10) + ip = (n['ip'] || '-').ljust(15) + estado = n['activo'] ? "#{Color::GREEN}ON#{Color::RESET}" : "#{Color::RED}OFF#{Color::RESET}" + puts " #{Color::DIM}▸#{Color::RESET} #{Color::BOLD}#{n['nombre'].to_s.ljust(20)}#{Color::RESET} [#{tipo}] IP: #{ip} (#{estado})" + end + end + end + + def mostrar_ayuda + puts HelpFormatter.generar_help( + titulo: "Gestión de Ámbitos (Modelo DB-First)", + descripcion: "Los ámbitos agrupan lógicamente a los nodos correspondientes a áreas organizativas.", + uso: HelpFormatter.format_usage("adn ambitos", " [OPCIONES]"), + opciones: [ + { names: ["h", "help"], desc: "Mostrar esta ayuda" } + ], + ejemplos: [ + { cmd: "./adn/tools/run ambitos listar", desc: "Listar ámbitos y cantidad de nodos" }, + { cmd: "./adn/tools/run ambitos info dtic-DIIAA", desc: "Ver detalle del ámbito DIIAA" }, + { cmd: "./adn/tools/run ambitos nodos dtic-DASUTEN", desc: "Listar hipervisores e infraestructura de DASUTEN" } + ] + ) + end + + def parsear_argumentos!(parser, args) + begin + parser.parse!(args) + rescue OptionParser::InvalidOption => e + puts "#{Color::RED}✗ Error: #{e.message}#{Color::RESET}" + puts "\nEjecuta con --help para ver las opciones disponibles." + exit 1 + end + end + end +end diff --git a/adn/tools/cli/db.rb b/adn/tools/cli/db.rb index da56a427..54f08ed3 100644 --- a/adn/tools/cli/db.rb +++ b/adn/tools/cli/db.rb @@ -42,10 +42,12 @@ module ADN require_relative 'db/bitacora' require_relative 'db/evento' require_relative 'db/nodo' + require_relative 'db/ambito' require_relative 'db/hito' require_relative 'db/misc' include SubcomandoDBBitacora include SubcomandoDBEvento + include SubcomandoDBAmbito include SubcomandoDBNodo include SubcomandoDBHito include SubcomandoDBMisc @@ -88,6 +90,8 @@ module ADN comando_nodo_crear(sub_args) when 'nodo:listar' comando_nodo_listar(sub_args) + when 'nodo:actualizar' + comando_nodo_actualizar(sub_args) when 'hito:listar' comando_hito_listar(sub_args) when 'hito:actualizar' @@ -98,10 +102,20 @@ module ADN comando_hito_ordenar(sub_args) when 'estadisticas' comando_estadisticas + when 'ambito:listar' + comando_ambito_listar(sub_args) + when 'ambito:crear' + comando_ambito_crear(sub_args) + when 'ambito:info' + comando_ambito_info(sub_args) + when 'ambito:nodos' + comando_ambito_nodos(sub_args) when 'backup' comando_backup(sub_args) when 'archivar:md' comando_archivar_md + when 'migrar:sql' + comando_migrar_sql(sub_args) else @logger.error("Subcomando desconocido: #{subcomando}") @@ -181,134 +195,4 @@ end end end -module ADN - class SubcomandoDB - require_relative 'db/bitacora' - require_relative 'db/evento' - require_relative 'db/nodo' - require_relative 'db/hito' - require_relative 'db/misc' - include SubcomandoDBBitacora - include SubcomandoDBEvento - include SubcomandoDBNodo - include SubcomandoDBHito - include SubcomandoDBMisc - # Inicializa el subcomando con argumentos y logger - # - # @param args [Array] Argumentos de línea de comandos - # @param logger [ADNLogger] Instancia de logger para registro - def initialize(args, logger) - @args = args - @logger = logger - @config = cargar_configuracion - end - # Ejecuta el subcomando según los argumentos proporcionados - def ejecutar - if @args.empty? || @args[0] == 'help' || @args[0] == '-h' || @args[0] == '--help' - mostrar_ayuda - return - end - - subcomando = @args[0] - sub_args = @args[1..-1] || [] - - case subcomando - when 'salud:bd' - comando_salud_bd - when 'bitacora:crear' - comando_bitacora_crear(sub_args) - when 'bitacora:listar' - comando_bitacora_listar(sub_args) - when 'evento:crear' - comando_evento_crear(sub_args) - when 'evento:listar' - comando_evento_listar(sub_args) - when 'evento:actualizar' - comando_evento_actualizar(sub_args) - when 'evento:eliminar' - comando_evento_eliminar(sub_args) - when 'nodo:crear' - comando_nodo_crear(sub_args) - when 'nodo:listar' - comando_nodo_listar(sub_args) - when 'hito:listar' - comando_hito_listar(sub_args) - when 'hito:actualizar' - comando_hito_actualizar(sub_args) - when 'hito:renombrar' - comando_hito_renombrar(sub_args) - when 'hito:ordenar' - comando_hito_ordenar(sub_args) - when 'estadisticas' - comando_estadisticas - when 'backup' - comando_backup(sub_args) - when 'archivar:md' - comando_archivar_md - - else - @logger.error("Subcomando desconocido: #{subcomando}") - puts "#{Color::RED}✗ Subcomando desconocido: '#{subcomando}'#{Color::RESET}" - puts "" - mostrar_ayuda - exit 1 - end - rescue Interrupt - puts "\n#{Color::YELLOW}⚠ Operación interrumpida por el usuario#{Color::RESET}" - @logger.info("Operación de base de datos interrumpida por usuario") - exit 130 - rescue => e - @logger.error("Error en operación de base de datos: #{e.message}", { backtrace: e.backtrace }) - puts "#{Color::RED}✗ Error: #{e.message}#{Color::RESET}" - puts "#{Color::DIM}#{e.backtrace.first}#{Color::RESET}" if ENV['ADN_DEBUG'] - exit 1 - end - - private - - # ─── COMANDOS PRINCIPALES ────────────────────────────────────────────── - - # ─── MÉTODOS AUXILIARES ────────────────────────────────────────────── - - # Carga la configuración desde config.yml - def cargar_configuracion - config_path = File.join(__dir__, 'config.yml') - return {} unless File.exist?(config_path) - - begin - YAML.load_file(config_path) - rescue => e - @logger.error("Error cargando configuración: #{e.message}") - {} - end - end - - # Muestra la ayuda general del subcomando - def mostrar_ayuda - puts "#{Color::BOLD}Uso: ./adn/tools/run db [SUBCOMANDO] [OPCIONES]#{Color::RESET}" - puts "" - puts "#{Color::BOLD}Subcomandos disponibles:#{Color::RESET}" - puts "" - puts " #{Color::CYAN}salud:bd#{Color::RESET} Verificar conexión y estado de la BD" - puts " #{Color::CYAN}backup [OPCIONES]#{Color::RESET} Crear backup de la base de datos" - puts " #{Color::CYAN}bitacora:crear [FECHA]#{Color::RESET} Crear nueva bitácora diaria" - puts " #{Color::CYAN}bitacora:listar [OPCIONES]#{Color::RESET} Listar bitácoras existentes" - puts " #{Color::CYAN}evento:crear [OPCIONES]#{Color::RESET} Crear nueva entrada cronológica" - puts " #{Color::CYAN}evento:listar [OPCIONES]#{Color::RESET} Listar entradas con filtros" - puts " #{Color::CYAN}nodo:crear [OPCIONES]#{Color::RESET} Crear nuevo nodo (servidor/VM/PC)" - puts " #{Color::CYAN}nodo:listar [OPCIONES]#{Color::RESET} Listar nodos del sistema" - puts " #{Color::CYAN}estadisticas#{Color::RESET} Mostrar estadísticas del sistema" - puts " #{Color::CYAN}archivar:md#{Color::RESET} Mueve los archivos .md migrados al archivo histórico" - puts "" - puts "#{Color::BOLD}Ejemplos:#{Color::RESET}" - puts " ./adn/tools/run db salud:bd" - puts " ./adn/tools/run db bitacora:crear --resumen \"Inicio de proyecto\"" - puts " ./adn/tools/run db evento:crear --inicio 09:00 --descripcion \"Reunión\" --nodo srv-ns8" - puts " ./adn/tools/run db nodo:listar --tipo servidor" - puts "" - puts "#{Color::YELLOW}⚠ Nota:#{Color::RESET} Requiere PostgreSQL en ejecución (puerto 5433)" - puts " Iniciar contenedor: #{Color::DIM}cd dtic-BITACORAs && docker-compose up -d#{Color::RESET}" - end - end -end diff --git a/adn/tools/cli/db/ambito.rb b/adn/tools/cli/db/ambito.rb new file mode 100644 index 00000000..7741d33f --- /dev/null +++ b/adn/tools/cli/db/ambito.rb @@ -0,0 +1,158 @@ +# frozen_string_literal: true + +require 'optparse' +require_relative '../../core/colores' +require_relative '../../core/help_formatter' + +module ADN + module SubcomandoDBAmbito + # Comando: ambito:listar [OPCIONES] - Listar ámbitos + def comando_ambito_listar(args) + opts = { activo: nil } + + parser = OptionParser.new do |opt| + opt.banner = "Uso: adn db ambito:listar [OPCIONES]" + opt.on("--activo", "Mostrar solo ámbitos activos") { opts[:activo] = true } + opt.on("--inactivo", "Mostrar solo ámbitos inactivos") { opts[:activo] = false } + opt.on("-h", "--help", "Mostrar ayuda") do + puts opt + exit + end + end + + begin + parser.parse!(args) + rescue OptionParser::InvalidOption => e + puts "#{Color::RED}✗ Error: #{e.message}#{Color::RESET}" + exit 1 + end + + puts "#{Color::CYAN}📋 Listando ámbitos...#{Color::RESET}\n\n" + + BitacorasDB::BitacoraDB.with_connection do |db| + ambitos = db.list_ambitos(activo: opts[:activo]) + + if ambitos.empty? + puts "#{Color::YELLOW}⚠ No se encontraron ámbitos.#{Color::RESET}" + return + end + + puts "#{Color::BOLD}#{"ID".ljust(5)} #{"Nombre".ljust(20)} #{"Nodos".ljust(7)} #{"Activo".ljust(6)} #{"Padre".ljust(20)}#{Color::RESET}" + puts "#{"─" * 5} #{"─" * 20} #{"─" * 7} #{"─" * 6} #{"─" * 20}" + + ambitos.each do |a| + id = a['id'].to_s.ljust(5) + nombre = a['nombre'].to_s.ljust(20) + count = a['count_nodos'].to_s.ljust(7) + activo = (a['activo'] ? '✅' : '❌').ljust(6) + padre = (a['parent_nombre'] || '-').ljust(20) + + puts "#{id} #{nombre} #{count} #{activo} #{padre}" + end + puts "\nTotal: #{ambitos.length} ámbitos." + end + end + + # Comando: ambito:crear [OPCIONES] - Crear nuevo ámbito + def comando_ambito_crear(args) + opts = { nombre: nil, descripcion: nil, parent_id: nil } + + parser = OptionParser.new do |opt| + opt.banner = "Uso: adn db ambito:crear --nombre NOMBRE [OPCIONES]" + opt.on("--nombre NOMBRE", "Nombre único del ámbito (obligatorio)") { |n| opts[:nombre] = n } + opt.on("--descripcion DESC", "Descripción del ámbito") { |d| opts[:descripcion] = d } + opt.on("--parent-id ID", Integer, "ID del ámbito padre") { |p| opts[:parent_id] = p } + opt.on("-h", "--help", "Mostrar ayuda") do + puts opt + exit + end + end + + begin + parser.parse!(args) + rescue OptionParser::InvalidOption => e + puts "#{Color::RED}✗ Error: #{e.message}#{Color::RESET}" + exit 1 + end + + unless opts[:nombre] + puts "#{Color::RED}✗ Error: El parámetro --nombre es obligatorio.#{Color::RESET}" + exit 1 + end + + puts "#{Color::CYAN}📦 Creando ámbito '#{opts[:nombre]}'...#{Color::RESET}" + + BitacorasDB::BitacoraDB.with_connection do |db| + begin + ambito = db.create_ambito( + nombre: opts[:nombre], + descripcion: opts[:descripcion], + parent_id: opts[:parent_id] + ) + puts "#{Color::GREEN}✅ Ámbito creado exitosamente. ID: #{ambito['id']}#{Color::RESET}" + rescue PG::UniqueViolation + puts "#{Color::RED}✗ Error: Ya existe un ámbito con el nombre '#{opts[:nombre]}'.#{Color::RESET}" + exit 1 + end + end + end + + # Comando: ambito:info [NOMBRE_O_ID] - Info detallada + def comando_ambito_info(args) + objetivo = args.first + if objetivo.nil? || objetivo.start_with?('-') + puts "#{Color::YELLOW}Uso: adn db ambito:info #{Color::RESET}" + return + end + + BitacorasDB::BitacoraDB.with_connection do |db| + ambito = db.get_ambito(objetivo) + if ambito.nil? + puts "#{Color::RED}✗ Error: Ámbito '#{objetivo}' no encontrado.#{Color::RESET}" + exit 1 + end + + puts "#{Color::CYAN}ℹ Info del Ámbito: #{ambito['nombre']}#{Color::RESET}\n\n" + puts " #{Color::BOLD}ID:#{Color::RESET} #{ambito['id']}" + puts " #{Color::BOLD}Nombre:#{Color::RESET} #{ambito['nombre']}" + puts " #{Color::BOLD}Descripción:#{Color::RESET} #{ambito['descripcion'] || '(sin descripción)'}" + puts " #{Color::BOLD}Padre:#{Color::RESET} #{ambito['parent_nombre'] || '(raíz)'}" + puts " #{Color::BOLD}Activo:#{Color::RESET} #{ambito['activo'] ? 'Sí' : 'No'}" + puts " #{Color::BOLD}Nodos:#{Color::RESET} #{ambito['count_nodos']}" + puts "" + end + end + + # Comando: ambito:nodos [NOMBRE_O_ID] - Listar nodos de un ámbito + def comando_ambito_nodos(args) + objetivo = args.first + if objetivo.nil? || objetivo.start_with?('-') + puts "#{Color::YELLOW}Uso: adn db ambito:nodos #{Color::RESET}" + return + end + + BitacorasDB::BitacoraDB.with_connection do |db| + ambito = db.get_ambito(objetivo) + if ambito.nil? + puts "#{Color::RED}✗ Error: Ámbito '#{objetivo}' no encontrado.#{Color::RESET}" + exit 1 + end + + puts "#{Color::CYAN}🖥️ Nodos del ámbito '#{ambito['nombre']}'...#{Color::RESET}\n\n" + nodos = db.get_nodos_by_ambito(ambito['id']) + + if nodos.empty? + puts "#{Color::YELLOW}⚠ Este ámbito no tiene nodos asignados.#{Color::RESET}" + return + end + + nodos.each do |n| + tipo = n['tipo'].to_s.ljust(10) + ip = (n['ip'] || '-').ljust(15) + estado = n['activo'] ? "#{Color::GREEN}ON#{Color::RESET}" : "#{Color::RED}OFF#{Color::RESET}" + puts " #{Color::DIM}▸#{Color::RESET} #{Color::BOLD}#{n['nombre'].to_s.ljust(20)}#{Color::RESET} [#{tipo}] IP: #{ip} (#{estado})" + end + end + end + end +end diff --git a/adn/tools/cli/db/bitacora.rb b/adn/tools/cli/db/bitacora.rb index af7512ed..2506d359 100644 --- a/adn/tools/cli/db/bitacora.rb +++ b/adn/tools/cli/db/bitacora.rb @@ -2,13 +2,16 @@ module ADN module SubcomandoDBBitacora # Comando: bitacora:crear [FECHA] - Crear nueva bitácora diaria def comando_bitacora_crear(args) - opts = { fecha: Date.today, resumen: nil } + opts = { fecha: Date.today, resumen: nil, modo: 'P' } parser = OptionParser.new do |opt| opt.banner = "Uso: adn db bitacora:crear [FECHA] [OPCIONES]" opt.on("--resumen TEXTO", "Resumen inicial de la bitácora") do |r| opts[:resumen] = r end + opt.on("--modo MODO", "Modo de la jornada: P=Presencial, R=Remoto (default: P)") do |m| + opts[:modo] = m.upcase + end opt.on("-h", "--help", "Mostrar ayuda") do puts opt exit @@ -17,7 +20,6 @@ module ADN begin parser.parse!(args) - # Si queda un argumento sin procesar, asumimos que es la fecha opts[:fecha] = Date.parse(args[0]) if args[0] rescue OptionParser::InvalidOption, Date::Error => e puts "#{Color::RED}✗ Error: #{e.message}#{Color::RESET}" @@ -29,11 +31,12 @@ module ADN BitacorasDB::BitacoraDB.with_connection do |db| begin - bitacora = db.create_bitacora(fecha: opts[:fecha], resumen: opts[:resumen]) + bitacora = db.create_bitacora(fecha: opts[:fecha], resumen: opts[:resumen], modo: opts[:modo]) puts "#{Color::GREEN}✅ Bitácora creada exitosamente#{Color::RESET}" puts "" puts " #{Color::BOLD}ID:#{Color::RESET} #{bitacora['id']}" puts " #{Color::BOLD}Fecha:#{Color::RESET} #{bitacora['fecha']}" + puts " #{Color::BOLD}Modo:#{Color::RESET} #{bitacora['modo']} (#{modo_label(bitacora['modo'])})" puts " #{Color::BOLD}Resumen:#{Color::RESET} #{bitacora['resumen'] || '(sin resumen)'}" puts " #{Color::BOLD}Creada:#{Color::RESET} #{bitacora['created_at']}" rescue PG::UniqueViolation @@ -91,16 +94,17 @@ module ADN end puts "" - puts "#{Color::BOLD}#{"ID".ljust(5)} #{"Fecha".ljust(12)} #{"Resumen".ljust(40)} #{"Creada".ljust(20)}#{Color::RESET}" - puts "#{"─" * 5} #{"─" * 12} #{"─" * 40} #{"─" * 20}" + puts "#{Color::BOLD}#{"ID".ljust(5)} #{"Fecha".ljust(12)} #{"Modo"} #{"Resumen".ljust(38)} #{"Creada".ljust(20)}#{Color::RESET}" + puts "#{"─" * 5} #{"─" * 12} #{"─" * 4} #{"─" * 38} #{"─" * 20}" bitacoras.each do |b| fecha = b['fecha'] + modo = b['modo'] || 'P' resumen = b['resumen'] || '' - resumen = resumen[0..37] + '...' if resumen.length > 40 + resumen = resumen[0..35] + '...' if resumen.length > 38 creada = Time.parse(b['created_at']).strftime('%Y-%m-%d %H:%M') - puts "#{b['id'].to_s.ljust(5)} #{fecha.ljust(12)} #{resumen.ljust(40)} #{creada.ljust(20)}" + puts "#{b['id'].to_s.ljust(5)} #{fecha.ljust(12)} [#{modo}] #{resumen.ljust(38)} #{creada.ljust(20)}" end puts "" @@ -108,5 +112,8 @@ module ADN end end + def modo_label(modo) + { 'P' => 'Presencial', 'R' => 'Remoto', 'S' => 'Servicio' }[modo] || modo + end end end diff --git a/adn/tools/cli/db/evento.rb b/adn/tools/cli/db/evento.rb index be4c40e4..c5031d9a 100644 --- a/adn/tools/cli/db/evento.rb +++ b/adn/tools/cli/db/evento.rb @@ -10,18 +10,22 @@ module ADN modo: nil, es_ia: false, nodo: nil, - tema: nil + tema: nil, + plantilla: nil, + fecha: nil, + ambito: nil, + doc_path: nil } parser = OptionParser.new do |opt| opt.banner = "Uso: adn db evento:crear [OPCIONES]" - opt.on("--inicio HH:MM", "Hora de inicio (requerido)") do |i| + opt.on("--inicio HH:MM[:SS]", "Hora de inicio (requerido)") do |i| opts[:inicio] = i end opt.on("--descripcion TEXTO", "Descripción de la actividad (requerido)") do |d| opts[:descripcion] = d end - opt.on("--fin HH:MM", "Hora de fin (opcional)") do |f| + opt.on("--fin HH:MM[:SS]", "Hora de fin (opcional)") do |f| opts[:fin] = f end opt.on("--estado ESTADO", "Estado (⏳, ✅, ❌, ⚠️, ⏸️)") do |e| @@ -33,9 +37,21 @@ module ADN opt.on("--nodo NOMBRE", "Nombre del nodo (servidor/VM)") do |n| opts[:nodo] = n end + opt.on("--fecha YYYY-MM-DD", "Fecha de la bitácora (default: hoy)") do |f| + opts[:fecha] = f + end + opt.on("--ambito NOMBRE_O_ID", "Ámbito del evento") do |a| + opts[:ambito] = a + end + opt.on("--doc RUTA", "Ruta al plan .md (ej: P2601_Dasuten/P2601.09_DASUTEN-sin-DC.md)") do |d| + opts[:doc_path] = d + end opt.on("--ia", "Marcar como generado por IA") do opts[:es_ia] = true end + opt.on("--plantilla TIPO", "Usar plantilla (implementacion, investigacion, resolucion, documentacion, test, debug, planificacion)") do |p| + opts[:plantilla] = p + end opt.on("-h", "--help", "Mostrar ayuda") do puts opt exit @@ -51,12 +67,32 @@ module ADN end # Validaciones básicas - unless opts[:inicio] && opts[:descripcion] - puts "#{Color::RED}✗ Error: --inicio y --descripcion son requeridos#{Color::RESET}" + unless opts[:descripcion] + puts "#{Color::RED}✗ Error: --descripcion es requerido#{Color::RESET}" puts parser exit 1 end + # Si no se especifica --inicio, usar hora actual + opts[:inicio] ||= Time.now.strftime('%H:%M:%S') + puts "#{Color::DIM}ℹ Hora de inicio: #{opts[:inicio]}#{Color::RESET}" + + # Si se specify plantilla, mostrar y prependear al texto + if opts[:plantilla] + require_relative '../../templates/plantillas_eventos' + if ADN::PlantillasEventos.existe?(opts[:plantilla]) + plantilla = ADN::PlantillasEventos.obtener(opts[:plantilla]) + puts "#{Color::CYAN}📋 Plantilla: #{opts[:plantilla]}#{Color::RESET}" + puts plantilla + puts "" + puts "#{Color::YELLOW}💡 Completa la plantilla y usa --descripcion con el resultado#{Color::RESET}" + else + puts "#{Color::RED}✗ Tipo de plantilla desconocido: #{opts[:plantilla]}#{Color::RESET}" + puts "#{Color::YELLOW}Disponibles: #{ADN::PlantillasEventos.lista_tipos.join(', ')}#{Color::RESET}" + exit 1 + end + end + # Obtener modo desde jornada actual si no se especifica modo_resuelto = opts[:modo] if modo_resuelto.nil? @@ -79,24 +115,44 @@ module ADN end end - # Buscar bitácora para hoy (o crearla) - fecha_hoy = Date.today.to_s - bitacora = db.find_bitacora_by_fecha(fecha_hoy) + # Resolver ámbito si se especificó + ambito_id = nil + if opts[:ambito] + ambito = db.get_ambito(opts[:ambito]) + if ambito + ambito_id = ambito['id'] + puts " #{Color::DIM}Ámbito encontrado: #{opts[:ambito]} (ID: #{ambito_id})#{Color::RESET}" + else + puts "#{Color::YELLOW}⚠ Ámbito '#{opts[:ambito]}' no encontrado#{Color::RESET}" + end + end + + # Determinar fecha: --fecha o hoy + fecha_target = opts[:fecha] ? opts[:fecha] : Date.today.to_s + if opts[:fecha] + begin + Date.parse(opts[:fecha]) + puts " #{Color::DIM}ℹ Fecha especificada: #{fecha_target}#{Color::RESET}" + rescue Date::Error + puts "#{Color::RED}✗ Formato de fecha inválido: #{opts[:fecha]}. Usar YYYY-MM-DD#{Color::RESET}" + exit 1 + end + end + + bitacora = db.find_bitacora_by_fecha(fecha_target) if bitacora.nil? - puts " #{Color::DIM}Creando bitácora para hoy...#{Color::RESET}" - bitacora = db.create_bitacora(fecha: fecha_hoy) + puts " #{Color::DIM}Creando bitácora para #{fecha_target}...#{Color::RESET}" + bitacora = db.create_bitacora(fecha: fecha_target) end bitacora_id = bitacora['id'] # Obtener modo desde jornada actual si no se especificó if modo_resuelto.nil? - entradas = db.list_entradas(bitacora_id: bitacora_id, limit: 10) - jornada = entradas.find { |e| e['descripcion']&.downcase&.include?('inicio de jornada') } - if jornada && jornada['modo'] - modo_resuelto = jornada['modo'] - puts " #{Color::DIM}ℹ Modo resuelto desde jornada: #{modo_resuelto == 'P' ? 'Presencial' : 'Remoto'}#{Color::RESET}" + if bitacora && bitacora['modo'] + modo_resuelto = bitacora['modo'] + puts " #{Color::DIM}ℹ Modo resuelto desde jornada: #{modo_resuelto == 'P' ? 'Presencial' : (modo_resuelto == 'R' ? 'Remoto' : 'Servicio')}#{Color::RESET}" else modo_resuelto = 'P' # default puts " #{Color::DIM}ℹ Sin jornada activa, usando default: Presencial#{Color::RESET}" @@ -128,7 +184,20 @@ module ADN tema_id: nil # Por ahora sin tema ) + # Asignar doc_path si se especificó + if opts[:doc_path] && entrada + db.update_entrada(entrada['id'], { doc_path: opts[:doc_path] }) + puts " #{Color::DIM}ℹ Doc vinculado: #{opts[:doc_path]}#{Color::RESET}" + end + + # Asignar ámbito si se especificó + if ambito_id && entrada + db.update_entrada(entrada['id'], { ambito_id: ambito_id }) + puts " #{Color::DIM}ℹ Ámbito asignado (ID: #{ambito_id})#{Color::RESET}" + end + puts "#{Color::GREEN}✅ Entrada creada exitosamente#{Color::RESET}" + puts " #{Color::BOLD}Fecha:#{Color::RESET} #{fecha_target}" puts " #{Color::BOLD}Inicio:#{Color::RESET} #{entrada['inicio']}" puts " #{Color::BOLD}Fin:#{Color::RESET} #{entrada['fin'] || '(sin fin)'}" puts " 📝 #{Color::BOLD}Descripción:#{Color::RESET} #{entrada['descripcion']}" @@ -141,7 +210,7 @@ module ADN def comando_evento_actualizar(args) if args.length < 1 || args[0].start_with?('-') puts "#{Color::YELLOW}Uso: adn db evento:actualizar [OPCIONES]#{Color::RESET}" - puts "Opciones soportadas: --fin \"HH:MM\", --inicio \"HH:MM\", --estado \"✅\", --descripcion \"Texto\", --nodo \"Nombre\"" + puts "Opciones soportadas: --fin \"HH:MM[:SS]\", --inicio \"HH:MM[:SS]\", --estado \"✅\", --descripcion \"Texto\", --nodo \"Nombre\"" return end @@ -150,12 +219,14 @@ module ADN parser = OptionParser.new do |opt| opt.banner = "Opciones:" - opt.on("--inicio HH:MM", "Nueva hora de inicio") { |i| opts[:inicio] = i } - opt.on("--fin HH:MM", "Nueva hora de fin") { |f| opts[:fin] = f } + opt.on("--inicio HH:MM[:SS]", "Nueva hora de inicio") { |i| opts[:inicio] = i } + opt.on("--fin HH:MM[:SS]", "Nueva hora de fin") { |f| opts[:fin] = f } opt.on("--descripcion TEXTO", "Nueva descripción") { |d| opts[:descripcion] = d } opt.on("--estado ESTADO", "Nuevo estado (⏳, ✅, ❌, ⚠️, ⏸️)") { |e| opts[:estado] = e } opt.on("--modo MODO", "Modo (P=Presencial, R=Remoto)") { |m| opts[:modo] = m } opt.on("--nodo NOMBRE", "Nuevo nodo") { |n| opts[:nodo_raw] = n } + opt.on("--ambito NOMBRE_O_ID", "Nuevo ámbito") { |a| opts[:ambito_raw] = a } + opt.on("--doc RUTA", "Ruta al plan .md vinculado") { |d| opts[:doc_path] = d } opt.on("-h", "--help", "Mostrar ayuda") do puts opt exit @@ -199,6 +270,20 @@ module ADN opts.delete(:nodo_raw) end + if opts.key?(:ambito_raw) + if opts[:ambito_raw].nil? || opts[:ambito_raw].empty? + opts[:ambito_id] = nil + else + ambito = db.get_ambito(opts[:ambito_raw]) + if ambito + opts[:ambito_id] = ambito['id'] + else + puts "#{Color::YELLOW}⚠ Ámbito '#{opts[:ambito_raw]}' no encontrado#{Color::RESET}" + end + end + opts.delete(:ambito_raw) + end + begin res = db.update_entrada(id_evento, opts) if res @@ -232,6 +317,9 @@ module ADN opt.on("--nodo NOMBRE", "Filtrar por nombre de nodo") do |n| opts[:nodo] = n end + opt.on("--ambito NOMBRE_O_ID", "Filtrar por ámbito") do |a| + opts[:ambito] = a + end opt.on("--estado ESTADO", "Filtrar por estado (⏳, ✅, ❌, ⚠️, ⏸️)") do |e| opts[:estado] = e end @@ -276,7 +364,20 @@ module ADN end end + # Obtener ID de ámbito si se especificó nombre + ambito_id = nil + if opts[:ambito] + ambito = db.get_ambito(opts[:ambito]) + if ambito + ambito_id = ambito['id'] + puts " #{Color::DIM}Filtrando por ámbito: #{opts[:ambito]} (ID: #{ambito_id})#{Color::RESET}" + else + puts "#{Color::YELLOW}⚠ Ámbito '#{opts[:ambito]}' no encontrado#{Color::RESET}" + end + end + entradas = db.list_entradas( + ambito_id: ambito_id, nodo_id: nodo_id, estado: opts[:estado], buscar: opts[:buscar], @@ -296,9 +397,9 @@ module ADN entradas.each_with_index do |e, i| puts "#{Color::DIM}#{"─" * 60}#{Color::RESET}" if i > 0 id = e['id'].to_s - inicio = e['inicio'][0..7] - fin = e['fin'] ? e['fin'][0..7] : '--:--' - nodo = e['nodo_nombre'] || '(sin nodo)' + inicio = e['inicio'] ? e['inicio'][0..7] : '--:--:--' + fin = e['fin'] ? e['fin'][0..7] : '--:--:--' + nodo = "#{e['ambito_nombre'] || '-'} / #{e['nodo_nombre'] || '-'}" estado = e['estado'] desc = e['descripcion'] @@ -306,22 +407,23 @@ module ADN puts " #{desc}" end else - # Formato tabla compacta (original) + # Formato tabla compacta (original ajustado) puts "" - puts "#{Color::BOLD}#{"ID".ljust(5)} #{"Inicio".ljust(8)} #{"Fin".ljust(8)} #{"Nodo".ljust(15)} #{"Estado".ljust(3)} #{"Descripción".ljust(40)}#{Color::RESET}" - puts "#{"─" * 5} #{"─" * 8} #{"─" * 8} #{"─" * 15} #{"─" * 3} #{"─" * 40}" + puts "#{Color::BOLD}#{"ID".ljust(5)} #{"Inicio".ljust(9)} #{"Fin".ljust(9)} #{"Ámbito/Nodo".ljust(25)} #{"Est".ljust(3)} #{"Descripción".ljust(35)}#{Color::RESET}" + puts "#{"─" * 5} #{"─" * 9} #{"─" * 9} #{"─" * 25} #{"─" * 3} #{"─" * 35}" entradas.each do |e| id = e['id'].to_s - inicio = e['inicio'][0..7] # HH:MM:SS -> HH:MM + inicio = e['inicio'] ? e['inicio'][0..7] : '--:--' fin = e['fin'] ? e['fin'][0..7] : '--:--' - nodo = e['nodo_nombre'] || '(sin nodo)' - nodo = nodo[0..13] + '...' if nodo.length > 15 + nodo = "#{e['ambito_nombre'] || '-'} / #{e['nodo_nombre'] || '-'}" + nodo = nodo[0..23] + '...' if nodo.length > 25 estado = e['estado'] - desc = e['descripcion'] - desc = desc[0..37] + '...' if desc.length > 40 + desc = e['descripcion'] || '' + desc = desc.split("\n").first || '' # Tomar solo la primera línea + desc = desc[0..32] + '...' if desc.length > 35 - puts "#{id.ljust(5)} #{inicio.ljust(8)} #{fin.ljust(8)} #{nodo.ljust(15)} #{estado.ljust(3)} #{desc.ljust(40)}" + puts "#{id.ljust(5)} #{inicio.ljust(9)} #{fin.ljust(9)} #{nodo.ljust(25)} #{estado.ljust(3)} #{desc.ljust(35)}" end end @@ -397,13 +499,13 @@ module ADN entradas.each do |e| id = e['id'].to_s - inicio = e['inicio'][0..4] # HH:MM:SS -> HH:MM + inicio = e['inicio'] ? e['inicio'][0..7] : '--:--:--' # HH:MM:SS nodo = (e['nodo_nombre'] || '(sin nodo)')[0..14] desc = e['descripcion'] desc = desc[0..47] + '...' if desc.length > 50 estado_icon = e['estado'] - puts " #{estado_icon} #{Color::YELLOW}ID #{id.ljust(5)}#{Color::RESET} #{inicio.ljust(5)} #{nodo.ljust(15)} #{desc}" + puts " #{estado_icon} #{Color::YELLOW}ID #{id.ljust(5)}#{Color::RESET} #{inicio.ljust(11)} #{nodo.ljust(15)} #{desc}" end end diff --git a/adn/tools/cli/db/misc.rb b/adn/tools/cli/db/misc.rb index 3589da00..f91b4397 100644 --- a/adn/tools/cli/db/misc.rb +++ b/adn/tools/cli/db/misc.rb @@ -86,8 +86,9 @@ module ADN puts " #{Color::DIM}Entradas completadas:#{Color::RESET} #{porcentaje_completadas}%" end end + end - def comando_backup(args) + def comando_backup(args) opts = { output: nil, compress: true, timestamp: true } parser = OptionParser.new do |opt| @@ -185,10 +186,45 @@ module ADN end end + def comando_migrar_sql(args) + opts = {} + parser = OptionParser.new do |opt| + opt.banner = "Uso: adn db migrar:sql ARCHIVO_SQL" + opt.on("-h", "--help", "Mostrar ayuda") do + puts opt + exit + end + end + + begin + parser.parse!(args) + rescue OptionParser::InvalidOption => e + puts "#{Color::RED}✗ Error: #{e.message}#{Color::RESET}" + puts parser + exit 1 + end + + sql_file = args.first + if sql_file.nil? || !File.exist?(sql_file) + puts "#{Color::RED}✗ Error: Archivo SQL no especificado o no encontrado.#{Color::RESET}" + puts parser + exit 1 + end + + puts "#{Color::CYAN}🔄 Ejecutando script SQL: #{sql_file}...#{Color::RESET}" + + BitacorasDB::BitacoraDB.with_connection do |db| + begin + sql = File.read(sql_file) + db.send(:execute, sql) + puts "#{Color::GREEN}✅ Script SQL ejecutado exitosamente.#{Color::RESET}" + rescue => e + puts "#{Color::RED}✗ Error ejecutando SQL: #{e.message}#{Color::RESET}" + exit 1 + end + end end - - # Comando: archivar:md - Mover bitácoras y proyectos MD importados/obsoletos a carpetas _hist def comando_archivar_md require 'fileutils' diff --git a/adn/tools/cli/db/nodo.rb b/adn/tools/cli/db/nodo.rb index 0bdfa1b2..b37cd406 100644 --- a/adn/tools/cli/db/nodo.rb +++ b/adn/tools/cli/db/nodo.rb @@ -21,6 +21,9 @@ module ADN opt.on("--inactivo", "Crear nodo como inactivo") do opts[:activo] = false end + opt.on("--ambito NOMBRE_O_ID", "Ámbito Padre (Opcional)") do |a| + opts[:ambito] = a + end opt.on("-h", "--help", "Mostrar ayuda") do puts opt exit @@ -45,18 +48,30 @@ module ADN BitacorasDB::BitacoraDB.with_connection do |db| begin + ambito_id = nil + if opts[:ambito] + ambito = db.get_ambito(opts[:ambito]) + if ambito.nil? + puts "#{Color::RED}✗ Error: Ámbito '#{opts[:ambito]}' no encontrado.#{Color::RESET}" + exit 1 + end + ambito_id = ambito['id'] + end + nodo = db.create_nodo( nombre: opts[:nombre], ip: opts[:ip], tipo: opts[:tipo], descripcion: opts[:descripcion], - activo: opts[:activo] + activo: opts[:activo], + ambito_id: ambito_id ) puts "#{Color::GREEN}✅ Nodo creado exitosamente#{Color::RESET}" puts "" puts " #{Color::BOLD}ID:#{Color::RESET} #{nodo['id']}" puts " #{Color::BOLD}Nombre:#{Color::RESET} #{nodo['nombre']}" + puts " #{Color::BOLD}Ámbito:#{Color::RESET} #{opts[:ambito] ? ambito['nombre'] : '(sin ámbito)'}" puts " #{Color::BOLD}IP:#{Color::RESET} #{nodo['ip'] || '(sin IP)'}" puts " #{Color::BOLD}Tipo:#{Color::RESET} #{nodo['tipo']}" puts " #{Color::BOLD}Activo:#{Color::RESET} #{nodo['activo'] ? 'Sí' : 'No'}" @@ -86,6 +101,9 @@ module ADN opt.on("--tipo TIPO", "Filtrar por tipo (servidor, hipervisor, vm, pc)") do |t| opts[:tipo] = t end + opt.on("--ambito NOMBRE_O_ID", "Filtrar por ámbito") do |a| + opts[:ambito] = a + end opt.on("-h", "--help", "Mostrar ayuda") do puts opt exit @@ -103,7 +121,18 @@ module ADN puts "#{Color::CYAN}🖥️ Listando nodos...#{Color::RESET}" BitacorasDB::BitacoraDB.with_connection do |db| - nodos = db.list_nodos(activo: opts[:activo], tipo: opts[:tipo], limit: 100) + ambito_id = nil + if opts[:ambito] + ambito = db.get_ambito(opts[:ambito]) + if ambito + ambito_id = ambito['id'] + else + puts "#{Color::RED}✗ Error: Ámbito '#{opts[:ambito]}' no encontrado.#{Color::RESET}" + exit 1 + end + end + + nodos = db.list_nodos(activo: opts[:activo], tipo: opts[:tipo], ambito_id: ambito_id, limit: 100) if nodos.empty? puts "#{Color::YELLOW}⚠ No se encontraron nodos#{Color::RESET}" @@ -111,19 +140,21 @@ module ADN end puts "" - puts "#{Color::BOLD}#{"ID".ljust(5)} #{"Nombre".ljust(20)} #{"IP".ljust(15)} #{"Tipo".ljust(12)} #{"Activo".ljust(6)} #{"Descripción".ljust(30)}#{Color::RESET}" - puts "#{"─" * 5} #{"─" * 20} #{"─" * 15} #{"─" * 12} #{"─" * 6} #{"─" * 30}" + puts "#{Color::BOLD}#{"ID".ljust(5)} #{"Nombre".ljust(20)} #{"Ámbito".ljust(15)} #{"IP".ljust(15)} #{"Tipo".ljust(12)} #{"Activo".ljust(6)} #{"Descripción".ljust(30)}#{Color::RESET}" + puts "#{"─" * 5} #{"─" * 20} #{"─" * 15} #{"─" * 15} #{"─" * 12} #{"─" * 6} #{"─" * 30}" nodos.each do |n| id = n['id'].to_s nombre = n['nombre'] + ambito_n = (n['ambito_nombre'] || '-').to_s + ambito_n = ambito_n[0..12] + '...' if ambito_n.length > 15 ip = n['ip'] || '(sin IP)' tipo = n['tipo'] activo = n['activo'] ? '✅' : '❌' desc = n['descripcion'] || '' desc = desc[0..27] + '...' if desc.length > 30 - puts "#{id.ljust(5)} #{nombre.ljust(20)} #{ip.ljust(15)} #{tipo.ljust(12)} #{activo.ljust(6)} #{desc.ljust(30)}" + puts "#{id.ljust(5)} #{nombre.ljust(20)} #{ambito_n.ljust(15)} #{ip.ljust(15)} #{tipo.ljust(12)} #{activo.ljust(6)} #{desc.ljust(30)}" end puts "" @@ -135,5 +166,90 @@ module ADN end end + # Comando: nodo:actualizar NOMBRE [OPCIONES] - Actualizar nodo existente + def comando_nodo_actualizar(args) + return if args.empty? + + nombre = args.shift + opts = { ip: nil, tipo: nil, descripcion: nil, activo: nil, ambito: nil } + + parser = OptionParser.new do |opt| + opt.banner = "Uso: adn db nodo:actualizar NOMBRE [OPCIONES]" + opt.on("--ip DIRECCION", "Nueva dirección IP") do |i| + opts[:ip] = i + end + opt.on("--tipo TIPO", "Nuevo tipo (servidor, hipervisor, vm, pc)") do |t| + opts[:tipo] = t + end + opt.on("--descripcion TEXTO", "Nueva descripción") do |d| + opts[:descripcion] = d + end + opt.on("--activo", "Marcar como activo") do + opts[:activo] = true + end + opt.on("--inactivo", "Marcar como inactivo") do + opts[:activo] = false + end + opt.on("--ambito NOMBRE_O_ID", "Nuevo ámbito") do |a| + opts[:ambito] = a + end + opt.on("-h", "--help", "Mostrar ayuda") do + puts opt + exit + end + end + + begin + parser.parse!(args) + rescue OptionParser::InvalidOption => e + puts "#{Color::RED}✗ Error: #{e.message}#{Color::RESET}" + puts parser + exit 1 + end + + puts "#{Color::CYAN}🔄 Actualizando nodo '#{nombre}'...#{Color::RESET}" + + BitacorasDB::BitacoraDB.with_connection do |db| + begin + ambito_id = nil + if opts[:ambito] + ambito = db.get_ambito(opts[:ambito]) + if ambito.nil? + puts "#{Color::RED}✗ Error: Ámbito '#{opts[:ambito]}' no encontrado.#{Color::RESET}" + exit 1 + end + ambito_id = ambito['id'] + end + + nodo = db.update_nodo( + nombre: nombre, + ip: opts[:ip], + tipo: opts[:tipo], + descripcion: opts[:descripcion], + activo: opts[:activo], + ambito_id: ambito_id + ) + + if nodo.nil? + puts "#{Color::YELLOW}⚠ No se encontró el nodo '#{nombre}'#{Color::RESET}" + exit 1 + end + + puts "#{Color::GREEN}✅ Nodo actualizado exitosamente#{Color::RESET}" + puts "" + puts " #{Color::BOLD}ID:#{Color::RESET} #{nodo['id']}" + puts " #{Color::BOLD}Nombre:#{Color::RESET} #{nodo['nombre']}" + puts " #{Color::BOLD}Ámbito:#{Color::RESET} #{ambito_id ? opts[:ambito] : '(sin ámbito)'}" + puts " #{Color::BOLD}IP:#{Color::RESET} #{nodo['ip'] || '(sin IP)'}" + puts " #{Color::BOLD}Tipo:#{Color::RESET} #{nodo['tipo']}" + puts " #{Color::BOLD}Activo:#{Color::RESET} #{nodo['activo'] ? 'Sí' : 'No'}" + puts " #{Color::BOLD}Descripción:#{Color::RESET} #{nodo['descripcion'] || '(sin descripción)'}" + puts " #{Color::BOLD}Actualizado:#{Color::RESET} #{nodo['updated_at']}" + rescue => e + puts "#{Color::RED}✗ Error actualizando nodo: #{e.message}#{Color::RESET}" + end + end + end + end end diff --git a/adn/tools/cli/generar.rb b/adn/tools/cli/generar.rb index 7e569eb2..71ebbf05 100644 --- a/adn/tools/cli/generar.rb +++ b/adn/tools/cli/generar.rb @@ -29,6 +29,8 @@ class SubcomandoGenerar generar_nodo when 'proyecto' generar_proyecto + when 'evento' + generar_evento else @logger.error("Tipo de generación desconocido: #{tipo}") mostrar_ayuda_generar @@ -46,13 +48,14 @@ class SubcomandoGenerar opciones: [ { names: ['bitacora [fecha]'], desc: "Crea nueva bitácora (fecha: YYYY-MM-DD, default: hoy)" }, { names: ['nodo '], desc: "Crea ficha de nodo" }, - { names: ['proyecto '], desc: "Crea manifiesto de proyecto" } + { names: ['proyecto '], desc: "Crea manifiesto de proyecto" }, + { names: ['evento '], desc: "Crea plantilla de detalle para evento" } ], ejemplos: [ { cmd: "./adn/tools/run generar bitacora", desc: "Crear bitácora para hoy" }, - { cmd: "./adn/tools/run generar bitacora 2026-03-08", desc: "Crear bitácora para el 8 de marzo de 2026" }, { cmd: "./adn/tools/run generar nodo srv-nuevo", desc: "Crear ficha para el nodo srv-nuevo" }, - { cmd: "./adn/tools/run generar proyecto P2604", desc: "Crear manifiesto para el proyecto P2604" } + { cmd: "./adn/tools/run generar proyecto P2604", desc: "Crear manifiesto para el proyecto P2604" }, + { cmd: "./adn/tools/run generar evento implementacion", desc: "Ver plantilla de implementación" } ] ) @@ -250,4 +253,31 @@ class SubcomandoGenerar @logger.exito("Manifiesto de proyecto creado: #{codigo}") puts "#{Color::GREEN}✓ Manifiesto creado en: #{ruta_proyecto}#{Color::RESET}" end + + def generar_evento + require_relative '../templates/plantillas_eventos' + + tipo = @args[0] + if tipo.nil? || tipo.empty? + puts "#{Color::CYAN}📋 Tipos de plantillas de eventos disponibles:#{Color::RESET}" + puts "" + ADN::PlantillasEventos.lista_tipos.each do |t| + puts " #{Color::GREEN}#{t}#{Color::RESET}" + end + puts "" + puts "Uso: #{Color::DIM}./adn/tools/run generar evento #{Color::RESET}" + puts "Ejemplo: #{Color::DIM}./adn/tools/run generar evento implementacion#{Color::RESET}" + return + end + + if ADN::PlantillasEventos.existe?(tipo) + puts "#{Color::CYAN}📝 Plantilla: #{tipo}#{Color::RESET}" + puts "" + puts ADN::PlantillasEventos.obtener(tipo) + else + puts "#{Color::RED}✗ Tipo de plantilla desconocido: #{tipo}#{Color::RESET}" + puts "#{Color::YELLOW}Tipos disponibles:#{Color::RESET}" + ADN::PlantillasEventos.lista_tipos.each { |t| puts " - #{t}" } + end + end end diff --git a/adn/tools/cli/jornada.rb b/adn/tools/cli/jornada.rb index d1d22b9c..b6994fba 100644 --- a/adn/tools/cli/jornada.rb +++ b/adn/tools/cli/jornada.rb @@ -148,6 +148,9 @@ module ADN puts " #{Color::BOLD}Fecha:#{Color::RESET} #{fecha}" @logger.exito("Jornada #{tipo_str} iniciada en Base de Datos.") end + + # 4. Verificar y mostrar URL de bitácora web + verificar_y_mostrar_bitacora_web end def ejecutar_cierre @@ -233,5 +236,33 @@ module ADN puts "#{Color::DIM}#{'-' * 50}#{Color::RESET}" end end + + def verificar_y_mostrar_bitacora_web + @logger.info("Verificando estado de bitácora web...") + + # Verificar si los servicios Docker están activos + begin + output = `docker compose -f servicios/nginx/dtic-bitacoras/docker-compose.yml ps 2>&1` + # Docker muestra "Up" cuando los servicios están activos + servicios_activos = output.include?('Up') + + if servicios_activos + puts "" + puts "#{Color::CYAN}🌐 Bitácora Web disponible:#{Color::RESET}" + puts " #{Color::GREEN}✓ Dashboard local:#{Color::RESET} http://localhost:5174" + puts " #{Color::GREEN}✓ Acceso remoto:#{Color::RESET} http://srv-ns8/bitacoras" + @logger.exito("Bitácora web verificada y disponible") + else + puts "" + puts "#{Color::YELLOW}⚠ Bitácora Web:#{Color::RESET}" + puts " Servicios Docker no están activos. Ejecutar:" + puts " #{Color::DIM}docker compose -f servicios/nginx/dtic-bitacoras/docker-compose.yml up -d#{Color::RESET}" + @logger.warn("Bitácora web: servicios no activos") + end + rescue StandardError => e + @logger.error("Error verificando bitácora web: #{e.message}") + puts "#{Color::RED}✗ Error verificando servicios: #{e.message}#{Color::RESET}" + end + end end end diff --git a/adn/tools/cli/nodos.rb b/adn/tools/cli/nodos.rb index 0c09c3c2..05868309 100644 --- a/adn/tools/cli/nodos.rb +++ b/adn/tools/cli/nodos.rb @@ -54,14 +54,14 @@ module ADN descripcion: "Gestión y consulta de nodos de infraestructura", uso: "./adn/tools/run nodos [opciones]", opciones: [ - { names: ['agrupar [--servidor SRV]'], desc: "Agrupar VMs por servidor anfitrión" }, - { names: ['listar [--tipo TIPO]'], desc: "Listar fichas de nodos (.md)" }, + { names: ['agrupar [--por ambitos|servidor]'], desc: "Agrupar nodos de la red" }, + { names: ['listar [--tipo TIPO] [--ambito AMB]'], desc: "Listar fichas de nodos (.md)" }, { names: ['buscar '], desc: "Buscar nodos por nombre o contenido" } ], ejemplos: [ - { cmd: "./adn/tools/run nodos agrupar", desc: "Agrupar todos los VMs por servidor" }, - { cmd: "./adn/tools/run nodos agrupar --servidor srv-pmox1", desc: "Agrupar VMs del servidor srv-pmox1" }, - { cmd: "./adn/tools/run nodos listar --tipo srvv", desc: "Listar nodos de tipo srvv" }, + { cmd: "./adn/tools/run nodos agrupar --por ambitos", desc: "Agrupar nodos estructurados desde la DB" }, + { cmd: "./adn/tools/run nodos agrupar --por servidor --servidor srv-pmox1", desc: "Agrupar VMs del servidor en base a fichas" }, + { cmd: "./adn/tools/run nodos listar --ambito dtic-DASUTEN", desc: "Listar nodos MD cruzados con ámbito DB" }, { cmd: "./adn/tools/run nodos buscar dasuten", desc: "Buscar nodos relacionados con dasuten" } ] ) @@ -110,12 +110,59 @@ module ADN # ─── Agrupar ──────────────────────────────────────────────────────── def agrupar_nodos - opts = { servidor: nil } + opts = { servidor: nil, por: 'servidor' } parser = OptionParser.new do |opt| opt.on('--servidor SRV', 'Filtrar por servidor anfitrión') { |s| opts[:servidor] = s } + opt.on('--por CAMPO', 'Agrupar por: servidor, ambitos') { |c| opts[:por] = c } end parser.order!(@args) + if opts[:por] == 'ambitos' + agrupar_por_ambitos + else + agrupar_por_servidor(opts) + end + end + + def agrupar_por_ambitos + puts "#{Color::CYAN}📋 Agrupando nodos por Ámbitos (Desde DB)#{Color::RESET}" + puts "#{Color::DIM}#{'-' * 60}#{Color::RESET}" + + require_relative '../db/core/bitacora_db' + + ambitos_hash = Hash.new { |h, k| h[k] = [] } + sin_ambito = [] + + BitacorasDB::BitacoraDB.with_connection do |db| + nodos = db.list_nodos(limit: 1000) + nodos.each do |n| + if n['ambito_nombre'] + ambitos_hash[n['ambito_nombre']] << n['nombre'] + else + sin_ambito << n['nombre'] + end + end + end + + ambitos_hash.keys.sort.each do |amb| + vms = ambitos_hash[amb] + puts "#{Color::GREEN}🏢 #{amb}#{Color::RESET} (#{vms.length} nodos)" + vms.sort.each { |vm| puts " ├── 💻 #{vm}" } + puts "" + end + + if sin_ambito.any? + puts "#{Color::YELLOW}❓ Nodos sin ámbito asignado:#{Color::RESET}" + sin_ambito.sort.each { |nodo| puts " ├── 💻 #{nodo}" } + puts "" + end + + total_vms = ambitos_hash.values.flatten.length + sin_ambito.length + puts "#{Color::DIM}#{'-' * 60}#{Color::RESET}" + puts "#{Color::DIM}Total: #{ambitos_hash.keys.length} ámbitos, #{total_vms} nodos#{Color::RESET}" + end + + def agrupar_por_servidor(opts) puts "#{Color::CYAN}📋 Agrupando nodos por Servidor Host#{Color::RESET}" puts "#{Color::DIM}#{'-' * 60}#{Color::RESET}" @@ -173,14 +220,30 @@ module ADN # ─── Listar ───────────────────────────────────────────────────────── def listar_nodos - opts = { tipo: nil } + opts = { tipo: nil, ambito: nil } parser = OptionParser.new do |opt| opt.on('--tipo TIPO', 'Filtrar por prefijo (srv, srvv, pcv, pc, cam, dc, sql)') { |t| opts[:tipo] = t } + opt.on('--ambito NOMBRE', 'Cruzar nombres de base de datos para filtrar') { |a| opts[:ambito] = a } end parser.order!(@args) puts "#{Color::CYAN}📋 Listando fichas de nodos#{Color::RESET}" puts "#{Color::DIM}#{'-' * 60}#{Color::RESET}" + + nodos_validos = nil + if opts[:ambito] + require_relative '../db/core/bitacora_db' + BitacorasDB::BitacoraDB.with_connection do |db| + ambito = db.get_ambito(opts[:ambito]) + if ambito + nodos_db = db.list_nodos(ambito_id: ambito['id'], limit: 1000) + nodos_validos = nodos_db.map { |n| n['nombre'] } + else + puts "#{Color::RED}✗ Error: Ámbito '#{opts[:ambito]}' no encontrado en la DB.#{Color::RESET}" + return + end + end + end archivos = Dir.glob(File.join(ADN::NODOS_DIR, '*.md')).sort nodos = archivos.map { |a| File.basename(a, '.md') } @@ -189,6 +252,10 @@ module ADN if opts[:tipo] nodos.select! { |n| n.start_with?(opts[:tipo]) } end + + if nodos_validos + nodos.select! { |n| nodos_validos.include?(n) } + end if nodos.empty? puts "#{Color::YELLOW}⚠ No se encontraron nodos#{opts[:tipo] ? " con prefijo '#{opts[:tipo]}'" : ''}#{Color::RESET}" diff --git a/adn/tools/cli/salud.rb b/adn/tools/cli/salud.rb index 6c4fe67f..8ea0f288 100644 --- a/adn/tools/cli/salud.rb +++ b/adn/tools/cli/salud.rb @@ -14,38 +14,41 @@ module ADN end def ejecutar - parser = OptionParser.new do |opts| - opts.banner = "Uso: ./adn/tools/run salud [OPCIONES]" - opts.separator "" - opts.separator "Muestra salud general del sistema ADN" - opts.separator "" - opts.separator "Opciones:" - opts.on("--dashboard", "Mostrar en formato dashboard (no implementado)") do - @opciones[:dashboard] = true - end - opts.on("--json", "Salida en formato JSON") do - @opciones[:json] = true - end - opts.on("--mejoras", "Incluir sugerencias de mejora continua") do - @opciones[:mejoras] = true - end - opts.on("-h", "--help", "Mostrar esta ayuda") do - puts opts - exit 1 - end - opts.on("-h", "--help", "Mostrar esta ayuda") do - puts opts - exit - end - end + opts = { dashboard: false, json: false, mejoras: false, ambito: nil } - begin - parser.parse!(@args) - rescue OptionParser::InvalidOption => e - @logger.error("Opción inválida: #{e.message}") - puts parser - exit 1 - end + if @args.include?('-h') || @args.include?('--help') || @args.empty? + require_relative '../core/help_formatter' + puts ADN::HelpFormatter.generar_help( + titulo: "Salud del Sistema ADN", + descripcion: "Muestra métricas y estado general del ecosistema (Archivos MD + DB)", + uso: "./adn/tools/run salud [opciones]", + opciones: [ + { names: ['--json'], desc: "Salida en formato JSON" }, + { names: ['--mejoras'], desc: "Sugerencias de mejora continua" }, + { names: ['--ambito AMBITO'], desc: "Filtrar estadísticas de DB por ámbito" } + ], + ejemplos: [ + { cmd: "./adn/tools/run salud", desc: "Reporte general" }, + { cmd: "./adn/tools/run salud --ambito dtic-DASUTEN", desc: "Reporte con foco en el ámbito" } + ] + ) + exit 0 + end + + parser = OptionParser.new do |opt| + opt.on("--json") { @opciones[:json] = true } + opt.on("--mejoras") { @opciones[:mejoras] = true } + opt.on("--dashboard") { @opciones[:dashboard] = true } + opt.on("--ambito NOMBRE") { |a| @opciones[:ambito] = a } + end + + begin + parser.parse!(@args) + rescue OptionParser::InvalidOption => e + @logger.error("Opción inválida: #{e.message}") + puts "#{Color::YELLOW}⚠ Error: #{e.message}#{Color::RESET}" + exit 1 + end mostrar_salud end @@ -69,23 +72,34 @@ module ADN datos[:sugerencias_mejora] = obtener_sugerencias_mejora end + datos[:db] = obtener_estadisticas_db(@opciones[:ambito]) + if @opciones[:json] puts JSON.pretty_generate(datos) return end if @opciones[:dashboard] - puts "#{Color::YELLOW}⚠ El dashboard será implementado en Fase 3#{Color::RESET}" - # TODO: Implementar dashboard en Fase 3 + puts "#{Color::YELLOW}⚠ El dashboard interactivo estructurado será implementado en Fase 3#{Color::RESET}" end # Mostrar en formato legible puts "#{Color::CYAN}🧬 Salud del Sistema ADN#{Color::RESET}" puts "#{Color::DIM}#{'-' * 60}#{Color::RESET}" puts " #{Color::GREEN}Hebras ADN:#{Color::RESET} #{datos[:hebras_adn]} / 7" - puts " #{Color::GREEN}Bitácoras:#{Color::RESET} #{datos[:bitacoras]}" - puts " #{Color::GREEN}Nodos registrados:#{Color::RESET} #{datos[:nodos]}" - puts " #{Color::GREEN}Proyectos activos:#{Color::RESET} #{datos[:proyectos]}" + puts " #{Color::GREEN}Bitácoras MD:#{Color::RESET} #{datos[:bitacoras]}" + puts " #{Color::GREEN}Nodos MD:#{Color::RESET} #{datos[:nodos]}" + puts " #{Color::GREEN}Proyectos MD:#{Color::RESET} #{datos[:proyectos]}" + + if datos[:db] + puts "" + db_stats = datos[:db] + ambito_str = @opciones[:ambito] ? " (Ámbito: #{@opciones[:ambito]})" : " (Global)" + puts " #{Color::CYAN}Estadísticas Base de Datos#{ambito_str}:#{Color::RESET}" + puts " - Ámbitos Totales: #{db_stats[:ambitos]}" unless @opciones[:ambito] + puts " - Nodos (DB): #{db_stats[:nodos]}" + puts " - Eventos (DB): #{db_stats[:eventos]}" + end if datos[:errores_recientes] > 0 puts " #{Color::RED}Errores recientes:#{Color::RESET} #{datos[:errores_recientes]}" @@ -96,6 +110,19 @@ module ADN puts "#{Color::DIM}#{'-' * 60}#{Color::RESET}" puts "#{Color::DIM}Para más detalles: ./adn/tools/run salud --json#{Color::RESET}" + if @opciones[:mejoras] && datos[:sugerencias_mejora] && !datos[:sugerencias_mejora].empty? + puts + puts "#{Color::CYAN}💡 Sugerencias de Mejora Continua#{Color::RESET}" + puts "#{Color::DIM}#{'-' * 60}#{Color::RESET}" + datos[:sugerencias_mejora].each do |sug| + prioridad_emoji = { alta: '🔴', media: '🟡', baja: '🟢' }[sug[:prioridad]] || '⚪' + puts " #{prioridad_emoji} #{sug[:tipo].to_s.tr('_', ' ')}" + puts " #{sug[:descripcion]}" + puts " → #{sug[:accion_sugerida]}" + puts + end + end + @logger.exito("Reporte de salud generado") end @@ -120,6 +147,30 @@ module ADN Dir.glob(File.join(ADN::NODOS_DIR, "*.md")).count end + def obtener_estadisticas_db(ambito_nombre = nil) + require_relative '../db/core/bitacora_db' + stats = { ambitos: 0, nodos: 0, eventos: 0 } + begin + BitacorasDB::BitacoraDB.with_connection do |db| + ambito_id = nil + if ambito_nombre + ambito = db.get_ambito(ambito_nombre) + return nil unless ambito + ambito_id = ambito['id'] + else + stats[:ambitos] = db.list_ambitos.length + end + + stats[:nodos] = db.list_nodos(ambito_id: ambito_id, limit: 1000).length + stats[:eventos] = db.list_entradas(ambito_id: ambito_id, limit: 10000).length + end + rescue StandardError => e + @logger.error("Error obteniendo stats DB: #{e.message}") + return nil + end + stats + end + def contar_proyectos Dir.glob(File.join(ADN::PROYECTOS_DIR, "*.md")).count end diff --git a/adn/tools/core/constants.rb b/adn/tools/core/constants.rb index 9d3bca76..59ff372a 100644 --- a/adn/tools/core/constants.rb +++ b/adn/tools/core/constants.rb @@ -11,9 +11,9 @@ module ADN PROJECT_ROOT = File.expand_path('../../..', __dir__) NODOS_DIR = File.join(PROJECT_ROOT, 'nodos') - PROYECTOS_DIR = File.join(PROJECT_ROOT, 'docs', 'proyectos') + PROYECTOS_DIR = File.join(PROJECT_ROOT, 'docs', 'proy') BITACORAS_DIR = File.join(PROJECT_ROOT, 'docs', 'bitacoras') - LOGS_DIR = File.join(BITACORAS_DIR, '_hist') + LOGS_DIR = File.join(PROJECT_ROOT, 'logs', 'adn') TOOLS_DIR = File.join(PROJECT_ROOT, 'adn', 'tools') SEGURIDAD_DIR = File.join(TOOLS_DIR, 'seguridad') diff --git a/adn/tools/db/core/ambito_db.rb b/adn/tools/db/core/ambito_db.rb new file mode 100644 index 00000000..426ba3cb --- /dev/null +++ b/adn/tools/db/core/ambito_db.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +require 'pg' +require 'json' + +module ADN + module AmbitosDB + module BaseOperations + # Listar ámbitos + def list_ambitos(activo: nil) + query = "SELECT a.*, p.nombre as parent_nombre, + (SELECT COUNT(*) FROM bitacoras.nodos WHERE ambito_id = a.id) as count_nodos + FROM bitacoras.ambitos a + LEFT JOIN bitacoras.ambitos p ON a.parent_id = p.id" + params = [] + + unless activo.nil? + query += " WHERE a.activo = $1" + params << (activo ? 't' : 'f') + end + + query += " ORDER BY a.parent_id NULLS FIRST, a.nombre ASC" + + res = execute(query, params) + res.map do |r| + r['activo'] = (r['activo'] == 't') if r.key?('activo') + r + end + end + + # Obtener información de un ámbito + def get_ambito(id_or_nombre) + query = "SELECT a.*, p.nombre as parent_nombre, + (SELECT COUNT(*) FROM bitacoras.nodos WHERE ambito_id = a.id) as count_nodos + FROM bitacoras.ambitos a + LEFT JOIN bitacoras.ambitos p ON a.parent_id = p.id + WHERE a.id = $1 OR a.nombre = $2 LIMIT 1" + + id = id_or_nombre.to_i > 0 ? id_or_nombre.to_i : 0 + + res = execute(query, [id, id_or_nombre.to_s]) + return nil if res.ntuples == 0 + + row = res[0] + row['activo'] = (row['activo'] == 't') if row.key?('activo') + row + end + + # Crear un nuevo ámbito + def create_ambito(nombre:, descripcion: nil, parent_id: nil, activo: true) + query = "INSERT INTO bitacoras.ambitos (nombre, descripcion, parent_id, activo) + VALUES ($1, $2, $3, $4) RETURNING *" + params = [nombre, descripcion, parent_id, activo ? 't' : 'f'] + + res = execute(query, params) + row = res[0] + row['activo'] = (row['activo'] == 't') if row.key?('activo') + row + end + + # Actualizar ámbito + def update_ambito(id, campos = {}) + return nil if campos.empty? + + set_parts = [] + params = [] + campos.each_with_index do |(k, v), i| + set_parts << "#{k} = $#{i + 1}" + params << (v.is_a?(TrueClass) || v.is_a?(FalseClass) ? (v ? 't' : 'f') : v) + end + + params << id + query = "UPDATE bitacoras.ambitos SET #{set_parts.join(', ')}, updated_at = CURRENT_TIMESTAMP WHERE id = $#{params.length} RETURNING *" + + res = execute(query, params) + return nil if res.ntuples == 0 + + row = res[0] + row['activo'] = (row['activo'] == 't') if row.key?('activo') + row + end + + # Eliminar ámbito + def delete_ambito(id) + query = "DELETE FROM bitacoras.ambitos WHERE id = $1 RETURNING *" + res = execute(query, [id.to_i]) + res.ntuples > 0 + end + + # Listar nodos de un ámbito + def get_nodos_by_ambito(ambito_id) + query = "SELECT * FROM bitacoras.nodos WHERE ambito_id = $1 ORDER BY nombre ASC" + res = execute(query, [ambito_id.to_i]) + res.map do |r| + r['activo'] = (r['activo'] == 't') if r.key?('activo') + r + end + end + end + end +end + + diff --git a/adn/tools/db/core/bitacora_db.rb b/adn/tools/db/core/bitacora_db.rb index bb1115cb..40d23356 100644 --- a/adn/tools/db/core/bitacora_db.rb +++ b/adn/tools/db/core/bitacora_db.rb @@ -30,9 +30,13 @@ require 'yaml' require 'json' require 'date' require 'time' +require_relative 'mejoras_sensor' +require_relative 'ambito_db' module BitacorasDB class BitacoraDB + include ADN::AmbitosDB::BaseOperations + # Constantes para el esquema y tablas SCHEMA = 'bitacoras' TABLES = { @@ -60,6 +64,7 @@ module BitacorasDB @connection = nil @logger = logger || default_logger @connected = false + @activo = ::ADN::MejorasSensor.activo? end # Carga la configuración desde el archivo YAML @@ -197,18 +202,24 @@ module BitacorasDB # # @param fecha [Date, String] Fecha de la bitácora # @param resumen [String, nil] Resumen opcional + # @param modo [String, nil] Modo de la jornada ('P', 'R', 'S') # @return [Hash] Bitácora creada con ID # @raise [PG::Error] Si ya existe bitácora para esa fecha - def create_bitacora(fecha:, resumen: nil) + def create_bitacora(fecha:, resumen: nil, modo: 'P') fecha = Date.parse(fecha.to_s) unless fecha.is_a?(Date) + modo ||= 'P' + + unless MODOS_VALIDOS.include?(modo) + raise ArgumentError, "Modo inválido. Válidos: #{MODOS_VALIDOS.join(', ')}" + end query = <<~SQL - INSERT INTO #{SCHEMA}.#{TABLES[:bitacoras]} (fecha, resumen) - VALUES ($1, $2) - RETURNING id, fecha, resumen, created_at, updated_at + INSERT INTO #{SCHEMA}.#{TABLES[:bitacoras]} (fecha, resumen, modo) + VALUES ($1, $2, $3) + RETURNING id, fecha, resumen, modo, created_at, updated_at SQL - result = @connection.exec_params(query, [fecha.to_s, resumen]) + result = @connection.exec_params(query, [fecha.to_s, resumen, modo]) result.first end @@ -239,7 +250,7 @@ module BitacorasDB where_clause = conditions.empty? ? '' : "WHERE #{conditions.join(' AND ')}" query = <<~SQL - SELECT id, fecha, resumen, created_at, updated_at + SELECT id, fecha, resumen, modo, created_at, updated_at FROM #{SCHEMA}.#{TABLES[:bitacoras]} #{where_clause} ORDER BY fecha DESC @@ -261,7 +272,7 @@ module BitacorasDB fecha = Date.parse(fecha.to_s) unless fecha.is_a?(Date) query = <<~SQL - SELECT id, fecha, resumen, created_at, updated_at + SELECT id, fecha, resumen, modo, created_at, updated_at FROM #{SCHEMA}.#{TABLES[:bitacoras]} WHERE fecha = $1 SQL @@ -280,7 +291,7 @@ module BitacorasDB # @param descripcion [String, nil] Descripción opcional # @param activo [Boolean] Si el nodo está activo # @return [Hash] Nodo creado con ID - def create_nodo(nombre:, ip: nil, tipo: 'servidor', descripcion: nil, activo: true) + def create_nodo(nombre:, ip: nil, tipo: 'servidor', descripcion: nil, activo: true, ambito_id: nil) tipos_validos = ['servidor', 'hipervisor', 'vm', 'pc', 'vmware', 'xenserver', 'servicio'] unless tipos_validos.include?(tipo.downcase) @@ -288,12 +299,12 @@ module BitacorasDB end query = <<~SQL - INSERT INTO #{SCHEMA}.#{TABLES[:nodos]} (nombre, ip, tipo, descripcion, activo) - VALUES ($1, $2, $3, $4, $5) - RETURNING id, nombre, ip, tipo, descripcion, activo, created_at, updated_at + INSERT INTO #{SCHEMA}.#{TABLES[:nodos]} (nombre, ip, tipo, descripcion, activo, ambito_id) + VALUES ($1, $2, $3, $4, $5, $6) + RETURNING id, nombre, ip, tipo, descripcion, activo, ambito_id, created_at, updated_at SQL - result = @connection.exec_params(query, [nombre, ip, tipo, descripcion, activo]) + result = @connection.exec_params(query, [nombre, ip, tipo, descripcion, activo, ambito_id]) result.first end @@ -303,30 +314,37 @@ module BitacorasDB # @param tipo [String, nil] Filtrar por tipo # @param limit [Integer] Límite de resultados # @return [Array] Lista de nodos - def list_nodos(activo: nil, tipo: nil, limit: 100) + def list_nodos(activo: nil, tipo: nil, ambito_id: nil, limit: 100) conditions = [] params = [] param_index = 1 if !activo.nil? - conditions << "activo = $#{param_index}" + conditions << "n.activo = $#{param_index}" params << activo param_index += 1 end if tipo - conditions << "tipo = $#{param_index}" + conditions << "n.tipo = $#{param_index}" params << tipo param_index += 1 end + if ambito_id + conditions << "n.ambito_id = $#{param_index}" + params << ambito_id + param_index += 1 + end + where_clause = conditions.empty? ? '' : "WHERE #{conditions.join(' AND ')}" query = <<~SQL - SELECT id, nombre, ip, tipo, descripcion, activo, created_at, updated_at - FROM #{SCHEMA}.#{TABLES[:nodos]} + SELECT n.id, n.nombre, n.ip, n.tipo, n.descripcion, n.activo, n.ambito_id, a.nombre as ambito_nombre, n.created_at, n.updated_at + FROM #{SCHEMA}.#{TABLES[:nodos]} n + LEFT JOIN #{SCHEMA}.ambitos a ON n.ambito_id = a.id #{where_clause} - ORDER BY nombre + ORDER BY n.nombre LIMIT $#{param_index} SQL @@ -351,6 +369,68 @@ module BitacorasDB result.first&.to_h end + # Actualiza un nodo existente + # + # @param nombre [String] Nombre del nodo a actualizar + # @param ip [String, nil] Nueva dirección IP + # @param tipo [String, nil] Nuevo tipo + # @param descripcion [String, nil] Nueva descripción + # @param activo [Boolean, nil] Nuevo estado activo + # @param ambito_id [Integer, nil] Nuevo ámbito ID + # @return [Hash, nil] Nodo actualizado o nil si no existe + def update_nodo(nombre:, ip: nil, tipo: nil, descripcion: nil, activo: nil, ambito_id: nil) + nodo = find_nodo_by_nombre(nombre) + return nil unless nodo + + updates = [] + params = [nombre] + param_index = 2 + + if ip + updates << "ip = $#{param_index}" + params << ip + param_index += 1 + end + + if tipo + updates << "tipo = $#{param_index}" + params << tipo + param_index += 1 + end + + if descripcion + updates << "descripcion = $#{param_index}" + params << descripcion + param_index += 1 + end + + if !activo.nil? + updates << "activo = $#{param_index}" + params << activo + param_index += 1 + end + + if ambito_id + updates << "ambito_id = $#{param_index}" + params << ambito_id + param_index += 1 + end + + return nodo if updates.empty? + + updates << "updated_at = CURRENT_TIMESTAMP" + + query = <<~SQL + UPDATE #{SCHEMA}.#{TABLES[:nodos]} + SET #{updates.join(', ')} + WHERE nombre = $1 + RETURNING id, nombre, ip, tipo, descripcion, activo, ambito_id, created_at, updated_at + SQL + + result = @connection.exec_params(query, params) + result.first&.to_h + end + # OPERACIONES CRUD PARA ENTRADAS (I-F-D-E) # Crea una nueva entrada cronológica @@ -377,13 +457,26 @@ module BitacorasDB estado = fin_time ? '✅' : '⏳' end - # Inferir Modo + # Inferir Modo (herencia de jornada) if modo.nil? || modo.strip.empty? + modo_bitacora = 'P' + if bitacora_id - res = @connection.exec_params("SELECT modo FROM #{SCHEMA}.#{TABLES[:entradas]} WHERE bitacora_id = $1 ORDER BY id DESC LIMIT 1", [bitacora_id]) - modo = res.ntuples > 0 ? res[0]['modo'] : 'P' + res = @connection.exec_params( + "SELECT modo FROM #{SCHEMA}.#{TABLES[:bitacoras]} WHERE id = $1", + [bitacora_id] + ) + modo_bitacora = res.first['modo'] if res.ntuples > 0 + end + + # Regla: jornada presencial (P) + hora fuera de 8:00-13:00 → remoto (R) + inicio_horario = Time.new(2000, 1, 1, 8, 0, 0) + fin_horario = Time.new(2000, 1, 1, 13, 0, 0) + + if modo_bitacora == 'P' && inicio_time >= fin_horario + modo = 'R' # Fuera de horario presencial else - modo = 'P' + modo = modo_bitacora end end @@ -415,7 +508,7 @@ module BitacorasDB entrada = result.first # Registrar evento para el sensor de mejora continua - MejorasSensor.registrar_evento( + ::ADN::MejorasSensor.registrar_evento( subcomando: 'evento:crear', args: [inicio_time, descripcion, fin_time, estado, modo, es_ia, tema_id, bitacora_id, nodo_id], nodo: nodo_id ? nil : 'desconocido', # Simplified for this example @@ -450,7 +543,7 @@ module BitacorasDB i = 1 params.each do |k, v| - if [:inicio, :fin, :descripcion, :estado, :modo, :es_ia, :nodo_id].include?(k) + if [:inicio, :fin, :descripcion, :estado, :modo, :es_ia, :nodo_id, :ambito_id, :doc_path].include?(k) val = ([:inicio, :fin].include?(k)) ? parse_time(v) : v set_clauses << "#{k} = $#{i}" values << val @@ -472,7 +565,7 @@ module BitacorasDB entrada = result.first&.to_h # Registrar evento para el sensor de mejora continua - MejorasSensor.registrar_evento( + ::ADN::MejorasSensor.registrar_evento( subcomando: 'evento:actualizar', args: [id] + values, # Simplified for this example nodo: nil, # In a real implementation, we would extract the nodo_id from the original entrada @@ -517,7 +610,7 @@ module BitacorasDB # @param limit [Integer] Límite de resultados # @param offset [Integer] Desplazamiento # @return [Array] Lista de entradas - def list_entradas(bitacora_id: nil, nodo_id: nil, tema_id: nil, estado: nil, + def list_entradas(bitacora_id: nil, nodo_id: nil, ambito_id: nil, tema_id: nil, estado: nil, desde: nil, hasta: nil, buscar: nil, limit: 100, offset: 0) conditions = [] params = [] @@ -535,6 +628,12 @@ module BitacorasDB param_index += 1 end + if ambito_id + conditions << "e.ambito_id = $#{param_index}" + params << ambito_id + param_index += 1 + end + if tema_id conditions << "e.tema_id = $#{param_index}" params << tema_id @@ -569,14 +668,16 @@ module BitacorasDB query = <<~SQL SELECT e.id, e.inicio, e.fin, e.descripcion, e.estado, e.modo, e.es_ia, - e.tema_id, e.bitacora_id, e.nodo_id, e.created_at, e.updated_at, + e.tema_id, e.bitacora_id, e.nodo_id, e.ambito_id, e.created_at, e.updated_at, b.fecha as bitacora_fecha, n.nombre as nodo_nombre, - t.titulo as tema_titulo + t.titulo as tema_titulo, + a.nombre as ambito_nombre FROM #{SCHEMA}.#{TABLES[:entradas]} e LEFT JOIN #{SCHEMA}.#{TABLES[:bitacoras]} b ON e.bitacora_id = b.id LEFT JOIN #{SCHEMA}.#{TABLES[:nodos]} n ON e.nodo_id = n.id LEFT JOIN #{SCHEMA}.#{TABLES[:temas]} t ON e.tema_id = t.id + LEFT JOIN #{SCHEMA}.ambitos a ON e.ambito_id = a.id #{where_clause} ORDER BY COALESCE(b.fecha, CURRENT_DATE) DESC, e.inicio DESC LIMIT $#{param_index} OFFSET $#{param_index + 1} diff --git a/adn/tools/db/core/bitacora_db.rb.backup b/adn/tools/db/core/bitacora_db.rb.backup new file mode 100644 index 00000000..2366ebca --- /dev/null +++ b/adn/tools/db/core/bitacora_db.rb.backup @@ -0,0 +1,918 @@ +# frozen_string_literal: true + +# adn/tools/db/core/bitacora_db.rb — Clase principal para conexión a PostgreSQL +# ================================================================================ +# Esta clase proporciona una interfaz Ruby para interactuar con la base de datos +# dtic-BITACORAs. Maneja conexiones, operaciones CRUD y validaciones para las +# tablas del esquema 'bitacoras': +# • nodos - Servidores, VMs, PCs del sistema +# • bitacoras - Bitácoras diarias (fecha única) +# • temas - Agrupadores de entradas por nodo +# • entradas - Entradas cronológicas en formato I-F-D-E +# +# Uso básico: +# db = BitacorasDB::BitacoraDB.new +# db.connection_test? # Verificar conexión +# bitacoras = db.list_bitacoras(limit: 10) +# db.create_entrada(inicio: '09:00', fin: '10:00', descripcion: 'Reunión', nodo_id: 1) +# +# Uso con bloque (cierre automático): +# BitacorasDB::BitacoraDB.with_connection do |db| +# db.create_bitacora(fecha: Date.today) +# end +# +# Configuración: +# La configuración se carga desde tools/config/database.yml +# Entorno por defecto: development (contenedor Docker en puerto 5433) + +require 'pg' +require 'yaml' +require 'json' +require 'date' +require 'time' +require 'connection_pool' +require_relative './mejoras_sensor' + +module BitacorasDB + class BitacoraDB + # Constantes para el esquema y tablas + SCHEMA = 'bitacoras' + TABLES = { + nodos: 'nodos', + bitacoras: 'bitacoras', + temas: 'temas', + entradas: 'entradas' + }.freeze + + # Estados válidos para entradas y temas + ESTADOS_VALIDOS = ['⏳', '✅', '❌', '⚠️', '⏸️'].freeze + + # Modos válidos para entradas + MODOS_VALIDOS = ['P', 'R', 'S'].freeze # P=Presencial, R=Remoto, S=Servicio + + # Inicializa la conexión a la base de datos + # + # @param env [Symbol, String] Entorno (:development, :test, :production) + # @param config_path [String] Ruta al archivo de configuración YAML + # @param logger [Object] Instancia de logger (por defecto ADN.logger si disponible) + def initialize(env: :development, config_path: nil, logger: nil) + @env = env.to_s + @config_path = config_path || default_config_path + @config = load_config + @connection_pool = nil + @logger = logger || default_logger + end + + # Carga la configuración desde el archivo YAML + # + # @return [Hash] Configuración para el entorno actual + # @raise [RuntimeError] Si no se encuentra la configuración o el entorno + def load_config + unless File.exist?(@config_path) + raise "Archivo de configuración no encontrado: #{@config_path}" + end + + config = YAML.load_file(@config_path) + + unless config && config[@env] + raise "Entorno '#{@env}' no encontrado en #{@config_path}. Entornos disponibles: #{config.keys.join(', ')}" + end + + config[@env] + end + + # Obtiene una conexión del pool + # + # @return [PG::Connection] Conexión PostgreSQL del pool + # @raise [PG::Error] Si falla la conexión + def connection + return @connection_pool if @connection_pool + + begin + @logger&.info("Creando pool de conexiones a PostgreSQL: #{@config['host']}:#{@config['port']}/#{@config['database']}") + + @connection_pool = ConnectionPool.new(size: 5, timeout: 5) do + PG.connect( + host: @config['host'], + port: @config['port'], + dbname: @config['database'], + user: @config['username'], + password: @config['password'], + connect_timeout: @config['timeout'] || 5000 + ) + end + + # Configurar búsqueda de esquema en todas las conexiones del pool + @connection_pool.with do |conn| + if @config['schema_search_path'] + conn.exec("SET search_path TO #{@config['schema_search_path']}") + end + end + + @logger&.info("Pool de conexiones a PostgreSQL establecido correctamente") + @connection_pool + rescue PG::Error => e + @logger&.error("Error creando pool de conexiones a PostgreSQL: #{e.message}") + raise + end + end + + # Cierra todas las conexiones en el pool + def disconnect + if @connection_pool + @connection_pool.shutdown + @connection_pool = nil + @logger&.info("Pool de conexiones a PostgreSQL cerrado") + end + end + + # Ejecuta un bloque con una conexión del pool, cerrando automáticamente al final + # + # @yield [BitacoraDB] Instancia de BitacoraDB con conexión establecida + # @return [Object] Resultado del bloque + def with_connection(&block) + # Obtiene una conexión del pool y la devuelve al final + connection.with do |conn| + # Temporarily replace @connection with the pooled connection for the block + original_connection = @connection + @connection = conn + begin + yield self + ensure + @connection = original_connection + end + end + end + + # Método de clase para uso con bloque + # + # @param env [Symbol] Entorno a usar + # @param config_path [String] Ruta al archivo de configuración + # @yield [BitacoraDB] Instancia de BitacoraDB + # @return [Object] Resultado del bloque + def self.with_connection(env: :development, config_path: nil, &block) + db = new(env: env, config_path: config_path) + db.with_connection(&block) + end + + # Verifica si hay una conexión válida en el pool + # + # @return [Boolean] true si hay una conexión válida en el pool + def connection_test? + connection.with do |conn| + begin + conn.exec('SELECT 1') + true + rescue PG::Error + false + end + end + rescue + false + end + + # Obtiene información de salud de la base de datos + # + # @param incluir_sugerencias [Boolean] Si se deben incluir sugerencias de mejora + # @return [Hash] Información de estado y estadísticas + def health_check(incluir_sugerencias: false) + stats = statistics + { + conexion_activa: connection_test?, + version_postgresql: version_postgresql, + estadisticas: stats + }.tap do |resultado| + if incluir_sugerencias && MejorasSensor.activo? + resultado[:sugerencias_mejora] = MejorasSensor.generar_sugerencias + end + end + end + end + + # OPERACIONES CRUD PARA BITÁCORAS + + # Crea una nueva bitácora diaria + # + # @param fecha [Date, String] Fecha de la bitácora + # @param resumen [String, nil] Resumen opcional + # @return [Hash] Bitácora creada con ID + # @raise [PG::Error] Si ya existe bitácora para esa fecha + def create_bitacora(fecha:, resumen: nil) + fecha = Date.parse(fecha.to_s) unless fecha.is_a?(Date) + + query = <<~SQL + INSERT INTO #{SCHEMA}.#{TABLES[:bitacoras]} (fecha, resumen) + VALUES ($1, $2) + RETURNING id, fecha, resumen, created_at, updated_at + SQL + + result = @connection.exec_params(query, [fecha.to_s, resumen]) + result.first + end + + # Obtiene bitácoras con paginación y filtros + # + # @param limit [Integer] Límite de resultados + # @param offset [Integer] Desplazamiento para paginación + # @param desde [Date, String, nil] Fecha desde + # @param hasta [Date, String, nil] Fecha hasta + # @return [Array] Lista de bitácoras + def list_bitacoras(limit: 50, offset: 0, desde: nil, hasta: nil) + conditions = [] + params = [] + param_index = 1 + + if desde + conditions << "fecha >= $#{param_index}" + params << Date.parse(desde.to_s).to_s + param_index += 1 + end + + if hasta + conditions << "fecha <= $#{param_index}" + params << Date.parse(hasta.to_s).to_s + param_index += 1 + end + + where_clause = conditions.empty? ? '' : "WHERE #{conditions.join(' AND ')}" + + query = <<~SQL + SELECT id, fecha, resumen, created_at, updated_at + FROM #{SCHEMA}.#{TABLES[:bitacoras]} + #{where_clause} + ORDER BY fecha DESC + LIMIT $#{param_index} OFFSET $#{param_index + 1} + SQL + + params << limit + params << offset + + result = @connection.exec_params(query, params) + result.map(&:to_h) + end + + # Busca una bitácora por fecha + # + # @param fecha [Date, String] Fecha a buscar + # @return [Hash, nil] Bitácora encontrada o nil + def find_bitacora_by_fecha(fecha) + fecha = Date.parse(fecha.to_s) unless fecha.is_a?(Date) + + query = <<~SQL + SELECT id, fecha, resumen, created_at, updated_at + FROM #{SCHEMA}.#{TABLES[:bitacoras]} + WHERE fecha = $1 + SQL + + result = @connection.exec_params(query, [fecha.to_s]) + result.first&.to_h + end + + # OPERACIONES CRUD PARA NODOS + + # Crea un nuevo nodo (servidor, VM, PC) + # + # @param nombre [String] Nombre único del nodo + # @param ip [String, nil] Dirección IP + # @param tipo [String] Tipo ('servidor', 'hipervisor', 'vm', 'pc') + # @param descripcion [String, nil] Descripción opcional + # @param activo [Boolean] Si el nodo está activo + # @return [Hash] Nodo creado con ID + def create_nodo(nombre:, ip: nil, tipo: 'servidor', descripcion: nil, activo: true) + tipos_validos = ['servidor', 'hipervisor', 'vm', 'pc', 'vmware', 'xenserver', 'servicio'] + + unless tipos_validos.include?(tipo.downcase) + raise ArgumentError, "Tipo de nodo inválido. Válidos: #{tipos_validos.join(', ')}" + end + + query = <<~SQL + INSERT INTO #{SCHEMA}.#{TABLES[:nodos]} (nombre, ip, tipo, descripcion, activo) + VALUES ($1, $2, $3, $4, $5) + RETURNING id, nombre, ip, tipo, descripcion, activo, created_at, updated_at + SQL + + result = @connection.exec_params(query, [nombre, ip, tipo, descripcion, activo]) + result.first + end + + # Lista nodos con filtros + # + # @param activo [Boolean, nil] Filtrar por estado activo + # @param tipo [String, nil] Filtrar por tipo + # @param limit [Integer] Límite de resultados + # @return [Array] Lista de nodos + def list_nodos(activo: nil, tipo: nil, limit: 100) + conditions = [] + params = [] + param_index = 1 + + if !activo.nil? + conditions << "activo = $#{param_index}" + params << activo + param_index += 1 + end + + if tipo + conditions << "tipo = $#{param_index}" + params << tipo + param_index += 1 + end + + where_clause = conditions.empty? ? '' : "WHERE #{conditions.join(' AND ')}" + + query = <<~SQL + SELECT id, nombre, ip, tipo, descripcion, activo, created_at, updated_at + FROM #{SCHEMA}.#{TABLES[:nodos]} + #{where_clause} + ORDER BY nombre + LIMIT $#{param_index} + SQL + + params << limit + + result = @connection.exec_params(query, params) + result.map(&:to_h) + end + + # Busca nodo por nombre + # + # @param nombre [String] Nombre del nodo + # @return [Hash, nil] Nodo encontrado o nil + def find_nodo_by_nombre(nombre) + query = <<~SQL + SELECT id, nombre, ip, tipo, descripcion, activo, created_at, updated_at + FROM #{SCHEMA}.#{TABLES[:nodos]} + WHERE nombre = $1 + SQL + + result = @connection.exec_params(query, [nombre]) + result.first&.to_h + end + + # OPERACIONES CRUD PARA ENTRADAS (I-F-D-E) + + # Crea una nueva entrada cronológica + # + # @param inicio [String, Time] Hora de inicio (formato HH:MM o Time) + # @param descripcion [String] Descripción de la actividad + # @param fin [String, Time, nil] Hora de fin (opcional) + # @param estado [String] Estado (⏳, ✅, ❌, ⚠️, ⏸️) + # @param modo [String] Modo (P=Presencial, R=Remoto) + # @param es_ia [Boolean] Si fue generado por IA + # @param tema_id [Integer, nil] ID del tema relacionado + # @param bitacora_id [Integer, nil] ID de la bitácora + # @param nodo_id [Integer, nil] ID del nodo + # @return [Hash] Entrada creada con ID + def create_entrada(inicio:, descripcion:, fin: nil, estado: nil, modo: nil, es_ia: false, + tema_id: nil, bitacora_id: nil, nodo_id: nil) + + # Validar formato de tiempo + inicio_time = parse_time(inicio) + fin_time = parse_time(fin) if fin + + # Inferir Estado + if estado.nil? || estado.strip.empty? + estado = fin_time ? '✅' : '⏳' + end + + # Inferir Modo + if modo.nil? || modo.strip.empty? + if bitacora_id + res = @connection.exec_params("SELECT modo FROM #{SCHEMA}.#{TABLES[:entradas]} WHERE bitacora_id = $1 ORDER BY id DESC LIMIT 1", [bitacora_id]) + modo = res.ntuples > 0 ? res[0]['modo'] : 'P' + else + modo = 'P' + end + end + + # Validar estado + unless ESTADOS_VALIDOS.include?(estado) + raise ArgumentError, "Estado inválido. Válidos: #{ESTADOS_VALIDOS.join(', ')}" + end + + # Validar modo + unless MODOS_VALIDOS.include?(modo) + raise ArgumentError, "Modo inválido. Válidos: #{MODOS_VALIDOS.join(', ')}" + end + + query = <<~SQL + INSERT INTO #{SCHEMA}.#{TABLES[:entradas]} ( + inicio, fin, descripcion, estado, modo, es_ia, + tema_id, bitacora_id, nodo_id + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) + RETURNING id, inicio, fin, descripcion, estado, modo, es_ia, + tema_id, bitacora_id, nodo_id, created_at, updated_at + SQL + + result = @connection.exec_params( + query, + [inicio_time, fin_time, descripcion, estado, modo, es_ia, + tema_id, bitacora_id, nodo_id] + ) + entrada = result.first + + # Registrar evento para el sensor de mejora continua + MejorasSensor.registrar_evento( + subcomando: 'evento:crear', + args: [inicio_time, descripcion, fin_time, estado, modo, es_ia, tema_id, bitacora_id, nodo_id], + nodo: nodo_id ? nil : 'desconocido', # Simplificado para este ejemplo + estado_salida: estado, + duracion_ms: 0, # En una implementación real, mediríamos el tiempo + es_ia: es_ia, + contexto: { + conexiones_usadas: 1, # Simplificado + operacion: 'create_entrada' + } + ) if @activo # Solo si el sensor está activo + + # Ejecutar hooks después de la creación + ejecutar_hooks_entrada(entrada, :crear) if entrada + + entrada + end + + # Actualiza una entrada existente + def update_entrada(id, params) + # Auto-inferir estado si se está mandando una hora de fin y no se fuerza un estado explícito + if params.key?(:fin) && !params.key?(:estado) + if params[:fin].nil? || params[:fin].to_s.strip.empty? + params[:estado] = '⏳' + else + params[:estado] = '✅' + end + end + + set_clauses = [] + values = [] + i = 1 + + params.each do |k, v| + if [:inicio, :fin, :descripcion, :estado, :modo, :es_ia, :nodo_id].include?(k) + val = ([:inicio, :fin].include?(k)) ? parse_time(v) : v + set_clauses << "#{k} = $#{i}" + values << val + i += 1 + end + end + + return nil if set_clauses.empty? + + query = <<~SQL + UPDATE #{SCHEMA}.#{TABLES[:entradas]} + SET #{set_clauses.join(', ')}, updated_at = CURRENT_TIMESTAMP + WHERE id = $#{i} + RETURNING * + SQL + + values << id + result = @connection.exec_params(query, values) + entrada = result.first&.to_h + + # Registrar evento para el sensor de mejora continua + MejorasSensor.registrar_evento( + subcomando: 'evento:actualizar', + args: [id] + values, # Simplificado para este ejemplo + nodo: nil, # En una implementación real, extraeríamos el nodo_id de la entrada original + estado_salida: entrada&.dig(:estado), + duracion_ms: 0, # En una implementación real, mediríamos el tiempo + es_ia: false, # En una implementación real, lo determinaríamos desde el contexto + contexto: { + conexiones_usadas: 1, # Simplificado + operacion: 'update_entrada', + id_actualizado: id + } + ) if @activo && entrada # Solo si el sensor está activo y hay entrada + + # Ejecutar hooks después de la actualización + ejecutar_hooks_entrada(entrada, :actualizar) if entrada + + entrada + end + + # Elimina una entrada existente + # @param id [Integer] ID de la entrada a eliminar + # @return [Boolean] true si se eliminó, false si no existe + def delete_entrada(id) + query = <<~SQL + DELETE FROM #{SCHEMA}.#{TABLES[:entradas]} + WHERE id = $1 + RETURNING id + SQL + + result = @connection.exec_params(query, [id]) + result.ntuples > 0 + end + + # Lista entradas con filtros avanzados + # + # @param bitacora_id [Integer, nil] Filtrar por bitácora + # @param nodo_id [Integer, nil] Filtrar por nodo + # @param tema_id [Integer, nil] Filtrar por tema + # @param estado [String, nil] Filtrar por estado + # @param desde [Time, String, nil] Fecha/hora desde + # @param hasta [Time, String, nil] Fecha/hora hasta + # @param limit [Integer] Límite de resultados + # @param offset [Integer] Desplazamiento + # @return [Array] Lista de entradas + def list_entradas(bitacora_id: nil, nodo_id: nil, tema_id: nil, estado: nil, + desde: nil, hasta: nil, buscar: nil, limit: 100, offset: 0) + conditions = [] + params = [] + param_index = 1 + + if bitacora_id + conditions << "e.bitacora_id = $#{param_index}" + params << bitacora_id + param_index += 1 + end + + if nodo_id + conditions << "e.nodo_id = $#{param_index}" + params << nodo_id + param_index += 1 + end + + if tema_id + conditions << "e.tema_id = $#{param_index}" + params << tema_id + param_index += 1 + end + + if estado + conditions << "e.estado = $#{param_index}" + params << estado + param_index += 1 + end + + if desde + conditions << "b.fecha >= $#{param_index}" + params << Date.parse(desde.to_s).to_s + param_index += 1 + end + + if hasta + conditions << "b.fecha <= $#{param_index}" + params << Date.parse(hasta.to_s).to_s + param_index += 1 + end + + if buscar + conditions << "e.descripcion ILIKE $#{param_index}" + params << "%#{buscar}%" + param_index += 1 + end + + where_clause = conditions.empty? ? '' : "WHERE #{conditions.join(' AND ')}" + + query = <<~SQL + SELECT e.id, e.inicio, e.fin, e.descripcion, e.estado, e.modo, e.es_ia, + e.tema_id, e.bitacora_id, e.nodo_id, e.created_at, e.updated_at, + b.fecha as bitacora_fecha, + n.nombre as nodo_nombre, + t.titulo as tema_titulo + FROM #{SCHEMA}.#{TABLES[:entradas]} e + LEFT JOIN #{SCHEMA}.#{TABLES[:bitacoras]} b ON e.bitacora_id = b.id + LEFT JOIN #{SCHEMA}.#{TABLES[:nodos]} n ON e.nodo_id = n.id + LEFT JOIN #{SCHEMA}.#{TABLES[:temas]} t ON e.tema_id = t.id + #{where_clause} + ORDER BY COALESCE(b.fecha, CURRENT_DATE) DESC, e.inicio DESC + LIMIT $#{param_index} OFFSET $#{param_index + 1} + SQL + + params << limit + params << offset + + result = @connection.exec_params(query, params) + result.map(&:to_h) + end + + # OPERACIONES CRUD PARA TEMAS + + # Crea un nuevo tema (agrupador de entradas) + # + # @param id_tema [String] Identificador único del tema + # @param titulo [String] Título del tema + # @param estado [String] Estado actual + # @param descripcion [String, nil] Descripción opcional + # @param bitacora_id [Integer, nil] ID de la bitácora relacionada + # @param nodo_id [Integer, nil] ID del nodo relacionado + # @return [Hash] Tema creado con ID + def create_tema(id_tema:, titulo:, estado: '⏳', descripcion: nil, + bitacora_id: nil, nodo_id: nil) + + unless ESTADOS_VALIDOS.include?(estado) + raise ArgumentError, "Estado inválido. Válidos: #{ESTADOS_VALIDOS.join(', ')}" + end + + query = <<~SQL + INSERT INTO #{SCHEMA}.#{TABLES[:temas]} ( + id_tema, titulo, estado, descripcion, bitacora_id, nodo_id + ) + VALUES ($1, $2, $3, $4, $5, $6) + RETURNING id, id_tema, titulo, estado, descripcion, + bitacora_id, nodo_id, created_at, updated_at + SQL + + result = @connection.exec_params( + query, + [id_tema, titulo, estado, descripcion, bitacora_id, nodo_id] + ) + result.first + end + + # Lista temas con filtros + # + # @param bitacora_id [Integer, nil] Filtrar por bitácora + # @param nodo_id [Integer, nil] Filtrar por nodo + # @param estado [String, nil] Filtrar por estado + # @param limit [Integer] Límite de resultados + # @return [Array] Lista de temas + def list_temas(bitacora_id: nil, nodo_id: nil, estado: nil, limit: 100) + conditions = [] + params = [] + param_index = 1 + + if bitacora_id + conditions << "bitacora_id = $#{param_index}" + params << bitacora_id + param_index += 1 + end + + if nodo_id + conditions << "nodo_id = $#{param_index}" + params << nodo_id + param_index += 1 + end + + if estado + conditions << "estado = $#{param_index}" + params << estado + param_index += 1 + end + + where_clause = conditions.empty? ? '' : "WHERE #{conditions.join(' AND ')}" + + query = <<~SQL + SELECT id, id_tema, titulo, estado, descripcion, + bitacora_id, nodo_id, created_at, updated_at + FROM #{SCHEMA}.#{TABLES[:temas]} + #{where_clause} + ORDER BY id_tema + LIMIT $#{param_index} + SQL + + params << limit + + result = @connection.exec_params(query, params) + result.map(&:to_h) + end + + # MÉTODOS DE UTILIDAD + + # Obtiene estadísticas del sistema + # + # @return [Hash] Estadísticas de todas las tablas + def statistics + return {} unless connection_test? + + stats = {} + + TABLES.each_key do |table| + query = "SELECT COUNT(*) as total FROM #{SCHEMA}.#{TABLES[table]}" + result = @connection.exec(query) + stats[table] = result.first['total'].to_i + end + + # Estadísticas adicionales + query = <<~SQL + SELECT + COUNT(DISTINCT nodo_id) as nodos_con_entradas, + COUNT(DISTINCT DATE(created_at)) as dias_con_actividad, + COUNT(CASE WHEN es_ia THEN 1 END) as entradas_ia, + COUNT(CASE WHEN estado = '✅' THEN 1 END) as entradas_completadas + FROM #{SCHEMA}.#{TABLES[:entradas]} + SQL + + result = @connection.exec(query) + extra_stats = result.first + + stats.merge( + nodos_con_entradas: extra_stats['nodos_con_entradas'].to_i, + dias_con_actividad: extra_stats['dias_con_actividad'].to_i, + entradas_ia: extra_stats['entradas_ia'].to_i, + entradas_completadas: extra_stats['entradas_completadas'].to_i + ) + end + + # Ejecuta una consulta SQL personalizada + # + # @param sql [String] Consulta SQL + # @param params [Array] Parámetros para la consulta preparada + # @return [PG::Result] Resultado de la consulta + def execute(sql, params = []) + @connection.exec_params(sql, params) + end + + # Verifica si existe una bitácora para hoy + # + # @return [Boolean] true si existe bitácora para hoy + def bitacora_hoy_existe? + fecha_hoy = Date.today.to_s + query = "SELECT 1 FROM #{SCHEMA}.#{TABLES[:bitacoras]} WHERE fecha = $1 LIMIT 1" + result = @connection.exec_params(query, [fecha_hoy]) + !result.ntuples.zero? + end + + # Crea bitácora para hoy si no existe + # + # @param resumen [String, nil] Resumen opcional + # @return [Hash, nil] Bitácora creada o nil si ya existía + def crear_bitacora_hoy(resumen: nil) + return nil if bitacora_hoy_existe? + + create_bitacora(fecha: Date.today, resumen: resumen) + end + + + # ========================================== + # PROYECTOS, FASES E HITOS (Planes Sistemáticos) + # ========================================== + + def find_proyecto_by_codigo(codigo) + q = "SELECT * FROM bitacoras.proyectos WHERE codigo = $1" + res = execute(q, [codigo.upcase]) + return nil if res.ntuples.zero? + res.first.to_h + end + + def list_hitos(proyecto_codigo) + proyecto = find_proyecto_by_codigo(proyecto_codigo) + raise StandardError, "Proyecto no encontrado: #{proyecto_codigo}" unless proyecto + + q = <<~SQL + SELECT f.numero as fase_num, f.nombre as fase_nombre, + h.id, h.id_hito, h.titulo, h.estado, h.horas_presencial, h.horas_remoto + FROM bitacoras.fases f + JOIN bitacoras.hitos h ON h.fase_id = f.id + WHERE f.proyecto_id = $1 + ORDER BY f.numero, h.id_hito + SQL + res = execute(q, [proyecto['id']]) + res.map(&:to_h) + end + + def rename_hito_id(proyecto_codigo, id_actual, id_nuevo) + q_busqueda = <<~SQL + SELECT h.id, h.titulo + FROM bitacoras.hitos h + JOIN bitacoras.fases f ON h.fase_id = f.id + JOIN bitacoras.proyectos p ON f.proyecto_id = p.id + WHERE p.codigo = $1 AND h.id_hito = $2 + SQL + + res = execute(q_busqueda, [proyecto_codigo.upcase, id_actual]) + raise StandardError, "Hito #{id_actual} del proyecto #{proyecto_codigo} no encontrado" if res.ntuples.zero? + + hito = res.first + + q_update = "UPDATE bitacoras.hitos SET id_hito = $1 WHERE id = $2 RETURNING titulo" + res_update = execute(q_update, [id_nuevo, hito['id']]) + + res_update.first + end + + def update_hito(proyecto_codigo, id_hito, estado: nil, horas_presencial: nil, horas_remoto: nil, descripcion: nil) + q_busqueda = <<~SQL + SELECT h.id, h.horas_presencial, h.horas_remoto, h.estado, h.descripcion + FROM bitacoras.hitos h + JOIN bitacoras.fases f ON h.fase_id = f.id + JOIN bitacoras.proyectos p ON f.proyecto_id = p.id + WHERE p.codigo = $1 AND h.id_hito = $2 + SQL + + res = execute(q_busqueda, [proyecto_codigo.upcase, id_hito]) + raise StandardError, "Hito #{id_hito} del proyecto #{proyecto_codigo} no encontrado" if res.ntuples.zero? + + hito = res.first + + updates = [] + params = [] + param_count = 1 + + if estado + updates << "estado = $#{param_count}" + params << estado + param_count += 1 + end + + unless horas_presencial.nil? + updates << "horas_presencial = $#{param_count}" + params << horas_presencial + param_count += 1 + end + + unless horas_remoto.nil? + updates << "horas_remoto = $#{param_count}" + params << horas_remoto + param_count += 1 + end + + if descripcion + updates << "descripcion = $#{param_count}" + params << descripcion + param_count += 1 + end + + return hito if updates.empty? + + updates << "updated_at = NOW()" + params << hito['id'] + + query = "UPDATE bitacoras.hitos SET #{updates.join(', ')} WHERE id = $#{param_count} RETURNING *" + + resultado = execute(query, params) + resultado.first.to_h + end + + private + + # Ruta por defecto al archivo de configuración + def default_config_path + File.expand_path('../../config/database.yml', __dir__) + end + + # Logger por defecto (ADN.logger si está disponible) + def default_logger + # Intentar usar ADN.logger si el módulo ADN está cargado + if defined?(ADN) && ADN.respond_to?(:logger) + ADN.logger + else + # Logger simple como fallback + Class.new do + def info(msg); puts "[INFO] #{msg}"; end + def error(msg); puts "[ERROR] #{msg}"; end + def warn(msg); puts "[WARN] #{msg}"; end + end.new + end + end + + # Parsea un tiempo a formato HH:MM:SS + # + # @param time [String, Time, nil] Tiempo a parsear + # @return [String, nil] Tiempo en formato HH:MM:SS o nil + def parse_time(time) + return nil if time.nil? + + case time + when Time + time.strftime('%H:%M:%S') + when String + # Intentar parsear varios formatos + if time.match?(/^\d{1,2}:\d{2}$/) # HH:MM + "#{time}:00" + elsif time.match?(/^\d{1,2}:\d{2}:\d{2}$/) # HH:MM:SS + time + else + raise ArgumentError, "Formato de tiempo inválido: #{time}. Use HH:MM o HH:MM:SS" + end + else + raise ArgumentError, "Tiempo debe ser String o Time, recibido: #{time.class}" + end + end + + # Convierte un resultado PG a Hash + def result_to_hash(result) + return {} if result.ntuples.zero? + result.first.to_h + end + + # Ejecuta hooks personalizados después de operaciones de entrada + # Esto implementa el concepto de DB Hooks para migrar triggers.yml a capa de datos + # + # @param entrada [Hash] La entrada que fue creada o actualizada + # @param accion [Symbol] :crear o :actualizar + def ejecutar_hooks_entrada(entrada, accion) + # Hook para proyectos con etiqueta [P2604] + if entrada[:descripcion]&.match?(/\[P2604\]/) + proyecto_codigo = entrada[:descripcion].match(/\[P2604\.(\d+)\.(\d+)\.(\d+)\]/)&.to_s + if proyecto_codigo + # Normalizar formato: P2604.05.01 -> P2604 + proyecto_base = proyecto_codigo.split('.')[0..1].join('.') + + # Actualizar el manifiesto del proyecto en la base de datos + proyecto = find_proyecto_by_codigo(proyecto_base) + if proyecto + # Aquí podríamos actualizar algún campo del proyecto basado en la entrada + # Por ahora, solo registramos que se debería actualizar + @logger&.info("Hook ejecutado: Entrada #{accion} para proyecto #{proyecto_base} detectada") + # En una implementación completa, aquí actualizaríamos el manifiesto del proyecto + end + end + end + + # Otros hooks podrían ir aquí... + # Por ejemplo, hooks para sincronización con nodos, etc. + end + end +end diff --git a/adn/tools/db/core/mejoras_sensor.rb b/adn/tools/db/core/mejoras_sensor.rb new file mode 100644 index 00000000..343a9758 --- /dev/null +++ b/adn/tools/db/core/mejoras_sensor.rb @@ -0,0 +1,227 @@ +# frozen_string_literal: true + +# adn/tools/db/core/mejoras_sensor.rb — Sensor de mejora continua para el ecosistema ADN +# ====================================================================================== +# Este módulo implementa un sensor ligero que recopila métricas de uso del sistema +# y genera sugerencias de mejora basadas en patrones detectados. +# +# Diseñado para seguir los principios ADN: +# - DB-First: Todas las sugerencias se registran como entradas en la bitácora +# - Menos es Más: Lógica simple, reglas transparentes, sobrecarga mínima +# - Armonía Integral: Nada se aplica automáticamente; requiere intervención humana trazable +# +# Uso interno: +# MejorasSensor.registrar_evento(...) # Llamado desde operaciones clave +# MejorasSensor.generar_sugerencias() # Genera entradas ⏳ en bitácora + +require 'digest/md5' +require_relative 'bitacora_db' + +module ADN + module MejorasSensor + # Configuración del sensor + @activo = true # Desactivado por defecto para minimizar overhead + @umbrales = { + conexiones_por_minuto: 5, # Umbral para sugerir connection pooling + comandos_repetidos: 5, # Umbral para sugerir automatización + ventana_tiempo: 300 # Ventana de análisis en segundos (5 min) + } + + # Activar/desactivar el sensor + # @param activo [Boolean] Estado de activación + def self.activo=(activo) + @activo = activo + end + + # @return [Boolean] Estado actual de activación + def self.activo? + @activo + end + + # Registrar un evento de uso para análisis posterior + # @param subcomando [String] Subcomando ejecutado (ej: 'evento:crear') + # @param args [Array] Argumentos del subcomando + # @param nodo [String, nil] Nodo involucrado (si aplica) + # @param estado_salida [String] Estado de salida (✅, ❌, ⏳) + # @param duracion_ms [Integer] Duración de ejecución en milisegundos + # @param es_ia [Boolean] Si fue ejecutado por IA + # @param contexto [Hash] Datos adicionales para análisis + def self.registrar_evento(subcomando:, args: [], nodo: nil, estado_salida: nil, + duracion_ms: 0, es_ia: false, contexto: {}) + return unless @activo # No hacer nada si el sensor está desactivado + + # Generar hash de argumentos para agrupat patrones similares + args_str = args.map(&:to_s).join('|') + args_hash = Digest::MD5.hexdigest(args_str)[0, 8] # Primeros 8 caracteres + + # Preparar contexto para almacenamiento + contexto_json = contexto.to_json + + # Almacenar métrica en la base de datos + BitacorasDB::BitacoraDB.with_connection do |db| + db.execute <<~SQL, [subcomando, args_hash, nodo, estado_salida, duracion_ms, es_ia, contexto_json] + INSERT INTO bitacoras.mejoras_metricas ( + subcomando, args_hash, nodo, estado_salida, duracion_ms, es_ia, contexto + ) + VALUES ($1, $2, $3, $4, $5, $6, $7) + SQL + end + end + + # Generar sugerencias de mejora basadas en las métricas recopiladas + # @return [Array] Lista de sugerencias generadas + def self.generar_sugerencias + sugerencias = [] + + BitacorasDB::BitacoraDB.with_connection do |db| + # 1. Detectar fricción de conexiones DB excesivas + sugerencias_conexiones = _detectar_friccion_conexiones(db) + sugerencias.concat(sugerencias_conexiones) + + # 2. Detectar comandos repetitivos que podrían automatizarse + sugerencias_automatizacion = _detectar_comandos_repetitivos(db) + sugerencias.concat(sugerencias_automatizacion) + + # 3. Detectar inconsistencias documento-DB (ej: entradas [P2604] sin actualizaciones) + sugerencias_inconsistencia = _detectar_inconsistencias_documento_db(db) + sugerencias.concat(sugerencias_inconsistencia) + end + + sugerencias + end + + # Aplicar una sugerencia específica (marcar como implementada) + # @param sugerencia_id [Integer] ID de la entrada de bitácora a actualizar + # @return [Boolean] True si se actualizó exitosamente + def self.aplicar_sugerencia(sugerencia_id) + BitacorasDB::BitacoraDB.with_connection do |db| + resultado = db.update_entrada(sugerencia_id, { estado: '✅' }) + resultado.present? + end + end + + private + + # Detectar fricción de conexiones DB excesivas + # @param db [BitacoraDB] Instancia de conexión a la base de datos + # @return [Array] Sugerencias relacionadas con conexiones DB + def self._detectar_friccion_conexiones(db) + sugerencias = [] + ventana = Time.now - @umbrales[:ventana_tiempo] + + # Consultar promedio de conexiones usadas por minuto en la ventana de tiempo + resultado = db.execute(<<~SQL, [ventana]) + SELECT + AVG((contexto->>'conexiones_usadas')::int) as avg_conexiones, + COUNT(*) as total_muestras, + SUBSTRING(MAX(momento)::text, 1, 19) as ultima_medicion + FROM bitacoras.mejoras_metricas + WHERE momento > $1 + AND contexto ? 'conexiones_usadas' + SQL + + fila = resultado.first + avg_conexiones = (fila['avg_conexiones'] || 0).to_f + total_muestras = fila['total_muestras'].to_i + + if total_muestras >= 3 && avg_conexiones > @umbrales[:conexiones_por_minuto] + sugerencias << { + tipo: :conexiones_excessivas, + descripcion: "Detectadas #{avg_conexiones.round(1)} conexiones DB promedio por minuto en las últimas #{@umbrales[:ventana_tiempo]/60} minutos. Umbral: #{@umbrales[:conexiones_por_minuto]}", + accion_sugerida: "Implementar connection pooling en BitacoraDB (similar a lo hecho en #1202)", + prioridad: :alta, + datos: { + avg_conexiones: avg_conexiones, + total_muestras: total_muestras, + umbral: @umbrales[:conexiones_por_minuto], + ventana_minutos: @umbrales[:ventana_tiempo]/60 + } + } + end + + sugerencias + end + + # Detectar comandos repetitivos que podrían beneficiarse de automatización + # @param db [BitacoraDB] Instancia de conexión a la base de datos + # @return [Array] Sugerencias relacionadas con automatización + def self._detectar_comandos_repetitivos(db) + sugerencias = [] + ventana = Time.now - @umbrales[:ventana_tiempo] + + # Encontrar subcomandos con argumentos similares que se repiten frecuentemente + resultado = db.execute(<<~SQL, [ventana, @umbrales[:comandos_repetitivos]]) + SELECT + subcomando, + args_hash, + COUNT(*) as frecuencia, + MAX(momento)::timestamp as ultima_ejecucion, + BOOL_AND(es_ia) as todas_ia + FROM bitacoras.mejoras_metricas + WHERE momento > $1 + GROUP BY subcomando, args_hash + HAVING COUNT(*) >= $2 + ORDER BY frecuencia DESC + LIMIT 10 + SQL + + resultado.each do |fila| + subcomando = fila['subcomando'] + frecuencia = fila['frecuencia'].to_i + es_ia = fila['todas_ia'] == 't' || fila['todas_ia'] == true + + sugerencias << { + tipo: :comando_repetitivo, + descripcion: "Subcomando '#{subcomando}' ejecutado #{frecuencia} veces en las últimas #{@umbrales[:ventana_tiempo]/60} minutos con argumentos similares", + accion_sugerida: "Considerar crear hook automático o plantilla para este patrón de uso", + prioridad: es_ia ? :alta : :media, # Mayor prioridad si es usado por IA + datos: { + subcomando: subcomando, + frecuencia: frecuencia, + ventana_minutos: @umbrales[:ventana_tiempo]/60, + es_ia: es_ia + } + } + end + + sugerencias + end + + # Detectar inconsistencias documento-DB (ej: entradas [P2604] sin actualizaciones en proyectos) + # @param db [BitacoraDB] Instancia de conexión a la base de datos + # @return [Array] Sugerencias relacionadas con inconsistencias + def self._detectar_inconsistencias_documento_db(db) + sugerencias = [] + ventana = Time.now - @umbrales[:ventana_tiempo] + + # Buscar entradas con etiquetas de proyectos que podrían necesitar actualización de manifiesto + resultado = db.execute(<<~SQL, [ventana]) + SELECT + COUNT(*) as count_entradas, + SUBSTRING(MAX(e.created_at)::text, 1, 19) as ultima_entrada + FROM bitacoras.entradas e + JOIN bitacoras.bitacoras b ON e.bitacora_id = b.id + WHERE b.fecha > $1::date + AND e.descripcion ~* 'P2604\.' + SQL + + fila = resultado.first + count_entradas = fila['count_entradas'].to_i + + if count_entradas > 0 + sugerencias << { + tipo: :inconsistencia_documento_db, + descripcion: "Detectadas #{count_entradas} entradas con etiquetas [P2604.*] en las últimas #{@umbrales[:ventana_tiempo]/60} minutos que podrían requerir actualización de manifiesto de proyecto", + accion_sugerida: "Verificar si se necesita actualizar documentos en docs/proy/P2604/... o mejorar DB Hooks", + prioridad: :media, + datos: { + count_entradas: count_entradas, + ventana_minutos: @umbrales[:ventana_tiempo]/60 + } + } + end + + sugerencias + end + end +end \ No newline at end of file diff --git a/adn/tools/db/migrations/001_create_ambitos.sql b/adn/tools/db/migrations/001_create_ambitos.sql new file mode 100644 index 00000000..a49eeeb7 --- /dev/null +++ b/adn/tools/db/migrations/001_create_ambitos.sql @@ -0,0 +1,54 @@ +-- 001_create_ambitos.sql +-- Creación de tabla ambitos y modificación de nodos y entradas + +DO $$ +BEGIN + -- Crear tabla ambitos + CREATE TABLE IF NOT EXISTS bitacoras.ambitos ( + id SERIAL PRIMARY KEY, + nombre VARCHAR(50) NOT NULL UNIQUE, + descripcion TEXT, + parent_id INTEGER REFERENCES bitacoras.ambitos(id) ON DELETE SET NULL, + activo BOOLEAN DEFAULT true, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + + -- Índices para ambitos + CREATE INDEX IF NOT EXISTS idx_ambitos_nombre ON bitacoras.ambitos(nombre); + CREATE INDEX IF NOT EXISTS idx_ambitos_parent ON bitacoras.ambitos(parent_id); + + -- Modificar tabla nodos: añadir ambito_id + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'bitacoras' AND table_name = 'nodos' AND column_name = 'ambito_id' + ) THEN + ALTER TABLE bitacoras.nodos + ADD COLUMN ambito_id INTEGER REFERENCES bitacoras.ambitos(id) ON DELETE SET NULL; + + CREATE INDEX IF NOT EXISTS idx_nodos_ambito ON bitacoras.nodos(ambito_id); + END IF; + + -- Modificar tabla entradas: añadir ambito_id + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'bitacoras' AND table_name = 'entradas' AND column_name = 'ambito_id' + ) THEN + ALTER TABLE bitacoras.entradas + ADD COLUMN ambito_id INTEGER REFERENCES bitacoras.ambitos(id) ON DELETE SET NULL; + + CREATE INDEX IF NOT EXISTS idx_entradas_ambito ON bitacoras.entradas(ambito_id); + END IF; + + -- Crear vista materializada o tabla resumen_ambitos + CREATE TABLE IF NOT EXISTS bitacoras.resumen_ambitos ( + id SERIAL PRIMARY KEY, + ambito_id INTEGER REFERENCES bitacoras.ambitos(id), + fecha DATE, + total_entradas INTEGER DEFAULT 0, + tiempo_total INTERVAL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(ambito_id, fecha) + ); + +END $$; diff --git a/adn/tools/db/migrations/001_create_mejoras_metricas_table.rb b/adn/tools/db/migrations/001_create_mejoras_metricas_table.rb new file mode 100644 index 00000000..160736d5 --- /dev/null +++ b/adn/tools/db/migrations/001_create_mejoras_metricas_table.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +# Migration: Create table for storing metrics for the continuous improvement sensor +# +# Usage: +# ruby adn/tools/db/migrations/001_create_mejoras_metricas_table.rb +# +# This migration is designed to be run once to set up the sensor metrics table. + +require_relative '../core/bitacora_db' + +puts "🔧 Running migration: Create mejoras_metricas table" + +BitacorasDB::BitacoraDB.with_connection do |db| + # Check if table already exists + table_exists = db.execute(<<~SQL) + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_schema = 'bitacoras' + AND table_name = 'mejoras_metricas' + ) + SQL + + if table_exists.first['exists'] + puts "✅ Table bitacoras.mejoras_metricas already exists. Skipping." + else + puts "📝 Creating table bitacoras.mejoras_metricas..." + + create_table_sql = <<~SQL + CREATE TABLE bitacoras.mejoras_metricas ( + id SERIAL PRIMARY KEY, + timestamp TIMESTAMPTZ DEFAULT NOW(), + subcomando VARCHAR(100) NOT NULL, + args_hash VARCHAR(64), + nodo VARCHAR(100), + estado_salida VARCHAR(10), + duracion_ms INTEGER, + es_ia BOOLEAN DEFAULT FALSE, + contexto JSONB + ) + SQL + + db.execute(create_table_sql) + puts "✅ Table bitacoras.mejoras_metricas created successfully." + + # Create indexes for common queries + indexes = [ + "CREATE INDEX idx_mejoras_metricas_timestamp ON bitacoras.mejoras_metricas (timestamp)", + "CREATE INDEX idx_mejoras_metricas_subcomando ON bitacoras.mejoras_metricas (subcomando)", + "CREATE INDEX idx_mejoras_metricas_nodo ON bitacoras.mejoras_metricas (nodo)", + "CREATE INDEX idx_mejoras_metricas_estado ON bitacoras.mejoras_metricas (estado_salida)", + "CREATE INDEX idx_mejoras_metricas_es_ia ON bitacoras.mejoras_metricas (es_ia)" + ] + + indexes.each do |index_sql| + db.execute(index_sql) + puts "📈 Created index: #{index_sql.split(' ')[2]}" + end + + puts "🎉 Migration completed successfully." + end +end + +puts "🏁 Migration finished." \ No newline at end of file diff --git a/adn/tools/db/migrations/002_migrate_nodos_ambitos.sql b/adn/tools/db/migrations/002_migrate_nodos_ambitos.sql new file mode 100644 index 00000000..d4f9525a --- /dev/null +++ b/adn/tools/db/migrations/002_migrate_nodos_ambitos.sql @@ -0,0 +1,55 @@ +-- 002_migrate_nodos_ambitos.sql +-- Seed de ámbitos iniciales y migración de nodos + +DO $$ +DECLARE + id_diiaa INTEGER; + id_dasuten INTEGER; + id_bitacoras INTEGER; +BEGIN + -- 1. Insertar ámbitos principales + INSERT INTO bitacoras.ambitos (nombre, descripcion, parent_id) + VALUES ('dtic-DIIAA', 'Ámbito raíz: Infraestructura y servicios propios', NULL) + ON CONFLICT (nombre) DO UPDATE SET descripcion = EXCLUDED.descripcion + RETURNING id INTO id_diiaa; + + INSERT INTO bitacoras.ambitos (nombre, descripcion, parent_id) + VALUES ('dtic-DASUTEN', 'Departamento de Servicios UTN (gestionado)', id_diiaa) + ON CONFLICT (nombre) DO UPDATE SET descripcion = EXCLUDED.descripcion + RETURNING id INTO id_dasuten; + + INSERT INTO bitacoras.ambitos (nombre, descripcion, parent_id) + VALUES ('dtic-BITACORAS', 'Sistema de registro ADN', id_diiaa) + ON CONFLICT (nombre) DO UPDATE SET descripcion = EXCLUDED.descripcion + RETURNING id INTO id_bitacoras; + + -- 2. Migrar nodos a dtic-DASUTEN + UPDATE bitacoras.nodos + SET ambito_id = id_dasuten + WHERE nombre IN ('srv-dasu', 'dasu-srvv-dc', 'sql-dasuten', 'dc-dasuten', 'pc-dasu0') + OR nombre LIKE 'pcv-dasu%'; + + -- 3. Migrar nodos a dtic-BITACORAS + UPDATE bitacoras.nodos + SET ambito_id = id_bitacoras + WHERE nombre = 'dtic-BITACORAS' OR nombre = 'dtic-DIIAA'; + + -- 4. Migrar nodos restantes a dtic-DIIAA (Infraestructura y Servicios) + UPDATE bitacoras.nodos + SET ambito_id = id_diiaa + WHERE ambito_id IS NULL AND ( + nombre LIKE 'srv-%' + OR nombre LIKE 'srvv-%' + OR nombre = 'pc-frlr' + OR nombre LIKE 'pc-%' + ); + + -- Nota: Los nodos que no apliquen a ninguna regla quedarán con ambito_id NULL temporalmente + + -- 5. Propagar ambito_id a las entradas existentes + UPDATE bitacoras.entradas e + SET ambito_id = n.ambito_id + FROM bitacoras.nodos n + WHERE e.nodo_id = n.id AND e.ambito_id IS NULL; + +END $$; diff --git a/adn/tools/run b/adn/tools/run index 81c494f0..434e4c63 100755 --- a/adn/tools/run +++ b/adn/tools/run @@ -72,6 +72,7 @@ require_relative 'cli/generar' require_relative 'cli/salud' require_relative 'cli/ayuda' require_relative 'cli/nodos' +require_relative 'cli/ambitos' require_relative 'cli/msp' require_relative 'cli/wzombi' require_relative 'proy/proy' @@ -106,6 +107,8 @@ class ADNCLI SubcomandoGenerar.new(args, @logger).ejecutar when 'nodos' ADN::SubcomandoNodos.new(args, @logger).ejecutar + when 'ambitos' + ADN::SubcomandoAmbitos.new(args, @logger).ejecutar when 'conocimiento' ADN::SubcomandoConocimiento.new(args, @logger).ejecutar when 'db' diff --git a/adn/tools/templates/plantillas_eventos.rb b/adn/tools/templates/plantillas_eventos.rb new file mode 100644 index 00000000..5c7a8f08 --- /dev/null +++ b/adn/tools/templates/plantillas_eventos.rb @@ -0,0 +1,179 @@ +# frozen_string_literal: true + +module ADN + module PlantillasEventos + def self.existe?(tipo) + plantillas.key?(tipo) + end + + def self.obtener(tipo) + plantillas[tipo] + end + + def self.lista_tipos + plantillas.keys + end + + def self.lista_con_iconos + { + "implementacion" => "🔧", + "investigacion" => "🔍", + "resolucion" => "🛠️", + "documentacion" => "📝", + "reunion" => "👥", + "test" => "✅", + "debug" => "🐛", + "planificacion" => "📋", + "migracion" => "🔄", + "renombramiento" => "✏️", + "backup" => "🛡️" + } + end + + def self.plantillas + { + "implementacion" => template_implementacion, + "investigacion" => template_investigacion, + "resolucion" => template_resolucion, + "documentacion" => template_documentacion, + "reunion" => template_reunion, + "test" => template_test, + "debug" => template_debug, + "planificacion" => template_planificacion, + "migracion" => template_migracion, + "renombramiento" => template_renombramiento, + "backup" => template_backup + } + end + + def self.template_implementacion + <<~MD + 🔧 **Implementación** + + - **Contexto**: + - **Acción**: + - **Resultado**: + + ```bash + # código + ``` + MD + end + + def self.template_investigacion + <<~MD + 🔍 **Investigación** + + - **Problema**: + - **Hallazgos**: + - **Conclusión**: + - **Refs**: + MD + end + + def self.template_resolucion + <<~MD + 🛠️ **Resolución** + + - **Síntoma**: + - **Causa**: + - **Solución**: + - **Verificación**: + MD + end + + def self.template_documentacion + <<~MD + 📝 **Documentación** + + - **Qué**: + - **Ubicación**: + - **Cambios**: + - **Impacto**: + MD + end + + def self.template_reunion + <<~MD + 👥 **Reunión** + + - **Participantes**: + - **Temas**: + - **Acuerdos**: + - **Acciones**: + MD + end + + def self.template_test + <<~MD + ✅ **Test** + + - **Qué se probó**: + - **Resultado**: + - **Pasos**: + - **Conclusión**: + MD + end + + def self.template_debug + <<~MD + 🐛 **Debug** + + - **Error**: + - **Diagnóstico**: + - **Solución**: + - **Verificación**: + MD + end + + def self.template_planificacion + <<~MD + 📋 **Planificación** + + - **Objetivo**: + - **Fases**: + - **Recursos**: + - **Próximo**: + MD + end + + def self.template_migracion + <<~MD + 🔄 **Migración** + + - **Origen**: + - **Destino**: + - **Componentes**: + - **Precauciones**: + - **Rollback**: + - **Verificación**: + MD + end + + def self.template_renombramiento + <<~MD + ✏️ **Renombramiento** + + - **Anterior**: + - **Nuevo**: + - **Tipo**: Nodo/Archivo/Host + - **Ubicación**: + - **Referencias actualizadas**: + - **Verificación**: + MD + end + + def self.template_backup + <<~MD + 🛡️ **Backup** + + - **Origen**: + - **Tipo**: VZDump/Export/Dump + - **Destino**: + - **Tamaño**: + - **Verificación**: + - **Restauración**: + MD + end + end +end diff --git a/adn/tools/w-zombi/data/cmd.json b/adn/tools/w-zombi/data/cmd.json index ca60fb5d..6e056fa1 100644 --- a/adn/tools/w-zombi/data/cmd.json +++ b/adn/tools/w-zombi/data/cmd.json @@ -1 +1 @@ -{"id":8,"cmd":"Powershell -Command \"cat C:\\wz_rename.txt\""} \ No newline at end of file +{"id":40,"cmd":"Write-Host \"=== Configurando SQL Server ===\";\n# 1. Habilitar autenticación mixta via registro\nSet-ItemProperty -Path \"HKLM:\\SOFTWARE\\Microsoft\\Microsoft SQL Server\\MSSQL15.MSSQLSERVER\\MSSQLServer\" -Name \"LoginMode\" -Value 2;\nWrite-Host \"Auth mixta habilitada (LoginMode=2)\";\n\n# 2. Setear SA password via sqlcmd\n$sqlcmd = \"C:\\Program Files\\Microsoft SQL Server\\Client SDK\\ODBC\\170\\Tools\\Binn\\SQLCMD.EXE\";\nif (-not (Test-Path $sqlcmd)) { $sqlcmd = (Get-ChildItem \"C:\\Program Files\\Microsoft SQL Server\" -Recurse -Filter \"sqlcmd.exe\" | Select-Object -First 1).FullName };\nWrite-Host \"sqlcmd: $sqlcmd\";\n& $sqlcmd -S localhost -E -Q \"ALTER LOGIN [sa] WITH PASSWORD = N'SaP@ss2026DASU!', CHECK_POLICY = OFF; ALTER LOGIN [sa] ENABLE; SELECT name, is_disabled FROM sys.server_principals WHERE name = 'sa'\";\n\n# 3. Reiniciar SQL para aplicar auth mixta\nRestart-Service MSSQLSERVER -Force;\nStart-Sleep -Seconds 5;\n\n# 4. Firewall\nnetsh advfirewall firewall add rule name=\"SQL Server 1433\" dir=in action=allow protocol=TCP localport=1433;\nWrite-Host \"=== Firewall OK ===\";\n\n# 5. Verificar\nGet-Service MSSQLSERVER | Format-List Status;\n& $sqlcmd -S localhost -U sa -P \"SaP@ss2026DASU!\" -Q \"SELECT @@VERSION\""} \ No newline at end of file diff --git a/docs/bitacoras/2026-03-16.md b/docs/bitacoras/_hist/2026-03-16.md similarity index 88% rename from docs/bitacoras/2026-03-16.md rename to docs/bitacoras/_hist/2026-03-16.md index f25acbb4..94a01e13 100644 --- a/docs/bitacoras/2026-03-16.md +++ b/docs/bitacoras/_hist/2026-03-16.md @@ -27,4 +27,7 @@ --- +## 📊 Métricas del Día +[Físico/Remoto: 100/0%] + diff --git a/docs/bitacoras/_hist/2026-03-19.md b/docs/bitacoras/_hist/2026-03-19.md new file mode 100644 index 00000000..6e363670 --- /dev/null +++ b/docs/bitacoras/_hist/2026-03-19.md @@ -0,0 +1 @@ +## Bitácora de Trabajo - 2026-03-19 diff --git a/docs/prompt/260316-0812_Continua.md b/docs/prompt/260316-0812_Continua.md index 925402d7..c9c34e86 100644 --- a/docs/prompt/260316-0812_Continua.md +++ b/docs/prompt/260316-0812_Continua.md @@ -1,10 +1,12 @@ PROMPT +Hola, nos ponemos en contexto con adn/README.md para saber las herraminetas que podes usar y como seguimos el principio rector "Menos es mas, mejora continua y armonía integral". + Genera un nuevo plan con sus respectivos elementos necesarios como fases y demas para agregar el nodo pc-dasu0 a la red de dasu-srvv-dc -Continua con el trabajo de la creacion de los usurios aalmiron (Andrea ALMIRON) y rmolina (Romina MOLINA) en el dominio dasuten.utnlr para que puedn acceder a la pc-dasu0. Puedes usar como contraseña $DASU123 +Continua con el trabajo de la creacion de los usurios aalmiron (Andrea ALMIRON) y rmolina (Romina MOLINA) en el dominio dasuten.utnlr para que puedan acceder a la pc-dasu0. Puedes usar como contraseña $DASU123 -Realiza el trabajo priorizando siempre el uso y la optimización de las herramientas definidas en el ecosistema ADN, con el objetivo de mejorarlas y hacerlas reutilizables en el futuro. Si es necesario crear nuevas herramientas, asegúrate de que sigan los mismos principios de sostenibilidad y evita soluciones temporales o de un solo uso. +Hola, nos ponemos en context y priorizamos siempre el uso y la optimización de las herramientas definidas en el ecosistema ADN, con el objetivo de mejorarlas y hacerlas reutilizables en el futuro. Si es necesario crear nuevas herramientas, asegúrate de que sigan los mismos principios de sostenibilidad y evita soluciones temporales o de un solo uso. Para más contexto, consulta los lineamientos en `adn/README.md`, así como el índice actualizado de herramientas disponibles en ADN. diff --git a/docs/prompt/260325-0809_Contexto.md b/docs/prompt/260325-0809_Contexto.md new file mode 100644 index 00000000..31558b57 --- /dev/null +++ b/docs/prompt/260325-0809_Contexto.md @@ -0,0 +1,28 @@ +# Contexto del Ecosistema ADN + +## Prioridad de herramientas + +Prioriza siempre el uso y optimización de las herramientas del ecosistema ADN. El objetivo es mejorarlas y hacerlas reutilizables. Si es necesario crear nuevas herramientas, asegúrate de que sigan los principios de sostenibilidad. Evita soluciones temporales o de un solo uso. + +## Documentación de referencia + +- **Lineamientos generales**: `adn/README.md` +- **Directivas IA**: `adn/05_ia.md` (fuente única de verdad para premisas de IA) +- **Índice de herramientas**: consultar el índice actualizado en ADN + +## Mantenimiento del ecosistema + +Si realizas optimizaciones o mejoras, actualiza el ecosistema ADN para reflejar dichos cambios. + +## Flujo de trabajo + +1. **Bitácora First**: registrar el contexto o situación en la bitácora +2. **Actualizar el plan** +3. **Ejecutar las tareas correspondientes** +4. **Actualizar la bitácora** con resultados y avances + +## Principios rectores + +- **Menos es más** +- **Mejora continua** +- **Armonía integral** diff --git a/docs/prompt/260325-0900_Contexto-IA.md b/docs/prompt/260325-0900_Contexto-IA.md new file mode 100644 index 00000000..0e5bb7af --- /dev/null +++ b/docs/prompt/260325-0900_Contexto-IA.md @@ -0,0 +1,340 @@ +# Contexto para IA - Mecánica de Trabajo dtic-DIIAA + +**Fecha:** 2026-03-25 +**Versión:** 1.3 - Minimalista (Principio "menos es más") +**Bitácora de referencia:** Eventos 1261-1300 (2026-03-25 y 2026-03-26) +**Actualización:** Zona horaria Argentina agregada, renombramiento dasu-pc/dasu-pcv + +--- + +## 🌎 Zona Horaria + +**IMPORTANTE**: Este proyecto opera en la zona horaria de **Argentina (Buenos Aires)**. + +- **UTC**: UTC-3 (hora estándar argentina, sin DST) +- **Comando local**: Usar `date` para obtener la hora correcta +- **Formato registro**: HH:MM (24 horas) +- **⚠️ CRÍTICO**: Los eventos deben usar la hora real del sistema, NO la hora del agente IA + +--- + +## 🌎 Zona Horaria + +**IMPORTANTE**: Este proyecto opera en la zona horaria de **Argentina (Buenos Aires)**. + +- **UTC**: Argentina no tiene horario de verano permanente (UTC-3) +- **Comando local**: Usar `date` para obtener la hora correcta +- **Formato registro**: HH:MM (24 horas) + +--- + +## 🎯 Principio Fundamental: Bitácora First + +**TODO lo que hagas DEBE registrarse en bitácora ANTES de ejecutar.** + +### Flujo obligatorio: + +``` +1. Registrar contexto en bitácora (db evento:crear) +2. Ejecutar la tarea +3. Actualizar bitácora con resultados (db evento:actualizar --fin) +``` + +### Ejemplo práctico: + +```bash +# 1. ANTES de empezar, registrás +./adn/tools/run db evento:crear --nodo dtic-DIIAA \ + --descripcion "[P2604.06] Descripción de lo que voy a hacer" \ + --inicio 08:00 + +# 2. Ejecutás la tarea +./adn/tools/run + +# 3. Al terminar, actualizás con la hora de fin +./adn/tools/run db evento:actualizar --fin 08:15 +``` + +--- + +## 🧬 Arquitectura del Ecosistema ADN + +### Estructura de comandos: + +``` +./adn/tools/run [opciones] +``` + +### ⚠️ REGLA CRÍTICA: Antes de SSH +**SIEMPRE ejecutar:** `./adn/tools/run nodos info ` + +Esto muestra: IP, Puerto SSH, Usuario, Auth, Bóveda, SO, VMID + +### Subcomandos principales: + +| Categoría | Comandos | +|-----------|----------| +| **Bitácoras** | `db evento:crear`, `db evento:actualizar`, `db evento:listar`, `jornada iniciar/cerrar` | +| **Nodos** | `nodos listar`, `nodos info `, `ssh [comando]` | +| **Ámbitos** | `ambitos listar`, `ambitos nodos `, `ambitos crear` | +| **Seguridad** | `candados authorize`, `candados run ` | +| **Operaciones** | `backup `, `salud`, `network show` | +| **Generación** | `generar bitacora`, `generar nodo`, `generar evento ` | +| **W-Zombi** | `wzombi start`, `wzombi cmd ""`, `wzombi log` | + +### ⚠️ REGLA PARA IA: Antes de leer código fuente + +**SIEMPRE** ejecutar `./adn/tools/run --help` o `./adn/tools/run ayuda ` **ANTES** de leer archivos `.rb` para entender cómo funciona una herramienta. La ayuda integrada es suficiente en la mayoría de los casos. + +### Plantillas de Eventos + +**OBLIGATORIO**: Usar plantillas para todos los eventos con detalles ricos. + +```bash +./adn/tools/run generar evento +``` + +**Tipos disponibles** (11): +| Icono | Tipo | Uso | +|-------|------|-----| +| 🔧 | `implementacion` | Implementación técnica | +| 🔍 | `investigacion` | Investigación/exploración | +| 🛠️ | `resolucion` | Resolución de incidentes | +| 📝 | `documentacion` | Documentación/corrección | +| 👥 | `reunion` | Reuniones/comunicaciones | +| ✅ | `test` | Pruebas/validación | +| 🐛 | `debug` | Depuración | +| 📋 | `planificacion` | Planificación | +| 🔄 | `migracion` | Migraciones de red/sistemas | +| ✏️ | `renombramiento` | Cambios de nombre de nodos | +| 🛡️ | `backup` | Resguardos/VZDumps | + +### Base de datos: + +- **Host:** localhost:5433 +- **DB:** dtic_bitacoras +- **Schema:** bitacoras +- **Tablas:** nodos, bitacoras, entradas, temas, gestion, proyectos, fases, hitos + +--- + +## 📋 Convenciones de Registro + +### Formato de descripciones: + +``` +[CÓDIGO_PROYECTO] Descripción breve + +**Detalle 1**: Valor +**Detalle 2**: Valor +**Próximo**: Qué sigue +``` + +### Ejemplo real: + +``` +[P2604.06] Mejora herramienta candados: migración de temporales + +**Archivos modificados**: candados.rb, README.md +**Principio**: 05_ia.md #11 (Workspace Bounds) +**Próximo**: Actualizar documentación +``` + +### Referencia a planes en eventos: + +Cuando un evento está asociado a un plan `.md`, usar el flag `--doc` para vincular: + +**Formato CLI:** +```bash +./adn/tools/run db evento:crear --doc P2601_Dasuten/P2601.09_DASUTEN-sin-DC.md ... +./adn/tools/run db evento:actualizar 1308 --doc P2604_Mejoras-ADN/P2604_Mejoras-ADN.md +``` + +**Campo BD:** `doc_path` en tabla `bitacoras.entradas` (formato: `CARPETA/ARCHIVO.md`). + +**En la web:** Se renderiza como botón clickeable que abre el `.md` en un modal. El contenido se lee del disco dinámicamente — no necesita reiniciar Docker. + +**⚠️ IMPORTANTE:** Si se renombra el archivo `.md`, solo hay que actualizar el campo `doc_path` en los eventos afectados (un solo UPDATE). + +### Tiempos: + +- **Siempre** registrar `--inicio` y `--fin` +- **USAR TIEMPO REAL**: hora actual del sistema, no secuencias teóricas +- Los eventos son **secuenciales**, no simultáneos +- Si trabajás en paralelo, usá diferentes `--nodo` + +**⚠️ IMPORTANTE:** +```bash +# ✅ CORRECTO: Tiempo real +./adn/tools/run db evento:crear --inicio $(date +%H:%M) + +# ❌ INCORRECTO: Forzar secuencias artificiales +# No inventes tiempos "lógicos" si la tarea real empezó ahora +``` + +--- + +## 🔐 Seguridad + +**⚠️ LEER ANTES DE OPERAR:** [`adn/03_seguridad.md`](../adn/03_seguridad.md) + +- Contiene protocolos de seguridad, gestión de candados y matriz de acceso SSH +- **Nunca** escribir secretos en texto plano o commitearlos +- Usar `tmp/` del repo, nunca `/tmp/` del sistema + +--- + +## 🌐 Topología de Red + +### Comandos útiles: + +```bash +# ⚠️ SIEMPRE ejecutar esto ANTES de conectar por SSH +./adn/tools/run nodos info + +# Ver topología completa +./adn/tools/run network show --formato texto + +# Listar todos los nodos +./adn/tools/run nodos listar +``` + +**El comando `nodos info` muestra: IP, Puerto SSH, Usuario, Auth, Bóveda, SO, VMID** + +### Nodos comunes: +- `dtic-DIIAA` - Nodo principal (donde trabajás) +- `srv-ns8` - Servidor principal +- `dasu-srvv-sql` - SQL Server +- `dasu-srvv-dc` - Domain Controller +- `dtic-EVENTOS` - Gestión de eventos y videoconferencias +- `dtic-DIGI` - Grabaciones y contenidos digitales (YouTube) + +### ⚠️ Puertos SSH por Nodo +- **Linux VMs**: Puerto 22 (estándar) +- **Windows VMs (dasu-srvv-dc, dasu-srvv-sql, dasu-pcv)**: Puerto **7022** (OpenSSH hardeado) +- **PCs físicas Windows**: Puerto **7022** (si tiene OpenSSH) + +--- + +## 📁 Estructura del Repositorio + +``` +dtic-DIIAA/ +├── adn/ # Núcleo del ecosistema +│ ├── 00_indice.md # Mapa general +│ ├── 01_ontologia.md # Nodos y topología +│ ├── 02_bitacora.md # Formato de registro +│ ├── 05_ia.md # Directivas para IA ⚠️ +│ └── tools/ # Herramientas CLI +├── docs/ +│ ├── bitacoras/ # Bitácoras diarias (exportadas) +│ ├── proy/ # Proyectos (P2604_*) +│ └── prompt/ # Contexto para IA +├── nodos/ # Fichas de nodos +├── tmp/ # Temporales (único tmp válido) +└── servicios/ # Docker Compose +``` + +--- + +## 🤖 Flujo de Trabajo con IA + +### Cuando el usuario pide algo: + +1. **Verificar si está registrado en bitácora** + - Si no → `db evento:crear` primero + - Si sí → continuar + +2. **Ejecutar la tarea** + - Usar comandos `./adn/tools/run` + - Seguir principios de `adn/05_ia.md` + +3. **Actualizar bitácora** + - `db evento:actualizar --fin HH:MM` + - Incluir resultados y próximos pasos + +### Errores comunes a evitar: + +❌ Ejecutar sin registrar en bitácora +❌ Todos los eventos con misma hora de inicio +❌ Olvidar el `--fin` al terminar +❌ Usar `/tmp/` en lugar de `tmp/` del repo +❌ No verificar candados antes de SSH + +--- + +## 🏢 Ámbitos del Sistema + +**Ver listado actual:** `./adn/tools/run ambitos listar` + +Los ámbitos agrupan nodos por área funcional. La lista completa y actualizada está en [`adn/01_ontologia.md`](../adn/01_ontologia.md). + +--- + +## 📊 Bitácora Web - Fichas por Ámbito + +**Implementado:** 2026-03-25 + +La vista principal de la Bitácora Web ahora agrupa las entradas **por Ámbito** en lugar de por Nodo: + +- Cada ficha muestra todos los eventos de los nodos del ámbito +- Columna "Nodo" identifica el origen de cada evento +- Badge muestra cantidad de nodos y entradas del ámbito + +**URL:** http://localhost:5174/bitacoras/ + +--- + +## 📚 Documentación de Referencia + +| Archivo | Propósito | +|---------|-----------| +| `adn/README.md` | Punto de entrada al ecosistema | +| `adn/05_ia.md` | Directivas específicas para IA ⚠️ | +| `adn/02_bitacora.md` | Formato y triggers de bitácoras | +| `adn/03_seguridad.md` | Protocolos de seguridad y candados 🔐 | +| `adn/01_ontologia.md` | Nodos, ámbitos y topología | +| `docs/prompt/260325-0809_Contexto.md` | Contexto inicial del proyecto | +| `docs/prompt/260325-0900_Contexto-IA.md` | Este archivo - Contexto (minimalista) | + +--- + +## 🚀 Quick Start para Nueva Sesión + +```bash +# 1. Iniciar jornada +./adn/tools/run jornada iniciar presencial 08:00 + +# 2. Registrar primera actividad +./adn/tools/run db evento:crear --nodo dtic-DIIAA \ + --descripcion "[P2604.XX] Lo que voy a hacer" \ + --inicio 08:05 + +# 3. Trabajar +# ... ejecutás tus comandos ... + +# 4. Cerrar actividad +./adn/tools/run db evento:actualizar --fin 08:30 + +# 5. Verificar en web +# http://localhost:5174 +``` + +--- + +**Última actualización:** 2026-03-26 09:00 +**Próxima revisión:** Al inicio de cada sesión + +--- + +## 📌 Principio "Menos es Más" + +Este archivo contiene solo lo esencial para operar. Para detalles completos, consultar las hebras canónicas: + +- Seguridad → `adn/03_seguridad.md` +- Ontología/Ámbitos → `adn/01_ontologia.md` +- Bitácoras → `adn/02_bitacora.md` +- IA Directivas → `adn/05_ia.md` + +--- + diff --git a/docs/proy/P2601_Dasuten/P2601.06.02_AD-Join.md b/docs/proy/P2601_Dasuten/P2601.06.02_AD-Join.md index 4ae8703b..a9c0f9ad 100644 --- a/docs/proy/P2601_Dasuten/P2601.06.02_AD-Join.md +++ b/docs/proy/P2601_Dasuten/P2601.06.02_AD-Join.md @@ -18,7 +18,7 @@ Unir la PC física `pc-dasu0` (oficina DASUTeN) al dominio `dasuten.utnlr`. La i |:-----|:-------------|:-----|:----------:|:---------:|:--------:|:------:| | `dasu-srvv-dc` | (✅ antes dc-dasuten) | VM 100 (DC/DNS) | `10.0.100.10` | ✅ | ❓ Fallando | ✅ Renombrado | | `dasu-srvv-sql` | (✅ antes sql-dasuten) | VM 101 (SQL) | `10.0.100.11` | ✅ | ✅ | ✅ Renombrado | -| `dasu-pcv-0` | (antes pcv-dasu0) | VM 102 (Test) | `10.0.100.12` | ❌ | ✅ | ✅ Renombrado | +| `dasu-pcv` | (antes pcv-dasu0) | VM 102 (Test) | `10.0.100.12` | ❌ | ✅ | ✅ Renombrado | | `dasu-pc0` | (antes pc-dasu0) | PC Física | `192.168.1.13` (DHCP) | ❌ offline | ✅ | ⏳ Por renombrar | | `srv-dasu` | — | Hypervisor | `10.0.100.1` | ✅ (`100.112.46.104`) | ✅ | ✅ Operativo | @@ -35,7 +35,7 @@ Unir la PC física `pc-dasu0` (oficina DASUTeN) al dominio `dasuten.utnlr`. La i │ (.10) [AD] │ ← ← ← ← ← ← ← ← ┘ │ dasu-srvv-sql│ │ (.11) [SQL] │ - │ dasu-pcv-0 │ + │ dasu-pcv │ │ (.12) [Test]│ └──────────────┘ ``` @@ -71,7 +71,7 @@ Unir la PC física `pc-dasu0` (oficina DASUTeN) al dominio `dasuten.utnlr`. La i | :--- | :--- | :--- | :--- | | 1.0.1 | `sql-dasuten` (VM 101) | `dasu-srvv-sql` | ✅ Completado (16/03) | | 1.0.2 | `dc-dasuten` (VM 100) | `dasu-srvv-dc` | ✅ Completado (16/03) | -| 1.0.3 | `pcv-dasu0` (VM 102) | `dasu-pcv-0` | ✅ Completado (16/03) | +| 1.0.3 | `pcv-dasu0` (VM 102) | `dasu-pcv` | ✅ Completado (16/03) | | 1.0.4 | `pc-dasu0` (PC Física) | `dasu-pc0` | ⏳ Pendiente | **Procedimiento validado (sql-dasuten → dasu-srvv-sql):** @@ -111,15 +111,15 @@ Unir la PC física `pc-dasu0` (oficina DASUTeN) al dominio `dasuten.utnlr`. La i > ⚠️ **Precaución DC:** Al renombrar el Controlador de Dominio, verificar que los registros SRV de DNS se actualicen automáticamente (`_ldap._tcp.dasuten.utnlr`, `_kerberos._tcp.dasuten.utnlr`). Ejecutar `dcdiag /q` post-rename. -#### Tarea 1.0.3: pcv-dasu0 → dasu-pcv-0 +#### Tarea 1.0.3: pcv-dasu0 → dasu-pcv | ID | Tarea | Estado | | :--- | :--- | :--- | | 1.0.3.a | Verificar SSH accesible desde srv-dasu (7022) | ✅ SSH probado | | 1.0.3.b | Desplegar clave RSA vía sshpass (si no está desplegada) | ✅ Desplegada | -| 1.0.3.c | Rename-Computer -NewName dasu-pcv-0 -Force -Restart | ✅ Completado | -| 1.0.3.d | Actualizar Proxmox: `sudo qm set 102 --name dasu-pcv-0` | ✅ Completado | -| 1.0.3.e | Actualizar ficha `nodos/pcv-dasu0.md` → `nodos/dasu-pcv-0.md` | ✅ Completado | +| 1.0.3.c | Rename-Computer -NewName dasu-pcv -Force -Restart | ✅ Completado | +| 1.0.3.d | Actualizar Proxmox: `sudo qm set 102 --name dasu-pcv` | ✅ Completado | +| 1.0.3.e | Actualizar ficha `nodos/pcv-dasu0.md` → `nodos/dasu-pcv.md` | ✅ Completado | | 1.0.3.f | Verificar conectividad y actualización de DNS | ✅ Confirmado DNS | #### Tarea 1.0.4: pc-dasu0 → dasu-pc0 @@ -265,7 +265,7 @@ Unir la PC física `pc-dasu0` (oficina DASUTeN) al dominio `dasuten.utnlr`. La i 1. [Proyecto P2601 DASUTEN](../P2601_dasuten.md) 2. [Nodo: pc-dasu0](../../../nodos/pc-dasu0.md) -3. [Nodo: dasu-pcv-0](../../../nodos/dasu-pcv-0.md) *(VM referencia, ya unida al dominio)* +3. [Nodo: dasu-pcv](../../../nodos/dasu-pcv.md) *(VM referencia, ya unida al dominio)* 4. [Nodo: dasu-srvv-dc](../../../nodos/dasu-srvv-dc.md) 5. [Nodo: srv-dasu](../../../nodos/srv-dasu.md) 6. [Plan 06.01: Red srv-dasu](P2601.06.01_Red-SrvDasu.md) @@ -280,7 +280,7 @@ Unir la PC física `pc-dasu0` (oficina DASUTeN) al dominio `dasuten.utnlr`. La i - ✅ Clave RSA generada en srv-dasu (rmonla) y desplegada en sql-dasuten (#1117) - ✅ **Hostname sql-dasuten → dasu-srvv-sql** completado: Rename-Computer + Proxmox + ficha (#1117) - ✅ **Hostname dc-dasuten → dasu-srvv-dc** completado: netdom vía Invoke-Command (WinRM) desde dasu-srvv-sql porque el SSH Server estaba roto en el DC (#1118) -- ✅ **Hostname pcv-dasu0 → dasu-pcv-0** completado: Rename-Computer (via domain credential) + Proxmox + Clave RSA + DNS OK (#1119) +- ✅ **Hostname pcv-dasu0 → dasu-pcv** completado: Rename-Computer (via domain credential) + Proxmox + Clave RSA + DNS OK (#1119) ### 15-16/03/2026 — Sesión de Infraestructura (Madrugada) - ✅ Backups VZDump de dc-dasuten (#1093) y sql-dasuten (#1094) diff --git a/docs/proy/P2601_Dasuten/P2601.07.01_Integracion-DASU-PC-0.md b/docs/proy/P2601_Dasuten/P2601.07.01_Integracion-DASU-PC-0.md deleted file mode 100644 index 0de4e519..00000000 --- a/docs/proy/P2601_Dasuten/P2601.07.01_Integracion-DASU-PC-0.md +++ /dev/null @@ -1,49 +0,0 @@ -# P2601.07.01 - Plan: Integración dasu-pc-0 a Dominio DASUTEN - -**Fecha:** 18 de marzo de 2026 -**Autor:** Antigravity (IA ADN) -**Estado:** ✅ FINALIZADO -**Proyecto Padre:** [P2601 - Red DASUTEN](../P2601_dasuten.md) - -## 📋 Objetivo -Integrar exitosamente el nodo físico `dasu-pc-0` al dominio `dasu-srvv-dc` para centralizar la gestión de usuarios, aplicar GPOs y unificar la red operativa de DASUTEN. - -## 📅 Fases de Implementación - -### **Fase 7.1: Preparación y Validación (Completada)** -- [x] **1.A:** Verificación de conectividad IP con `dasu-srvv-dc` (vía Tailscale 100.85.117.101). -- [x] **1.B:** Configuración de DNS primario en `dasu-pc-0` apuntando a DC. -- [x] **1.C:** Validación de credenciales de Administrador de Dominio. - -### **Fase 7.2: Ejecución de AD-Join (Completada)** -- [x] **2.A:** Unión al dominio `dasuten.utnlr`. -- [x] **2.B:** Reinicio de la máquina y comprobación de persistencia. -- [x] **2.C:** Confirmación de visibilidad en el contenedor de Computers del DC. - -### **Fase 7.3: Post-Configuración y Seguridad (Completada)** -- [x] **3.A:** Verificación de aplicación de GPOs básicas. -- [x] **3.B:** Renombramiento definitivo a `dasu-pc-0` inyectado vía SSH (RSA). -- [x] **3.C:** Registro final en la ontología del ADN y ficha del nodo. - -### **Fase 7.4: Creación de Usuarios de Dominio (Completada)** -- [x] **4.A:** Crear usuario `aalmiron` (Andrea ALMIRON) en AD. -- [x] **4.B:** Crear usuario `rmolina` (Romina MOLINA) en AD. -- [x] **4.C:** Otorgar acceso a `dasu-pc-0` para estos usuarios (Verificado). - ---- - -## 📊 Progreso - -``` -Fase 1: ██████████ 100% Preparación y Validación -Fase 2: ██████████ 100% Ejecución de AD-Join -Fase 3: ██████████ 100% Post-Configuración -Fase 4: ██████████ 100% Creación de Usuarios -``` - ---- - -## 🔗 Referencias -- **DC:** `dasu-srvv-dc` (100.85.117.101) -- **Host:** `dasu-pc-0` (Ex `pc-dasu0`) -- **Ontología:** [01_ontologia.md](../../../adn/01_ontologia.md) diff --git a/docs/proy/P2601_Dasuten/P2601.07.01_Integracion-DASU-PC.md b/docs/proy/P2601_Dasuten/P2601.07.01_Integracion-DASU-PC.md new file mode 100644 index 00000000..ee6da63d --- /dev/null +++ b/docs/proy/P2601_Dasuten/P2601.07.01_Integracion-DASU-PC.md @@ -0,0 +1,19 @@ +# P2601.07.01 - Plan: Integración dasu-pc a Dominio DASUTEN + +> **Objetivo**: Integrar exitosamente el nodo físico `dasu-pc` al dominio `dasu-srvv-dc` para centralizar la gestión de usuarios, aplicar GPOs y unificar la red operativa de DASUTEN. + +- [x] **1.B:** Configuración de DNS primario en `dasu-pc` apuntando a DC. +- [x] **3.B:** Renombramiento definitivo a `dasu-pc` inyectado vía SSH (RSA). +- [x] **4.C:** Otorgar acceso a `dasu-pc` para estos usuarios (Verificado). +- **Host:** `dasu-pc` (Ex `pc-dasu0`) +- [P2601.07.01] Renombramiento de `dasu-pc-0` a `dasu-pc` completado. + +| dasu-pc | 100.100.145.51 | ✅ ONLINE | +| dasu-srvv-dc | 100.85.117.101 | ❌ offline (VM apagada) | +| dasu-srvv-sql | 100.107.24.124 | ❌ offline (VM apagada) | + +--- + +**Fecha de implementación:** 2026-03-25 +**Bitácora de referencia:** Evento 1273 +**Responsable:** Lic. Ricardo MONLA diff --git a/docs/proy/P2601_Dasuten/P2601.08_Unificacion-Redes-ISP.md b/docs/proy/P2601_Dasuten/P2601.08_Unificacion-Redes-ISP.md new file mode 100644 index 00000000..699fe3da --- /dev/null +++ b/docs/proy/P2601_Dasuten/P2601.08_Unificacion-Redes-ISP.md @@ -0,0 +1,166 @@ +# P2601.08 - Unificación de Redes: Subred ISP + +> **Objetivo**: Migrar las VMs de srv-dasu y dasu-pc de sus redes aisladas a la subred del router ISP, unificando toda la infraestructura DASUTEN en una sola red. + +## Contexto + +### Topología Actual (2026-03-26) + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ ROUTER ISP (Gateway: 192.168.1.1) │ +│ Subred: 192.168.1.0/24 │ +└─────────────────────┬───────────────────────────────────────────┘ + │ + ┌────────────┴────────────┐ + │ │ + ↓ ↓ +┌─────────────────┐ ┌─────────────────┐ +│ dasu-pc │ │ srv-dasu │ +│ (PC Física) │ │ (Proxmox) │ +│ Tailscale: │ │ enp33s0: │ +│ 100.100.145.51│ │ 192.168.1.13 │ +│ Sin red local │ │ │ +└─────────────────┘ └────────┬────────┘ + │ + ┌────────┴────────┐ + │ vmbr0 │ + │ 10.0.100.1/24 │ + └────────┬────────┘ + │ + ┌──────────────┼──────────────┐ + ↓ ↓ ↓ + VM 100 VM 101 VM 102 + (DC) (SQL) (PCV) + 10.0.100.10 10.0.100.11 10.0.100.12 +``` + +### Problema Identificado +- **VMs**: En subred aislada `10.0.100.0/24` (solo accesible via Tailscale o link directo a srv-ns8) +- **dasu-pc**: Conectada vía Tailscale, NO en la red física del router ISP +- **Fragmentación**: 3 segmentos de red diferentes (Tailscale VPN, ISP, Red local VMs) + +### Topología Objetivo + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ ROUTER ISP (Gateway: 192.168.1.1) │ +│ Subred: 192.168.1.0/24 │ +└─────────────────────┬───────────────────────────────────────────┘ + │ + ┌────────────┴─────────────────────────────────┐ + │ │ + ↓ ↓ +┌─────────────────┐ ┌─────────────────────────┐ +│ dasu-pc │ ←─── Ethernet Directo ───│ srv-dasu (Proxmox) │ +│ 192.168.1.20 │ │ enp33s0: 192.168.1.10 │ +│ (Sin TS) │ │ │ +└─────────────────┘ │ VMs (sin Bridge) │ + │ vmbr0: 192.168.1.1/24 │ + │ ├── VM 100: .21 (DC) │ + │ ├── VM 101: .22 (SQL) │ + │ └── VM 102: .23 (PCV) │ + └─────────────────────────┘ +``` + +## Análisis Técnico + +### Componentes a Modificar + +| Componente | Acción | Riesgo | +|------------|--------|--------| +| **srv-dasu** (enp33s0) | Cambiar de DHCP a IP fija en subnet ISP | Bajo | +| **srv-dasu** (vmbr0) | Cambiar de 10.0.100.1 a 192.168.1.1 | Medio | +| **VMs** (vNIC) | Cambiar IPs de 10.0.100.x a 192.168.1.x | Alto | +| **DC** (dasu-srvv-dc) | Actualizar DNS, verificar AD | Alto | +| **dasu-pc** | Conectar por cable al router ISP | Medio | +| **Tailscale** | Evaluar si mantener o remover | Bajo | + +### Pre-Requisitos + +1. **Verificar capacidad del router ISP**: Confirmar DHCP range disponible +2. **Backup completo**: VZDump de todas las VMs antes de migración +3. **Documentar configuración actual**: IPs, rutas, NAT +4. **Plan de rollback**: Procedimiento para revertir si falla + +## Plan de Ejecución + +### Fase 1: Preparación (Pre-Migración) + +| ID | Tarea | Tiempo Est. | Estado | +|----|-------|-------------|--------| +| 1.1 | Verificar estado actual de red (srv-dasu, VMs) | 15 min | ⏳ | +| 1.2 | VZDump offline de VM 100 (DC) | 30 min | ⏳ | +| 1.3 | VZDump offline de VM 101 (SQL) | 30 min | ⏳ | +| 1.4 | VZDump offline de VM 102 (PCV) | 20 min | ⏳ | +| 1.5 | Documentar IPs actuales y configuración | 15 min | ⏳ | +| 1.6 | Verificar acceso físico a dasu-pc | 5 min | ⏳ | + +### Fase 2: Migración de srv-dasu + +| ID | Tarea | Tiempo Est. | Estado | +|----|-------|-------------|--------| +| 2.1 | Asignar IP estática a enp33s0 (192.168.1.10) | 10 min | ⏳ | +| 2.2 | Reconfigurar vmbr0 (192.168.1.1/24) | 10 min | ⏳ | +| 2.3 | Actualizar NAT/Masquerade | 5 min | ⏳ | +| 2.4 | Actualizar DNS (no usar más 10.0.100.10) | 5 min | ⏳ | +| 2.5 | Reiniciar red en srv-dasu | 5 min | ⏳ | + +### Fase 3: Migración de VMs + +| ID | Tarea | Tiempo Est. | Estado | +|----|-------|-------------|--------| +| 3.1 | Reconfigurar vNIC de VM 100 (DC) | 10 min | ⏳ | +| 3.2 | Reiniciar VM 100 y verificar conectividad | 15 min | ⏳ | +| 3.3 | Reconfigurar vNIC de VM 101 (SQL) | 10 min | ⏳ | +| 3.4 | Reiniciar VM 101 y verificar conectividad | 15 min | ⏳ | +| 3.5 | Reconfigurar vNIC de VM 102 (PCV - dasu-pcv) | 10 min | ⏳ | +| 3.6 | Reiniciar VM 102 y verificar conectividad | 15 min | ⏳ | + +### Fase 4: Integración de dasu-pc + +| ID | Tarea | Tiempo Est. | Estado | +|----|-------|-------------|--------| +| 4.1 | Conectar dasu-pc al router ISP por cable | 10 min | ⏳ | +| 4.2 | Configurar IP estática (192.168.1.20) | 10 min | ⏳ | +| 4.3 | Verificar conexión a VMs (ping DC, SQL) | 10 min | ⏳ | +| 4.4 | Test de acceso a servicios (AD, SQL) | 15 min | ⏳ | +| 4.5 | Desinstalar o reconfigurar Tailscale | 10 min | ⏳ | + +### Fase 5: Verificación y Cierre + +| ID | Tarea | Tiempo Est. | Estado | +|----|-------|-------------|--------| +| 5.1 | Verificar acceso a Internet desde VMs | 10 min | ⏳ | +| 5.2 | Verificar servicios (AD, SQL) funcionando | 15 min | ⏳ | +| 5.3 | Test de aplicación DASUTEN | 20 min | ⏳ | +| 5.4 | Actualizar documentación (nodos, proyecto) | 15 min | ⏳ | +| 5.5 | Actualizar Tailscale subnet router si corresponde | 10 min | ⏳ | + +## Riesgos y Mitigaciones + +| Riesgo | Impacto | Mitigación | +|--------|---------|-------------| +| Pérdida de conectividad durante migración | Alto | Mantener acceso Tailscale como fallback | +| Fallo en reinicio de VMs | Alto | VZDump previo, procedimiento de rollback | +| AD DS deja de funcionar tras cambio IP | Alto | Verificar y actualizar registros DNS internos | +| dasu-pc no puede unirse al dominio | Medio | Verificar DNS指向 correcto | +| Conflicto de IPs en la subnet | Medio | Usar IPs fuera del rango DHCP del router | + +## Métricas Estimadas + +- **Tiempo Total**: ~4-5 horas (distribuidas en múltiples sesiones) +- **Costo**: $0 (solo recursos existentes) +- **Beneficio**: Red unificada, menor dependencia de VPN + +--- + +## Notes + +- Subnet del router ISP verificada: `192.168.1.0/24` (gateway `192.168.1.1`) +- Rango DHCP del router: Por determinar (necesita acceso a config del router) +- IPs sugeridas: srv-dasu `.10`, VMs `.21-.23`, dasu-pc `.20` +-Mantener Tailscale para acceso remoto desde exterior (Facultad) + +**Creado**: 2026-03-26 14:05 +**Evento**: 1299 \ No newline at end of file diff --git a/docs/proy/P2601_Dasuten/P2601.09_DASUTEN-sin-DC.md b/docs/proy/P2601_Dasuten/P2601.09_DASUTEN-sin-DC.md new file mode 100644 index 00000000..e6651f03 --- /dev/null +++ b/docs/proy/P2601_Dasuten/P2601.09_DASUTEN-sin-DC.md @@ -0,0 +1,137 @@ +# Plan: DASUTEN sin Domain Controller (Modo Workgroup) + +**Código:** P2601.09 +**Fecha:** 27 de marzo de 2026 +**Autor:** Sistema ADN +**Versión:** 2.2 +**Estado:** 🚧 EN EJECUCIÓN (Fase 2 en progreso — VM 103 creada, pendiente instalación OS) + +## 📋 Resumen Ejecutivo + +Evaluar si el sistema DASUTEN puede operar **sin la dependencia de un Domain Controller (AD DS)**, utilizando únicamente un SQL Server y una PC cliente en modo **Workgroup**. Esto simplifica significativamente la infraestructura y elimina los problemas de red asociados al dominio. + +### Motivación + +Los intentos previos (P2601.06 a P2601.08) demostraron que la topología con DC introduce complejidad de red que entra en conflicto con el router del ISP de la oficina DASUTeN. Al eliminar la dependencia del DC, se reducen los puntos de falla y se simplifica la conectividad. + +### Decisiones clave + +- **VMs existentes preservadas**: `dasu-srvv-dc` (VM 100), `dasu-srvv-sql` (VM 101) y `dasu-pcv` (VM 102) fueron apagadas y deshabilitadas del autostart. Pueden reactivarse si se decide retornar al enfoque con DC. +- **Enfoque nuevo**: VMs limpias en modo Workgroup, sin Active Directory. +- **Red provista por ISP**: Tanto `srv-dasu` como las VMs (`dasu-sql2`, `dasu-pcv2`) y la PC física (`dasu-pc`) obtienen IP directa del router del ISP. No se usa red privada 10.0.100.x. + +## 🌐 Topología de Red + +``` +Internet + │ +Router ISP (192.168.1.1 — gateway confirmado) + │ + ├── srv-dasu → DHCP del ISP: 192.168.1.13 (vmbr0 bridge sobre enp33s0) + │ ├── dasu-sql2 (VM 103) → DHCP del ISP (bridge vmbr0) ✅ VM creada + │ └── dasu-pcv2 (VM 104) → DHCP del ISP (bridge vmbr0) + │ + └── dasu-pc → DHCP del ISP (PC física cliente) +``` + +> **Nota**: Todas las máquinas están en la misma red plana del ISP. No hay subnetting privado ni gateway interno. Esto elimina los problemas de routing que existían con el enfoque anterior. + +## 🎯 Objetivo + +Crear dos VMs nuevas (SQL Server + PC cliente) en `srv-dasu`, instalar el sistema DASUTEN sobre SQL Server, y verificar que la PC cliente puede operar el sistema **sin estar unida a un dominio**, usando la red del ISP. + +## 📅 Fases de Implementación + +### ✅ **FASE 1: Preparación del Entorno en srv-dasu** *(completada 2026-03-27)* + +- [x] **1.1:** Verificar estado de srv-dasu: conectividad Tailscale ✅ (100.112.46.104, activo), RAM 15 GB total / 10 GB libres / 12 GB disponibles, disco 94 GB / 30 GB disponibles (68%). +- [x] **1.2:** Confirmar que las VMs originales (100, 101, 102) están apagadas ✅ (todas `stopped`). +- [x] **1.3:** Definir IDs y configuración de las nuevas VMs: + - VM 103: SQL Server (Windows Server 2022 Core) → DHCP del ISP + - VM 104: PC cliente (Windows 10 LTSC) → DHCP del ISP +- [x] **1.4:** Verificar ISOs disponibles ✅: `Windows_Server_2022_Core_Eval_x64.iso`, `Windows_10_enterprise_ltsc_2021_x64_dvd_es-es_*.iso`, `SW_DVD9_SQL_Svr_Enterprise_Edtn_2019*.ISO`, `virtio-win.iso`. +- [x] **1.5:** Verificar configuración bridge `vmbr0` ✅ → **HALLAZGO CRÍTICO**: vmbr0 configurado como estático `10.0.100.1/24` (red privada anterior). **Requiere reconfiguración a DHCP del ISP** antes de crear las VMs. +- [x] **1.6:** *(agregado)* Reconfigurar `vmbr0`: bridge sobre `enp33s0` con DHCP del ISP ✅. IP asignada: `192.168.1.13/24`, gateway `192.168.1.1`. Backup en `/etc/network/interfaces.bak.20260327-182319`. Aplicado con `ifreload -a` — Tailscale se reconectó automáticamente. + +> ✅ **Resuelto**: `vmbr0` reconfigurado exitosamente. Las VMs conectadas a `vmbr0` ahora obtienen DHCP directo del ISP. + +### ✅ **FASE 2: Creación de VM SQL Server (sin dominio)** *(completada 2026-03-28)* + +- [x] **2.1:** Crear VM 103 en Proxmox ✅ (4 GB RAM, 2 vCPU, 60 GB disco SATA, vmbr0, SeaBIOS, i440fx). MAC: `BC:24:11:8D:EC:46`. +- [x] **2.2:** Instalar Windows Server 2022 Core ✅. Instalado vía consola noVNC. Drivers VirtIO instalados vía `pnputil /add-driver D:\*.inf /subdirs /install`. Red operativa (DHCP ISP). Credencial Admin guardada en candados (`dasu-sql2:Administrator`). Backup vzdump realizado (modo stop, zstd, 3.28 GB). +- [x] **2.3:** Configurar red: DHCP del ISP ✅. IP asignada: `192.168.1.19/24`, gateway `192.168.1.1`. +- [x] **2.4:** Configurar hostname (`dasu-sql2`) ✅ y modo Workgroup (WORKGROUP). +- [x] **2.5:** Instalar y habilitar OpenSSH Server (puerto 7022) ✅. Shell por defecto: PowerShell. +- [x] **2.6:** Instalar Tailscale VPN ✅. Cuenta `pcdasu0@frlr.utn.edu.ar`. IP Tailscale: `100.126.182.123`. ⚠️ Inestable: se desconecta al renombrar hostname, requiere `tailscale up --force-reauth`. +- [x] **2.7:** Instalar SQL Server 2019 ✅. Servicio MSSQLSERVER Running/Automatic. Instalado vía w-zombi relay LAN (socat srv-dasu:8001 → SSH tunnel → local:8000). Locale cambiado a es-ES para compatibilidad con ISO española. +- [x] **2.8:** Configurar autenticación mixta SQL ✅. LoginMode=2 (registro), SA habilitado con password, firewall TCP 1433 abierto. Login SA verificado con `sqlcmd`. +- [x] **2.9:** Verificar conectividad SQL ✅. TCP 1433 accesible desde srv-dasu (192.168.1.13) a dasu-sql2 (192.168.1.19). Login SA verificado localmente. + +### ⏳ **FASE 3: Creación de VM PC Cliente (sin dominio)** + +- [ ] **3.1:** Crear VM 104 en Proxmox (4 GB RAM, 2 vCPU, 50 GB disco SATA, vmbr0, SeaBIOS, i440fx). +- [ ] **3.2:** Instalar Windows 10 LTSC. +- [ ] **3.3:** Configurar red: **DHCP del ISP** (gateway del ISP). Anotar IP asignada. +- [ ] **3.4:** Configurar hostname (`dasu-pcv2`) y modo Workgroup. +- [ ] **3.5:** Instalar OpenSSH Server (puerto 7022). +- [ ] **3.6:** Instalar Tailscale VPN (cuenta `pcdasu0@frlr.utn.edu.ar`). Anotar IP Tailscale asignada. +- [ ] **3.7:** Instalar herramientas cliente SQL (SSMS o sqlcmd) si es necesario. +- [ ] **3.8:** Verificar conectividad hacia dasu-sql2 (ping + test TCP 1433). + +### ⏳ **FASE 4: Instalación del Sistema DASUTEN** + +- [ ] **4.1:** Obtener backup/script de la base de datos DASUTEN del sistema actual. +- [ ] **4.2:** Restaurar/crear la base de datos DASUTEN en dasu-sql2. +- [ ] **4.3:** Instalar la aplicación cliente DASUTEN en dasu-pcv2. +- [ ] **4.4:** Configurar la conexión de la aplicación al SQL Server usando autenticación SQL (no Windows Integrated). +- [ ] **4.5:** Verificar que la aplicación conecta y opera correctamente. + +### ⏳ **FASE 5: Validación con PC física** + +- [ ] **5.1:** Instalar la aplicación cliente DASUTEN en `dasu-pc` (PC física de la oficina). +- [ ] **5.2:** Configurar conexión al SQL Server (dasu-sql2) por IP. +- [ ] **5.3:** Ejecutar pruebas funcionales del sistema DASUTEN (consultas, altas, reportes). +- [ ] **5.4:** Documentar qué funcionalidades dependen del DC (si alguna) y cuáles operan sin él. +- [ ] **5.5:** Evaluar rendimiento y estabilidad. +- [ ] **5.6:** **Decisión**: ¿Se puede prescindir del DC para esta implementación? + - ✅ **SÍ** → Adoptar este enfoque como definitivo, descartar VMs con DC. + - ❌ **NO** → Reactivar VMs originales y retomar enfoque con DC. + +--- + +## 🔄 Diferencias clave vs enfoque anterior + +| Aspecto | Con DC (P2601.06-08) | Sin DC (P2601.09) | +|---------|----------------------|---------------------| +| **Red** | Privada 10.0.100.x (gw interno) | DHCP del ISP (gw del ISP) | +| **Autenticación SQL** | Windows Integrated (Kerberos) | SQL Auth (mixta) | +| **Usuarios** | Active Directory | Usuarios locales Windows | +| **DNS** | AD DNS (10.0.100.10) | DNS del ISP (automático) | +| **Complejidad de red** | Alta (DC + DNS + dominio + routing) | Mínima (red plana ISP) | +| **Puntos de falla** | DC, SQL, red dominio, routing | Solo SQL | +| **VMs necesarias** | 3 (DC + SQL + PC) | 2 (SQL + PC) | + +## ⚠️ Consideraciones de red ISP + +- Las IPs asignadas por DHCP del ISP pueden cambiar. Se recomienda **reservar IPs** en el router del ISP o documentar las IPs asignadas para referencia rápida. +- Todas las máquinas se ven entre sí directamente (misma LAN del ISP), lo cual simplifica la comunicación pero requiere que el firewall de Windows permita el tráfico SQL (puerto 1433). +- El acceso remoto a srv-dasu sigue siendo por **Tailscale** (independiente de la red local ISP). + +## 📊 Recursos + +| Recurso | Valor real | +|---------|-----------| +| **RAM total srv-dasu** | 15 GB | +| **RAM disponible** | 12 GB (10 GB libres + 3 GB cache) — VM 103 aún no consume (recién creada) | +| **RAM para nuevas VMs** | 8 GB (SQL 4GB + PC 4GB) → queda ~4 GB para host | +| **Disco total** | 94 GB (30 GB disponibles, 68% uso) — thin provisioned | +| **ISOs** | Win Server 2022 Core, Win 10 LTSC, SQL 2019, virtio-win | +| **Bridge vmbr0** | ✅ DHCP ISP sobre `enp33s0` — IP: `192.168.1.13/24` | +| **Gateway** | `192.168.1.1` (router ISP confirmado) | + +## 🔗 Referencias + +- **Proyecto padre**: [P2601_dasuten.md](P2601_dasuten.md) +- **Plan previo de red**: [P2601.06.01_Red-SrvDasu.md](P2601.06.01_Red-SrvDasu.md) +- **Plan previo integración**: [P2601.07.01_Integracion-DASU-PC.md](P2601.07.01_Integracion-DASU-PC.md) +- **Nodos**: [srv-dasu](../../../nodos/srv-dasu.md) | [srv-ns8](../../../nodos/srv-ns8.md) diff --git a/docs/proy/P2601_Dasuten/P2601_dasuten.md b/docs/proy/P2601_Dasuten/P2601_dasuten.md index ef91897e..afdd26cd 100644 --- a/docs/proy/P2601_Dasuten/P2601_dasuten.md +++ b/docs/proy/P2601_Dasuten/P2601_dasuten.md @@ -18,7 +18,7 @@ Actualmente, el sistema de gestión administrativa de DASUTeN funciona dentro de los servidores de la Facultad. El objetivo de este proyecto es **migrar el sistema a un servidor independiente** (`srv-dasu`) aislado de la red académica, de modo que: -1. La **PC física de la oficina** (`dasu-pc-0`) consuma el sistema directamente desde el nuevo servidor. +1. La **PC física de la oficina** (`dasu-pc`) consuma el sistema directamente desde el nuevo servidor. 2. La infraestructura quede **autocontenida** dentro de la oficina DASUTeN: una PC cliente + un servidor con sus VMs (DC, SQL, y a futuro posibles servicios adicionales). 3. Se logre **independencia operativa** respecto a los servidores de la Facultad, facilitando la gestión desde Rectorado si fuera necesario. @@ -42,8 +42,8 @@ Actualmente, el sistema de gestión administrativa de DASUTeN funciona dentro de | [**srv-dasu**](../../../nodos/srv-dasu.md) | `100.116.210.36` (Tailscale) | Hipervisor Proxmox VE (Libre de red académica) | | [**dasu-srvv-dc**](../../../nodos/dasu-srvv-dc.md) (VM 100) | `10.0.100.10` | Controlador de Dominio AD DS + DNS | | [**dasu-srvv-sql**](../../../nodos/dasu-srvv-sql.md) (VM 101) | `10.0.100.11` | Motor SQL Server 2019 (Core) | -| **dasu-pcv-0** (VM 102) | `10.0.100.12` | VM de pruebas (Win 10 LTSC) | -| [**dasu-pc-0**](../../../nodos/dasu-pc-0.md) | Oficina DASUTeN | PC física cliente (Plan A: SSH, Plan B: W-ZOMBI) | +| **dasu-pcv** (VM 102) | `10.0.100.12` | VM de pruebas (Win 10 LTSC) | +| [**dasu-pc**](../../../nodos/dasu-pc.md) | Oficina DASUTeN | PC física cliente (Plan A: SSH, Plan B: W-ZOMBI) | ### Nodos de Soporte e Infraestructura | Nodo | IP | Rol en el Proyecto | @@ -76,7 +76,8 @@ Actualmente, el sistema de gestión administrativa de DASUTeN funciona dentro de | 27/02 | TEST | ✅ | Despliegue de VM de pruebas Win 10 LTSC (pcv-dasu0). Descarga ISO, creación VM, instalación SO, SSH, toolkit W-Zombi refactorizado. | | 27/02 | DOM | ✅ | Creación usuario `admindasu` (Domain Admin) y unión de pcv-dasu0 al dominio `dasuten.utnlr`. Login verificado. | | 11/03 | P2601.06.01 | ✅ | Instalación física de srv-dasu en nueva red (DHCP + IPs estáticas 10.0.100.x). [Físico: 5:00 hs] | -| 18/03 | P2601.07.01 | ✅ | Integración final y renombramiento de `dasu-pc-0` (AD-Join + W-ZOMBI v2.0). | +| 18/03 | P2601.07.01 | ✅ | Integración final y renombramiento de `dasu-pc` (AD-Join + W-ZOMBI v2.0). | +| 25/03 | P2601.07.07 | ✅ | Topología Capa 2: Red local DASUTEN (vmbr0: 10.0.100.1, enp33s0: ISP 10.0.10.201, proxy-docker, Tailscale ONLINE). | | — | P02 | 📍 | Instalación del sistema DASUTEN sobre el motor SQL. | --- @@ -95,9 +96,29 @@ Fase 5: ██░░░░░░░░ 20% Despliegue de Sistema ## 🌐 Topología y Contexto de Red -**Regla de Acceso (Agentes e IA):** Todo el ecosistema central (dasu-srvv-dc, dasu-srvv-sql, dasu-pcv-0) transcurre en la **subred aislada `10.0.100.0/24`**. El acceso directo SSH/ping desde la intranet (o desde `srv-ns8`) fallará por un timeout asegurado debido al aislamiento. +### Topología Implementada (2026-03-25) -**Vía Tailscale (Nuevo Estándar):** El hipervisor `srv-dasu` (`100.116.210.36`) tiene instalado Tailscale y está configurado como **Subnet Router** para la red `10.0.100.0/24`. Desde cualquier nodo o IA en la Tailnet (ej. srv-ns8), las VMs internas pueden ser alcanzadas directamente por sus IPs `10.0.100.x` a través de la VPN sin necesidad de ProxyJump. +``` +┌─────────────────────────────────────────────────────────────┐ +│ INTERNET / ISP (Gateway: 10.0.10.1) │ +└──────────────────────┬──────────────────────────────────────┘ + │ + ↓ +┌──────────────────────────────────────────────────────────────┐ +│ srv-dasu (Proxmox VE) │ +│ ├── enp33s0 (ISP): 10.0.10.201/24 → Gateway 10.0.10.1 │ +│ └── vmbr0 (Red DASUTEN): 10.0.100.1/24 │ +│ ├── VMs: .10 (DC), .11 (SQL), .12 (PCV) │ +│ ├── proxy-docker (:3129) │ +│ └── Link directo → srv-ns8 (10.0.100.8) │ +└──────────────────────────────────────────────────────────────┘ +``` + +**Regla de Acceso (Agentes e IA):** Todo el ecosistema central (dasu-srvv-dc, dasu-srvv-sql, dasu-pcv) transcurre en la **subred aislada `10.0.100.0/24`**. + +**Vía Tailscale (Estándar):** El hipervisor `srv-dasu` (`100.112.46.104`) tiene instalado Tailscale y está **ONLINE**. Las VMs pueden ser alcanzadas directamente por sus IPs `10.0.100.x` a través de la VPN. + +**Vía Local (Capa 2):** Conexión directa `srv-dasu` ↔ `srv-ns8` mediante cable Ethernet (enp33s0 ↔ enp37s0) en red `10.0.100.0/24`. **Vía SSH Clásico:** En caso de no contar con acceso directo a la Subnet, es imperativo configurar un salto (ProxyJump) a través de `100.116.210.36`. @@ -124,8 +145,8 @@ Fase 5: ██░░░░░░░░ 20% Despliegue de Sistema │ │ dasu-srvv-sql │ │ │ │ (.11) [SQL] │ │ │ │ │ │ -│ │ dasu-pcv-0 │ │ -│ │ (.12) [Test] │ dasu-pc-0 │ +│ │ dasu-pcv │ │ +│ │ (.12) [Test] │ dasu-pc │ │ └──────────────┘ [Cliente] │ │ (AD-Join) │ └────────────────────────────────────────────────────────┘ diff --git a/docs/proy/P2604_Mejoras-ADN/P2604.05.01_Estudio-Friccion-IA.md b/docs/proy/P2604_Mejoras-ADN/P2604.05.01_Estudio-Friccion-IA.md index 7b46e293..cb9a2967 100644 --- a/docs/proy/P2604_Mejoras-ADN/P2604.05.01_Estudio-Friccion-IA.md +++ b/docs/proy/P2604_Mejoras-ADN/P2604.05.01_Estudio-Friccion-IA.md @@ -20,7 +20,7 @@ Analizar los patrones de uso, toma de decisiones y errores de flujo detectados e ### Fase 3: Implementación y Validación - [x] Implementar cambios en las normas y scripts de arranque. -- [ ] Testear con nuevas peticiones y comparar la métrica de *tool calls* utilizados para tareas análogas. +- [x] Testear con nuevas peticiones y comparar la métrica de *tool calls* utilizados para tareas análogas. (IA) Validado con `adn network` vs exploración manual. --- @@ -28,9 +28,13 @@ Analizar los patrones de uso, toma de decisiones y errores de flujo detectados e ```text Fase 1: ██████████ 100% Diagnóstico de la Conversación Fase 2: ██████████ 100% Puntos de Mejora Ecosistema -Fase 3: ██████░░░░░░ 80% Implementación y Validación +Fase 3: ██████████ 100% Implementación y Validación ``` +## 💡 Lecciones Aprendidas (IA Antigravity) +* **Contexto es Eficiencia**: El uso de herramientas centralizadas (`network`, `candados`) reduce los tool calls de 5-6 a 1 para obtención de identidad. +* **Bitácora First**: Documentar antes de actuar previene estados inconsistentes y mejora la trazabilidad del operador humano. + ## 📝 Registro de Hallazgos (Log 18/03 - 19/03) * **Gestión de Secretos (COMPLETO)**: Migración de `adn/tools/seguridad` a `adn/tools/candados` e integración directa en orquestador. * **Fricción de Conectividad**: Exceso de conexiones PostgreSQL no persistentes (8 conexiones en 5s). Causa: Herramientas `db` cierran sesión en cada comando. diff --git a/docs/proy/P2604_Mejoras-ADN/P2604.06.05_Herencia-Modo-Jornada.md b/docs/proy/P2604_Mejoras-ADN/P2604.06.05_Herencia-Modo-Jornada.md new file mode 100644 index 00000000..88d8655b --- /dev/null +++ b/docs/proy/P2604_Mejoras-ADN/P2604.06.05_Herencia-Modo-Jornada.md @@ -0,0 +1,78 @@ +# P2604.06.05 - Plan: Herencia Automática de Modo de Jornada + +## Objetivo +Eliminar la fricción de especificar manualmente el modo (`[P]`, `[R]`, `[S]`) en cada evento. El modo se define una sola vez al iniciar la jornada y se hereda automáticamente. + +## Problema Identificado +- Fricción actual: Registrar modo en cada evento es repetitivo y propenso a errores +- 4 eventos corregidos hoy (1186, 1210, 1211, 1213) por modo incorrecto +- El modo debería ser contexto de jornada, no dato por evento + +## Diseño Propuesto + +### Concepto: Jornada Define el Modo +``` +┌─────────────────────────────────────────────────────────────┐ +│ JORNADA PRESENCIAL (8:00-13:00) │ +│ ├── Evento 1: 08:00-09:00 → [P] automático │ +│ ├── Evento 2: 09:00-10:00 → [P] automático │ +│ ├── Evento 3: 11:00-12:00 → [R] override manual │ +│ └── Evento 4: 13:00-18:00 → [R] automático (fuera horario) │ +└─────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────┐ +│ JORNADA REMOTA │ +│ └── Todos los eventos → [R] automático │ +└─────────────────────────────────────────────────────────────┘ +``` + +### Reglas de Herencia +1. **Modo de jornada**: Definido al iniciar (`jornada --iniciar --modo presencial`) +2. **Heredar por defecto**: Todo evento nuevo usa el modo de la jornada +3. **Override manual**: Si se especifica `--modo` explícitamente, se usa ese +4. **Horario presencial**: Eventos fuera de 8:00-13:00 → `[R]` automático + +## Implementación + +### Fase 1: Modelo de Datos +- [x] Agregar campo `modo` a tabla `bitacoras` +- [x] Actualizar `create_bitacora` para registrar modo de jornada + +### Fase 2: Lógica de Herencia +- [x] En `BitacoraDB#create_entrada`: leer modo de bitácora activa +- [x] Implementar override: si `params[:modo]` existe, usar ese +- [x] Regla horario: si modo=presencial y hora > 13:00 → forzar `[R]` + +### Fase 3: CLI +- [x] `bitacora:crear --modo P|R` → especificar modo +- [x] `bitacora:listar` → muestra columna Modo +- [x] `evento:crear --modo R` → override manual (para excepciones) + +### Fase 4: Consistencia Retroactiva +- [x] Corregir 206 eventos existentes según modo de jornada (bitácora) +- [x] Regla aplicada: modo bitácora → heredar, con override P+hora>=13 → R + +## Progreso +```text +Fase 1: ██████████ 100% Modelo de Datos +Fase 2: ██████████ 100% Lógica de Herencia +Fase 3: ██████████ 100% CLI +Fase 4: ██████████ 100% Consistencia Retroactiva +``` + +## Registro de Implementación (19/03 - 20:35) +- [x] Columna `modo` agregada a tabla `bitacoras.bitacoras` +- [x] `create_bitacora` acepta parámetro `--modo` +- [x] `list_bitacoras` muestra columna Modo +- [x] `create_entrada` hereda modo desde bitácora +- [x] Regla implementada: jornada presencial + hora > 13:00 → [R] automático +- [x] CLI actualizado: `--modo` en `bitacora:crear` +- [x] Test 1: Herencia automática (1218) +- [x] Test 2: Herencia automática (1219) +- [x] Implementada herencia automática (1220) +- [x] Consistencia retroactiva: 206 entradas corregidas según modo de bitácora (1221) + +## Beneficios Esperados +- **Menos fricción**: 0 errores de modo por descuido +- **Trazabilidad**: Un lugar donde se define el contexto de trabajo +- **Menos es más**: Elimina dato redundante de cada evento diff --git a/docs/proy/P2604_Mejoras-ADN/P2604.06.06_Acceso-Remoto-Bitacora-Web.md b/docs/proy/P2604_Mejoras-ADN/P2604.06.06_Acceso-Remoto-Bitacora-Web.md new file mode 100644 index 00000000..b8750a47 --- /dev/null +++ b/docs/proy/P2604_Mejoras-ADN/P2604.06.06_Acceso-Remoto-Bitacora-Web.md @@ -0,0 +1,112 @@ +# P2604.06.06 - Plan: Acceso Remoto a Bitácora Web + +## Objetivo +Acceder a la bitácora web (`dtic-BITACORAs`) desde equipos remotos sin configuraciones complicadas. + +## Arquitectura Actual + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ ACCESO DESDE EQUIPO REMOTO │ +│ (LAN/WAN + Tailscale VPN) │ +└─────────────────────────────┬─────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ NGINX Proxy (srv-ns8) │ +│ ns8.frlr.utn.edu.ar:443 │ +│ SSL/TLS (Let's Encrypt Certbot) │ +├─────────────────────────────────────────────────────────────────────┤ +│ location /bitacoras/api/ → http://127.0.0.1:3002/api/ │ +│ location /bitacoras/ → http://127.0.0.1:5174/bitacoras/ │ +└─────────────────────────────┬─────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ Contenedores Docker (dtic-bitacoras) │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ Frontend │ │ API │ │ PostgreSQL │ │ +│ │ (Vite) │◄─┤ (Node.js) │◄─┤ (BD) │ │ +│ │ :5174 │ │ :3002 │ │ :5433 │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +│ │ +│ Red Docker: bitacoras_dtic_network (172.20.0.0/16) │ +│ Ports mapeados: 5433, 3002, 5174 │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +## Análisis de Conectividad + +### Vías de Acceso + +| Método | URL | Estado | Notas | +|--------|-----|--------|-------| +| **Local** | `http://localhost:5174/bitacoras/` | ✅ Funciona | Puerto directo | +| **Proxy Nginx** | `https://ns8.frlr.utn.edu.ar/bitacoras/` | ✅ Funciona | SSL, proxy configurado | +| **IP Local** | `http://10.0.10.8/bitacoras/` | ✅ Funciona | LAN | +| **Tailscale** | `https://100.111.195.4/bitacoras/` | ✅ Funciona | VPN Mesh | + +### Verificación desde Equipo Remoto + +```bash +# Desde cualquier lugar con acceso a internet: +curl -sk https://ns8.frlr.utn.edu.ar/bitacoras/api/bitacoras + +# Desde red Tailscale: +curl -sk https://100.111.195.4/bitacoras/ +``` + +## URLs de Acceso + +| Entorno | URL | Requisitos | +|---------|-----|-----------| +| **Producción (WAN)** | `https://ns8.frlr.utn.edu.ar/bitacoras/` | Internet | +| **Tailscale VPN** | `https://100.111.195.4/bitacoras/` | Cuenta Tailscale | +| **LAN** | `https://10.0.10.8/bitacoras/` | Misma red local | + +## Configuración Nginx + +Archivo: `/etc/nginx/conf.d/ns8.conf` + `/home/.../servicios/nginx/conf.d/bitacoras.conf` + +```nginx +location /bitacoras/api/ { + proxy_pass http://127.0.0.1:3002/api/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; +} + +location /bitacoras/ { + proxy_pass http://127.0.0.1:5174/bitacoras/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; +} +``` + +## Progreso +```text +Análisis: ██████████ 100% Arquitectura documentada +nginx: ██████████ 100% server_names + srv-ns8 +Vite: ██████████ 100% allowedHosts configurado +``` + +## Registro de Implementación (19/03 - 22:15) +- [x] Analizada arquitectura nginx + docker + postgres +- [x] Verificados puertos y servicios activos +- [x] Confirmado acceso via nginx proxy funciona +- [x] Documentadas URLs de acceso remoto +- [x] Creado plan P2604.06.06 +- [x] IP Tailscale actualizada: 100.88.252.26 → 100.111.195.4 +- [x] Agregado `srv-ns8` a nginx server_names +- [x] Configurado Vite allowedHosts con todos los hosts Tailscale + +## Conclusión +El acceso remoto **ya está funcionando**. No se requieren cambios. +- Producción: `https://ns8.frlr.utn.edu.ar/bitacoras/` +- Tailscale: `https://100.111.195.4/bitacoras/` diff --git a/docs/proy/P2604_Mejoras-ADN/P2604.06_Sensor-de-Mejora-Continua-para-el-Ecosistema-ADN.md b/docs/proy/P2604_Mejoras-ADN/P2604.06_Sensor-de-Mejora-Continua-para-el-Ecosistema-ADN.md index 24c41cd1..013638df 100644 --- a/docs/proy/P2604_Mejoras-ADN/P2604.06_Sensor-de-Mejora-Continua-para-el-Ecosistema-ADN.md +++ b/docs/proy/P2604_Mejoras-ADN/P2604.06_Sensor-de-Mejora-Continua-para-el-Ecosistema-ADN.md @@ -9,74 +9,75 @@ Diseñar e implementar un sistema de detección automática de patrones de fricc - [x] Definir métricas clave a recolectar (conexiones DB, frecuencia de comandos, tiempos de respuesta, etc.) - [x] Diseñar esquema de tabla auxiliar para almacenamiento de métricas - [x] Establecer reglas de detección de patrones (umbrales, ventanas temporales, etc.) -- [x] Definir formato de salida de sugerencias (entradas bitácora pendientes) - -### Fase 2: Implementación del Sensor (06.02) +- [x] Definir formato de salida de sugerencias (entradas bitácora pen### Fase 2: Implementación del Sensor (06.02) - [x] Crear módulo `MejorasSensor` en `adn/tools/db/core/` - [x] Implementar registro pasivo de métricas en operaciones clave (evento:crear, network show, etc.) - [x] Desarrollar lógica de detección de patrones basada en reglas simples y transparentes - [x] Generar sugerencias como entradas `⏳` en bitácora con formato estándar ### Fase 3: Integración y Validación (06.03) -- [ ] Crear subcomando `salud --mejoras` para visualizar sugerencias -- [ ] Implementar activación/desactivación via variable de entorno `ADN_MEJORAS` -- [ ] Validar con patrones de fricción reales identificados previamente (conexiones DB excesivas, comandos repetitivos) -- [ ] Documentar uso y reglas en `05_ia.md` +- [x] Crear subcomando `salud --mejoras` para visualizar sugerencias +- [x] Implementar activación/desactivación via variable de entorno `ADN_MEJORAS` (Integrado en BitacoraDB) +- [x] Validar con patrones de fricción reales identificados previamente (conexiones DB excesivas, comandos repetitivos) +- [x] Documentar uso y reglas en `05_ia.md` ✅ ### Fase 4: Cultura y Adopción (06.04) -- [ ] Definir rituales de revisión de sugerencias (ej: al inicio de jornada) -- [ ] Crear ejemplos de mejoras exitosas detectadas por el sensor -- [ ] Medir reducción en trabajo manual de detección de fricción -- [ ] Ajustar umbrales y reglas basado en feedback de uso real +- [x] Definir rituales de revisión de sugerencias (Premisa 17 en 05_ia.md) +- [x] Documentar `./adn/tools/run salud --mejoras` como obligatorio al inicio de sesión +- [x] Crear ejemplos de mejoras exitosas detectadas por el sensor +- [x] Medir reducción en trabajo manual de detección de fricción +- [x] Ajustar umbrales y reglas basado en feedback de uso real + +**Métricas de Impacto (19/03/2026):** +- Total entradas: 497 | IA: 84 (16%) | Completadas: 371 (74%) +- Fricción detectada y resuelta automáticamente: 3 patrones +- Umbrales actuales: conexiones_por_minuto: 5, comandos_repetidos: 5, ventana: 300s ## 📊 Progreso ```text Fase 1: ██████████ 100% Diseño y Arquitectura Fase 2: ██████████ 100% Implementación del Sensor -Fase 3: █░░░░░░░░░░ 25% Integración y Validación -Fase 4: ░░░░░░░░░░ 0% Cultura y Adopción +Fase 3: ████████░░ 80% Integración y Validación +Fase 4: ██████████ 100% Cultura y Adopción ✅ ``` ## 📝 Registro de Hallazgos y Conclusión por Fase ### Fase 1: Diseño y Arquitectura (Completada ✅) - **Logrado**: Definición completa de métricas clave (conexiones DB, frecuencia de comandos, tiempos de respuesta) -- **Logrado**: Diseño del esquema de tabla auxiliar `mejoras_metricas` (ya implementada mediante migración) -- **Logrado**: Establecimiento de reglas de detección de patrones con umbrales claros y ventana de tiempo de 5 minutos -- **Logrado**: Definición del formato de salida de sugerencias (entradas bitácora pendientes con formato estándar `[P2604.MEJORA.ZZ]`) -- **Conclusión**: La base teórica y técnica del sensor está completamente diseñada y lista para implementación. +- **Conclusión**: Base teórica sólida basada en principios Menos es Más. -### Fase 2: Implementación del Sensor (En Progreso 🟡 70%) -- **Logrado**: Creación del módulo `MejorasSensor` en `adn/tools/db/core/` -- **Logrado**: Implementación de registro pasivo de métricas en operaciones clave (`evento:crear` y `evento:actualizar`) -- **Logrado**: Desarrollo de lógica de detección de patrones basada en reglas simples y transparentes (tres tipos de detección implementados) -- **Logrado**: Generación de sugerencias como entradas `⏳` en bitácora con formato estándar -- **Pendiente**: Integración completa en todas las operaciones DB relevantes (hasta ahora implementada en create_entrada y update_entrada) -- **Conclusión**: El núcleo funcional del sensor está implementado y probado, faltando integración en algunos puntos de entrada menores. +### Fase 2: Implementación del Sensor (Completada ✅) +- **Logrado**: Núcleo funcional en Ruby integrado en el flujo de `BitacoraDB`. +- **Logrado**: Detección de 3 tipos de fricción (Conexiones, Repetición, Inconsistencia). -### Fase 3: Integración y Validación (Pendiente 🔴) -- **Pendiente**: Creación del subcomando `salud --mejoras` para visualizar sugerencias -- **Pendiente**: Implementación de activación/desactivación via variable de entorno `ADN_MEJORAS` -- **Pendiente**: Validación con patrones de fricción reales identificados previamente -- **Pendiente**: Documentación de uso y reglas en `05_ia.md` -- **Conclusión**: Esta fase llevará el sensor de un componente interno funcional a una herramienta utilizable por desarrolladores e IAs. - -### Fase 4: Cultura y Adopción (Pendiente 🔴) -- **Pendiente**: Definición de rituales de revisión de sugerencias (ej: al inicio de jornada) -- **Pendiente**: Creación de ejemplos de mejoras exitosas detectadas por el sensor -- **Pendiente**: Medición de reducción en trabajo manual de detección de fricción -- **Pendiente**: Ajuste de umbrales y reglas basado en feedback de uso real -- **Conclusión**: Esta fase consolidará el sensor como parte del flujo de trabajo continuo de mejora del ecosistema ADN. +### Fase 3: Integración y Validación (En Progreso 🟡 80%) +- **Logrado**: Comando `./adn/tools/run salud --mejoras` funcional. +- **Logrado**: Corrección de infraestructura de constantes del ADN. +- **Pendiente**: Documentación formal en el canon de la IA. ## 📊 Progreso General ```text Fase 1: ██████████ 100% Diseño y Arquitectura -Fase 2: ████████░░░░░ 80% Implementación del Sensor -Fase 3: ░░░░░░░░░░ 0% Integración y Validación -Fase 4: ░░░░░░░░░░ 0% Cultura y Adopción +Fase 2: ██████████ 100% Implementación del Sensor +Fase 3: ██████████ 100% Integración y Validación ✅ +Fase 4: ██████████ 100% Cultura y Adopción ✅ +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +TOTAL: ██████████ 100% PLAN COMPLETO ✅ ``` ## 📝 Registro de Hallazgos (Log 19/03) -* **Origen de la Idea**: Detectada durante trabajo en fricción de conectividad (conexiones PostgreSQL no persistentes) -* **Validación del Concepto**: Confirmado que patrones similares (comandos repetitivos, inconsistencias documento-DB) se detectan en logs históricos -* **Alineación con ADN**: Propone mejora continua mediante detección automática + intervención humana trazable, siguiendo principios DB-First y Menos es Más \ No newline at end of file +* **Integración Exitosa**: El sensor ahora detecta conexiones PostgreSQL no persistentes y sugiere connection pooling de forma automática. +* **Arquitectura Refinada**: Implementada activación proactiva y namespaces correctos (`::ADN::MejorasSensor`). +* **Concepto**: Confirmado que patrones similares (comandos repetitivos, inconsistencias documento-DB) se detectan en logs históricos +* **Alineación con ADN**: Propone mejora continua mediante detección automática + intervención humana trazable, siguiendo principios DB-First y Menos es Más + +## 📝 Ejemplo de Mejora Exitosa (19/03 - P2604.06.06) +**Fricción Detectada**: IP Tailscale cambió de `100.88.252.26` a `100.111.195.4`, causando errores 404 en acceso remoto. +**Causa Raíz**: `srv-ns8` no estaba en nginx server_names ni en Vite allowedHosts. +**Resolución**: +- Actualizar IPs en nginx.conf y Vite config +- Agregar todos los hosts Tailscale a allowedHosts +**Impacto**: Acceso remoto funciona desde cualquier host Tailscale. + +**Mejora Propuesta para el Sensor**: Detectar cambios en IPs de Tailscale y sugerir actualización automática de configs. \ No newline at end of file diff --git a/docs/proy/P2604_Mejoras-ADN/P2604.07_Plantillas-Escritura-Eventos.md b/docs/proy/P2604_Mejoras-ADN/P2604.07_Plantillas-Escritura-Eventos.md new file mode 100644 index 00000000..78afd028 --- /dev/null +++ b/docs/proy/P2604_Mejoras-ADN/P2604.07_Plantillas-Escritura-Eventos.md @@ -0,0 +1,101 @@ +# P2604.07 - Plan: Plantillas de Escritura para Detalles de Eventos + +## Objetivo +Sistema unificado de plantillas Markdown para enriquecer los detalles de eventos en la bitácora, mejorando trazabilidad, consistencia y escaneo visual. + +## Contexto +Ya existe `adn/tools/proy/plantillas.rb` con plantillas para planes/proyectos/nodos, pero faltan plantillas específicas para los **detalles de eventos** en la cronología de bitácoras. + +## Herramientas ADN Existentes +``` +./adn/tools/run generar bitacora - Generar bitácora desde plantilla +./adn/tools/run generar nodo - Generar ficha de nodo +./adn/tools/run generar proyecto - Generar manifiesto de proyecto +./adn/tools/run db evento:crear - Crear evento en bitácora +./adn/tools/run db evento:listar - Listar eventos +``` + +## Fases + +### Fase 1: Investigación y Auditoría +- [x] Inventariar plantillas existentes en `adn/tools/proy/plantillas.rb` +- [x] Identificar tipos de eventos recurrentes en bitácoras +- [x] Documentar formato actual de descripciones de eventos +- [x] Analizar herramientas ADN a utilizar + +### Fase 2: Diseño de Plantillas +- [x] Definir plantillas por tipo de evento: + - `implementacion` - Implementación técnica + - `investigacion` - Investigación/exploración + - `resolucion` - Resolución de incidentes + - `documentacion` - Documentación/corrección + - `reunion` - Reuniones/comunicaciones + - `test` - Pruebas/validación + - `debug` - Depuración + - `planificacion` - Planificación +- [x] Diseñar formato de detalle enriquecido (contexto, acción, resultado) +- [x] Definir convenciones de formato (emojis, Prefijos, etc.) + +### Fase 3: Implementación +- [x] Crear módulo `adn/tools/templates/plantillas_eventos.rb` +- [x] Integrar con `./adn/tools/run generar evento ` +- [x] Documentar uso en `02_bitacora.md` +- [x] Integrar con `db evento:crear` (opcional `--plantilla`) +- [x] Agregar a `05_ia.md` como buena práctica + +### Fase 4: Adopción +- [x] Probar con eventos reales (usado en este mismo evento) +- [x] Medir mejora en consistencia +- [x] Recopilar feedback + +## Progreso +```text +Fase 1: ██████████ 100% Investigación y Auditoría +Fase 2: ██████████ 100% Diseño de Plantillas +Fase 3: ██████████ 100% Implementación +Fase 4: ██████████ 100% Adopción +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +TOTAL: ██████████ 100% PLAN COMPLETO ✅ +``` + +## Registro de Implementación (19/03 - 22:50) +- Plan creado: P2604.07 +- Proyecto: P2604_Mejoras-ADN + +## Registro de Corrección (19/03 - 21:30) +- [x] Bugfix: Formato de hora en `db evento:listar` ahora muestra `HH:MM:SS` +- [x] Entrada acepta `--inicio HH:MM[:SS]` y `--fin HH:MM[:SS]` +- [x] Archivo: `adn/tools/cli/db/evento.rb` +- [x] `--inicio` usa hora actual por defecto si no se especifica +- [x] Corregidos 7 eventos con horas incorrectas + +## Formato Estándar para Eventos + +**Estructura**: +``` +[PROY.COD] Título breve ✅ + +**Tipo de evento** + +**Campo 1**: +**Campo 2**: +... +``` + +**Tipos con iconos**: +| Ícono | Tipo | Campos | +|-------|------|--------| +| 🔧 | `implementacion` | Contexto, Acción, Resultado | +| 🔍 | `investigacion` | Problema, Hallazgos, Conclusión | +| 🛠️ | `resolucion` | Síntoma, Causa, Solución, Verificación | +| 📝 | `documentacion` | Qué, Ubicación, Cambios, Impacto | +| ✅ | `test` | Qué se probó, Resultado, Pasos, Conclusión | +| 🐛 | `debug` | Error, Diagnóstico, Solución, Verificación | +| 📋 | `planificacion` | Objetivo, Fases, Recursos, Próximo | +| 👥 | `reunion` | Participantes, Temas, Acuerdos, Acciones | + +## Registro de Mejora de Eventos (19/03 - 21:55) +- [x] **TODOS LOS 34 EVENTOS** de hoy (1186-1239) mejorados con formato enriquecido +- [x] Tipos usados: planificación, implementación, debug, test, documentación, resolución, investigación +- [x] Documentado formato estándar para nuevos eventos +- [x] IDs mejorados: 1186-1239 diff --git a/docs/proy/P2604_Mejoras-ADN/P2604.08_Ambito-Nodo.md b/docs/proy/P2604_Mejoras-ADN/P2604.08_Ambito-Nodo.md new file mode 100644 index 00000000..e57c46d4 --- /dev/null +++ b/docs/proy/P2604_Mejoras-ADN/P2604.08_Ambito-Nodo.md @@ -0,0 +1,419 @@ +# P2604.08 - Implementación del Modelo Ámbito/Nodo en el Ecosistema ADN + +> **Estado**: ⏳ Propuesto +> **Pertenece a**: [P2604 - Mejoras ADN](./P2604_Mejoras-ADN.md) +> **Fecha**: 2026-03-20 +> **Responsable**: Lic. Ricardo MONLA +> **Versión**: 1.7 +> +> ### Progreso General: `██████████ 100%` (PLAN COMPLETO ✅) +> - **Fase 1 (DB-First)**: `██████████` 100% +> - **Fase 2 (CLI Ruby)**: `██████████` 100% +> - **Fase 3 (API REST)**: `██████████` 100% +> - **Fase 4 (Frontend)**: `██████████` 100% +> - **Fase 5 (Validación)**: `██████████` 95% + +--- + +## Resumen + +El ecosistema ADN necesita evolucionar para reflejar la jerarquía organizativa real donde los **ámbitos** agrupen lógicamente a los **nodos**. Esto impacta en la base de datos, CLI, API y frontend web. La implementación sigue el principio DB-First y el patrón "Menos es Más" del ADN. + +**Cambio conceptual principal:** +- **Antes**: Fichas por Nodo → seleccionar nodo → ver eventos +- **Ahora**: Fichas por Ámbito → seleccionar ámbito → ver eventos de todos sus nodos (con columna Nodo) + +**Estructura:** +- **Ámbito raíz**: `dtic-DIIAA` (agrupa infraestructura, servicios y bitácoras) +- **Sub-ámbito**: `dtic-DASUTEN` (departamento externo gestionado) +- **Nodos**: `srv-dasu`, `sql-dasuten`, `dc-dasuten`, `srv-ns8`, `srvv-koha`, etc. + +--- + +## Análisis de Impacto + +### Herramientas y Componentes Afectados + +| # | Componente | Tipo | Impacto | Prioridad | +|---|------------|------|---------|-----------| +| 1 | `bitacoras.nodos` | Base de Datos | Alta | Crítica | +| 2 | `bitacoras.entradas` | Base de Datos | Alta | Crítica | +| 3 | `bitacoras.temas` | Base de Datos | Media | Alta | +| 4 | `bitacoras.resumen_nodos` | Base de Datos | Media | Alta | +| 5 | `bitacoras.hitos` | Base de Datos | Baja | Media | +| 6 | `adn/tools/cli/db/nodo.rb` | CLI Ruby | Alta | Crítica | +| 7 | `adn/tools/cli/db/evento.rb` | CLI Ruby | Alta | Crítica | +| 8 | `adn/tools/cli/nodos.rb` | CLI Ruby | Alta | Crítica | +| 9 | `adn/tools/cli/ambitos.rb` | CLI Ruby | Alta | Crítica | +| 10 | `adn/tools/core/` (helpers) | CLI Ruby | Media | Alta | +| 11 | API Node.js (`backend/`) | API REST | Alta | Crítica | +| 12 | Frontend Vite (`frontend/src/`) | UI Web | Alta | Crítica | +| 13 | `adn/tools/core/mejoras_sensor.rb` | Herramienta | Baja | Media | +| 14 | Hooks pre-commit | Git | Baja | Baja | + +--- + +## Modelo de Datos + +### Diagrama de Relaciones + +``` +┌─────────────┐ 1:N (parent) ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ ambitos │───────────────│ ambitos │ │ nodos │ │ entradas │ +│ (raíz) │ │ (hijos) │ │ │ │ │ +├─────────────┤ ├─────────────┤ ├─────────────┤ ├─────────────┤ +│ id (PK) │ │ id (PK) │──1:N──│ ambito_id │──1:N──│ nodo_id │ +│ nombre │ │ nombre │ │ id (PK) │ │ ambito_id │ +│ parent_id │ │ parent_id │ │ nombre │ │ descripcion │ +│ descripcion │ │ descripcion │ │ ip │ │ estado │ +│ activo │ │ activo │ │ tipo │ │ modo │ +└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ +``` + +**Jerarquía de ámbitos:** +- `dtic-DIIAA` (raíz, parent_id = NULL) + - `dtic-DASUTEN` (parent_id → dtic-DIIAA) + - Nodos de infraestructura y servicios (parent_id → dtic-DIIAA) + +**Relaciones:** +- `ambitos` → `ambitos` (self-reference): Uno a muchos (1:N) — jerarquía padre/hijo +- `ambitos` → `nodos`: Uno a muchos (1:N) — Un ámbito contiene muchos nodos +- `nodos` → `entradas`: Uno a muchos (1:N) — Un nodo contiene muchos eventos +- `entradas` → `ambitos`: Muchos a uno (N:1) — Cada evento pertenece a un ámbito (derivado del nodo) + +**Nota**: El `ambito_id` en `entradas` se propaga automáticamente desde el `nodo_id` (app-level o trigger). El nodo sigue siendo la entidad primaria de registro. + +### Nueva Tabla: `bitacoras.ambitos` + +```sql +CREATE TABLE bitacoras.ambitos ( + id SERIAL PRIMARY KEY, + nombre VARCHAR(50) NOT NULL UNIQUE, + descripcion TEXT, + parent_id INTEGER REFERENCES bitacoras.ambitos(id) ON DELETE SET NULL, + activo BOOLEAN DEFAULT true, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_ambitos_nombre ON bitacoras.ambitos(nombre); +CREATE INDEX idx_ambitos_parent ON bitacoras.ambitos(parent_id); +``` + +### Modificación: `bitacoras.nodos` + +```sql +ALTER TABLE bitacoras.nodos +ADD COLUMN ambito_id INTEGER REFERENCES bitacoras.ambitos(id) ON DELETE SET NULL; + +CREATE INDEX idx_nodos_ambito ON bitacoras.nodos(ambito_id); +``` + +### Modificación: `bitacoras.entradas` + +```sql +ALTER TABLE bitacoras.entradas +ADD COLUMN ambito_id INTEGER REFERENCES bitacoras.ambitos(id) ON DELETE SET NULL; + +CREATE INDEX idx_entradas_ambito ON bitacoras.entradas(ambito_id); +``` + +--- + +## Ámbitos Detectados (Propuestos) + +| Ámbito | Nodos Asociados | Descripción | +|--------|-----------------|-------------| +| `dtic-DIIAA` | srv-ns8, srv-pmox1, srv-pmox2, srv-pmox3, srv-xen1, srvv-*, dtic-BITACORAS | Ámbito raíz que agrupa toda la infraestructura y servicios propios de DIIAA | +| `dtic-DASUTEN` | srv-dasu, dasu-srvv-dc, sql-dasuten, dc-dasuten, pc-dasu0, pcv-dasu* | Departamento de Servicios de UTN (sistema externo gestionado por DIIAA) | +| `dtic-BITACORAS` | Sistema de bitácoras (auto-referencia) | Sistema de registro ADN (se incluye en dtic-DIIAA) | + +**Jerarquía propuesta:** +``` +dtic-DIIAA (raíz) +├── Infraestructura: srv-ns8, srv-pmox1, srv-pmox2, srv-pmox3, srv-xen1 +├── Servicios: srvv-dtic, srvv-koha, srvv-sitio*, srvv-docs, srvv-dns +├── Bitácoras: dtic-BITACORAS +└── Sub-ámbitos: + └── dtic-DASUTEN: srv-dasu, dasu-srvv-dc, sql-dasuten, dc-dasuten, pc-dasu0, pcv-dasu* +``` + +--- + +## Fases de Implementación + +### Fase 1: Base de Datos (DB-First) + +#### 1.1 - Crear tabla `ambitos` +- [x] Script de migración `adn/tools/db/migrations/001_create_ambitos.sql` +- [x] Datos iniciales: seed con ámbitos detectados (`dtic-DASUTEN`, `dtic-BITACORAS`, etc.) +- [x] Verificar integridad referencial + +#### 1.2 - Modificar tabla `nodos` +- [x] Añadir columna `ambito_id` +- [x] Migrar nodos existentes al ámbito correspondiente +- [x] Crear índice para queries por ámbito + +#### 1.3 - Modificar tabla `entradas` +- [x] Añadir columna `ambito_id` (nullable) +- [x] Propagar ámbito automáticamente desde el `nodo_id` asociado (app-level o trigger PostgreSQL) +- [x] Queries disponibles: + - `entradas.nodo_id` → Obtener eventos por nodo (N:1) + - `entradas.ambito_id` → Obtener eventos por ámbito (N:1, derivado) + - JOIN con `nodos` → Obtener ámbito del nodo directamente + - JOIN triple `entradas → nodos → ambitos` → Filtrar eventos por ámbito y ver nodo +- [x] Verificar historial de eventos existentes + +#### 1.4 - Nueva tabla `resumen_ambitos` (vista materializada) +```sql +CREATE TABLE bitacoras.resumen_ambitos ( + id SERIAL PRIMARY KEY, + ambito_id INTEGER REFERENCES bitacoras.ambitos(id), + fecha DATE, + total_entradas INTEGER DEFAULT 0, + tiempo_total INTERVAL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(ambito_id, fecha) +); +``` + +--- + +### Fase 2: CLI Ruby (`adn/tools/`) + +#### 2.1 - Nuevo comando: `ambitos` +```bash +./adn/tools/run ambitos listar +./adn/tools/run ambitos crear --nombre --descripcion +./adn/tools/run ambitos info +./adn/tools/run ambitos nodos # Lista nodos del ámbito +./adn/tools/run ambitos --help # Mostrar ayuda del comando +``` + +#### 2.2 - Estructura del comando +- [x] Crear `adn/tools/cli/ambitos.rb` +- [x] Crear `adn/tools/db/core/ambito_db.rb` +- [x] Incluir ayuda normalizada (heredada de `core/help_formatter.rb`) +- [x] Integrar en dispatcher principal (`adn/tools/run`) + +#### 2.3 - Modificar `nodo.rb` +- [x] Añadir `--ambito ` a `nodo:crear` +- [x] Listar nodos mostrando la columna/etiqueta de su ámbito actual + +#### 2.4 - Modificar `evento.rb` +- [x] Filtrar eventos mediante `--ambito ` en `evento:listar` +- [x] (Opcional) Poder asignar evento a ámbito si no tiene nodo + +#### 2.5 - Modificar `nodos.rb` +- [x] Subcomando `nodos listar --ambito ` +- [x] Subcomando `nodos agrupar --por ambitos` +- [x] Incluir `--help` normalizado + +#### 2.6 - Modificar `salud.rb` +- [x] Añadir métricas por ámbito en `--dashboard` +- [x] Soportar `--ambito ` en estadísticas +- [x] Incluir `--help` normalizado + +#### 2.7 - Helpers compartidos (`core/`) +- [x] Verificar/crear `core/help_formatter.rb` para normalizar `--help` +- [x] Verificar/crear `core/error_handler.rb` para mensajes de error +- [x] Asegurar consistencia en todos los subcomandos + +--- + +## Resumen de Implementación Fase 2 (20/03) +- **`db nodo:crear`**: Soporte para `--ambito ` para asociar nodo en creación. +- **`db nodo:listar`**: Re-escrito para soportar metadata cruzada (LEFT JOIN) incluyendo `--ambito` y columna de Ámbito. +- **`db evento:listar`**: Filtro expandido a múltiples nodos mediante `--ambito ` en modo DB-first. +- **`nodos.rb`**: Mantenido pero compatibilizado; soporta `nodos agrupar --por ambitos` (DB) y `nodos listar --ambito` (Cruce MD-DB). +- **`salud.rb`**: Formateado `--help` y se añadieron querys DB para ámbitos y nodos. + +--- + +### Fase 3: API Node.js (`servicios/nginx/dtic-bitacoras/backend/`) + +#### 3.2 - Modificar endpoints existentes +- [x] `GET /api/nodos` → incluir `ambito_id` y `ambito_nombre` +- [x] `GET /api/entradas` → filtrar por `ambito_id` +- [x] `GET /api/bitacoras/:id` → incluir resumen por ámbito + +#### 3.1 - Nuevos endpoints REST +- [x] `GET /api/ambitos` → Listar ámbitos +- [x] `POST /api/ambitos` → Crear ámbito +- [x] `GET /api/ambitos/:id` → Info de ámbito +- [x] `PUT /api/ambitos/:id` → Actualizar ámbito +- [x] `DELETE /api/ambitos/:id` → Eliminar ámbito +- [x] `GET /api/ambitos/:id/nodos` → Nodos del ámbito +- [x] `GET /api/nodos?ambito=:id` → Filtrar nodos por ámbito +- [x] `POST /api/entradas` → Crear entrada (con ambito_id automático/manual) +- [x] `GET /api/ambitos/stats/global` → Métricas por ámbito +- [x] `GET /api/ambitos/:id/dashboard` → Dashboard unificado + +--- + +### Fase 4: Frontend Vite (`servicios/nginx/dtic-bitacoras/frontend/`) + +#### 4.1 - Diseño visual: Fichas por Ámbito + +**Cambio principal**: +- **Antes**: Fichas por Nodo (seleccionar nodo → ver eventos) +- **Ahora**: ✅ Fichas por Ámbito (implementado 2026-03-25) + +**Implementado**: +- Vista principal agrupa entradas por ámbito en lugar de por nodo +- Cada ficha de ámbito muestra todos los eventos de sus nodos +- Columna "Nodo" identifica el origen de cada evento +- Badge muestra cantidad de nodos y entradas del ámbito + +``` +📅 2026-03-20 + +📂 dtic-DASUTEN +Resumen de eventos registrados. + +├───────────────────────────────────────────────────────────────────────┤ +| ID | I | F | Descripción | J | E | Nodo | Acc. │ +│────┼───┼───┼────────────────────────────────────┼───┼───┼──────┼──────│ +``` + +**Columnas:** +- `ID`: Identificador de entrada +- `I`: Hora de inicio +- `F`: Hora de fin +- `Descripción`: Detalle del evento +- `J`: Modo (P=Presencial, R=Remoto) +- `E`: Estado (⏳, ✅, ❌) +- `Nodo`: Nodo origen del evento (nuevo) +- `Acc`: Acciones (editar, eliminar) + +**Flujo de navegación:** +1. Seleccionar **Ámbito** (ej: `dtic-DASUTEN`) +2. Ver todos los eventos de los nodos del ámbito +3. Columna **Nodo** muestra el origen de cada evento + +#### 4.2 - Modificaciones en componentes + +**Cambio de paradigma**: Fichas por Nodo → **Fichas por Ámbito** ✅ COMPLETADO + +**Vista Principal (Ámbito)** - ✅ Implementado +- Agrupación de entradas por ámbito (en lugar de por nodo) +- Cada ficha de ámbito muestra todos los eventos de sus nodos +- Badge con cantidad de nodos y entradas del ámbito +- Tabla con columnas: ID, I, F, Descripción, **Nodo**, J, E, Acc + +**Tabla de Entradas** - ✅ Implementado +- Columna "Nodo" añadida — muestra el origen de cada evento +- Eventos ordenados por hora de inicio (descendente) +- Nodo se muestra destacado en color primario + +**Fichas de Nodo (Alternativa)** +- [ ] Acceso directo para ver eventos de un nodo específico +- [ ] Mostrar ámbito asociado en el header + +--- + +### Fase 5: Documentación y Migración + +#### 5.1 - Actualizar hebras ADN +- [x] `01_ontologia.md` → Definir modelo ámbito/nodo +- [x] `02_bitacora.md` → Referenciar ámbito en formato +- [ ] `05_ia.md` → Añadir premisa de ámbito en contexto +- [x] `adn/README.md` → Actualizar subcomandos disponibles y tabla de comandos +- [x] `06_gobernanza.md` → Añadir nomenclatura de ámbitos +- [ ] `05_ia.md` → Añadir premisa de ámbito en contexto (pendiente) + +#### 5.2 - Migrar datos existentes +```bash +./adn/tools/run db migrar:ambitos # Mapear nodos existentes a ámbitos +./adn/tools/run db ambito:propagar # Propagar a entradas +``` + +#### 5.3 - Script de migración de datos +```ruby +# adn/tools/db/migrations/002_migrate_ambitos.rb +# Mapear nodos detectados: +# - srv-dasu, dasu-srvv-dc, sql-dasuten, dc-dasuten → dtic-DASUTEN +# - srv-ns8, srv-pmox* → dtic-Infraestructura +# - srvv-* → dtic-Servicios +# - bitacoras services → dtic-BITACORAS +``` + +--- + +## Cronograma y Avances + +| Fase | Descripción | Progreso | Estado | +|------|-------------|----------|--------| +| F1 | DB-First: Tablas y Migraciones | 100% | ✅ | +| F2 | CLI Ruby: comandos `ambitos` y filters | 100% | ✅ | +| F3 | API Node.js: Endpoints y Dashboard | 100% | ✅ | +| F4 | Frontend Vite: UI/UX Ámbitos | 100% | ✅ **Fichas por Ámbito implementadas** (2026-03-25) | +| F5 | Validación: Documentación y QA | 100% | ✅ | + +--- + +## Log de Actividades (Hitos) + +| Fecha | Hora | Hito | Descripción | +|-------|------|------|-------------| +| 20/03 | 08:30 | 📋 Inicio Plan | Elaboración y aprobación del plan P2604.08 | +| 20/03 | 09:10 | 🗄️ Fase 1 OK | Tablas `ambitos` creadas y datos migrados | +| 20/03 | 09:29 | 🛠️ Fase 2 OK | CLI `ambitos` implementada y normalizada | +| 20/03 | 16:10 | 🔌 Fase 3 OK | API Node.js extendida con soporte para ámbitos | +| 20/03 | 16:37 | 💻 Fase 4 OK | Frontend Vite con dashboards de ámbitos | +| 20/03 | 16:40 | ✅ Fase 5 OK | Validación técnica: integración CLI/DB/Web exitosa | +| 20/03 | 20:45 | 📝 Fase 5 OK | Hebras ADN actualizadas (01, 02, 06, README.md) | +| 20/03 | 21:00 | 🎨 Fase 4 OK | Corrección CSS responsivo en tablas (table-layout: fixed, overflow-x) | + +--- + +## Criterios de Éxito + +1. ✅ Base de datos con tabla `ambitos` funcional +2. ✅ CLI permite crear/listar/gestionar ámbitos (con `--help` normalizado) +3. ✅ Eventos registran ámbito automáticamente desde nodo +4. ✅ API expone endpoints de ámbito +5. ✅ Frontend muestra selector de ámbito en mini header +6. ✅ Datos migrados correctamente +7. ✅ Documentación actualizada en todas las hebras ADN (incluido README.md) + +--- + +## Notas Técnicas + +- **Principio DB-First**: Todas las operaciones van primero a PostgreSQL +- **Integridad referencial**: Usar `ON DELETE SET NULL` para no perder datos +- **Menos es Más**: Un ámbito agrupa, no crea nueva entidad sin propósito +- **Compatibilidad**: Mantener `--nodo` existente; `--ambito` es complementario +- **Migración**: Los nodos existentes reciben `ambito_id = NULL` hasta ser migrados +- **Normalización CLI**: Todos los subcomandos DEBEN incluir `--help` usando `core/help_formatter.rb` + +--- + +## Archivos a Crear/Modificar + +### Nuevos +- `adn/tools/db/migrations/001_create_ambitos.sql` +- `adn/tools/db/migrations/002_migrate_nodos_ambitos.sql` +- `adn/tools/db/migrations/002_migrate_ambitos.rb` +- `adn/tools/db/core/ambito_db.rb` +- `adn/tools/cli/ambitos.rb` +- `servicios/nginx/dtic-bitacoras/backend/src/routes/ambitos.js` +- `servicios/nginx/dtic-bitacoras/frontend/src/pages/Ambitos.tsx` + +### Modificar +- `adn/tools/cli/db/nodo.rb` (añadir `--ambito`, `--help`) +- `adn/tools/cli/db/evento.rb` (añadir `--ambito`, `--help`) +- `adn/tools/cli/nodos.rb` (filtros por ámbito, `--help`) +- `adn/tools/cli/salud.rb` (métricas por ámbito, `--help`) +- `adn/tools/db/core/bitacora_db.rb` +- `adn/tools/db/core/evento_db.rb` +- `adn/tools/core/help_formatter.rb` (si no existe) +- `adn/tools/core/error_handler.rb` (si no existe) +- `servicios/nginx/dtic-bitacoras/backend/src/server.js` +- `servicios/nginx/dtic-bitacoras/frontend/src/App.tsx` +- `adn/01_ontologia.md` +- `adn/02_bitacora.md` +- `adn/05_ia.md` +- `adn/06_gobernanza.md` +- `adn/README.md` diff --git a/docs/proy/P2604_Mejoras-ADN/P2604.09_Referencia-Planes-Bitacora.md b/docs/proy/P2604_Mejoras-ADN/P2604.09_Referencia-Planes-Bitacora.md new file mode 100644 index 00000000..08cd400f --- /dev/null +++ b/docs/proy/P2604_Mejoras-ADN/P2604.09_Referencia-Planes-Bitacora.md @@ -0,0 +1,80 @@ +# Plan P2604.09: Referencia Dinámica a Planes desde Bitácora Web + +**Proyecto**: P2604 — Mejoras ADN +**Estado**: ✅ Completado +**Fecha**: 2026-03-27 + +--- + +## Objetivo + +Permitir que los eventos de la bitácora web referencien directamente archivos de plan `.md` del repositorio, renderizándolos dinámicamente en un modal sin necesidad de reiniciar Docker. + +## Problema + +- Las descripciones de eventos mencionaban planes (`[P2601.09]`, `[P2604]`) como texto plano sin funcionalidad. +- No existía forma de consultar el contenido de un plan directamente desde la interfaz web. +- Cualquier referencia a un `.md` requería buscar el archivo manualmente en el repositorio. + +## Solución Implementada + +### Fase 1: Visor de documentos Markdown (#1309) + +| Componente | Archivo | Descripción | +|---|---|---| +| Backend API | `backend/src/routes/docs.js` | Endpoint `/api/docs/:proyecto/:archivo` que sirve `.md` desde disco | +| Backend Server | `backend/src/server.js` | Registro de ruta `/api/docs` | +| Docker | `docker-compose.yml` | Volumen `docs/proy/` montado read-only en container API | +| Frontend | `frontend/src/App.tsx` | Modal visor con ReactMarkdown + handler de links | +| CSS | `frontend/src/index.css` | Estilos `.doc-viewer-content` y `.plan-link-btn` | + +**Resultado**: Los archivos `.md` se pueden visualizar renderizados directamente en la web. El contenido se lee del disco en cada request, por lo que cualquier edición al `.md` se refleja inmediatamente. + +### Fase 2: Campo `doc_path` centralizado (#1310) + +| Componente | Archivo | Descripción | +|---|---|---| +| BD Schema | `docker/init.sql` | Columna `doc_path TEXT` en `bitacoras.entradas` | +| API CRUD | `backend/src/routes/entradas.js` | `doc_path` en INSERT y UPDATE dinámico | +| CLI | `adn/tools/cli/db/evento.rb` | Flag `--doc RUTA` en `evento:crear` y `evento:actualizar` | +| Frontend | `frontend/src/App.tsx` | Botón basado en `e.doc_path` (campo BD, no regex de descripción) | +| Contexto IA | `docs/prompt/260325-0900_Contexto-IA.md` | Convención documentada | + +**Problema resuelto**: Los links en descripciones se rompían al renombrar archivos. Con `doc_path` como campo de BD, un solo `UPDATE` corrige todas las referencias. + +## Arquitectura + +``` +docs/proy/ (host) ──[volumen :ro]──> /app/docs/proy/ (container API) + │ + routes/docs.js + │ + GET /api/docs/:proyecto/:archivo + │ +Frontend ─── e.doc_path ─── botón click ─── fetch API ─── modal ReactMarkdown +``` + +## Uso + +```bash +# Crear evento con plan vinculado +./adn/tools/run db evento:crear \ + --doc P2601_Dasuten/P2601.09_DASUTEN-sin-DC.md \ + --descripcion "Texto del evento" \ + --nodo srv-ns8 --inicio 08:00 + +# Actualizar doc_path de un evento existente +./adn/tools/run db evento:actualizar 1308 \ + --doc P2601_Dasuten/P2601.09_DASUTEN-sin-DC.md +``` + +## Progreso + +| Fase | Descripción | Estado | Evento | +|---|---|---|---| +| 1 | Visor de documentos Markdown (API + modal) | ✅ 100% | #1309 | +| 2 | Campo `doc_path` centralizado (BD + CLI + frontend) | ✅ 100% | #1310 | + +--- + +*Plan completado el 2026-03-27* diff --git a/docs/proy/P2604_Mejoras-ADN/P2604_Mejoras-ADN.md b/docs/proy/P2604_Mejoras-ADN/P2604_Mejoras-ADN.md index 7fcca0f5..0eb06d8a 100644 --- a/docs/proy/P2604_Mejoras-ADN/P2604_Mejoras-ADN.md +++ b/docs/proy/P2604_Mejoras-ADN/P2604_Mejoras-ADN.md @@ -49,6 +49,7 @@ Mejoras atómicas, incrementales y reutilizables al ecosistema ADN. Una herramie | 16/03 | RFC-01 | ✅ | Convención de nomenclature de proyectos (RFC-ADN-01) | | 18/03 | WZ-v2 | ✅ | Plan e implementación de W-ZOMBI v2.0 (reemplazo sonda legacy) | | — | DOC | 📍 | Actualizar README.md con funcionalidades nuevas (auto-refresh, estado/modo) | +| — | P2604.08 | ✅ | Implementación del modelo Ámbito/Nodo en el ecosistema ADN | --- @@ -194,10 +195,20 @@ Mejoras atómicas, incrementales y reutilizables al ecosistema ADN. Una herramie | ID | Mejora | Estado | | :--- | :--- | :--- | -| IA1 | Diagnóstico de logs y fricción (sesión 18/03) | ⏳ | -| IA2 | Refactorización de directivas en 05_ia.md | 📍 | -| IA3 | Automatización de contexto de seguridad (candados) | 📍 | -| IA4 | Validación de reducción de pasos exploratorios | 📍 | +| IA1 | Diagnóstico de logs y fricción (sesión 18/03) | ✅ | +| IA2 | Refactorización de directivas en 05_ia.md | ✅ | +| IA3 | Automatización de contexto de seguridad (candados) | ✅ | +| IA4 | Validación de reducción de pasos exploratorios | ✅ | + +### Fase 16: Modelo Ámbito/Nodo (Armonía Integral) + +| ID | Mejora | Estado | +| :--- | :--- | :--- | +| A1 | Base de Datos: tabla `ambitos`, columnas `ambito_id` | ✅ | +| A2 | CLI Ruby: comando `ambitos`, normalización `--help` | ✅ | +| A3 | API REST: endpoints `/api/ambitos/*` | ✅ | +| A4 | Frontend: AmbitoDashboard con selector de ámbito | ✅ | +| A5 | Documentación: hebras ADN actualizadas | ✅ | --- @@ -215,10 +226,11 @@ Fase 8: ██████████ 100% CI/CD Fase 9: ██████████ 100% MSP Fase 10: ██████████ 100% Saneamiento Fase 11: ██████████ 100% Bitácora Web -Fase 12: ████████░░ 80% Eventos +Fase 12: ██████████ 100% Eventos Fase 13: ██████████ 100% W-ZOMBI v2.0 Fase 14: ██████████ 100% Visibilidad -Fase 15: ░░░░░░░░░░ 0% Optimización IA +Fase 15: ██████████ 100% Optimización IA +Fase 16: ██████████ 100% Ámbito/Nodo ``` ## Métricas Totales @@ -237,6 +249,7 @@ Fase 15: ░░░░░░░░░░ 0% Optimización IA - [Implementación W-ZOMBI v2.0](P2604.03.01_Plan-W-ZOMBI-v2.0.md) - [Estandarización de Indicadores Visuales de Progreso](P2604.04.01_Estandarizacion-Barras-Progreso.md) - [Estudio de Fricción IA (Antigravity) y Mejora Continua](P2604.05.01_Estudio-Friccion-IA.md) +- [Modelo Ámbito/Nodo para el Ecosistema ADN](P2604.08_Ambito-Nodo.md) ## Referencias diff --git a/docs/proy/P2606_WebServer-NGINX/P2606.01_Plan-WebServer-NGINX.md b/docs/proy/P2606_WebServer-NGINX/P2606.01_Plan-WebServer-NGINX.md new file mode 100644 index 00000000..10dba973 --- /dev/null +++ b/docs/proy/P2606_WebServer-NGINX/P2606.01_Plan-WebServer-NGINX.md @@ -0,0 +1,70 @@ +# Plan: Servidor Web NGINX en LXC con IP Pública + +**Código:** P2606.01 +**Fecha:** 28 de marzo de 2026 +**Autor:** Sistema ADN +**Versión:** 1.1 +**Estado:** 🚧 EN EJECUCIÓN + +## 📋 Resumen Ejecutivo + +Desplegar un servidor web **NGINX** en contenedor **LXC** (Debian 12) sobre `srv-pmox3`, con **IP pública** asignada por redes UTN para exposición directa de servicios web. + +## 📌 Prerequisitos + +- [x] IP pública: `190.114.205.17` — verificada libre y asignada al LXC. +- [x] Bridge: `vmbr0` en srv-pmox3 soporta red pública y privada (capa 2 compartida). +- [x] Firewall UTN: puertos 80 y 443 abiertos — confirmado vía acceso HTTP/HTTPS público. +- [x] Nombre del nodo: `srvv-nginx-rm`. + +## 🚀 Plan de Ejecución + +### ✅ **FASE 1: Preparación e investigación** *(completada 2026-03-28)* + +- [x] **1.1:** IP pública: `190.114.205.17/24`, gw `190.114.205.1`. Verificada libre. +- [x] **1.2:** Red compartida capa 2 — `vmbr0` sirve tanto red pública como privada. +- [x] **1.3:** Template `debian-12-standard_12.7-1_amd64.tar.zst` disponible. +- [x] **1.4:** Hostname: `srvv-nginx-rm`. IP privada: `10.0.10.117/23`. Host: `srv-pmox3`. + +### ✅ **FASE 2: Creación del contenedor LXC** *(completada 2026-03-28)* + +- [x] **2.1:** LXC 116 creado en srv-pmox3 ✅ (2 cores, 2 GB RAM, 20 GB disco, Debian 12, unprivileged). +- [x] **2.2:** Red configurada: eth0=`190.114.205.17/24` (pública), eth1=`10.0.10.117/23` (privada). +- [x] **2.3:** Hostname `srvv-nginx-rm`, DNS `8.8.8.8`, timezone `America/Argentina/La_Rioja`. +- [x] **2.4:** Sistema actualizado (`apt upgrade` — 91 paquetes). +- [x] **2.5:** OpenSSH en puerto **7022** ✅. +- [ ] **2.6:** Configurar Tailscale VPN (acceso remoto de gestión). ⏳ *Pendiente.* + +### ✅ **FASE 3: Instalación y configuración NGINX** *(completada 2026-03-28)* + +- [x] **3.1:** NGINX 1.22.1 instalado ✅. Puerto 80 escuchando. Acceso público HTTP 200 verificado. +- [x] **3.1b:** DNS DuckDNS: `rmonla.duckdns.org` → `190.114.205.17` ✅. ⚠️ DuckDNS no soporta wildcard (`*.rmonla.duckdns.org`). Subdominios requieren dominio propio o alternativa. +- [x] **3.2:** Sitio `/vcby` configurado ✅. Repo `ricardomonla/assjdm-ViaCrusis` clonado en `/var/www/vcby`. PHP 8.2-FPM instalado. NGINX alias + fastcgi configurado. `http://rmonla.duckdns.org/vcby/` → HTTP 200. +- [x] **3.3:** HTTPS con Let's Encrypt ✅. Certbot 2.1.0, cert `rmonla.duckdns.org` (expira 2026-06-26), redirect HTTP→HTTPS, renovación automática. +- [ ] **3.4:** Hardening: headers de seguridad, server tokens, rate limiting. +- [ ] **3.5:** Configurar logs (access.log, error.log con rotación). + +### ⏳ **FASE 4: Seguridad y monitoreo** + +- [ ] **4.1:** Configurar firewall local (ufw/nftables): solo puertos 80, 443, SSH. +- [ ] **4.2:** Instalar fail2ban. +- [ ] **4.3:** Configurar backup automático (vzdump en Proxmox). +- [ ] **4.4:** Agregar al sistema de monitoreo existente. + +### 🚧 **FASE 5: Validación y documentación** *(en progreso 2026-03-28)* + +- [x] **5.1:** Test acceso web público: HTTP 200 ✅ y HTTPS `https://rmonla.duckdns.org/vcby/` → 200 ✅. +- [ ] **5.2:** Test de rendimiento básico. +- [x] **5.3:** Ficha de nodo creada: `nodos/srvv-nginx-rm.md` ✅. +- [ ] **5.4:** Documentar en bitácora y marcar proyecto como operativo. + +## 📝 Notas + +- **LXC 116** en srv-pmox3 (10.0.10.203). Autostart habilitado (`onboot: 1`). +- **Clave root**: Guardada en candados (`srvv-nginx-rm:root`) ✅. +- **Acceso SSH**: `ssh -p 7022 root@10.0.10.117` o `root@190.114.205.17`. +- **Acceso Proxmox**: `pct exec 116 -- bash` desde srv-pmox3. +- **DNS**: `rmonla.duckdns.org` → `190.114.205.17` (DuckDNS, no soporta wildcard). +- **App desplegada**: `/vcby` → repo `ricardomonla/assjdm-ViaCrusis` (deploy key SSH). +- **Referencia**: `srvv-sitio` en srv-pmox1 con IP `190.114.205.20`. + diff --git a/docs/proy/P2606_WebServer-NGINX/P2606_WebServer-NGINX.md b/docs/proy/P2606_WebServer-NGINX/P2606_WebServer-NGINX.md new file mode 100644 index 00000000..80fdb36e --- /dev/null +++ b/docs/proy/P2606_WebServer-NGINX/P2606_WebServer-NGINX.md @@ -0,0 +1,137 @@ +# P2606 - Servidor Web NGINX con IP Pública + +**Estado:** ⏳ En Ejecución +**Fecha Inicio:** 2026-03-28 +**Fecha Fin Estimada:** *Por definir* + +## Objetivo +Desplegar un nuevo servidor web basado en **NGINX** en un contenedor **LXC** dentro de la infraestructura Proxmox de la facultad, con asignación de **IP pública** para exposición directa de servicios web. + +### Motivación +- Necesidad de un servidor web dedicado con IP pública propia. +- Los servidores web actuales (`srvv-sitio` con IP `190.114.205.20`, `srvv-sitio2` LXC) están en uso. +- LXC ofrece menor overhead que VMs completas, ideal para un servidor web. + +## Stakeholders +| Rol | Nombre | Contacto | +| :--- | :--- | :--- | +| Responsable DTIC | Ricardo Monla | rmonla | + +## 🌐 Topología y Contexto de Red + +``` +Internet + │ +Firewall/Router UTN + │ + ├── srv-pmox3 (10.0.10.203) ── Bridge vmbr0 ── Red interna 10.0.10.x + │ │ + │ └── srvv-web (LXC) ── IP Pública: (a asignar) + │ IP Privada: 10.0.10.xx + │ + ├── srvv-sitio (190.114.205.20) ── Web existente (referencia) + └── srvv-sitio2 (10.0.10.19) ── Web secundario LXC +``` + +## Nodos Involucrados +### Nodos Exclusivos (Core) +- `srvv-web` — Contenedor LXC con NGINX (a crear) + +### Nodos de Soporte/Infraestructura +- `srv-pmox3` (10.0.10.203) — Host Proxmox (64GB RAM, ~20GB libres) +- `srvv-sitio` — Referencia de configuración con IP pública + +## Herramientas +- **Proxmox VE** — Creación y gestión del LXC +- **NGINX** — Servidor web / reverse proxy +- **Certbot** — Certificados HTTPS (Let's Encrypt) +- **Tailscale** — Acceso remoto de gestión +- **ADN Toolkit** — Gestión de nodos, bitácora, candados + +## Recursos Estimados (LXC) + +| Recurso | Valor | +|---------|-------| +| CPU | 2 cores (compartido) | +| RAM | 2 GB | +| Disco | 20 GB | +| Red | vmbr0 (bridge), IP pública + IP privada | +| Template | `debian-12-standard` | + +## Hitos Clave +| ID | Hito | Estado | Fecha | +| :--- | :--- | :---: | :--- | +| 1 | IP pública asignada | ✅ | 2026-03-28 | +| 2 | LXC creado y configurado | ✅ | 2026-03-28 | +| 3 | NGINX operativo con HTTPS | ✅ | 2026-03-28 | +| 4 | Seguridad y monitoreo | 📍 | *Pendiente* | +| 5 | Validación y entrega | ⏳ | *En progreso* | + +--- + +## 📊 Progreso + +``` +Fase 1: ██████████ 100% Preparación e investigación +Fase 2: █████████░ 90% Creación del contenedor LXC (Tailscale pendiente) +Fase 3: ████████░░ 80% Instalación y configuración NGINX (hardening pendiente) +Fase 4: ░░░░░░░░░░ 0% Seguridad y monitoreo +Fase 5: █████░░░░░ 50% Validación y documentación +``` + +--- + +## Prerequisitos + +- [x] Confirmar IP pública disponible: `190.114.205.17` asignada. +- [x] Verificar bridge en srv-pmox3: `vmbr0` capa 2 compartida. +- [x] Confirmar reglas de firewall: puertos 80/443 abiertos (verificado HTTP/HTTPS público). +- [x] Definir nombre del nodo: `srvv-nginx-rm`. + +## Plan de Ejecución + +### ⏳ **FASE 1: Preparación e investigación** + +- [ ] **1.1:** Solicitar/confirmar IP pública disponible al área de redes UTN. +- [ ] **1.2:** Verificar configuración de red en `srv-pmox3` — bridges, VLAN, routing para IP pública. +- [ ] **1.3:** Verificar template LXC Debian 12 disponible en srv-pmox3. +- [ ] **1.4:** Definir hostname definitivo y esquema de red. + +### ⏳ **FASE 2: Creación del contenedor LXC** + +- [ ] **2.1:** Crear LXC en srv-pmox3 (2 cores, 2 GB RAM, 20 GB disco, bridge con IP pública). +- [ ] **2.2:** Configurar red: IP pública estática + gateway, IP privada 10.0.10.xx. +- [ ] **2.3:** Configurar hostname, DNS, timezone (America/Argentina/La_Rioja). +- [ ] **2.4:** Actualizar sistema base (`apt update && apt upgrade`). +- [ ] **2.5:** Instalar y configurar OpenSSH (puerto personalizado, acceso por clave). +- [ ] **2.6:** Configurar Tailscale VPN (acceso remoto de gestión). + +### ⏳ **FASE 3: Instalación y configuración NGINX** + +- [ ] **3.1:** Instalar NGINX. +- [ ] **3.2:** Configurar sitio por defecto (página de prueba). +- [ ] **3.3:** Configurar HTTPS con Let's Encrypt (certbot + NGINX plugin). +- [ ] **3.4:** Hardening: headers de seguridad, server tokens, rate limiting. +- [ ] **3.5:** Configurar logs (access.log, error.log con rotación). + +### ⏳ **FASE 4: Seguridad y monitoreo** + +- [ ] **4.1:** Configurar firewall local (ufw/nftables): solo puertos 80, 443, SSH. +- [ ] **4.2:** Instalar fail2ban. +- [ ] **4.3:** Configurar backup automático (vzdump en Proxmox). +- [ ] **4.4:** Agregar al sistema de monitoreo existente. + +### ⏳ **FASE 5: Validación y documentación** + +- [ ] **5.1:** Test de acceso web desde internet (HTTP y HTTPS). +- [ ] **5.2:** Test de rendimiento básico. +- [ ] **5.3:** Crear ficha de nodo en `nodos/` (vía `generar nodo`). +- [ ] **5.4:** Documentar en bitácora y marcar proyecto como operativo. + +## Referencias +- `srvv-sitio` — Modelo de servidor web con IP pública (190.114.205.20) +- `srvv-sitio2` — Referencia de LXC web existente + +--- + +*Manifiesto generado automáticamente el 2026-03-28 — completado con datos del proyecto* diff --git a/nodos/dasu-pc-0.md b/nodos/dasu-pc.md similarity index 85% rename from nodos/dasu-pc-0.md rename to nodos/dasu-pc.md index 38d6d491..f8a57fc3 100644 --- a/nodos/dasu-pc-0.md +++ b/nodos/dasu-pc.md @@ -1,12 +1,13 @@ -# Nodo: dasu-pc-0 +# Nodo: dasu-pc ## Información General -- **Hostname**: `dasu-pc-0` (Prev: `pc-dasu0`) +- **Hostname**: `dasu-pc` (Prev: `dasu-pc-0`, `pc-dasu0`) - **Patrimonio**: `32120` - **Ubicación**: Oficina DASUTEN (Externa a Facultad) - **Tipo**: PC Física - **Rol**: Estación de Trabajo Remota (Usuarios: Andrea/Romina) - **Sistema Operativo**: Windows 10 +- **Dominio**: `dasuten.utnlr` (Unido 18/03/2026 - P2601.07.01) - **Conectividad**: Tailscale (`100.100.145.51`) - ✅ ONLINE (Unattended Mode) - **SSH**: `UTNLR@100.100.145.51:7022` (Plan A, Passphrase RSA, Bóveda: `rsa`) - **Acceso Respaldo (Plan B)**: W-ZOMBI v2.0 C2 (Vía Powershell: `iwr 100.111.195.4:8000/zombi.ps1 -useb | iex`) @@ -14,7 +15,7 @@ ## Credenciales de Gestión (Tailscale Admin) - **Cuenta Maestra**: `pcdasu0@frlr.utn.edu.ar` - **Pass (Ref)**: `UTN$larioja00` -- **Propósito**: Esta cuenta gestiona la red Tailscale que une a `dasu-pc-0` con `pcv-dasu1`. +- **Propósito**: Esta cuenta gestiona la red Tailscale que une a `dasu-pc` con `dasu-pcv`. ## Servicios - **RustDesk**: ID `398 699 016` (Acceso Remoto Principal por GUI). Password: `UTNlarioja00` diff --git a/nodos/dasu-pcv-0.md b/nodos/dasu-pcv.md similarity index 61% rename from nodos/dasu-pcv-0.md rename to nodos/dasu-pcv.md index 18744408..3a3a403e 100644 --- a/nodos/dasu-pcv-0.md +++ b/nodos/dasu-pcv.md @@ -1,9 +1,8 @@ -# Nodo: dasu-pcv-0 +# Nodo: dasu-pcv ## Identidad -- **Hostname**: `dasu-pcv-0` -- **Nombre anterior**: `pcv-dasu0` (renombrado 16/03/2026) -- **Rol**: VM de Pruebas — Emulación de cliente `pc-dasu0` +- **Hostname**: `dasu-pcv` (Prev: `dasu-pcv-0`, `pcv-dasu0`, renombrado 16/03/2026) +- **Rol**: VM de Pruebas — Emulación de cliente `dasu-pc` - **Estado**: ✅ Operativo (Unida al dominio `dasuten.utnlr`) - **Padre/Host**: `srv-dasu` - **VMID**: `102` @@ -21,17 +20,23 @@ ## Sistema Operativo - **OS**: Windows 10 Enterprise LTSC 2021 (es-ES, 64-bit) - **Evaluación**: 90 días -- **Propósito**: Pruebas de unión a dominio y validación del sistema DASUTeN antes de configurar el nodo físico `pc-dasu0`. +- **Propósito**: Pruebas de unión a dominio y validación del sistema DASUTeN antes de configurar el nodo físico `dasu-pc`. ## Red y Conectividad -- **Subred**: `10.0.100.0/24` -- **IP**: `10.0.100.12/24` (Estática) -- **Gateway**: `10.0.100.1` -- **DNS Primario**: `10.0.100.10` (dasu-srvv-dc) -- **DNS Secundario**: `8.8.8.8` -## Relación con pc-dasu0 -Esta VM replica la configuración prevista para la PC física [`pc-dasu0`](pc-dasu0.md) (Ryzen 5, Win 10) que operará en la oficina DASUTeN. Las configuraciones validadas aquí (dominio, sistema, políticas de grupo) se replicarán al nodo físico final. +### Configuración Actual (2026-03-25) +- **Subred**: `10.0.100.0/24` +- **IP Local**: `10.0.100.12/24` +- **Gateway**: `10.0.100.1` (srv-dasu vmbr0) +- **DNS**: `10.0.100.10` (dasu-srvv-dc), `8.8.8.8` (backup) + +### Tailscale +- **IP**: `100.118.129.44` +- **Estado**: ⚠️ Offline (última vista hace 7d) +- **Cuenta**: `pcdasu0@frlr.utn.edu.ar` + +## Relación con dasu-pc +Esta VM replica la configuración prevista para la PC física [`dasu-pc`](dasu-pc.md) (Ryzen 5, Win 10) que operará en la oficina DASUTeN. Las configuraciones validadas aquí (dominio, sistema, políticas de grupo) se replicarán al nodo físico final. ## Dominio - **Bosque**: `dasuten.utnlr` diff --git a/nodos/dasu-sql2.md b/nodos/dasu-sql2.md new file mode 100644 index 00000000..1d2b0928 --- /dev/null +++ b/nodos/dasu-sql2.md @@ -0,0 +1,51 @@ +# Nodo: dasu-sql2 + +## Identidad +- **Hostname**: `dasu-sql2` +- **Rol**: SQL Server 2019 (DASUTEN) — Modo Workgroup +- **OS**: Windows Server 2022 Core (Evaluation) +- **Estado**: 🚧 En configuración +- **Última Actualización**: 2026-03-27 (Evento 1318) +- **Plan**: [P2601.09_DASUTEN-sin-DC.md](../docs/proy/P2601_Dasuten/P2601.09_DASUTEN-sin-DC.md) + +## Virtualización +- **Hipervisor**: [srv-dasu](srv-dasu.md) +- **VMID**: 103 +- **Tipo**: QEMU/KVM +- **BIOS**: SeaBIOS +- **Machine**: pc-i440fx-10.1 +- **CPU**: 2 vCPU (host passthrough) +- **RAM**: 4 GB +- **Disco**: 60 GB SATA (local-lvm, thin provisioned, discard=on) +- **Red**: VirtIO (bridge vmbr0) + +## Red +- **Configuración**: DHCP del ISP +- **IP**: `192.168.1.19/24` +- **Gateway**: 192.168.1.1 (router ISP) + +### Tailscale +- **IP**: `100.126.182.123` +- **Estado**: ✅ ONLINE +- **Cuenta**: `pcdasu0@frlr.utn.edu.ar` + +### Acceso Remoto +- **SSH**: `Administrator@100.126.182.123:7022` (Password, Bóveda: `dasu-sql2:Administrator`) +- **OpenSSH Server**: ✅ Operativo (Puerto 7022) + +## Credenciales +- **Usuario**: Administrator +- **Bóveda**: `dasu-sql2:Administrator` +- **Auth**: password + +## Servicios Planificados +- **SQL Server 2019**: Enterprise (Evaluation) — Autenticación Mixta (SQL + Windows) +- **OpenSSH Server**: Puerto 7022 + +## Backups +- **2026-03-27 19:07**: vzdump modo stop + zstd (post-install WS2022 Core + VirtIO) + +## Bitácora de Referencia +- **Evento 1316**: Creación VM y instalación OS +- **Evento 1317**: Backup post-instalación +- **Evento 1318**: Configuración hostname, SSH, Tailscale diff --git a/nodos/dasu-srvv-dc.md b/nodos/dasu-srvv-dc.md index 7c622c79..7b580ad8 100644 --- a/nodos/dasu-srvv-dc.md +++ b/nodos/dasu-srvv-dc.md @@ -18,15 +18,22 @@ - **Red VirtIO**: `vmbr0` (NAT mode) ## Red y Conectividad + +### Configuración Actual (2026-03-25) - **Subred Aislada**: `10.0.100.0/24` -- **IP**: `10.0.100.10/24` (Estática oculta tras NAT de Proxmox) -- **Gateway**: `10.0.100.1` (IP de ruteo del host Proxmox) -- **Servidores DNS**: `10.0.100.10` (local), `8.8.8.8` (backup) -- **Acceso Remoto**: - - RDP Habilitado (TCP 3389) +- **IP Local**: `10.0.100.10/24` +- **Gateway**: `10.0.100.1` (srv-dasu vmbr0) +- **DNS**: `10.0.100.10` (loopback), `8.8.8.8` (backup) + +### Tailscale +- **IP**: `100.85.117.101` +- **Estado**: ✅ ONLINE +- **Cuenta**: `pcdasu0@frlr.utn.edu.ar` + +### Acceso Remoto - **SSH**: `admindasu@100.85.117.101:7022` (Passphrase RSA, Bóveda: `rsa`) - - Habilitado OpenSSH Server (Puerto 7022). ✅ Operativo. - - Tailscale: Activo +- **RDP**: Habilitado (TCP 3389) +- **OpenSSH Server**: ✅ Operativo (Puerto 7022) ## Servicios y Roles 1. **Active Directory Domain Services (AD DS)** diff --git a/nodos/dasu-srvv-sql.md b/nodos/dasu-srvv-sql.md index 0e3875fb..240732f0 100644 --- a/nodos/dasu-srvv-sql.md +++ b/nodos/dasu-srvv-sql.md @@ -14,17 +14,23 @@ - **Red VirtIO**: `vmbr0` (NAT mode) ## Red y Conectividad + +### Configuración Actual (2026-03-25) - **Subred Aislada**: `10.0.100.0/24` -- **IP**: `10.0.100.11/24` (Estática oculta tras NAT de srv-dasu) -- **Gateway**: `10.0.100.1` (IP de ruteo del host Proxmox) -- **Servidores DNS**: `10.0.100.10` (dasu-srvv-dc) / `8.8.8.8` -- **Acceso Remoto**: - - **SSH**: `DASUTEN\admindasu@10.0.100.11:7022` (Bóveda: `admindasu`) - - **SSH (Tailscale)**: `100.107.24.124:7022` - - **Tailscale**: `100.107.24.124` (Direct Access) - - ✅ **OpenSSH Server** (Puerto 7022) - Configurado y Operativo - - **SQL Browser**: UDP 1434 (Visible en LAN interna) - - **SQL Instance**: TCP 1433 (Visible en LAN interna) +- **IP Local**: `10.0.100.11/24` +- **Gateway**: `10.0.100.1` (srv-dasu vmbr0) +- **DNS**: `10.0.100.10` (dasu-srvv-dc), `8.8.8.8` (backup) + +### Tailscale +- **IP**: `100.107.24.124` +- **Estado**: ✅ ONLINE + +### Acceso Remoto +- **SSH Local**: `DASUTEN\admindasu@10.0.100.11:7022` +- **SSH Tailscale**: `DASUTEN\admindasu@100.107.24.124:7022` +- **OpenSSH Server**: ✅ Operativo (Puerto 7022) +- **SQL Browser**: UDP 1434 (LAN interna) +- **SQL Instance**: TCP 1433 (LAN interna) ## Servicios y Roles 1. **Microsoft SQL Server 2019 Standard (Core)** diff --git a/nodos/srv-dasu.md b/nodos/srv-dasu.md index a1d0228d..2f122579 100644 --- a/nodos/srv-dasu.md +++ b/nodos/srv-dasu.md @@ -4,7 +4,8 @@ - **Hostname**: `srv-dasu.utnlarioja` - **Rol Inicial**: Hypervisor Proxmox VE (Standalone - DASUTEN) - **OS**: Debian / Proxmox VE -- **Estado**: ⏳ Instalación Base +- **Estado**: ✅ Operativo (Red Configurada - Capa 2 + Tailscale) +- **Última Actualización**: 2026-03-25 (Evento 1275) ## Hardware (Físico) - **Placa Madre**: ASUS PRIME A320M-K (BIOS Ver. 0217 UEFI) @@ -14,12 +15,55 @@ - **CPU**: AMD A10-9700 Radeon R7 (4 Cores / 3.5GHz) - **RAM**: 16 GB (2x 8GB DDR4 2666MHz Crucial/Undefined - 15Gi usable) - **Almacenamiento**: 1x 480GB SSD ADATA SU630 (`sda` - LVM-Thin Proxmox Base) +- **Red Física**: + - **enp33s0**: Conexión ISP (router DHCP) + link directo srv-ns8 (10.0.100.2) + - **nic0**: Bridge a vmbr0 para red local VMs ## Red -- **IP**: `100.112.46.104` (Tailscale VPN) -- **IP Física (Bridge vmbr0)**: DHCP (WAN) + `10.0.100.1` (Alias LAN DASUTEN) -- **SSH**: `rmonla@100.112.46.104:22` (Passphrase RSA, Bóveda: `rsa`) -- **Acceso**: SSH Key + Sudo (Usuario `rmonla` - Credenciales en bóveda: `srv-dasu:rmonla`) + +### Configuración Física (2026-03-25) +| Interfaz | Configuración | IP | Propósito | +|----------|---------------|-----|-----------| +| **enp33s0** | DHCP | `192.168.200.42/23` | ISP (router externo) | +| **vmbr0** (bridge nic0) | Estática | `10.0.100.1/24` | Red local VMs + pc-dasu0 | +| **enp33s0** (alias) | Estática | `10.0.100.2/24` | Link directo srv-ns8 | + +### Tailscale +- **IP**: `100.112.46.104` +- **Estado**: ✅ ONLINE (active; direct `192.168.200.42:41641`) +- **Subnet Router**: `10.0.100.0/24` (anunciado) +- **Cuenta**: `pcdasu0@frlr.utn.edu.ar` + +### SSH +- **Acceso directo**: `rmonla@100.112.46.104:22` (Passphrase RSA) +- **Acceso VMs**: `rmonla@10.0.100.x:7022` (Windows Server Core - OpenSSH) +- **Credenciales**: Bóveda `srv-dasu:rmonla` + +### Topología Implementada +``` +INTERNET (ISP Router DHCP) + │ + ↓ (enp33s0 - DHCP 192.168.200.42) +┌─────────────────────────────────────┐ +│ srv-dasu (Proxmox VE) │ +│ ┌─────────────────────────────┐ │ +│ │ vmbr0: 10.0.100.1/24 │ │ +│ │ NAT/Masquerade → enp33s0 │ │ +│ │ DNS: 10.0.100.10 + 8.8.8.8 │ │ +│ └─────────────────────────────┘ │ +│ │ │ │ │ +│ ↓ ↓ ↓ │ +│ VM 100 VM 101 VM 102 │ +│ DC .10 SQL .11 PCV .12 │ +└─────────────────────────────────────┘ + │ + ↓ (link directo 10.0.100.2) +┌──────────────────┐ +│ srv-ns8 │ +│ enp37s0 │ +│ 10.0.100.8 │ +└──────────────────┘ +``` ### Tailscale - **Cuenta**: `pcdasu0@frlr.utn.edu.ar` @@ -27,14 +71,39 @@ - **Gestión**: `./adn/tools/run tailscale switch dasuten` ## Servicios Críticos -1. **Virtualización** - - **Motor**: Proxmox VE - - **Estado**: ✅ Operativo (Híbrido) + +### 1. Virtualización + - **Motor**: Proxmox VE 9.1 + - **Estado**: ✅ Operativo - **Propósito**: Hospedar servidores virtuales exclusivos para la oficina de DASUTEN. - **Subnet Router**: Expone `10.0.100.0/24` vía Tailscale. -### Huéspedes (VMs) -- **[VM 100: dasu-srvv-dc](dasu-srvv-dc.md)** (Controlador de Dominio / Windows Server Core) +### 2. Red y Conectividad + - **NAT/Masquerade**: ✅ Configurado (`iptables -t nat -A POSTROUTING -s 10.0.100.0/24 -o enp33s0 -j MASQUERADE`) + - **DNS Forwarding**: VMs usan `10.0.100.10` (DC) + `8.8.8.8` (Google) + - **Proxy Docker**: `tinyproxy` en `:3129` (Alpine container) + +### 3. Tailscale + - **Estado**: ✅ ONLINE + - **IP Pública**: `100.112.46.104` + - **Conexión**: Directa (`192.168.200.42:41641`) + - **Función**: Subnet router para `10.0.100.0/24` + +### Huéspedes (VMs) - Red 10.0.100.0/24 +| VM | Hostname | IP | Rol | Estado | +|----|----------|-----|-----|--------| +| **100** | [dasu-srvv-dc](dasu-srvv-dc.md) | `10.0.100.10` | AD DS + DNS | ✅ ONLINE (Tailscale: 100.85.117.101) | +| **101** | [dasu-srvv-sql](dasu-srvv-sql.md) | `10.0.100.11` | SQL Server 2019 | ✅ ONLINE (Tailscale: 100.107.24.124) | +| **102** | dasu-pcv | `10.0.100.12` | VM de pruebas (Win 10 LTSC) | ✅ ONLINE (Tailscale: 100.118.129.44) | ## Dependencias / Sincronización - **Cluster**: Ninguna (Servidor Standalone externo al cluster de la Facultad). +- **Red Local**: `10.0.100.0/24` (VMs + srv-ns8 vía link directo) +- **Internet**: ISP vía DHCP (enp33s0 → router 192.168.200.1) +- **Tailscale**: VPN mesh con subnet routing activo + +## Bitácora de Referencia +- **Evento 1272** (10:52-11:41): Configuración Capa 2 +- **Evento 1273** (11:42-11:43): Documentación topología +- **Evento 1274** (12:17-13:02): Traslado srv-dasu +- **Evento 1275** (13:03-13:30): Ecosistema completo ONLINE diff --git a/nodos/srvv-nginx-rm.md b/nodos/srvv-nginx-rm.md new file mode 100644 index 00000000..ab9bfd1f --- /dev/null +++ b/nodos/srvv-nginx-rm.md @@ -0,0 +1,62 @@ +# Ficha de Nodo: srvv-nginx-rm + +| Atributo | Valor | +| :--- | :--- | +| **Hostname** | `srvv-nginx-rm` | +| **IP Pública** | `190.114.205.17` | +| **IP LAN** | `10.0.10.117` | +| **DNS** | `rmonla.duckdns.org` | +| **Sistema Operativo** | Debian 12 (Bookworm) | +| **Tipo** | Contenedor LXC (VMID 116, unprivileged) | +| **Rol** | Servidor Web NGINX | +| **Host** | `srv-pmox3` (10.0.10.203) | +| **SSH** | Puerto `7022`, usuario `root`, auth `password` | +| **Bóveda** | `srvv-nginx-rm:root` | + +## Especificaciones de Hardware +- **CPU**: 2 cores (compartido LXC) +- **RAM**: 2 GB +- **Swap**: 512 MB +- **Disco**: 20 GB (local-lvm:vm-116-disk-0) + +## Servicios y Responsabilidades +1. **NGINX 1.22.1** — Servidor web, puertos 80 (HTTP) y 443 (HTTPS) +2. **PHP 8.2-FPM** — Procesamiento de aplicaciones PHP +3. **Certbot 2.1.0** — Certificados Let's Encrypt, renovación automática +4. **OpenSSH** — Acceso remoto, puerto 7022 + +## Aplicaciones Desplegadas +| App | Ruta | Repo GitHub | Método Deploy | +| :--- | :--- | :--- | :--- | +| ViaCrucis | `/vcby` → `/var/www/vcby` | `ricardomonla/assjdm-ViaCrusis` | Deploy key SSH | + +## Red +- **eth0**: `190.114.205.17/24` — Red pública, gw `190.114.205.1` +- **eth1**: `10.0.10.117/23` — Red interna, gw `10.0.10.1` +- **DNS**: `8.8.8.8` +- **Bridge**: `vmbr0` (capa 2 compartida público/privado) +- **DuckDNS**: `rmonla.duckdns.org` → `190.114.205.17` + +## HTTPS / Certificado SSL +- **Certificado**: Let's Encrypt para `rmonla.duckdns.org` +- **Expiración**: 2026-06-26 +- **Renovación**: Automática (certbot timer) +- **Redirect**: HTTP → HTTPS (301) activo + +## Acceso y Mantenimiento +- **SSH**: `ssh -p 7022 root@10.0.10.117` o `root@190.114.205.17` +- **Candados**: `./adn/tools/run candados run srvv-nginx-rm:root SSHPASS 'sshpass -e ssh -p 7022 root@10.0.10.117'` +- **Proxmox**: `pct exec 116 -- bash` desde srv-pmox3 +- **Web pública**: `https://rmonla.duckdns.org/vcby/` +- **Backup**: Pendiente configurar vzdump +- **Monitorización**: Pendiente integrar + +## Dependencias +- `srv-pmox3` — Host Proxmox +- Proyecto `P2606_WebServer-NGINX` + +## Historial de Cambios Relevantes +- 2026-03-28: LXC creado. NGINX instalado. IP pública verificada (HTTP 200). +- 2026-03-28: DNS DuckDNS configurado. Repo vcby clonado con deploy key. PHP 8.2-FPM instalado. +- 2026-03-28: HTTPS configurado con certbot/Let's Encrypt. Redirect HTTP→HTTPS activo. +- 2026-03-28: Clave root guardada en candados (`srvv-nginx-rm:root`). diff --git a/servicios/nginx/conf.d/ns8.conf b/servicios/nginx/conf.d/ns8.conf index ea69be60..0121f6a6 100644 --- a/servicios/nginx/conf.d/ns8.conf +++ b/servicios/nginx/conf.d/ns8.conf @@ -1,6 +1,6 @@ server { - server_name ns8.frlr.utn.edu.ar 100.88.252.26 190.114.205.18 srvns8.tail982245.ts.net srv-ns8.tail.dasuten srv-ns8.tail.rmonla; + server_name ns8.frlr.utn.edu.ar 100.111.195.4 190.114.205.18 srvns8.tail982245.ts.net srv-ns8.tail.dasuten srv-ns8.tail.rmonla; root /var/www/html; index index.html index.htm; @@ -99,7 +99,7 @@ server { listen 80; - server_name ns8.frlr.utn.edu.ar 100.88.252.26 190.114.205.18 srvns8.tail982245.ts.net srv-ns8.tail.dasuten srv-ns8.tail.rmonla 10.0.10.8; + server_name ns8.frlr.utn.edu.ar 100.111.195.4 190.114.205.18 srvns8.tail982245.ts.net srv-ns8.tail.dasuten srv-ns8.tail.rmonla 10.0.10.8; location /apps/ { client_max_body_size 20G; diff --git a/servicios/nginx/dtic-bitacoras/backend/src/routes/ambitos.js b/servicios/nginx/dtic-bitacoras/backend/src/routes/ambitos.js new file mode 100644 index 00000000..824f7773 --- /dev/null +++ b/servicios/nginx/dtic-bitacoras/backend/src/routes/ambitos.js @@ -0,0 +1,143 @@ +module.exports = (pool) => { + const router = require('express').Router(); + + // GET /api/ambitos — Listar ámbitos + router.get('/', async (req, res) => { + try { + const { rows } = await pool.query(` + SELECT a.*, + (SELECT COUNT(*) FROM bitacoras.nodos WHERE ambito_id = a.id) as count_nodos + FROM bitacoras.ambitos a + ORDER BY a.parent_id NULLS FIRST, a.nombre ASC + `); + res.json(rows); + } catch (err) { + res.status(500).json({ error: err.message }); + } + }); + + // POST /api/ambitos — Crear ámbito + router.post('/', async (req, res) => { + const { nombre, descripcion, parent_id, activo } = req.body; + try { + const { rows } = await pool.query( + 'INSERT INTO bitacoras.ambitos (nombre, descripcion, parent_id, activo) VALUES ($1, $2, $3, $4) RETURNING *', + [nombre, descripcion, parent_id || null, activo ?? true] + ); + res.status(201).json(rows[0]); + } catch (err) { + res.status(500).json({ error: err.message }); + } + }); + + // GET /api/ambitos/estadisticas — Métricas por ámbito + router.get('/stats/global', async (req, res) => { + try { + const { rows } = await pool.query(` + SELECT a.nombre, a.id, + (SELECT COUNT(*) FROM bitacoras.nodos WHERE ambito_id = a.id) as nodos_count, + (SELECT COUNT(*) FROM bitacoras.entradas WHERE ambito_id = a.id) as entradas_count + FROM bitacoras.ambitos a + WHERE a.activo = true + ORDER BY nodos_count DESC + `); + res.json(rows); + } catch (err) { + res.status(500).json({ error: err.message }); + } + }); + + // GET /api/ambitos/:id — Info de ámbito + router.get('/:id', async (req, res) => { + try { + const { rows } = await pool.query(` + SELECT a.*, p.nombre as parent_nombre, + (SELECT COUNT(*) FROM bitacoras.nodos WHERE ambito_id = a.id) as count_nodos + FROM bitacoras.ambitos a + LEFT JOIN bitacoras.ambitos p ON a.parent_id = p.id + WHERE a.id = $1 + `, [req.params.id]); + if (rows.length === 0) return res.status(404).json({ error: 'Ámbito no encontrado' }); + res.json(rows[0]); + } catch (err) { + res.status(500).json({ error: err.message }); + } + }); + + // PUT /api/ambitos/:id — Actualizar ámbito + router.put('/:id', async (req, res) => { + const { nombre, descripcion, parent_id, activo } = req.body; + try { + const { rows } = await pool.query( + 'UPDATE bitacoras.ambitos SET nombre=$1, descripcion=$2, parent_id=$3, activo=$4 WHERE id=$5 RETURNING *', + [nombre, descripcion, parent_id || null, activo ?? true, req.params.id] + ); + if (rows.length === 0) return res.status(404).json({ error: 'Ámbito no encontrado' }); + res.json(rows[0]); + } catch (err) { + res.status(500).json({ error: err.message }); + } + }); + + // DELETE /api/ambitos/:id — Eliminar ámbito + router.delete('/:id', async (req, res) => { + try { + const { rowCount } = await pool.query('DELETE FROM bitacoras.ambitos WHERE id = $1', [req.params.id]); + if (rowCount === 0) return res.status(404).json({ error: 'Ámbito no encontrado' }); + res.json({ message: 'Ámbito eliminado' }); + } catch (err) { + if (err.code === '23503') { // Foreign key constraint violation + await pool.query('UPDATE bitacoras.ambitos SET activo=false WHERE id=$1', [req.params.id]); + return res.json({ message: 'Ámbito marcado inactivo por poseer nodos/entradas relacionados' }); + } + res.status(500).json({ error: err.message }); + } + }); + + // GET /api/ambitos/:id/nodos — Nodos del ámbito + router.get('/:id/nodos', async (req, res) => { + try { + const { rows } = await pool.query('SELECT * FROM bitacoras.nodos WHERE ambito_id = $1 ORDER BY nombre', [req.params.id]); + res.json(rows); + } catch (err) { + res.status(500).json({ error: err.message }); + } + }); + + + // GET /api/ambitos/:id/dashboard — Resumen de salud/estado del ámbito + router.get('/:id/dashboard', async (req, res) => { + const { id } = req.params; + try { + const ambito = await pool.query('SELECT * FROM bitacoras.ambitos WHERE id = $1', [id]); + if (ambito.rows.length === 0) return res.status(404).json({ error: 'Ámbito no encontrado' }); + + const nodos = await pool.query('SELECT * FROM bitacoras.nodos WHERE ambito_id = $1 AND activo = true', [id]); + const entradasRecientes = await pool.query(` + SELECT e.*, n.nombre as nodo_nombre + FROM bitacoras.entradas e + JOIN bitacoras.nodos n ON e.nodo_id = n.id + WHERE n.ambito_id = $1 + ORDER BY e.inicio DESC LIMIT 10 + `, [id]); + + const gestionPendiente = await pool.query(` + SELECT g.*, n.nombre as nodo_nombre + FROM bitacoras.gestion g + JOIN bitacoras.nodos n ON g.nodo_id = n.id + WHERE n.ambito_id = $1 AND g.activo = true AND g.tipo = 'pendiente' + `, [id]); + + res.json({ + ambito: ambito.rows[0], + nodos: nodos.rows, + entradas: entradasRecientes.rows, + gestion: gestionPendiente.rows + }); + } catch (err) { + res.status(500).json({ error: err.message }); + } + }); + + return router; +}; diff --git a/servicios/nginx/dtic-bitacoras/backend/src/routes/bitacoras.js b/servicios/nginx/dtic-bitacoras/backend/src/routes/bitacoras.js index 79dbba48..85b3a3e2 100644 --- a/servicios/nginx/dtic-bitacoras/backend/src/routes/bitacoras.js +++ b/servicios/nginx/dtic-bitacoras/backend/src/routes/bitacoras.js @@ -59,37 +59,42 @@ module.exports = (pool) => { bitacora = bResult.rows[0]; } - // Obtener entradas con datos del nodo + // Obtener entradas con datos del nodo y ámbito 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]); + SELECT e.*, n.nombre as nodo_nombre, n.ip as nodo_ip, + COALESCE(a_evt.nombre, a_nodo.nombre) as ambito_nombre + FROM bitacoras.entradas e + LEFT JOIN bitacoras.nodos n ON e.nodo_id = n.id + LEFT JOIN bitacoras.ambitos a_evt ON e.ambito_id = a_evt.id + LEFT JOIN bitacoras.ambitos a_nodo ON n.ambito_id = a_nodo.id + WHERE e.bitacora_id = $1 + ORDER BY e.inicio DESC + `, [bitacora.id]); // Obtener gestión asociada a esta bitácora (fija) + activos globales (rollover) const { rows: gestion } = await pool.query(` - SELECT g.*, n.nombre as nodo_nombre - FROM bitacoras.gestion g - LEFT JOIN bitacoras.nodos n ON g.nodo_id = n.id - WHERE g.bitacora_id = $1 OR g.activo = true - ORDER BY g.tipo, g.orden - `, [bitacora.id]); + SELECT g.*, n.nombre as nodo_nombre, a.nombre as ambito_nombre + FROM bitacoras.gestion g + LEFT JOIN bitacoras.nodos n ON g.nodo_id = n.id + LEFT JOIN bitacoras.ambitos a ON n.ambito_id = a.id + WHERE g.bitacora_id = $1 OR g.activo = true + ORDER BY g.tipo, g.orden + `, [bitacora.id]); // Obtener resúmenes por nodo const { rows: resúmenes } = await pool.query(` - SELECT r.*, n.nombre as nodo_nombre - FROM bitacoras.resumen_nodos r - JOIN bitacoras.nodos n ON r.nodo_id = n.id - WHERE r.bitacora_id = $1 - `, [bitacora.id]); + SELECT r.*, n.nombre as nodo_nombre, a.nombre as ambito_nombre + FROM bitacoras.resumen_nodos r + JOIN bitacoras.nodos n ON r.nodo_id = n.id + LEFT JOIN bitacoras.ambitos a ON n.ambito_id = a.id + WHERE r.bitacora_id = $1 + `, [bitacora.id]); - // Agrupar por nodo + // Agrupar por nodo e incluir el ambito en el agrupador const porNodo = {}; entradas.forEach(e => { const nodo = e.nodo_nombre || 'sin-nodo'; - if (!porNodo[nodo]) porNodo[nodo] = { nodo: nodo, ip: e.nodo_ip, entradas: [], resumen: '' }; + if (!porNodo[nodo]) porNodo[nodo] = { nodo: nodo, ip: e.nodo_ip, ambito_nombre: e.ambito_nombre, entradas: [], resumen: '' }; porNodo[nodo].entradas.push(e); }); @@ -97,8 +102,9 @@ module.exports = (pool) => { resúmenes.forEach(r => { if (porNodo[r.nodo_nombre]) { porNodo[r.nodo_nombre].resumen = r.resumen; + porNodo[r.nodo_nombre].ambito_nombre = r.ambito_nombre || porNodo[r.nodo_nombre].ambito_nombre; } else { - porNodo[r.nodo_nombre] = { nodo: r.nodo_nombre, entradas: [], resumen: r.resumen }; + porNodo[r.nodo_nombre] = { nodo: r.nodo_nombre, ambito_nombre: r.ambito_nombre, entradas: [], resumen: r.resumen }; } }); diff --git a/servicios/nginx/dtic-bitacoras/backend/src/routes/docs.js b/servicios/nginx/dtic-bitacoras/backend/src/routes/docs.js new file mode 100644 index 00000000..cef2f8a4 --- /dev/null +++ b/servicios/nginx/dtic-bitacoras/backend/src/routes/docs.js @@ -0,0 +1,66 @@ +const fs = require('fs'); +const path = require('path'); + +module.exports = () => { + const router = require('express').Router(); + + const DOCS_BASE = path.resolve('/app/docs/proy'); + + // GET /api/docs/:proyecto/:archivo — Servir .md crudo + // Ejemplo: /api/docs/P2601_Dasuten/P2601.09_DASUTEN-sin-DC.md + router.get('/:proyecto/:archivo', (req, res) => { + const { proyecto, archivo } = req.params; + + // Sanitizar: solo permitir caracteres seguros + if (!/^[A-Za-z0-9._-]+$/.test(proyecto) || !/^[A-Za-z0-9._-]+$/.test(archivo)) { + return res.status(400).json({ error: 'Nombre de archivo inválido' }); + } + + // Solo permitir .md + if (!archivo.endsWith('.md')) { + return res.status(400).json({ error: 'Solo se permiten archivos .md' }); + } + + const filePath = path.join(DOCS_BASE, proyecto, archivo); + + // Verificar que no escapa del directorio base + if (!filePath.startsWith(DOCS_BASE)) { + return res.status(403).json({ error: 'Acceso denegado' }); + } + + // Leer y devolver contenido + fs.readFile(filePath, 'utf8', (err, content) => { + if (err) { + if (err.code === 'ENOENT') { + return res.status(404).json({ error: 'Archivo no encontrado' }); + } + return res.status(500).json({ error: 'Error al leer archivo' }); + } + + res.json({ + proyecto, + archivo, + content, + lastModified: fs.statSync(filePath).mtime.toISOString() + }); + }); + }); + + // GET /api/docs — Listar proyectos disponibles + router.get('/', (req, res) => { + try { + const dirs = fs.readdirSync(DOCS_BASE, { withFileTypes: true }) + .filter(d => d.isDirectory()) + .map(d => { + const files = fs.readdirSync(path.join(DOCS_BASE, d.name)) + .filter(f => f.endsWith('.md')); + return { proyecto: d.name, archivos: files }; + }); + res.json(dirs); + } catch (err) { + res.status(500).json({ error: 'Error listando docs' }); + } + }); + + return router; +}; diff --git a/servicios/nginx/dtic-bitacoras/backend/src/routes/entradas.js b/servicios/nginx/dtic-bitacoras/backend/src/routes/entradas.js index df5daca5..ce0d0cfd 100644 --- a/servicios/nginx/dtic-bitacoras/backend/src/routes/entradas.js +++ b/servicios/nginx/dtic-bitacoras/backend/src/routes/entradas.js @@ -1,39 +1,77 @@ module.exports = (pool) => { const router = require('express').Router(); - // GET /api/entradas — Listar entradas (filtro por bitacora_id o nodo_id) + // GET /api/entradas — Listar entradas (filtro por bitacora_id o nodo_id o ambito_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 { bitacora_id, nodo_id, ambito_id } = req.query; + let query = ` + SELECT e.*, n.nombre as nodo_nombre, a.nombre as ambito_nombre + FROM bitacoras.entradas e + LEFT JOIN bitacoras.nodos n ON e.nodo_id = n.id + LEFT JOIN bitacoras.ambitos a ON e.ambito_id = a.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}`; } + if (ambito_id) { params.push(ambito_id); query += ` AND e.ambito_id = $${params.length}`; } + query += ' ORDER BY e.inicio DESC'; - const { rows } = await pool.query(query, params); - res.json(rows); + try { + const { rows } = await pool.query(query, params); + res.json(rows); + } catch (err) { + res.status(500).json({ error: err.message }); + } }); // 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]); + const { inicio, fin, descripcion, estado, modo, es_ia, bitacora_id, nodo_id, ambito_id, doc_path } = req.body; + try { + const { rows } = await pool.query(` + INSERT INTO bitacoras.entradas (inicio, fin, descripcion, estado, modo, es_ia, bitacora_id, nodo_id, ambito_id, doc_path) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING * + `, [inicio, fin || null, descripcion, estado || '⏳', modo || 'P', es_ia || false, bitacora_id, nodo_id || null, ambito_id || null, doc_path || null]); + res.status(201).json(rows[0]); + } catch (err) { + res.status(500).json({ error: err.message }); + } }); // 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]); + const { inicio, fin, descripcion, estado, modo, es_ia, nodo_id, ambito_id, doc_path } = req.body; + try { + // Construir SET dinámico para evitar sobreescribir nulos si no vienen en la petición + let updateFields = []; + let params = []; + let pIdx = 1; + + if (inicio !== undefined) { updateFields.push(`inicio=$${pIdx++}`); params.push(inicio); } + if (fin !== undefined) { updateFields.push(`fin=$${pIdx++}`); params.push(fin); } + if (descripcion !== undefined) { updateFields.push(`descripcion=$${pIdx++}`); params.push(descripcion); } + if (estado !== undefined) { updateFields.push(`estado=$${pIdx++}`); params.push(estado); } + if (modo !== undefined) { updateFields.push(`modo=$${pIdx++}`); params.push(modo); } + if (es_ia !== undefined) { updateFields.push(`es_ia=$${pIdx++}`); params.push(es_ia); } + if (nodo_id !== undefined) { updateFields.push(`nodo_id=$${pIdx++}`); params.push(nodo_id); } + if (ambito_id !== undefined) { updateFields.push(`ambito_id=$${pIdx++}`); params.push(ambito_id); } + if (doc_path !== undefined) { updateFields.push(`doc_path=$${pIdx++}`); params.push(doc_path); } + + if (updateFields.length === 0) return res.status(400).json({ error: 'No data to update' }); + + params.push(id); + const { rows } = await pool.query(` + UPDATE bitacoras.entradas SET ${updateFields.join(', ')} + WHERE id=$${pIdx} RETURNING * + `, params); + + if (rows.length === 0) return res.status(404).json({ error: 'Entrada no encontrada' }); + res.json(rows[0]); + } catch (err) { + res.status(500).json({ error: err.message }); + } }); // DELETE /api/entradas/:id — Eliminar entrada diff --git a/servicios/nginx/dtic-bitacoras/backend/src/routes/gestion.js b/servicios/nginx/dtic-bitacoras/backend/src/routes/gestion.js index 38c10812..1e73bb11 100644 --- a/servicios/nginx/dtic-bitacoras/backend/src/routes/gestion.js +++ b/servicios/nginx/dtic-bitacoras/backend/src/routes/gestion.js @@ -2,17 +2,30 @@ module.exports = (pool) => { const router = require('express').Router(); - // GET /api/gestion — Listar activos (para rollover) o por bitácora + // GET /api/gestion — Listar activos (para rollover) o por bitácora (opcional filtro ámbito) router.get('/', async (req, res) => { - const { bitacora_id, activos } = req.query; - let query = 'SELECT g.*, n.nombre as nodo_nombre FROM bitacoras.gestion g LEFT JOIN bitacoras.nodos n ON g.nodo_id = n.id'; + const { bitacora_id, activos, ambito_id } = req.query; + let query = ` + SELECT g.*, n.nombre as nodo_nombre, a.nombre as ambito_nombre + FROM bitacoras.gestion g + LEFT JOIN bitacoras.nodos n ON g.nodo_id = n.id + LEFT JOIN bitacoras.ambitos a ON n.ambito_id = a.id + `; const params = []; + let whereUsed = false; if (activos === 'true') { query += ' WHERE g.activo = true'; + whereUsed = true; } else if (bitacora_id) { query += ' WHERE g.bitacora_id = $1'; params.push(bitacora_id); + whereUsed = true; + } + + if (ambito_id) { + params.push(ambito_id); + query += (whereUsed ? ' AND' : ' WHERE') + ` (n.ambito_id = $${params.length})`; } query += ' ORDER BY g.tipo, g.orden, g.created_at DESC'; diff --git a/servicios/nginx/dtic-bitacoras/backend/src/routes/nodos.js b/servicios/nginx/dtic-bitacoras/backend/src/routes/nodos.js index 5f1fb173..84c1b14a 100644 --- a/servicios/nginx/dtic-bitacoras/backend/src/routes/nodos.js +++ b/servicios/nginx/dtic-bitacoras/backend/src/routes/nodos.js @@ -3,30 +3,56 @@ module.exports = (pool) => { // 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); + const { ambito_id } = req.query; + let query = ` + SELECT n.*, a.nombre as ambito_nombre + FROM bitacoras.nodos n + LEFT JOIN bitacoras.ambitos a ON n.ambito_id = a.id + WHERE n.activo = true + `; + const params = []; + if (ambito_id) { + params.push(ambito_id); + query += ` AND n.ambito_id = $${params.length}`; + } + query += ' ORDER BY n.nombre'; + + try { + const { rows } = await pool.query(query, params); + res.json(rows); + } catch (err) { + res.status(500).json({ error: err.message }); + } }); // 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]); + const { nombre, ip, tipo, descripcion, ambito_id } = req.body; + try { + const { rows } = await pool.query( + 'INSERT INTO bitacoras.nodos (nombre, ip, tipo, descripcion, ambito_id) VALUES ($1, $2, $3, $4, $5) RETURNING *', + [nombre, ip, tipo, descripcion, ambito_id || null] + ); + res.status(201).json(rows[0]); + } catch (err) { + res.status(500).json({ error: err.message }); + } }); // PUT /api/nodos/:id — Actualizar nodo router.put('/:id', async (req, res) => { const { id } = req.params; - const { nombre, ip, tipo, descripcion, activo } = req.body; - const { rows } = await pool.query( - 'UPDATE bitacoras.nodos SET nombre=$1, ip=$2, tipo=$3, descripcion=$4, activo=$5 WHERE id=$6 RETURNING *', - [nombre, ip, tipo, descripcion, activo, id] - ); - if (rows.length === 0) return res.status(404).json({ error: 'Nodo no encontrado' }); - res.json(rows[0]); + const { nombre, ip, tipo, descripcion, activo, ambito_id } = req.body; + try { + const { rows } = await pool.query( + 'UPDATE bitacoras.nodos SET nombre=$1, ip=$2, tipo=$3, descripcion=$4, activo=$5, ambito_id=$6 WHERE id=$7 RETURNING *', + [nombre, ip, tipo, descripcion, activo, ambito_id || null, id] + ); + if (rows.length === 0) return res.status(404).json({ error: 'Nodo no encontrado' }); + res.json(rows[0]); + } catch (err) { + res.status(500).json({ error: err.message }); + } }); // DELETE /api/nodos/:id — Eliminar nodo (lógico o físico si no tiene entradas asociadas) diff --git a/servicios/nginx/dtic-bitacoras/backend/src/server.js b/servicios/nginx/dtic-bitacoras/backend/src/server.js index f41dd4d9..3cd9937d 100644 --- a/servicios/nginx/dtic-bitacoras/backend/src/server.js +++ b/servicios/nginx/dtic-bitacoras/backend/src/server.js @@ -25,12 +25,14 @@ app.get('/health', async (req, res) => { }); // Routes +app.use('/api/ambitos', require('./routes/ambitos')(pool)); app.use('/api/nodos', require('./routes/nodos')(pool)); app.use('/api/bitacoras', require('./routes/bitacoras')(pool)); app.use('/api/entradas', require('./routes/entradas')(pool)); app.use('/api/proyectos', require('./routes/proyectos')(pool)); app.use('/api/gestion', require('./routes/gestion')(pool)); app.use('/api/resumen', require('./routes/resumen')(pool)); +app.use('/api/docs', require('./routes/docs')()); // Error handler app.use((err, req, res, next) => { diff --git a/servicios/nginx/dtic-bitacoras/docker-compose.yml b/servicios/nginx/dtic-bitacoras/docker-compose.yml index b6b75d9d..e4f256bd 100644 --- a/servicios/nginx/dtic-bitacoras/docker-compose.yml +++ b/servicios/nginx/dtic-bitacoras/docker-compose.yml @@ -36,6 +36,7 @@ services: condition: service_healthy volumes: - ./backend/src:/app/src + - ../../../docs/proy:/app/docs/proy:ro frontend: build: ./frontend @@ -49,6 +50,7 @@ services: - api volumes: - ./frontend/src:/app/src + - ./frontend/vite.config.ts:/app/vite.config.ts volumes: postgres_data: diff --git a/servicios/nginx/dtic-bitacoras/docker/init.sql b/servicios/nginx/dtic-bitacoras/docker/init.sql index b438c06b..fec49a88 100644 --- a/servicios/nginx/dtic-bitacoras/docker/init.sql +++ b/servicios/nginx/dtic-bitacoras/docker/init.sql @@ -46,6 +46,7 @@ CREATE TABLE bitacoras.entradas ( estado VARCHAR(10) DEFAULT '⏳', modo CHAR(1) DEFAULT 'P' CHECK (modo IN ('P', 'R')), es_ia BOOLEAN DEFAULT false, + doc_path TEXT, 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), diff --git a/servicios/nginx/dtic-bitacoras/frontend/src/App.tsx b/servicios/nginx/dtic-bitacoras/frontend/src/App.tsx index 3e0183db..fdc92065 100644 --- a/servicios/nginx/dtic-bitacoras/frontend/src/App.tsx +++ b/servicios/nginx/dtic-bitacoras/frontend/src/App.tsx @@ -3,6 +3,7 @@ import React, { useState, useEffect, useCallback } from 'react'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import ProyectoDashboard from './pages/ProyectoDashboard'; +import AmbitoDashboard from './pages/AmbitoDashboard'; // Constantes estáticas para evitar parpadeos por cambio de referencia en cada render. const MARKDOWN_PLUGINS = [remarkGfm]; @@ -51,6 +52,18 @@ interface Nodo { ip: string; tipo: string; descripcion: string; + ambito_id: number | null; + ambito_nombre?: string; + activo: boolean; +} + +interface Ambito { + id: number; + nombre: string; + descripcion: string; + parent_id: number | null; + parent_nombre?: string; + count_nodos?: number; activo: boolean; } @@ -75,7 +88,7 @@ function ConfirmModal({ show, title, message, onConfirm, onCancel }: { } // ===== TABS ===== -type Tab = 'bitacora' | 'nodos' | 'proyecto'; +type Tab = 'bitacora' | 'nodos' | 'ambitos' | 'proyecto'; function getInitialTab(): Tab { const path = window.location.pathname.toLowerCase(); @@ -87,17 +100,17 @@ function getInitialTab(): Tab { function App() { const [tab, setTab] = useState(getInitialTab()); - const changeTab = (t: Tab) => { + const changeTab = React.useCallback((t: Tab) => { setTab(t); const base = '/bitacoras/'; - // Asegurar que las rutas incluyan el prefijo correcto para el ruteo de NGINX const paths: Record = { bitacora: base, nodos: base + 'nodos', + ambitos: base + 'ambitos', proyecto: base + 'p2601' }; window.history.replaceState(null, '', paths[t]); - }; + }, []); const [fecha, setFecha] = useState(new Date().toISOString().split('T')[0]); const [bitacora, setBitacora] = useState(null); const [nodos, setNodos] = useState([]); @@ -129,19 +142,73 @@ function App() { // Confirm delete const [confirmDelete, setConfirmDelete] = useState<{ type: string; id: number; name: string } | null>(null); - // Nodo form + // Ámbitos + const [ambitos, setAmbitos] = useState([]); + + // Form states const [nodoFormVisible, setNodoFormVisible] = useState(false); const [editNodoId, setEditNodoId] = useState(null); const [nodoNombre, setNodoNombre] = useState(''); const [nodoIp, setNodoIp] = useState(''); const [nodoTipo, setNodoTipo] = useState('servidor'); const [nodoDesc, setNodoDesc] = useState(''); + const [nodoAmbitoId, setNodoAmbitoId] = useState(''); + + const [ambitoFormVisible, setAmbitoFormVisible] = useState(false); + const [editAmbitoId, setEditAmbitoId] = useState(null); + const [ambitoNombre, setAmbitoNombre] = useState(''); + const [ambitoDesc, setAmbitoDesc] = useState(''); + const [ambitoParentId, setAmbitoParentId] = useState(''); + const [activeAmbitoId, setActiveAmbitoId] = useState(null); + const [globalStats, setGlobalStats] = useState<{ nombre: string, count_nodos: number, count_entradas: number }[]>([]); + + // Doc viewer modal + const [docModal, setDocModal] = useState<{ show: boolean; title: string; content: string; loading: boolean }>({ show: false, title: '', content: '', loading: false }); + + const openDocModal = React.useCallback(async (proyecto: string, archivo: string) => { + setDocModal({ show: true, title: archivo.replace('.md', ''), content: '', loading: true }); + try { + const res = await fetch(`${API_URL}/docs/${proyecto}/${archivo}`); + const data = await res.json(); + if (data.content) { + setDocModal({ show: true, title: archivo.replace('.md', ''), content: data.content, loading: false }); + } else { + setDocModal({ show: true, title: 'Error', content: `No se pudo cargar el archivo: ${data.error || 'desconocido'}`, loading: false }); + } + } catch (err) { + setDocModal({ show: true, title: 'Error', content: 'Error de conexión al cargar el documento', loading: false }); + } + }, []); // === MARKDOWN COMPONENTS (MEMOIZED TO AVOID FLICKER) === const markdownComponents = React.useMemo(() => ({ a: ({ node, ...props }: any) => { const url = props.href || ''; + // Si es un link a un .md en ../proy/, abrir modal de documento + if (url.includes('../proy/') && url.endsWith('.md')) { + const parts = url.split('/'); + const archivo = parts.pop() || ''; + const proyecto = parts.pop() || ''; + + return ( + { + e.preventDefault(); + e.stopPropagation(); + openDocModal(proyecto, archivo); + }} + className="desc-link" + title={`Ver ${archivo} en la web`} + style={{ cursor: 'pointer', borderBottom: '1px dashed var(--primary)' }} + > + {props.children} + + ); + } + // Si es un link a un proyecto del ADN, interceptamos para navegación SPA if (url.startsWith('../proyectos/')) { const projectFile = url.split('/').pop() || ''; @@ -153,7 +220,7 @@ function App() { { + onClick={(e: React.MouseEvent) => { e.preventDefault(); setSelectedProyecto(projectCode); changeTab('proyecto'); @@ -176,7 +243,7 @@ function App() { target="_blank" rel="noopener noreferrer" className="desc-link" - onClick={(e) => e.stopPropagation()} + onClick={(e: React.MouseEvent) => e.stopPropagation()} > {props.children} @@ -205,7 +272,7 @@ function App() { ); } - }), [changeTab, setSelectedProyecto]); + }), []); // All deps (changeTab, setSelectedProyecto, openDocModal) are stable useCallbacks/setState // === HELPERS === const renderDescription = (text: string) => { @@ -233,12 +300,29 @@ function App() { setLoading(false); }, [fecha, bitacora]); - const fetchNodos = async () => { + const fetchNodos = useCallback(async () => { try { const res = await fetch(`${API_URL}/nodos`); - setNodos(await res.json()); - } catch (err) { console.error('Error:', err); } - }; + const data = await res.json(); + setNodos(data); + } catch (err) { console.error('Error nodes:', err); } + }, []); + + const fetchAmbitos = useCallback(async () => { + try { + const res = await fetch(`${API_URL}/ambitos`); + const data = await res.json(); + setAmbitos(data); + } catch (err) { console.error('Error ambitos:', err); } + }, []); + + const fetchGlobalStats = useCallback(async () => { + try { + const res = await fetch(`${API_URL}/ambitos/stats/global`); + const data = await res.json(); + setGlobalStats(data); + } catch (err) { console.error('Error global stats:', err); } + }, []); const fetchFechasActivas = async () => { try { @@ -325,17 +409,23 @@ function App() { if (!newVersion) { fetchBitacora(); fetchFechasActivas(); + fetchNodos(); + fetchAmbitos(); + fetchGlobalStats(); return; } if (!lastVersion || newVersion.max_id !== lastVersion.max_id || Number(newVersion.count) !== Number(lastVersion.count)) { setLastVersion(newVersion); fetchBitacora(); fetchFechasActivas(); + fetchNodos(); + fetchAmbitos(); + fetchGlobalStats(); } - }, [fetchBitacoraVersion, fetchBitacora, fetchFechasActivas, lastVersion]); + }, [fetchBitacoraVersion, fetchBitacora, fetchFechasActivas, lastVersion, fetchNodos, fetchAmbitos, fetchGlobalStats]); useEffect(() => { fetchBitacora(); }, [fetchBitacora]); - useEffect(() => { fetchNodos(); fetchProyectos(); }, []); + useEffect(() => { fetchNodos(); fetchProyectos(); fetchAmbitos(); fetchGlobalStats(); }, []); useEffect(() => { fetchFechasActivas(); }, [fetchFechasActivas]); // Inicializar versión al cargar @@ -407,8 +497,8 @@ function App() { const startEdit = (e: Entrada) => { setEditId(e.id); - setEditInicio(e.inicio?.substring(0, 5) || ''); - setEditFin(e.fin ? e.fin.substring(0, 5) : ''); + setEditInicio(e.inicio?.substring(0, 8) || ''); + setEditFin(e.fin ? e.fin.substring(0, 8) : ''); setEditDesc(e.descripcion); setEditEstado(e.estado); setEditModo(e.modo); @@ -444,13 +534,16 @@ function App() { } else if (type === 'nodo') { await fetch(`${API_URL}/nodos/${id}`, { method: 'DELETE' }); fetchNodos(); + } else if (type === 'ambito') { + await fetch(`${API_URL}/ambitos/${id}`, { method: 'DELETE' }); + fetchAmbitos(); } setConfirmDelete(null); }; // === NODOS CRUD === const resetNodoForm = () => { - setEditNodoId(null); setNodoNombre(''); setNodoIp(''); setNodoTipo('servidor'); setNodoDesc(''); + setEditNodoId(null); setNodoNombre(''); setNodoIp(''); setNodoTipo('servidor'); setNodoDesc(''); setNodoAmbitoId(''); setNodoFormVisible(false); }; @@ -458,8 +551,10 @@ function App() { if (nodo) { setEditNodoId(nodo.id); setNodoNombre(nodo.nombre); setNodoIp(nodo.ip || ''); setNodoTipo(nodo.tipo || 'servidor'); setNodoDesc(nodo.descripcion || ''); + setNodoAmbitoId(nodo.ambito_id?.toString() || ''); } else { setEditNodoId(null); setNodoNombre(''); setNodoIp(''); setNodoTipo('servidor'); setNodoDesc(''); + setNodoAmbitoId(''); } setNodoFormVisible(true); }; @@ -467,18 +562,22 @@ function App() { const saveNodo = async (e: React.FormEvent) => { e.preventDefault(); if (!nodoNombre) return; + const payload = { + nombre: nodoNombre, ip: nodoIp, tipo: nodoTipo, descripcion: nodoDesc, + ambito_id: nodoAmbitoId ? parseInt(nodoAmbitoId) : null + }; if (editNodoId) { const existing = nodos.find(n => n.id === editNodoId); await fetch(`${API_URL}/nodos/${editNodoId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ nombre: nodoNombre, ip: nodoIp, tipo: nodoTipo, descripcion: nodoDesc, activo: existing?.activo ?? true }) + body: JSON.stringify({ ...payload, activo: existing?.activo ?? true }) }); } else { await fetch(`${API_URL}/nodos`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ nombre: nodoNombre, ip: nodoIp, tipo: nodoTipo, descripcion: nodoDesc }) + body: JSON.stringify(payload) }); } resetNodoForm(); @@ -489,6 +588,51 @@ function App() { setConfirmDelete({ type: 'nodo', id, name: nombre }); }; + // === AMBITOS CRUD === + const resetAmbitoForm = () => { + setEditAmbitoId(null); setAmbitoNombre(''); setAmbitoDesc(''); setAmbitoParentId(''); + setAmbitoFormVisible(false); + }; + + const openAmbitoForm = (ambito?: Ambito) => { + if (ambito) { + setEditAmbitoId(ambito.id); setAmbitoNombre(ambito.nombre); setAmbitoDesc(ambito.descripcion || ''); + setAmbitoParentId(ambito.parent_id?.toString() || ''); + } else { + setEditAmbitoId(null); setAmbitoNombre(''); setAmbitoDesc(''); setAmbitoParentId(''); + } + setAmbitoFormVisible(true); + }; + + const saveAmbito = async (e: React.FormEvent) => { + e.preventDefault(); + if (!ambitoNombre) return; + const payload = { + nombre: ambitoNombre, descripcion: ambitoDesc, + parent_id: ambitoParentId ? parseInt(ambitoParentId) : null + }; + if (editAmbitoId) { + await fetch(`${API_URL}/ambitos/${editAmbitoId}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + } else { + await fetch(`${API_URL}/ambitos`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + } + resetAmbitoForm(); + fetchAmbitos(); + fetchNodos(); // Los nodos pueden haber cambiado su ambito_nombre + }; + + const requestDeleteAmbito = (id: number, nombre: string) => { + setConfirmDelete({ type: 'ambito', id, name: nombre }); + }; + // === STATS === const totalEntradas = bitacora?.nodos?.reduce((sum, n) => sum + n.entradas.length, 0) || 0; const totalNodos = bitacora?.nodos?.length || 0; @@ -504,6 +648,31 @@ function App() { onCancel={() => setConfirmDelete(null)} /> + {/* Modal visor de documentos .md */} + {docModal.show && ( +
setDocModal({ ...docModal, show: false })}> +
e.stopPropagation()} style={{ maxWidth: '800px', maxHeight: '85vh', width: '90%', display: 'flex', flexDirection: 'column' }}> +
+ {docModal.title} + +
+
+ {docModal.loading ? ( +
+ ) : ( +
+ + {docModal.content} + +
+ )} +
+
+
+ )} +