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
@@ -42,15 +42,45 @@ module.exports = (pool) => {
ORDER BY e.inicio DESC
`, [bitacora.id]);
// Obtener gestión asociada a esta bitácora (fija) + activos globales (rollover)
const { rows: gestion } = await pool.query(`
SELECT g.*, n.nombre as nodo_nombre
FROM bitacoras.gestion g
LEFT JOIN bitacoras.nodos n ON g.nodo_id = n.id
WHERE g.bitacora_id = $1 OR g.activo = true
ORDER BY g.tipo, g.orden
`, [bitacora.id]);
// Obtener resúmenes por nodo
const { rows: resúmenes } = await pool.query(`
SELECT r.*, n.nombre as nodo_nombre
FROM bitacoras.resumen_nodos r
JOIN bitacoras.nodos n ON r.nodo_id = n.id
WHERE r.bitacora_id = $1
`, [bitacora.id]);
// Agrupar por nodo
const porNodo = {};
entradas.forEach(e => {
const nodo = e.nodo_nombre || 'sin-nodo';
if (!porNodo[nodo]) porNodo[nodo] = { nodo: nodo, ip: e.nodo_ip, entradas: [] };
if (!porNodo[nodo]) porNodo[nodo] = { nodo: nodo, ip: e.nodo_ip, entradas: [], resumen: '' };
porNodo[nodo].entradas.push(e);
});
res.json({ ...bitacora, nodos: Object.values(porNodo) });
// Adjuntar resúmenes a la agrupación por nodo
resúmenes.forEach(r => {
if (porNodo[r.nodo_nombre]) {
porNodo[r.nodo_nombre].resumen = r.resumen;
} else {
porNodo[r.nodo_nombre] = { nodo: r.nodo_nombre, entradas: [], resumen: r.resumen };
}
});
res.json({
...bitacora,
nodos: Object.values(porNodo),
gestion: gestion
});
});
// GET /api/bitacoras/:fecha/export — Exportar a Markdown
@@ -61,17 +91,41 @@ module.exports = (pool) => {
const bitacora = bResult.rows[0];
const { rows: entradas } = await pool.query(`
SELECT e.*, n.nombre as nodo_nombre
FROM bitacoras.entradas e
LEFT JOIN bitacoras.nodos n ON e.nodo_id = n.id
WHERE e.bitacora_id = $1
ORDER BY n.nombre, e.inicio DESC
`, [bitacora.id]);
SELECT e.*, n.nombre as nodo_nombre FROM bitacoras.entradas e
LEFT JOIN bitacoras.nodos n ON e.nodo_id = n.id
WHERE e.bitacora_id = $1 ORDER BY n.nombre, e.inicio ASC
`, [bitacora.id]);
const { rows: gestion } = await pool.query(`
SELECT g.*, n.nombre as nodo_nombre FROM bitacoras.gestion g
LEFT JOIN bitacoras.nodos n ON g.nodo_id = n.id
WHERE g.bitacora_id = $1 OR g.activo = true ORDER BY g.tipo, g.orden
`, [bitacora.id]);
const { rows: resumenes } = await pool.query(`
SELECT r.*, n.nombre as nodo_nombre FROM bitacoras.resumen_nodos r
JOIN bitacoras.nodos n ON r.nodo_id = n.id WHERE r.bitacora_id = $1
`, [bitacora.id]);
// Generar Markdown compatible con ADN
const [y, m, d] = fecha.split('-');
let md = `# Bitácora de Operaciones - ${d}/${m}/${y}\n\n`;
md += `## 📂 Actividades Detalladas\n\n`;
let md = `# Bitácora ${fecha}\n\n---\n\n## 📋 Control de Gestión\n\n### Pendientes\n\n| ID | Nodo | Detalle |\n| :--- | :--- | :--- |\n`;
gestion.filter(g => g.tipo === 'pendiente').forEach(g => {
md += `| ${g.id} | ${g.nodo_nombre || '—'} | ${g.detalle} |\n`;
});
md += `\n### En Proceso\n\n| ID | Nodo | Detalle |\n| :--- | :--- | :--- |\n`;
gestion.filter(g => g.tipo === 'proceso').forEach(g => {
md += `| ${g.id} | ${g.nodo_nombre || '—'} | ${g.detalle} |\n`;
});
md += `\n### Resumen de Actividades\n\n| Nodo | Resumen Integral |\n| :--- | :--- |\n`;
resumenes.forEach(r => {
md += `| ${r.nodo_nombre} | ${r.resumen} |\n`;
});
md += `\n---\n\n## 📝 Actividades Detalladas\n\n`;
const porNodo = {};
entradas.forEach(e => {
@@ -82,18 +136,18 @@ module.exports = (pool) => {
Object.entries(porNodo).forEach(([nodo, entries]) => {
md += `### ${nodo}\n\n`;
md += `| I | F | Descripción | E |\n`;
md += `| :--- | :--- | :--- | :--- |\n`;
entries.forEach(e => {
// Agrupar por tema si es necesario, o lista simple
const inicio = e.inicio ? e.inicio.substring(0, 5) : '-';
const fin = e.fin ? e.fin.substring(0, 5) : '-';
const ia = e.es_ia ? '(IA) ' : '';
const estado = `[${e.modo}] ${e.estado}`;
md += `| ${inicio} | ${fin} | ${ia}${e.descripcion} | ${estado} |\n`;
md += `| ${inicio} | ${fin} | ${e.descripcion} | ${e.modo} | ${e.estado} |\n`;
});
md += '\n';
});
md += `\n---\n<!-- 🤖 PREMISAS DE TRABAJO (IA): Fuente de Verdad → adn/05_ia.md -->\n`;
res.set('Content-Type', 'text/markdown');
res.send(md);
});
@@ -31,6 +31,7 @@ module.exports = (pool) => {
UPDATE bitacoras.entradas SET inicio=$1, fin=$2, descripcion=$3, estado=$4, modo=$5, es_ia=$6
WHERE id=$7 RETURNING *
`, [inicio, fin, descripcion, estado, modo, es_ia, id]);
if (rows.length === 0) return res.status(404).json({ error: 'Entrada no encontrada' });
res.json(rows[0]);
});
@@ -0,0 +1,53 @@
module.exports = (pool) => {
const router = require('express').Router();
// GET /api/gestion — Listar activos (para rollover) o por bitácora
router.get('/', async (req, res) => {
const { bitacora_id, activos } = req.query;
let query = 'SELECT g.*, n.nombre as nodo_nombre FROM bitacoras.gestion g LEFT JOIN bitacoras.nodos n ON g.nodo_id = n.id';
const params = [];
if (activos === 'true') {
query += ' WHERE g.activo = true';
} else if (bitacora_id) {
query += ' WHERE g.bitacora_id = $1';
params.push(bitacora_id);
}
query += ' ORDER BY g.tipo, g.orden, g.created_at DESC';
const { rows } = await pool.query(query, params);
res.json(rows);
});
// POST /api/gestion — Crear nuevo pendiente/proceso
router.post('/', async (req, res) => {
const { tipo, nodo_id, detalle, estado, bitacora_id, orden } = req.body;
const { rows } = await pool.query(
'INSERT INTO bitacoras.gestion (tipo, nodo_id, detalle, estado, bitacora_id, orden) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *',
[tipo, nodo_id || null, detalle, estado || '⏳', bitacora_id, orden || 0]
);
res.status(201).json(rows[0]);
});
// PUT /api/gestion/:id — Actualizar
router.put('/:id', async (req, res) => {
const { id } = req.params;
const { tipo, nodo_id, detalle, estado, activo, orden } = req.body;
const { rows } = await pool.query(
'UPDATE bitacoras.gestion SET tipo=$1, nodo_id=$2, detalle=$3, estado=$4, activo=$5, orden=$6, updated_at=CURRENT_TIMESTAMP WHERE id=$7 RETURNING *',
[tipo, nodo_id || null, detalle, estado, activo !== undefined ? activo : true, orden || 0, id]
);
if (rows.length === 0) return res.status(404).json({ error: 'No encontrado' });
res.json(rows[0]);
});
// DELETE /api/gestion/:id
router.delete('/:id', async (req, res) => {
const { rowCount } = await pool.query('DELETE FROM bitacoras.gestion WHERE id = $1', [req.params.id]);
if (rowCount === 0) return res.status(404).json({ error: 'No encontrado' });
res.json({ message: 'Eliminado' });
});
return router;
};
@@ -0,0 +1,32 @@
module.exports = (pool) => {
const router = require('express').Router();
// GET /api/resumen — Listar resúmenes por bitácora
router.get('/', async (req, res) => {
const { bitacora_id } = req.query;
if (!bitacora_id) return res.status(400).json({ error: 'Falta bitacora_id' });
const { rows } = await pool.query(
'SELECT r.*, n.nombre as nodo_nombre FROM bitacoras.resumen_nodos r JOIN bitacoras.nodos n ON r.nodo_id = n.id WHERE r.bitacora_id = $1',
[bitacora_id]
);
res.json(rows);
});
// POST /api/resumen — Upsert resumen por nodo
router.post('/', async (req, res) => {
const { bitacora_id, nodo_id, resumen } = req.body;
const { rows } = await pool.query(
`INSERT INTO bitacoras.resumen_nodos (bitacora_id, nodo_id, resumen)
VALUES ($1, $2, $3)
ON CONFLICT (bitacora_id, nodo_id)
DO UPDATE SET resumen = EXCLUDED.resumen, updated_at = CURRENT_TIMESTAMP
RETURNING *`,
[bitacora_id, nodo_id, resumen]
);
res.status(201).json(rows[0]);
});
return router;
};
@@ -29,6 +29,8 @@ app.use('/api/nodos', require('./routes/nodos')(pool));
app.use('/api/bitacoras', require('./routes/bitacoras')(pool));
app.use('/api/entradas', require('./routes/entradas')(pool));
app.use('/api/proyectos', require('./routes/proyectos')(pool));
app.use('/api/gestion', require('./routes/gestion')(pool));
app.use('/api/resumen', require('./routes/resumen')(pool));
// Error handler
app.use((err, req, res, next) => {