feat: module toggles in POS config and Instance Manager

- Add GET/PUT /pos/api/config/modules endpoints in POS config_bp.py
- Update sidebar.js to filter nav items based on enabled modules
- Add Modules section to POS config.html with toggles for WhatsApp, Marketplace, MercadoLibre
- Add module load/save logic to POS config.js
- Preload modules in app-init.js for sidebar caching

- Add tenant module management to Instance Manager
  - get_tenant_modules / update_tenant_modules in tenant_service.py
  - GET/PUT /api/tenants/<id>/modules endpoints in tenants_bp.py
  - Add modules modal to manager index.html
  - Add module editing UI and logic to manager.js
  - Add toggle-switch CSS to manager.css
This commit is contained in:
2026-05-28 00:21:52 +00:00
parent 999591e248
commit 718fa06888
26 changed files with 2614 additions and 429 deletions

View File

@@ -28,8 +28,9 @@ const sendQueue = [];
let queueTimer = null;
let connectWatchdog = null;
let staleWatchdog = null;
const WATCHDOG_MS = 90000;
const STALE_MS = 90000;
let queueFlushInterval = null;
const WATCHDOG_MS = 300000; // 5 minutos para dar tiempo al QR scanning
const STALE_MS = 1800000; // 30 minutos (keepalive ya maneja pings, no forzamos reconexión por inactividad)
let lastActivity = Date.now();
function updateActivity() {
@@ -67,19 +68,14 @@ function flushSendQueue() {
function clearWatchdog() {
if (connectWatchdog) { clearTimeout(connectWatchdog); connectWatchdog = null; }
if (staleWatchdog) { clearInterval(staleWatchdog); staleWatchdog = null; }
if (queueFlushInterval) { clearInterval(queueFlushInterval); queueFlushInterval = null; }
}
function scheduleStaleWatchdog() {
if (staleWatchdog) clearInterval(staleWatchdog);
staleWatchdog = setInterval(() => {
if (connectionState === 'open' && (Date.now() - lastActivity > STALE_MS)) {
console.log(`[Tenant ${TENANT_ID}] Stale watchdog: no activity for ${STALE_MS/1000}s while open, forcing reconnect`);
try { sock?.ws?.close(); } catch (e) {}
sock = null;
connectionState = 'disconnected';
setTimeout(connectWhatsApp, 30000);
}
}, 30000);
// ELIMINADO: el stale watchdog forzaba reconexión cada 90s sin mensajes,
// lo cual destruía la sesión de Baileys y provocaba 440 + limpieza de auth.
// Baileys ya envía keepalive cada 15s (keepAliveIntervalMs). No es necesario.
if (staleWatchdog) { clearInterval(staleWatchdog); staleWatchdog = null; }
}
function scheduleWatchdog() {
@@ -118,7 +114,7 @@ async function connectWhatsApp() {
auth: state,
logger,
// Use a generic Ubuntu + Chrome fingerprint — less suspicious than raw Linux
browser: ['Ubuntu', 'Chrome', '120.0.0.0'],
browser: ['Chrome', 'Windows', '124.0.0.0'],
defaultQueryTimeoutMs: 60000,
keepAliveIntervalMs: 15000,
markOnlineOnConnect: false,
@@ -157,23 +153,24 @@ async function connectWhatsApp() {
}
if (reason === 440) {
// 440 = conflict/replaced. The session data is permanently invalid.
// Clean auth immediately and wait 5 min so WhatsApp forgets the old session.
console.log(`[Tenant ${TENANT_ID}] 440 Session replaced — clearing auth, waiting 5 min`);
clearAuthState();
sock = null;
// 440 = conflict/replaced. WhatsApp reemplazó esta sesión (puede ser
// por reconexión muy rápida, o porque el teléfono abrió otra sesión).
// NUNCA limpiamos auth automáticamente — solo esperamos más tiempo.
retry440Count++;
const delay = retry440Count >= 3 ? 600000 : 300000; // 10 min after 3 failures, else 5 min
const delay = retry440Count >= 3 ? 600000 : 120000; // 2min → 10min
console.log(`[Tenant ${TENANT_ID}] 440 Session replaced — reconnecting in ${delay/1000}s with existing creds (attempt ${retry440Count})`);
sock = null;
setTimeout(connectWhatsApp, delay);
return;
}
if (reason === 515) {
// 515 = stream error, often precedes 440. Treat same as 440.
console.log(`[Tenant ${TENANT_ID}] 515 Stream error — clearing auth, waiting 5 min`);
clearAuthState();
// 515 = stream error / restart required. WhatsApp sends this after
// successful pairing to force a reconnect with the new credentials.
// DO NOT clear auth — the credentials were just saved by creds.update.
console.log(`[Tenant ${TENANT_ID}] 515 Restart required — reconnecting in 5s with saved creds`);
sock = null;
setTimeout(connectWhatsApp, 300000);
setTimeout(connectWhatsApp, 5000);
return;
}
@@ -185,19 +182,11 @@ async function connectWhatsApp() {
}
if (reason === 408) {
// 408 during init queries usually means the server is overloaded
// or our auth is partially invalid. Clear auth if this happens repeatedly.
console.log(`[Tenant ${TENANT_ID}] 408 Timeout — waiting 60s`);
// 408 during init queries = rate-limit o auth parcialmente inválido.
// No limpiamos auth automáticamente; esperamos más tiempo.
console.log(`[Tenant ${TENANT_ID}] 408 Timeout — waiting 120s`);
sock = null;
retry440Count++;
if (retry440Count >= 5) {
console.log(`[Tenant ${TENANT_ID}] Too many timeouts — clearing auth for fresh QR`);
clearAuthState();
retry440Count = 0;
setTimeout(connectWhatsApp, 300000);
return;
}
setTimeout(connectWhatsApp, 60000);
setTimeout(connectWhatsApp, 120000);
return;
}
@@ -208,20 +197,33 @@ async function connectWhatsApp() {
}
if (connection === 'open') {
// Race-condition guard: if sock was nulled by a concurrent disconnect,
// ignore this stale 'open' event.
if (!sock) {
console.log(`[Tenant ${TENANT_ID}] Ignoring stale 'open' event (sock is null)`);
return;
}
clearWatchdog();
connectionState = 'open';
qrCode = null;
retry440Count = 0;
updateActivity();
scheduleStaleWatchdog();
// Stale watchdog eliminado — Baileys ya mantiene keepalive.
console.log(`[Tenant ${TENANT_ID}] Connected!`);
flushSendQueue();
if (!queueFlushInterval) {
queueFlushInterval = setInterval(() => {
if (connectionState === 'open') flushSendQueue();
}, 5000);
}
}
});
sock.ev.on('messages.upsert', async ({ messages }) => {
for (const msg of messages) {
if (msg.key.fromMe) continue;
// Skip system/receipt messages with no meaningful content
if (!msg.message || Object.keys(msg.message).length === 0) continue;
const phone = msg.key.remoteJid.replace('@s.whatsapp.net', '');
const message = msg.message || {};
@@ -289,7 +291,8 @@ async function connectWhatsApp() {
media_ptt,
latitude,
longitude,
push_name: msg.pushName || ''
push_name: msg.pushName || '',
sender_pn: msg.key?.senderPn || ''
}
}),
signal: controller.signal
@@ -332,21 +335,22 @@ app.post('/send', async (req, res) => {
return res.status(400).json({ error: 'phone and message required' });
}
const jid = phone.includes('@') ? phone : phone + '@s.whatsapp.net';
console.log(`[Tenant ${TENANT_ID}] /send called for ${jid}. state=${connectionState}, sock=${!!sock}`);
if (connectionState !== 'open' || !sock) {
sendQueue.push({ jid, message });
console.log(`[Tenant ${TENANT_ID}] Message queued for ${jid} (state: ${connectionState}, queue size: ${sendQueue.length})`);
return res.status(202).json({ queued: true, state: connectionState });
if (connectionState === 'open' && sock) {
try {
const r = await sock.sendMessage(jid, { text: message });
res.json({ success: true, id: r.key.id });
return;
} catch (e) {
console.log(`[Tenant ${TENANT_ID}] Send failed, will queue:`, e.message);
// fall through to queue
}
}
try {
const r = await sock.sendMessage(jid, { text: message });
res.json({ success: true, id: r.key.id });
} catch (e) {
sendQueue.push({ jid, message });
console.log(`[Tenant ${TENANT_ID}] Send failed, queued for retry:`, e.message);
res.status(202).json({ queued: true, error: e.message });
}
sendQueue.push({ jid, message });
console.log(`[Tenant ${TENANT_ID}] Message queued for ${jid} (state: ${connectionState}, queue size: ${sendQueue.length})`);
res.status(202).json({ queued: true, state: connectionState });
});
app.post('/send-image', async (req, res) => {
const { phone, caption, base64 } = req.body;