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 (
No tiene permisos para acceder a esta sección.
+ +Herramientas avanzadas para administradores
+{item.content}
+ Advertencia: Esto sobreescribirá todos los datos actuales.
+{logs}
+