docs: Plan P2603.01 Automatización Zoom y actualizaciones P2601.09
This commit is contained in:
@@ -4,24 +4,21 @@ const path = require('path');
|
||||
module.exports = () => {
|
||||
const router = require('express').Router();
|
||||
|
||||
const DOCS_BASE = path.resolve('/app/docs/proy');
|
||||
const DOCS_BASE = path.resolve('/app/docs');
|
||||
|
||||
// GET /api/docs/:proyecto/:archivo — Servir .md crudo
|
||||
// Ejemplo: /api/docs/P2601_Dasuten/P2601.09_DASUTEN-sin-DC.md
|
||||
router.get('/:proyecto/:archivo', (req, res) => {
|
||||
const { proyecto, archivo } = req.params;
|
||||
// GET /api/docs/* — Servir .md crudo
|
||||
// Ejemplo: /api/docs/proy/P2601_Dasuten/P2601.09_DASUTEN-sin-DC.md
|
||||
router.get('/*', (req, res) => {
|
||||
if (req.path === '/') return; // Se maneja abajo
|
||||
|
||||
const fileRelPath = req.params[0];
|
||||
|
||||
// Sanitizar: solo permitir caracteres seguros
|
||||
if (!/^[A-Za-z0-9._-]+$/.test(proyecto) || !/^[A-Za-z0-9._-]+$/.test(archivo)) {
|
||||
return res.status(400).json({ error: 'Nombre de archivo inválido' });
|
||||
// Sanitizar y verificar extensión
|
||||
if (!fileRelPath || !fileRelPath.endsWith('.md') || fileRelPath.includes('..')) {
|
||||
return res.status(400).json({ error: 'Ruta de archivo inválida' });
|
||||
}
|
||||
|
||||
// Solo permitir .md
|
||||
if (!archivo.endsWith('.md')) {
|
||||
return res.status(400).json({ error: 'Solo se permiten archivos .md' });
|
||||
}
|
||||
|
||||
const filePath = path.join(DOCS_BASE, proyecto, archivo);
|
||||
const filePath = path.join(DOCS_BASE, fileRelPath);
|
||||
|
||||
// Verificar que no escapa del directorio base
|
||||
if (!filePath.startsWith(DOCS_BASE)) {
|
||||
@@ -38,8 +35,9 @@ module.exports = () => {
|
||||
}
|
||||
|
||||
res.json({
|
||||
proyecto,
|
||||
archivo,
|
||||
doc_path: fileRelPath,
|
||||
proyecto: path.basename(path.dirname(fileRelPath)),
|
||||
archivo: path.basename(fileRelPath),
|
||||
content,
|
||||
lastModified: fs.statSync(filePath).mtime.toISOString()
|
||||
});
|
||||
|
||||
@@ -165,10 +165,11 @@ function App() {
|
||||
// Doc viewer modal
|
||||
const [docModal, setDocModal] = useState<{ show: boolean; title: string; content: string; loading: boolean }>({ show: false, title: '', content: '', loading: false });
|
||||
|
||||
const openDocModal = React.useCallback(async (proyecto: string, archivo: string) => {
|
||||
const openDocModal = React.useCallback(async (fileRelPath: string) => {
|
||||
const archivo = fileRelPath.split('/').pop() || '';
|
||||
setDocModal({ show: true, title: archivo.replace('.md', ''), content: '', loading: true });
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/docs/${proyecto}/${archivo}`);
|
||||
const res = await fetch(`${API_URL}/docs/${fileRelPath}`);
|
||||
const data = await res.json();
|
||||
if (data.content) {
|
||||
setDocModal({ show: true, title: archivo.replace('.md', ''), content: data.content, loading: false });
|
||||
@@ -185,11 +186,12 @@ function App() {
|
||||
a: ({ node, ...props }: any) => {
|
||||
const url = props.href || '';
|
||||
|
||||
// Si es un link a un .md en ../proy/, abrir modal de documento
|
||||
if (url.includes('../proy/') && url.endsWith('.md')) {
|
||||
const parts = url.split('/');
|
||||
const archivo = parts.pop() || '';
|
||||
const proyecto = parts.pop() || '';
|
||||
// Si es un link a un .md en ../proy/ o ../ambito/, abrir modal de documento
|
||||
if ((url.includes('../proy/') || url.includes('../ambito/')) && url.endsWith('.md')) {
|
||||
// Obtener todo lo que viene después de "docs/" basándonos en si fue proy/ o ambito/
|
||||
const linkType = url.includes('../proy/') ? '../proy/' : '../ambito/';
|
||||
const docsIndex = url.indexOf(linkType) + 3; // +3 skips "../" so we start at "proy/" or "ambito/"
|
||||
const fileRelPath = url.substring(docsIndex); // ex: proy/P2601_Dasuten/P26...
|
||||
|
||||
return (
|
||||
<a
|
||||
@@ -198,10 +200,10 @@ function App() {
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
openDocModal(proyecto, archivo);
|
||||
openDocModal(fileRelPath);
|
||||
}}
|
||||
className="desc-link"
|
||||
title={`Ver ${archivo} en la web`}
|
||||
title={`Ver ${url.split('/').pop()} en la web`}
|
||||
style={{ cursor: 'pointer', borderBottom: '1px dashed var(--primary)' }}
|
||||
>
|
||||
{props.children}
|
||||
@@ -879,23 +881,29 @@ function App() {
|
||||
|
||||
{/* Agrupar entradas por ÁMBITO en lugar de por nodo */}
|
||||
{(() => {
|
||||
// Agrupar por ámbito
|
||||
const porAmbito = {};
|
||||
// Agrupar por ámbito a nivel de cada entrada
|
||||
const porAmbito: Record<string, any> = {};
|
||||
bitacora?.nodos?.forEach(nodoGroup => {
|
||||
const ambito = nodoGroup.ambito_nombre || 'Sin Ámbito';
|
||||
if (!porAmbito[ambito]) {
|
||||
porAmbito[ambito] = {
|
||||
ambito: ambito,
|
||||
nodos: new Set(),
|
||||
entradas: [],
|
||||
resumenes: []
|
||||
};
|
||||
}
|
||||
porAmbito[ambito].entradas.push(...nodoGroup.entradas);
|
||||
porAmbito[ambito].nodos.add(nodoGroup.nodo);
|
||||
if (nodoGroup.resumen) {
|
||||
porAmbito[ambito].resumenes.push({ nodo: nodoGroup.nodo, resumen: nodoGroup.resumen });
|
||||
// Si el nodo tiene resumen genérico, lo guardamos en su primer ámbito disponible o "Global"
|
||||
const defaultAmbito = nodoGroup.entradas.length > 0 ? (nodoGroup.entradas[0].ambito_nombre || nodoGroup.ambito_nombre || 'Sin Ámbito') : 'Sin Ámbito';
|
||||
if (!porAmbito[defaultAmbito]) porAmbito[defaultAmbito] = { ambito: defaultAmbito, nodos: new Set(), entradas: [], resumenes: [] };
|
||||
porAmbito[defaultAmbito].resumenes.push({ nodo: nodoGroup.nodo, resumen: nodoGroup.resumen });
|
||||
}
|
||||
|
||||
nodoGroup.entradas.forEach(e => {
|
||||
const ambito = e.ambito_nombre || nodoGroup.ambito_nombre || 'Sin Ámbito';
|
||||
if (!porAmbito[ambito]) {
|
||||
porAmbito[ambito] = {
|
||||
ambito: ambito,
|
||||
nodos: new Set(),
|
||||
entradas: [],
|
||||
resumenes: []
|
||||
};
|
||||
}
|
||||
porAmbito[ambito].entradas.push(e);
|
||||
porAmbito[ambito].nodos.add(nodoGroup.nodo || e.nodo_nombre);
|
||||
});
|
||||
});
|
||||
|
||||
return Object.values(porAmbito).map((ambitoGroup, idx) => (
|
||||
|
||||
Reference in New Issue
Block a user