[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:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user