From 205c8f1c7c7ae958fec6dd872fc7641bed08987a Mon Sep 17 00:00:00 2001 From: snlavaise Date: Wed, 12 Aug 2026 03:17:28 +0000 Subject: [PATCH] feat: Agregar modulo de Sistema (mongosh, logs, export/import) --- server/api-mongodb.js | 120 ++++++++++++++- server/db-mongodb.js | 65 +++++++++ src/App.tsx | 11 ++ src/sections/Layout.tsx | 4 +- src/sections/SeccionSistema.tsx | 251 ++++++++++++++++++++++++++++++++ src/types/index.ts | 2 +- 6 files changed, 450 insertions(+), 3 deletions(-) create mode 100644 src/sections/SeccionSistema.tsx diff --git a/server/api-mongodb.js b/server/api-mongodb.js index 74e52a1..15a594d 100644 --- a/server/api-mongodb.js +++ b/server/api-mongodb.js @@ -17,9 +17,43 @@ import { initDb, getAllIndicaciones, createIndicacion, updateIndicacion, deleteIndicacion, getAllMovimientosIndicaciones, createMovimientoIndicacion, getAllPendientes, createPendiente, updatePendiente, deletePendiente, - getValue, setValue + getValue, setValue, + getDb, exportAllData, importAllData } from './db-mongodb.js'; +// Capture live logs in-memory +const logBuffer = []; +const originalLog = console.log; +const originalError = console.error; +const originalWarn = console.warn; + +function addLog(type, args) { + const message = args.map(arg => { + if (typeof arg === 'object') { + try { return JSON.stringify(arg); } catch { return String(arg); } + } + return String(arg); + }).join(' '); + const logEntry = `[${new Date().toISOString()}] [${type}] ${message}`; + logBuffer.push(logEntry); + if (logBuffer.length > 500) { + logBuffer.shift(); + } +} + +console.log = (...args) => { + addLog('INFO', args); + originalLog.apply(console, args); +}; +console.error = (...args) => { + addLog('ERROR', args); + originalError.apply(console, args); +}; +console.warn = (...args) => { + addLog('WARN', args); + originalWarn.apply(console, args); +}; + const app = express(); app.use(cors({ origin: true, @@ -760,6 +794,90 @@ app.put('/api/auth/update-email', async (req, res) => { } }); +// ========== ADMIN SYSTEM ENDPOINTS ========== + +app.post('/api/admin/mongosh', async (req, res) => { + try { + const { command } = req.body; + if (!command) { + return res.status(400).json({ error: 'Comando requerido' }); + } + + const dbInstance = getDb(); + if (!dbInstance) { + return res.json({ result: 'Advertencia: MongoDB no está conectado. Utilizando store en memoria. Las consultas de mongosh no están disponibles.', isMemStore: true }); + } + + const dbProxy = new Proxy(dbInstance, { + get(target, prop) { + if (typeof target[prop] !== 'undefined') { + if (typeof target[prop] === 'function') { + return target[prop].bind(target); + } + return target[prop]; + } + return target.collection(prop); + } + }); + + const { ObjectId } = await import('mongodb'); + + const evalFn = new Function('db', 'ObjectId', ` + return (async () => { + ${command.trim().includes('return') || command.trim().startsWith('{') || command.trim().includes(';') ? command : 'return (' + command + ')'} + })(); + `); + + const result = await evalFn(dbProxy, ObjectId); + res.json({ result }); + } catch (err) { + res.status(400).json({ error: err.message || String(err) }); + } +}); + +app.get('/api/admin/db/export', async (req, res) => { + try { + const dump = await exportAllData(); + res.setHeader('Content-Type', 'application/json'); + res.setHeader('Content-Disposition', 'attachment; filename=hospital_backup.json'); + res.json(dump); + } catch (err) { + res.status(500).json({ error: 'Error al exportar base de datos: ' + err.message }); + } +}); + +app.post('/api/admin/db/import', async (req, res) => { + try { + const dump = req.body; + if (!dump || typeof dump !== 'object') { + return res.status(400).json({ error: 'Formato de importación inválido' }); + } + await importAllData(dump); + res.json({ ok: true }); + } catch (err) { + res.status(500).json({ error: 'Error al importar base de datos: ' + err.message }); + } +}); + +app.get('/api/admin/logs', async (req, res) => { + try { + let fileLogs = ''; + try { + const fs = await import('fs/promises'); + fileLogs = await fs.readFile('server_log.txt', 'utf8'); + } catch { + // ignore + } + + const memoryLogs = logBuffer.join('\n'); + const combined = [fileLogs, '\n--- LOGS DE LA SESION EN VIVO ---', memoryLogs].filter(Boolean).join('\n'); + res.setHeader('Content-Type', 'text/plain; charset=utf-8'); + res.send(combined); + } catch (err) { + res.status(500).send('Error al leer logs: ' + err.message); + } +}); + // Initialize DB and start server const PORT = process.env.PORT || 4000; const HOST = process.env.HOST || '0.0.0.0'; diff --git a/server/db-mongodb.js b/server/db-mongodb.js index ba657ff..20b596e 100644 --- a/server/db-mongodb.js +++ b/server/db-mongodb.js @@ -993,3 +993,68 @@ export async function setValue(key, value) { memStore.kv[key] = value; } } + +export function getDb() { + return db; +} + +export async function exportAllData() { + const collections = [ + 'usuarios', 'pacientes', 'areas', 'camas', 'internaciones', + 'evoluciones', 'laboratorios', 'glucemias', 'acidosbase', + 'cultivos', 'estudiosComplementarios', 'interconsultas', 'atb', + 'indicaciones', 'movimientos_indicaciones', 'pendientes', 'kv' + ]; + const dump = {}; + if (db) { + for (const name of collections) { + const docs = await db.collection(name).find().toArray(); + dump[name] = docs; + } + } else { + for (const name of collections) { + dump[name] = [...(memStore[name] || [])]; + } + } + return dump; +} + +export async function importAllData(dump) { + const collections = [ + 'usuarios', 'pacientes', 'areas', 'camas', 'internaciones', + 'evoluciones', 'laboratorios', 'glucemias', 'acidosbase', + 'cultivos', 'estudiosComplementarios', 'interconsultas', 'atb', + 'indicaciones', 'movimientos_indicaciones', 'pendientes', 'kv' + ]; + if (db) { + for (const name of collections) { + if (Array.isArray(dump[name])) { + try { + await db.collection(name).deleteMany({}); + } catch (e) { + console.warn(`Could not clear collection ${name}:`, e); + } + if (dump[name].length > 0) { + const cleaned = dump[name].map(doc => { + const copy = { ...doc }; + if (copy._id) { + if (ObjectId.isValid(copy._id)) { + copy._id = new ObjectId(copy._id); + } else { + delete copy._id; + } + } + return copy; + }); + await db.collection(name).insertMany(cleaned); + } + } + } + } else { + for (const name of collections) { + if (Array.isArray(dump[name])) { + memStore[name] = [...dump[name]]; + } + } + } +} diff --git a/src/App.tsx b/src/App.tsx index d90359d..a5d7ca6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -14,6 +14,7 @@ import { EditIngreso } from '@/sections/EditIngreso'; import { Login } from '@/sections/Login'; import { GestionUsuarios } from '@/sections/GestionUsuarios'; import { PendientesSala } from '@/sections/PendientesSala'; +import { SeccionSistema } from '@/sections/SeccionSistema'; import { Spinner } from '@/components/ui/spinner'; import { Button } from '@/components/ui/button'; import { ThemeProvider } from "@/components/theme-provider"; @@ -306,6 +307,16 @@ function AppContent() { return ( ); + case 'sistema': + if (store.currentUser?.rol !== 'admin') { + return ( +
+

