[P2604] Fase 15: Optimización IA e integración de herramienta candados. Sincronización de bitácoras y actualización de planes de proyecto.

This commit is contained in:
Ricardo Monla
2026-03-19 09:52:29 -03:00
parent 183dbf6fb6
commit 5aa37e7221
138 changed files with 8777 additions and 464 deletions
+2 -1
View File
@@ -1,6 +1,7 @@
# Archivo: bitacoras.conf
# Ubicación original centralizada: /home/rmonla/Documentos/GitHub/dtic-DIIAA/servicios/nginx/conf.d/bitacoras.conf
# Ubicación: /servicios/nginx/conf.d/bitacoras.conf
# Este bloque de NGINX maneja todos los endpoints de dtic-BITACORAs
# (Fragmento sin server block - se incluye desde ns8.conf)
# P2603 - dtic-BITACORAs API
location /bitacoras/api/ {
File diff suppressed because it is too large Load Diff
@@ -9,7 +9,9 @@
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"react-markdown": "^10.1.0",
"remark-gfm": "^4.0.1"
},
"devDependencies": {
"@types/react": "^18.2.43",
@@ -18,4 +20,4 @@
"typescript": "^5.2.2",
"vite": "^5.1.0"
}
}
}
@@ -1,7 +1,12 @@
/// <reference types="vite/client" />
import React, { useState, useEffect, useCallback } from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import ProyectoDashboard from './pages/ProyectoDashboard';
// Constantes estáticas para evitar parpadeos por cambio de referencia en cada render.
const MARKDOWN_PLUGINS = [remarkGfm];
const API_URL = import.meta.env.VITE_API_URL || '/bitacoras/api';
interface Entrada {
@@ -131,36 +136,22 @@ function App() {
const [nodoIp, setNodoIp] = useState('');
const [nodoTipo, setNodoTipo] = useState('servidor');
const [nodoDesc, setNodoDesc] = useState('');
// === HELPERS ===
const renderDescription = (text: string) => {
if (!text) return '';
// Regex para links markdown: [texto](url)
const markdownLinkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
const parts: (string | JSX.Element)[] = [];
let lastIndex = 0;
let match;
while ((match = markdownLinkRegex.exec(text)) !== null) {
if (match.index > lastIndex) {
parts.push(text.substring(lastIndex, match.index));
}
const label = match[1];
let url = match[2];
// Si es un link a un proyecto, interceptamos para navegación SPA
// === MARKDOWN COMPONENTS (MEMOIZED TO AVOID FLICKER) ===
const markdownComponents = React.useMemo(() => ({
a: ({ node, ...props }: any) => {
const url = props.href || '';
// Si es un link a un proyecto del ADN, interceptamos para navegación SPA
if (url.startsWith('../proyectos/')) {
const projectFile = url.split('/').pop() || '';
const projectCodeMatch = projectFile.match(/^(P\d{4})/);
if (projectCodeMatch) {
const projectCode = projectCodeMatch[1];
parts.push(
return (
<a
key={match.index}
{...props}
href="#"
onClick={(e) => {
e.preventDefault();
@@ -170,43 +161,77 @@ function App() {
className="desc-link"
title={`Ver plan ${projectCode} en la web`}
>
{label}
{props.children}
</a>
);
lastIndex = markdownLinkRegex.lastIndex;
continue;
}
// Si no tiene el formato PXXXX, fallback a link estático
url = '/bitacoras/docs/proyectos/' + projectFile;
// Si no tiene el formato PXXXX, fallback a link estático en la carpeta de docs
props.href = '/bitacoras/docs/proyectos/' + projectFile;
}
parts.push(
<a key={match.index} href={url} target="_blank" rel="noopener noreferrer" className="desc-link">
{label}
return (
<a
{...props}
target="_blank"
rel="noopener noreferrer"
className="desc-link"
onClick={(e) => e.stopPropagation()}
>
{props.children}
</a>
);
lastIndex = markdownLinkRegex.lastIndex;
},
// Estilos para código (bloque e inline)
code: ({ node, className, children, ...props }: any) => {
const match = /language-(\w+)/.exec(className || '');
const isInline = !match;
return (
<code
{...props}
className={className}
style={{
background: isInline ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.3)',
padding: isInline ? '0.1rem 0.3rem' : '1rem',
borderRadius: '4px',
fontFamily: 'monospace',
display: isInline ? 'inline' : 'block',
overflowX: isInline ? 'initial' : 'auto',
margin: isInline ? '0' : '0.5rem 0'
}}
>
{children}
</code>
);
}
}), [changeTab, setSelectedProyecto]);
if (lastIndex < text.length) {
parts.push(text.substring(lastIndex));
}
return parts.length > 0 ? <>{parts}</> : text;
// === HELPERS ===
const renderDescription = (text: string) => {
if (!text) return '';
return (
<ReactMarkdown
remarkPlugins={MARKDOWN_PLUGINS}
components={markdownComponents as any}
>
{text}
</ReactMarkdown>
);
};
// === FETCH ===
const fetchBitacora = useCallback(async () => {
setLoading(true);
// Solo mostramos loading si es la primera carga (sin datos)
if (!bitacora) setLoading(true);
try {
const res = await fetch(`${API_URL}/bitacoras/${fecha}/completa`);
const data = await res.json();
setBitacora(data);
} catch (err) { console.error('Error:', err); }
} catch (err) { console.error('Error fetchBitacora:', err); }
setLoading(false);
}, [fecha]);
}, [fecha, bitacora]);
const fetchNodos = async () => {
try {
@@ -684,6 +709,7 @@ function App() {
<table className="table-ifde">
<thead>
<tr>
<th style={{ width: '40px', color: 'var(--text-muted)', fontSize: '0.85rem' }}>ID</th>
<th className="col-time">I</th>
<th className="col-time">F</th>
<th>Descripción</th>
@@ -697,6 +723,7 @@ function App() {
editId === e.id ? (
/* ==== FILA EN EDICIÓN ==== */
<tr key={e.id} style={{ background: 'rgba(102, 126, 234, 0.1)' }}>
<td style={{ color: 'var(--text-muted)', fontSize: '0.8rem' }}>{e.id}</td>
<td><input type="time" value={editInicio} onChange={ev => setEditInicio(ev.target.value)} className="edit-input" /></td>
<td><input type="time" value={editFin} onChange={ev => setEditFin(ev.target.value)} className="edit-input" /></td>
<td>
@@ -726,6 +753,7 @@ function App() {
) : (
/* ==== FILA NORMAL ==== */
<tr key={e.id}>
<td style={{ color: 'var(--text-muted)', fontSize: '0.8rem' }}>{e.id}</td>
<td className="col-time">{e.inicio?.substring(0, 5)}</td>
<td className="col-time">{e.fin ? e.fin.substring(0, 5) : '—'}</td>
<td className="col-desc">
@@ -1,4 +1,7 @@
/// <reference types="vite/client" />
import React, { useState, useEffect } from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
const API_URL = import.meta.env.VITE_API_URL || '/bitacoras/api';
@@ -266,7 +269,9 @@ export default function ProyectoDashboard({ codigo = 'P2601' }: { codigo?: strin
{descExpanded && (
<div style={{ padding: '0 1rem 1rem', lineHeight: 1.6, fontSize: '0.82rem', color: 'var(--text)' }}>
{proyecto.detalles_json?.contexto?.map((p, i) => (
<p key={i} style={{ margin: '0 0 0.8rem' }} dangerouslySetInnerHTML={{ __html: p.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>') }} />
<div key={i} style={{ margin: '0 0 0.8rem', lineHeight: 1.6 }}>
<ReactMarkdown remarkPlugins={[remarkGfm]}>{p}</ReactMarkdown>
</div>
))}
<div style={{
@@ -287,7 +292,9 @@ export default function ProyectoDashboard({ codigo = 'P2601' }: { codigo?: strin
<div style={subheadStyle}>👥 Responsables</div>
<ul style={listStyle}>
{proyecto.detalles_json.responsables.map((r, i) => (
<li key={i} dangerouslySetInnerHTML={{ __html: r.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>') }} />
<li key={i}>
<ReactMarkdown remarkPlugins={[remarkGfm]} components={{ p: 'span' }}>{r}</ReactMarkdown>
</li>
))}
</ul>
</>
@@ -578,7 +585,7 @@ function FaseDetailCol({ icon, title, text }: { icon: string; title: string; tex
{icon} {title}
</div>
<div style={{ fontSize: '0.76rem', color: 'var(--text-muted)', lineHeight: 1.5 }}>
{text}
<ReactMarkdown remarkPlugins={[remarkGfm]}>{text}</ReactMarkdown>
</div>
</div>
);