Feat(ADN): Implementado y ejecutado archivar:md para mover Markdowns obsoletos a histórico DB-First

This commit is contained in:
Ricardo Monla
2026-03-10 16:02:03 -03:00
parent 8aa1532396
commit 8d27a8958e
74 changed files with 3195 additions and 2073 deletions
@@ -20,6 +20,7 @@ interface NodoGroup {
nodo: string;
ip: string;
entradas: Entrada[];
resumen?: string;
}
interface Bitacora {
@@ -27,6 +28,16 @@ interface Bitacora {
fecha: string;
resumen: string;
nodos: NodoGroup[];
gestion: GestionItem[];
}
interface GestionItem {
id: number;
tipo: 'pendiente' | 'proceso';
nodo_nombre: string;
detalle: string;
estado: string;
activo: boolean;
}
interface Nodo {
@@ -85,6 +96,8 @@ function App() {
const [fecha, setFecha] = useState(new Date().toISOString().split('T')[0]);
const [bitacora, setBitacora] = useState<Bitacora | null>(null);
const [nodos, setNodos] = useState<Nodo[]>([]);
const [proyectos, setProyectos] = useState<{ id: number, codigo: string, nombre: string }[]>([]);
const [selectedProyecto, setSelectedProyecto] = useState<string>('P2601');
const [loading, setLoading] = useState(true);
// Form: nueva entrada
@@ -116,6 +129,71 @@ function App() {
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
if (url.startsWith('../proyectos/')) {
const projectFile = url.split('/').pop() || '';
const projectCodeMatch = projectFile.match(/^(P\d{4})/);
if (projectCodeMatch) {
const projectCode = projectCodeMatch[1];
parts.push(
<a
key={match.index}
href="#"
onClick={(e) => {
e.preventDefault();
setSelectedProyecto(projectCode);
changeTab('proyecto');
}}
className="desc-link"
title={`Ver plan ${projectCode} en la web`}
>
{label}
</a>
);
lastIndex = markdownLinkRegex.lastIndex;
continue;
}
// Si no tiene el formato PXXXX, fallback a link estático
url = '/bitacoras/docs/proyectos/' + projectFile;
}
parts.push(
<a key={match.index} href={url} target="_blank" rel="noopener noreferrer" className="desc-link">
{label}
</a>
);
lastIndex = markdownLinkRegex.lastIndex;
}
if (lastIndex < text.length) {
parts.push(text.substring(lastIndex));
}
return parts.length > 0 ? <>{parts}</> : text;
};
// === FETCH ===
const fetchBitacora = useCallback(async () => {
setLoading(true);
@@ -134,8 +212,70 @@ function App() {
} catch (err) { console.error('Error:', err); }
};
const fetchProyectos = async () => {
try {
const res = await fetch(`${API_URL}/proyectos`);
const data = await res.json();
setProyectos(data);
if (data.length > 0) {
// Al cargar por primera vez, si no hay P2601 explícito o se quiere forzar al último
setSelectedProyecto(prev => prev === 'P2601' && prev ? data[data.length - 1].codigo : prev || data[data.length - 1].codigo);
}
} catch (err) { console.error('Error:', err); }
};
const addGestion = async (tipo: 'pendiente' | 'proceso') => {
if (!bitacora) return;
const detalle = prompt(`Nuevo ${tipo}:`);
if (!detalle) return;
await fetch(`${API_URL}/gestion`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
tipo, detalle, bitacora_id: bitacora.id, estado: '⏳'
})
});
fetchBitacora();
};
const deleteGestion = async (id: number) => {
if (!confirm('¿Eliminar item de gestión?')) return;
await fetch(`${API_URL}/gestion/${id}`, { method: 'DELETE' });
fetchBitacora();
};
const updateResumenNodo = async (nodoNombre: string, current: string) => {
if (!bitacora) return;
const newResumen = prompt(`Resumen Integral para ${nodoNombre}:`, current);
if (newResumen === null) return;
const nodo = nodos.find(n => n.nombre === nodoNombre);
if (!nodo) return;
await fetch(`${API_URL}/resumen`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
bitacora_id: bitacora.id,
nodo_id: nodo.id,
resumen: newResumen
})
});
fetchBitacora();
};
const navigateProyecto = (direction: number) => {
if (proyectos.length === 0) return;
const currentIndex = proyectos.findIndex(p => p.codigo === selectedProyecto);
let nextIndex = currentIndex + direction;
if (nextIndex < 0) nextIndex = proyectos.length - 1;
if (nextIndex >= proyectos.length) nextIndex = 0;
setSelectedProyecto(proyectos[nextIndex].codigo);
};
useEffect(() => { fetchBitacora(); }, [fetchBitacora]);
useEffect(() => { fetchNodos(); }, []);
useEffect(() => { fetchNodos(); fetchProyectos(); }, []);
// === ENTRADAS CRUD ===
const handleSubmit = async (e: React.FormEvent) => {
@@ -276,7 +416,7 @@ function App() {
className={`btn btn-sm ${tab === 'proyecto' ? 'btn-primary' : ''}`}
onClick={() => changeTab('proyecto')}
style={tab !== 'proyecto' ? { background: 'var(--bg-card)', color: 'var(--text)', border: '1px solid var(--border)' } : {}}
><i className="fas fa-project-diagram"></i> P2601</button>
><i className="fas fa-project-diagram"></i> Planes</button>
{tab === 'bitacora' && (
<a href={`${API_URL}/bitacoras/${fecha}/export`} target="_blank" rel="noopener"
className="btn btn-sm btn-primary">
@@ -308,11 +448,11 @@ function App() {
{/* Stats */}
<div className="stats-bar">
<div className="stat-card">
<div className="stat-value">{totalEntradas}</div>
<div className="stat-value">{bitacora?.nodos?.reduce((s, n) => s + n.entradas.length, 0) || 0}</div>
<div className="stat-label">Entradas</div>
</div>
<div className="stat-card">
<div className="stat-value">{totalNodos}</div>
<div className="stat-value">{bitacora?.nodos?.length || 0}</div>
<div className="stat-label">Nodos Activos</div>
</div>
<div className="stat-card">
@@ -325,6 +465,58 @@ function App() {
</div>
</div>
{/* =================== CONTROL DE GESTIÓN =================== */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1.5rem', marginBottom: '2rem' }}>
<div className="card">
<div className="card-header" style={{ borderBottom: '1px solid var(--border)', display: 'flex', justifyContent: 'space-between' }}>
<span><i className="fas fa-list-ul" style={{ color: '#fbbf24' }}></i> Pendientes</span>
<button className="btn btn-sm" onClick={() => addGestion('pendiente')} style={{ padding: '0.1rem 0.4rem', fontSize: '0.7rem' }}><i className="fas fa-plus"></i></button>
</div>
<div style={{ padding: '0.5rem', minHeight: '60px' }}>
<table style={{ width: '100%', fontSize: '0.85rem' }}>
<tbody>
{bitacora?.gestion?.filter(g => g.tipo === 'pendiente').map(g => (
<tr key={g.id} style={{ borderBottom: '1px solid var(--border-light)' }}>
<td style={{ padding: '0.4rem', color: 'var(--text-secondary)', width: '20%' }}>{g.nodo_nombre || '—'}</td>
<td style={{ padding: '0.4rem' }}>{g.detalle}</td>
<td style={{ padding: '0.4rem', textAlign: 'right', width: '60px' }}>
<div style={{ display: 'flex', gap: '0.3rem', alignItems: 'center' }}>
<span>{g.estado}</span>
<button className="btn btn-sm btn-danger" onClick={() => deleteGestion(g.id)} style={{ padding: '0 0.2rem', fontSize: '0.6rem' }}><i className="fas fa-trash"></i></button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
<div className="card">
<div className="card-header" style={{ borderBottom: '1px solid var(--border)', display: 'flex', justifyContent: 'space-between' }}>
<span><i className="fas fa-spinner" style={{ color: '#3b82f6' }}></i> En Proceso</span>
<button className="btn btn-sm" onClick={() => addGestion('proceso')} style={{ padding: '0.1rem 0.4rem', fontSize: '0.7rem' }}><i className="fas fa-plus"></i></button>
</div>
<div style={{ padding: '0.5rem', minHeight: '60px' }}>
<table style={{ width: '100%', fontSize: '0.85rem' }}>
<tbody>
{bitacora?.gestion?.filter(g => g.tipo === 'proceso').map(g => (
<tr key={g.id} style={{ borderBottom: '1px solid var(--border-light)' }}>
<td style={{ padding: '0.4rem', color: 'var(--text-secondary)', width: '20%' }}>{g.nodo_nombre || '—'}</td>
<td style={{ padding: '0.4rem' }}>{g.detalle}</td>
<td style={{ padding: '0.4rem', textAlign: 'right', width: '60px' }}>
<div style={{ display: 'flex', gap: '0.3rem', alignItems: 'center' }}>
<span>{g.estado}</span>
<button className="btn btn-sm btn-danger" onClick={() => deleteGestion(g.id)} style={{ padding: '0 0.2rem', fontSize: '0.6rem' }}><i className="fas fa-trash"></i></button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
{/* New Entry Form */}
<div className="card" style={{ marginBottom: '2rem' }}>
<div className="card-header"><i className="fas fa-plus-circle"></i> Nueva Entrada</div>
@@ -362,12 +554,22 @@ function App() {
{nodoGroup.ip && <span className="nodo-ip">({nodoGroup.ip})</span>}
<span className="badge badge-gradient" style={{ marginLeft: 'auto' }}>{nodoGroup.entradas.length}</span>
</div>
<div style={{ padding: '0.8rem 1rem', background: 'rgba(102, 126, 234, 0.05)', borderBottom: '1px solid var(--border)', fontSize: '0.85rem', color: 'var(--text-secondary)' }}>
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
<i className="fas fa-quote-left" style={{ opacity: 0.5 }}></i>
<div style={{ fontStyle: 'italic', flex: 1 }}>{nodoGroup.resumen || 'Sin resumen integral registrado.'}</div>
<button className="btn btn-sm" onClick={() => updateResumenNodo(nodoGroup.nodo, nodoGroup.resumen || '')} style={{ padding: '0.1rem 0.4rem', fontSize: '0.7rem' }} title="Editar Resumen"><i className="fas fa-pen"></i></button>
</div>
</div>
<table className="table-ifde">
<thead>
<tr>
<th className="col-time">I</th>
<th className="col-time">F</th>
<th>Descripción</th>
<th className="col-j-header" style={{ width: '35px', textAlign: 'center' }}>J</th>
<th className="col-status">E</th>
<th style={{ width: '80px' }}>Acc.</th>
</tr>
@@ -381,20 +583,21 @@ function App() {
<td><input type="time" value={editFin} onChange={ev => setEditFin(ev.target.value)} className="edit-input" /></td>
<td>
<input type="text" value={editDesc} onChange={ev => setEditDesc(ev.target.value)} className="edit-input" style={{ width: '100%' }} />
<div style={{ display: 'flex', gap: '0.3rem', marginTop: '0.3rem', alignItems: 'center' }}>
<select value={editEstado} onChange={ev => setEditEstado(ev.target.value)} className="edit-input">
<option value="⏳"></option><option value="✅"></option>
<option value="⚠️"></option><option value="❌"></option><option value="👁️">👁</option>
</select>
<select value={editModo} onChange={ev => setEditModo(ev.target.value)} className="edit-input">
<option value="P">[P]</option><option value="R">[R]</option>
</select>
<label style={{ fontSize: '0.75rem', color: 'var(--text-secondary)', display: 'flex', alignItems: 'center', gap: '0.2rem' }}>
<input type="checkbox" checked={editEsIa} onChange={ev => setEditEsIa(ev.target.checked)} /> IA
</label>
</div>
<label style={{ fontSize: '0.75rem', color: 'var(--text-secondary)', display: 'flex', alignItems: 'center', gap: '0.2rem', marginTop: '0.3rem' }}>
<input type="checkbox" checked={editEsIa} onChange={ev => setEditEsIa(ev.target.checked)} /> IA
</label>
</td>
<td style={{ textAlign: 'center' }}>
<select value={editModo} onChange={ev => setEditModo(ev.target.value)} className="edit-input" style={{ width: '45px', padding: '0.35rem 0.2rem' }}>
<option value="P">P</option><option value="R">R</option><option value="S">S</option>
</select>
</td>
<td style={{ textAlign: 'center' }}>
<select value={editEstado} onChange={ev => setEditEstado(ev.target.value)} className="edit-input" style={{ width: '45px', padding: '0.35rem 0.2rem' }}>
<option value="⏳"></option><option value="✅"></option>
<option value="⚠️"></option><option value="❌"></option><option value="👁️">👁</option>
</select>
</td>
<td></td>
<td>
<div style={{ display: 'flex', gap: '0.2rem' }}>
<button className="btn btn-sm btn-success" onClick={saveEdit} title="Guardar"><i className="fas fa-check"></i></button>
@@ -409,9 +612,10 @@ function App() {
<td className="col-time">{e.fin ? e.fin.substring(0, 5) : '—'}</td>
<td className="col-desc">
{e.es_ia && <span className="ia-tag">IA</span>}
{e.descripcion}
{renderDescription(e.descripcion)}
</td>
<td className="col-status">[{e.modo}] {e.estado}</td>
<td style={{ textAlign: 'center', fontWeight: 600, color: 'var(--text-secondary)' }}>{e.modo}</td>
<td className="col-status">{e.estado}</td>
<td>
<div style={{ display: 'flex', gap: '0.2rem' }}>
<button className="btn btn-sm" onClick={() => startEdit(e)} title="Editar"
@@ -518,7 +722,38 @@ function App() {
{/* =================== TAB PROYECTO =================== */}
{tab === 'proyecto' && (
<ProyectoDashboard codigo="P2601" />
<>
<div className="date-selector" style={{ marginBottom: '1.5rem', justifyContent: 'center' }}>
<button className="btn btn-sm btn-primary" onClick={() => navigateProyecto(-1)}>
<i className="fas fa-chevron-left"></i>
</button>
<span style={{ fontSize: '0.85rem', fontWeight: 600, color: 'var(--text-secondary)' }}>
<i className="fas fa-project-diagram"></i> Plan:
</span>
<select
value={selectedProyecto}
onChange={e => setSelectedProyecto(e.target.value)}
style={{
background: 'var(--bg-card)',
color: 'var(--text)',
border: '1px solid var(--border)',
padding: '0.4rem 1rem',
borderRadius: '8px',
fontSize: '0.9rem',
fontWeight: 600,
minWidth: '200px'
}}
>
{proyectos.map(p => (
<option key={p.id} value={p.codigo}>{p.codigo} - {p.nombre}</option>
))}
</select>
<button className="btn btn-sm btn-primary" onClick={() => navigateProyecto(1)}>
<i className="fas fa-chevron-right"></i>
</button>
</div>
<ProyectoDashboard codigo={selectedProyecto} />
</>
)}
</main>
</div>