No tiene permisos para acceder a esta sección.

+ +
+ ); + } + return ; default: return null; } diff --git a/src/sections/Layout.tsx b/src/sections/Layout.tsx index 9ef5275..eb2a4fd 100644 --- a/src/sections/Layout.tsx +++ b/src/sections/Layout.tsx @@ -11,7 +11,8 @@ import { Sun, Moon, LogOut, - UserCog + UserCog, + Terminal } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Sheet, SheetContent, SheetTrigger, SheetTitle, SheetDescription, SheetHeader } from '@/components/ui/sheet'; @@ -59,6 +60,7 @@ export function Layout({ children, vistaActual, onCambiarVista, currentUser, onL if ((currentUser?.rol as string) === 'admin') { filteredMenuItems.push({ vista: 'usuarios', label: 'Usuarios', icon: UserCog }); + filteredMenuItems.push({ vista: 'sistema', label: 'Sistema', icon: Terminal }); } const renderNavContent = (onItemClick?: () => void) => ( diff --git a/src/sections/SeccionSistema.tsx b/src/sections/SeccionSistema.tsx new file mode 100644 index 0000000..7ce34c9 --- /dev/null +++ b/src/sections/SeccionSistema.tsx @@ -0,0 +1,251 @@ +import { useState, useRef, useEffect } from 'react'; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Terminal, Download, Upload, FileText, Send, AlertCircle, RefreshCw } from 'lucide-react'; +import { toast } from 'sonner'; + +export function SeccionSistema() { + const [mongoshCommand, setMongoshCommand] = useState(''); + const [consoleHistory, setConsoleHistory] = useState<{ id: number; type: 'input' | 'output' | 'error'; content: string }[]>([]); + const [logs, setLogs] = useState(''); + const [isLogsModalOpen, setIsLogsModalOpen] = useState(false); + const [isExecuting, setIsExecuting] = useState(false); + const historyCounter = useRef(0); + const consoleEndRef = useRef(null); + + const scrollToBottom = () => { + consoleEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }; + + useEffect(() => { + scrollToBottom(); + }, [consoleHistory]); + + const addHistory = (type: 'input' | 'output' | 'error', content: string) => { + setConsoleHistory(prev => [...prev, { id: historyCounter.current++, type, content }]); + }; + + const handleExecuteMongosh = async () => { + if (!mongoshCommand.trim()) return; + + addHistory('input', mongoshCommand); + const commandToRun = mongoshCommand; + setMongoshCommand(''); + setIsExecuting(true); + + try { + const response = await fetch('/api/admin/mongosh', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ command: commandToRun }) + }); + + const data = await response.json(); + + if (!response.ok) { + addHistory('error', data.error || 'Error desconocido'); + } else { + const outputString = typeof data.result === 'object' ? JSON.stringify(data.result, null, 2) : String(data.result); + addHistory('output', outputString); + if (data.isMemStore) { + toast.warning('MongoDB no está conectado. Utilizando modo memoria.'); + } + } + } catch (error) { + addHistory('error', String(error)); + } finally { + setIsExecuting(false); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + handleExecuteMongosh(); + } + }; + + const handleViewLogs = async () => { + try { + const response = await fetch('/api/admin/logs'); + if (!response.ok) throw new Error('Error al obtener logs'); + const data = await response.text(); + setLogs(data || 'No hay logs disponibles.'); + setIsLogsModalOpen(true); + } catch (e) { + toast.error('Error al cargar logs: ' + String(e)); + } + }; + + const handleExportData = () => { + window.location.href = '/api/admin/db/export'; + toast.success('Descarga iniciada'); + }; + + const handleImportData = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = async (event) => { + try { + const content = event.target?.result as string; + const dump = JSON.parse(content); + + const response = await fetch('/api/admin/db/import', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(dump) + }); + + if (!response.ok) { + const data = await response.json(); + throw new Error(data.error || 'Error al importar base de datos'); + } + + toast.success('Base de datos importada exitosamente. Recarga la página para ver los cambios.'); + // Opcional: window.location.reload(); + } catch (error) { + toast.error('Error: ' + String(error)); + } + }; + reader.readAsText(file); + e.target.value = ''; // Reset input + }; + + const handleClearConsole = () => { + setConsoleHistory([]); + }; + + return ( +
+
+
+

Sistema y Depuración

+

Herramientas avanzadas para administradores

+
+
+ +
+ + +
+
+ + Consola mongosh + + Ejecuta comandos de consulta directamente en la base de datos. +
+ +
+
+ +
+ {consoleHistory.length === 0 && ( +
Conectado. Esperando comandos... Ej: db.pacientes.find()
+ )} + {consoleHistory.map((item) => ( +
+ {item.type === 'input' && {'>'}} + {item.type === 'output' && {'<-'}} + {item.type === 'error' && {'!'}} +
{item.content}
+
+ ))} +
+
+ +
+ setMongoshCommand(e.target.value)} + onKeyDown={handleKeyDown} + disabled={isExecuting} + /> + +
+
+ + Soporta asincronía. Variables globales: `db`, `ObjectId`. Las promesas se resuelven automáticamente. +
+ + + + + + + Logs del Servidor + + Visualiza los registros recientes del servidor. + + + + + + + + + + Exportar Datos + + Descarga una copia completa de la base de datos (JSON). + + + + + + + + + + Importar Datos + + Restaura la base de datos desde un archivo (JSON). + + +
+ + +
+

Advertencia: Esto sobreescribirá todos los datos actuales.

+
+
+
+ + {isLogsModalOpen && ( +
+
+

+ Logs del Servidor + +

+
+
{logs}
+
+
+ +
+
+
+ )} +
+ ); +} diff --git a/src/types/index.ts b/src/types/index.ts index 6cf79ff..b42387a 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -237,7 +237,7 @@ export interface MovimientoIndicacion { observaciones?: string; } -export type Vista = 'dashboard' | 'camas' | 'pacientes' | 'internaciones' | 'evoluciones' | 'laboratorios' | 'acidobase' | 'cultivos' | 'historiaclinica' | 'nuevoingreso' | 'editaringreso' | 'usuarios' | 'pendientessala'; +export type Vista = 'dashboard' | 'camas' | 'pacientes' | 'internaciones' | 'evoluciones' | 'laboratorios' | 'acidobase' | 'cultivos' | 'historiaclinica' | 'nuevoingreso' | 'editaringreso' | 'usuarios' | 'pendientessala' | 'sistema'; export type RolUsuario = 'admin' | 'medico' | 'enfermero';