Revisión de Casa Viva: chats sin respuesta, audios perdidos, preguntas en grupos, salidas del bot, avisos, pedidos web abiertos; /casaviva y /casaviva-atiende

Fuente de verdad de todos los proyectos:
docs/00-FUENTE-DE-VERDAD/LEEME.mdDónde va cada proyecto:ESTADO.md
The repository also contains the active NEXO Business POS/business-OS work.
Any coding AI touching NEXO Business must start here:
docs/nexo-business/STATUS.mddocs/nexo-business/AI_HANDOFF.mddocs/nexo-business/MASTER_BLUEPRINT.mddocs/nexo-business/TECHNICAL_ARCHITECTURE.mddocs/nexo-business/CONNECTORS_AND_SYNC.mddocs/nexo-business/COLO_SHOP_PILOT.mdCurrent pilots:
Do not create a separate NEXO Sync repository. NEXO Sync is an internal capability of NEXO Business.
If another chat/agent is advancing the Tauri/Windows implementation, inspect recent commits and current code before editing. Never overwrite newer working implementation to match stale chat context.
First end-to-end build produced by NEXO Skill Master.
npm install
npm run dev
hooks/register.ts 120 lines1import type { Register } from 'claude-code'
2
3// Revisión de Casa Viva para Ernesto. Lee (solo lectura) dos scripts en los servidores:
4// - VPS del bot: /root/supervisor/casaviva-revision.mjs (chats, salidas del bot, avisos, códigos de caja)
5// - Web: ~/casaviva-pedidos.php (pedidos web abiertos)
6// Copias en ernesto-rondon-nexo/scripts/casaviva-revision/.
7
8const VPS = ['ssh', '-o', 'BatchMode=yes', '-o', 'ConnectTimeout=15', '-i', 'C:/Users/Ernesto/.ssh/vivabot_vps', 'root@179.236.242.212', 'cd /root/supervisor && node casaviva-revision.mjs 3']
9const WEB = ['ssh', '-o', 'BatchMode=yes', '-o', 'ConnectTimeout=15', '-i', 'C:/Users/Ernesto/.ssh/casa-viva-github-actions', '-p', '65002', 'u824654880@88.223.85.72', 'cd domains/casaviva.company/public_html && wp eval-file ~/casaviva-pedidos.php']
10
11type Fila = Record<string, string | number | null | undefined>
12type Revision = {
13 sinResponder: Fila[]; perdidos: Fila[]; preguntasGrupos: Fila[]; salidasConError: Fila[]; salidasAtascadas: Fila[]
14 avisosAbiertos: Fila[]; preguntasSupervisor: Fila[]; codigosCajaSinUsar: Fila[]; pedidosAbiertos: Fila[]; fallos: string[]
15}
16
17const vacia = (): Revision => ({ sinResponder: [], perdidos: [], preguntasGrupos: [], salidasConError: [], salidasAtascadas: [], avisosAbiertos: [], preguntasSupervisor: [], codigosCajaSinUsar: [], pedidosAbiertos: [], fallos: [] })
18
19async function leer($: any, argv: string[], nombre: string, r: Revision): Promise<Record<string, unknown>> {
20 try {
21 const res = await $.process.run(argv, { timeoutMs: 60000 })
22 const linea = String(res.stdout || '').trim().split('\n').filter((l: string) => l.startsWith('{')).pop()
23 if (res.exitCode !== 0 || !linea) { r.fallos.push(`${nombre}: no respondió (código ${res.exitCode})`); return {} }
24 return JSON.parse(linea)
25 } catch (err) {
26 r.fallos.push(`${nombre}: ${String((err as Error)?.message || err).slice(0, 120)}`)
27 return {}
28 }
29}
30
31async function revisar($: any): Promise<Revision> {
32 const r = vacia()
33 const [a, b] = await Promise.all([leer($, VPS, 'servidor del bot', r), leer($, WEB, 'web', r)])
34 Object.assign(r, a, b)
35 return r
36}
37
38const hora = (iso: unknown) => {
39 if (!iso) return ''
40 try { return new Date(String(iso)).toLocaleTimeString('es-ES', { timeZone: 'America/Havana', hour: '2-digit', minute: '2-digit' }) } catch { return '' }
41}
42
43export function resumen(r: Revision): string {
44 const partes: string[] = []
45 if (r.sinResponder.length) partes.push(`${r.sinResponder.length} sin responder`)
46 if (r.perdidos.length) partes.push(`${r.perdidos.length} perdidos`)
47 if (r.pedidosAbiertos.length) partes.push(`${r.pedidosAbiertos.length} pedidos abiertos`)
48 if (r.salidasConError.length + r.salidasAtascadas.length) partes.push(`${r.salidasConError.length + r.salidasAtascadas.length} envíos con problema`)
49 if (r.avisosAbiertos.length) partes.push(`${r.avisosAbiertos.length} avisos`)
50 if (r.fallos.length) partes.push('⚠ revisión incompleta')
51 return 'Casa Viva: ' + (partes.length ? partes.join(' · ') : 'todo al día ✓')
52}
53
54export function informe(r: Revision): string {
55 const L: string[] = ['## Revisión de Casa Viva (últimas 3 h)']
56 const sec = (titulo: string, filas: Fila[], fmt: (f: Fila) => string) => {
57 L.push('', `### ${titulo} (${filas.length})`)
58 L.push(...(filas.length ? filas.slice(0, 15).map((f) => '- ' + fmt(f)) : ['- Nada.']))
59 if (filas.length > 15) L.push(`- …y ${filas.length - 15} más`)
60 }
61 sec('Chats sin respuesta (más de 3 min)', r.sinResponder, (f) => `${hora(f.hora)} · ${f.nombre} (…${f.tel}${f.rol ? ', ' + f.rol : ''}): "${f.texto}"`)
62 sec('Mensajes que el bot no procesó', r.perdidos, (f) => `${hora(f.hora)} · ${f.nombre} (…${f.tel}): "${f.texto}"`)
63 sec('Pedidos web abiertos', r.pedidosAbiertos, (f) => `#${f.pedido} · hace ${f.minutos} min · ${f.estado}/${f.operacion}/${f.reparto} · ${f.entrega}${f.gestora ? ' · gestora ' + f.gestora : ''}${f.mensajero ? ' · lo lleva ' + f.mensajero : ''}${f.elegido ? ' · elegido ' + f.elegido : ''} · ${f.productos} · ${f.total} USD`)
64 sec('Preguntas en los grupos', r.preguntasGrupos, (f) => `${hora(f.hora)} · ${f.grupo} · ${f.nombre}: "${f.texto}"`)
65 sec('Envíos del bot con error', r.salidasConError, (f) => `#${f.id} ${hora(f.hora)}: ${f.error}`)
66 sec('Envíos atascados (más de 5 min)', r.salidasAtascadas, (f) => `#${f.id} ${f.estado} desde ${hora(f.hora)}`)
67 sec('Avisos abiertos del bot', r.avisosAbiertos, (f) => `#${f.id} ${f.tipo} (${hora(f.hora)}): ${f.motivo}`)
68 sec('Preguntas esperando tu SÍ/NO', r.preguntasSupervisor, (f) => `#${f.id}: ${f.texto}`)
69 sec('Códigos de caja sin usar', r.codigosCajaSinUsar, (f) => `${f.caja} · vence ${hora(f.vence)}`)
70 if (r.fallos.length) L.push('', '### ⚠ No se pudo revisar todo', ...r.fallos.map((x) => '- ' + x))
71 return L.join('\n')
72}
73
74const REGLAS = `Atiende lo pendiente del informe de Casa Viva de arriba. Reglas (de Ernesto, 11-oct):
75- Habla a Ernesto en español sencillo, paso a paso. Lee primero docs/00-FUENTE-DE-VERDAD/LEEME.md y la última sección de ESTADO.md si no lo has hecho en esta sesión.
76- Antes de contestar a alguien, lee su chat completo (crm_messages y crm_wa_messages) y comprueba los datos reales (pedido, existencias, cuenta). Nunca afirmes algo sin comprobarlo.
77- Las respuestas van por el buzón del bot (public.crm_outbox, kind 'texto', created_by 'claude'; a teléfonos por to_phone, a grupos por to_group). Después comprueba que salieron (status 'enviado').
78- No prometas lo que no puedes hacer. En pedidos de una gestora, habla solo con la gestora, nunca con su cliente.
79- Contactos con bot_mode 'ignorar' son personales de Ernesto: no se les escribe.
80- Puedes cancelar pedidos de prueba que su gestora pidió cancelar. Cambios de dinero, borrar datos o tocar código en producción: pregunta antes a Ernesto, salvo lo ya autorizado.
81- No reinicies el bot si alguien escribió en el último minuto.
82- Si el bot contestó mal, corrígelo (respuesta en crm_knowledge o regla en src/brain.js) y avisa a la persona.
83- Al terminar, resume a Ernesto qué hiciste y qué necesita decidir él, y apúntalo en ESTADO.md.`
84
85let ultimoUrgente = 0
86
87async function refrescar($: any) {
88 const r = await revisar($)
89 await $.ui.status(resumen(r))
90 const urgente = r.sinResponder.length + r.perdidos.length + r.salidasConError.length
91 if (urgente > ultimoUrgente) await $.ui.toast(`Casa Viva: ${urgente} sin atender. Escribe /casaviva para verlo.`)
92 ultimoUrgente = urgente
93}
94
95const register: Register = (on) => {
96 on('session.start', async ($, e, next) => {
97 await $.command.register({ name: 'casaviva', description: 'Revisa chats, pedidos y avisos de Casa Viva (solo lectura)' })
98 await $.command.register({ name: 'casaviva-atiende', description: 'Revisa Casa Viva y pide a Claude que atienda lo pendiente' })
99 $.clock.after(2000, () => refrescar($))
100 $.clock.every(5 * 60 * 1000, () => refrescar($))
101 return next(e)
102 })
103
104 on('command.run', { command: 'casaviva' }, async ($) => {
105 const r = await revisar($)
106 await $.ui.status(resumen(r))
107 return { text: informe(r) }
108 })
109
110 on('command.run', { command: 'casaviva-atiende' }, async ($) => {
111 const r = await revisar($)
112 await $.ui.status(resumen(r))
113 const texto = informe(r)
114 await $.prompt.submit({ text: `${texto}\n\n${REGLAS}` })
115 return { text: 'Revisión hecha. Claude empieza a atender lo pendiente.' }
116 })
117}
118
119export { register }
120