[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
@@ -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">