[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,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/ {
|
||||
|
||||
+1470
-7
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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user