diff --git a/docs/segunda-instancia.md b/docs/segunda-instancia.md new file mode 100644 index 0000000..217fb24 --- /dev/null +++ b/docs/segunda-instancia.md @@ -0,0 +1,91 @@ +# Segunda instancia white-label (sin branding SKEEN) + +Segunda clínica sobre el mismo código y la misma VPS. La marca no vive en el +código: sale del parámetro `skeen.clinic.brand` (ver `GET /skeen/frontend/v1/brand`), +así que esta y futuras instancias son solo infra + config. + +## Mapa de la instalación + +| Componente | SKEEN (instancia 1) | Clínica 2 (instancia white-label) | +|---|---|---| +| Odoo HTTP | 8069 | **8070** | +| Odoo longpolling | 8072 | **8073** | +| Base de datos | `skeen_odoo` | **`clinica2_odoo`** | +| Conf | `/root/skeen-odoo/odoo.conf` | **`/root/clinica2-odoo.conf`** | +| Systemd | `odoo-skeen.service` | **`odoo-clinica2.service`** | +| Frontend | `/var/www/skeen-frontend` | **`/var/www/clinica2-frontend`** | +| Nginx | vhost `skeen-crm` (80) | **vhost `clinica2` (80 + 8880)** | +| Acceso LAN | http://192.168.10.114 | **http://192.168.10.114:8880** | +| Acceso público | skeen.consultoria-as.com | **clinica2.consultoria-as.com (pendiente: dashboard del túnel)** | +| WhatsApp (WACRM) | integrado | **oculto + bloqueado (escenario B)** | + +`addons_path` se comparte (read-only): `/root/skeen-odoo/addons`. Un solo +código, N bases de datos. + +## Cómo se levantó (orden de pasos) + +Los archivos canónicos están en `infra/clinica2/`: + +1. `odoo.conf` — copia del de SKEEN con `db_name=clinica2_odoo`, `http_port=8070`, + `longpolling_port=8073`, `data_dir=/var/lib/odoo-clinica2`. +2. Crear la base (el rol `skeen` no tiene CREATEDB; se crea como `postgres`): + `CREATE DATABASE clinica2_odoo OWNER skeen ...` +3. Instalar módulos limpio (sin demo): + `odoo-bin -c /root/clinica2-odoo.conf -d clinica2_odoo --without-demo=all --init=skeen_whatsapp,skeen_chat,skeen_visitas --stop-after-init --no-http` + (skeen_whatsapp arrastra citas/pacientes/ventas/pagos/monedero; visitas + arrastra inventario). +4. `postinstall.py` — brand neutro (`skeen.clinic.brand`), menú WACRM vacío + para todos (`skeen.menu_roles`), password de admin propio + + `must_change_password`, empresa renombrada. +5. `systemctl enable --now odoo-clinica2`. +6. `nginx-vhost` → `/etc/nginx/sites-enabled/clinica2` + `nginx -t && reload`. +7. Frontend: mismo build de SKEEN (`rsync -a dist/ /var/www/clinica2-frontend/`) + más `public/brand/` (logos SVG neutros + favicon, versionados en el repo). + +## Pendiente para acceso público (único paso manual) + +El túnel cloudflared corre con `--token` (remotamente administrado): el +`config.yml` local NO aplica. En el dashboard de Cloudflare (Zero Trust → +Networks → Tunnels → f2517233…) agregar el ingress: + +```yaml +- hostname: clinica2.consultoria-as.com + service: http://localhost:80 +``` + +El vhost nginx ya escucha en el 80 con ese `server_name`; al agregar el +hostname el túnel enruta solo. El 8880 queda como acceso LAN permanente. + +## Aislamiento de datos (escenario B: sin WhatsApp) + +- Sin `SUPABASE_URL` / `WACRM_API_*` / `HERMES_BRIDGE_URL` en la unit systemd + (el backend respondería "Falta configuración"). +- Aun así, el backend tiene fallback a `localhost:3000`; por eso el vhost + nginx corta `/api/odoo/skeen/frontend/v1/wacrm/` con `return 403;` — doble + candado, la instancia 2 no puede leer ni escribir las conversaciones de + SKEEN aunque alguien llame la API a mano. +- Menú WACRM oculto para todos los roles vía matriz (`/wacrm/*: []`). + +Para activar WhatsApp en la instancia 2 (escenario A) se necesita: número +propio de WhatsApp Business + decisión de WACRM multi-clínica (estimar aparte). + +## Fix de menús que esta fase destapó (aplica a TODAS las instancias) + +Las vistas XML referenciaban `skeen_citas.menu_skeen_root`, pero `skeen_citas` +depende de `skeen_pacientes`: en instalación limpia de una sola pasada el +xmlid aún no existe y la carga falla (la base de SKEEN se construyó por fases +y por eso nunca explotó). Se movió el menú raíz a `skeen_pacientes` (módulo +raíz de la jerarquía) y todas las referencias apuntan a +`skeen_pacientes.menu_skeen_root`. En bases existentes se transfirió el +ownership del xmlid (`UPDATE ir_model_data SET module='skeen_pacientes' …`), +así actualizar no duplica el menú. Mismo tratamiento para el menú padre de +Citas (`skeen_citas/views/menus.xml` carga antes que bloqueos/lista de espera). + +## Operación habitual + +- Logs: `tail -f /tmp/odoo2.log` (instancia 2) vs `/tmp/odoo.log` (SKEEN). +- Ambas instancias comparten código: un `git pull` + rebuild afecta el build + de ambas solo si se vuelven a copiar los `dist/` (cada uno tiene el suyo). +- Para una tercera clínica: repetir pasos 1–7 con `clinica3` (puertos 8074/8075, + db `clinica3_odoo`, vhost propio) y setear su brand desde + Configuración → Clínica o con `PUT /brand`. diff --git a/frontend/public/brand/favicon.svg b/frontend/public/brand/favicon.svg new file mode 100644 index 0000000..8d743f4 --- /dev/null +++ b/frontend/public/brand/favicon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/public/brand/logo-blanco.svg b/frontend/public/brand/logo-blanco.svg new file mode 100644 index 0000000..3492999 --- /dev/null +++ b/frontend/public/brand/logo-blanco.svg @@ -0,0 +1,5 @@ + + + + CLÍNICA + diff --git a/frontend/public/brand/logo-negro.svg b/frontend/public/brand/logo-negro.svg new file mode 100644 index 0000000..c505bc2 --- /dev/null +++ b/frontend/public/brand/logo-negro.svg @@ -0,0 +1,5 @@ + + + + CLÍNICA + diff --git a/infra/clinica2/nginx-vhost b/infra/clinica2/nginx-vhost new file mode 100644 index 0000000..01b55fd --- /dev/null +++ b/infra/clinica2/nginx-vhost @@ -0,0 +1,47 @@ +# Segunda instancia (white-label) — frontend React + API Odoo en puerto 8070. +# +# Acceso LAN inmediato: http://192.168.10.114:8880 +# Acceso publico: requiere agregar el hostname en el dashboard del tunel +# cloudflared (remotamente administrado, sin config local): +# ingress: - hostname: clinica2.consultoria-as.com +# service: http://localhost:80 +server { + listen 80; + listen 8880; + server_name clinica2.consultoria-as.com; + + # Frontend React (mismo build que SKEEN; el brand sale del API /brand) + location / { + root /var/www/clinica2-frontend; + try_files $uri $uri/ /index.html; + add_header Cache-Control "no-cache"; + } + + # Odoo API para el frontend + location /api/odoo/ { + proxy_pass http://127.0.0.1:8070/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 120s; + } + + # Backend Odoo (administracion directa, opcional) + location /web/ { + proxy_pass http://127.0.0.1:8070/web/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 120s; + } + + # Escenario B: la instancia 2 no tiene WhatsApp. Aunque el backend tenga + # fallback a localhost:3000, aqui se corta la ruta: 403 fijo, sin excepciones. + location /api/odoo/skeen/frontend/v1/wacrm/ { + return 403; + } +} diff --git a/infra/clinica2/odoo-clinica2.service b/infra/clinica2/odoo-clinica2.service new file mode 100644 index 0000000..3c80648 --- /dev/null +++ b/infra/clinica2/odoo-clinica2.service @@ -0,0 +1,19 @@ +[Unit] +Description=Odoo CLINICA2 (skeen-odoo venv, instancia white-label) +After=network.target postgresql.service +Wants=postgresql.service + +[Service] +Type=simple +User=root +WorkingDirectory=/root/skeen-odoo +# Sin SUPABASE_URL / WACRM_API_* / HERMES_BRIDGE_URL a proposito: la instancia +# 2 arranca en escenario B (sin WhatsApp). Los endpoints WACRM responden +# "Falta configuracion" en vez de tocar la instancia compartida de SKEEN. +ExecStart=/root/odoo-venv/bin/python /root/skeen-odoo/odoo-bin -c /root/clinica2-odoo.conf --logfile=/tmp/odoo2.log +Restart=always +RestartSec=5 +TimeoutStartSec=120 + +[Install] +WantedBy=multi-user.target diff --git a/infra/clinica2/odoo.conf b/infra/clinica2/odoo.conf new file mode 100644 index 0000000..f2b5ce0 --- /dev/null +++ b/infra/clinica2/odoo.conf @@ -0,0 +1,55 @@ +[options] +addons_path = /root/skeen-odoo/addons,/root/skeen-odoo/odoo/addons +admin_passwd = skeen_admin_2026 +csv_internal_sep = , +data_dir = /var/lib/odoo-clinica2 +db_host = localhost +db_maxconn = 64 +db_name = clinica2_odoo +db_password = skeen_dev_2026 +db_port = 5432 +db_sslmode = prefer +db_user = skeen +demo = {} +email_from = False +http_enable = True +http_interface = +http_port = 8070 +import_partial = +limit_memory_hard = 2684354560 +limit_memory_soft = 2147483648 +limit_request = 8192 +limit_time_cpu = 60 +limit_time_real = 120 +limit_time_real_cron = -1 +list_db = True +log_db = False +log_db_level = warning +log_handler = :INFO +log_level = info +logfile = +longpolling_port = 8073 +max_cron_threads = 2 +osv_memory_age_limit = False +osv_memory_count_limit = False +pg_path = +pidfile = +proxy_mode = True +reportgz = False +screencasts = +screenshots = /tmp/odoo_screenshots_clinica2 +server_wide_modules = base,web +smtp_password = False +smtp_port = 25 +smtp_server = localhost +smtp_ssl = False +smtp_ssl_certificate_filename = False +smtp_ssl_private_key_filename = False +smtp_user = False +test_file = +test_tags = None +translate_modules = ['all'] +unaccent = False +upgrade_path = +without_demo = False +workers = 0 diff --git a/infra/clinica2/postinstall.py b/infra/clinica2/postinstall.py new file mode 100644 index 0000000..0eb334d --- /dev/null +++ b/infra/clinica2/postinstall.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Post-instalación de la instancia clinica2_odoo (white-label, sin WACRM). + +- Brand neutro (skeen.clinic.brand) apuntando a logos genericos servidos por + el nginx de la instancia (no SKEEN). +- skeen.menu_roles con /wacrm/* en lista vacia: modulo WhatsApp oculto para + todos (escenario B: la clinica nueva aun no tiene numero de WhatsApp). +- Admin con password distinto a la instancia SKEEN y must_change_password. +- Empresa principal renombrada al nombre neutro. +""" +import json +import sys + +sys.path.insert(0, '/root/skeen-odoo') + +import odoo +from odoo.tools import config + +config.parse_config([ + '-c', '/root/clinica2-odoo.conf', + '--no-http', + '--log-level', 'error', +]) + +odoo.service.server.load_server_wide_modules() +registry = odoo.registry(config['db_name']) + +BRAND = { + 'nombre': 'Clínica', + 'logo_claro': '/brand/logo-blanco.svg', + 'logo_oscuro': '/brand/logo-negro.svg', + 'pie': 'Sistema de gestión clínica', +} + +ADMIN_PASSWORD = 'Clinica2Admin2026!' + +with registry.cursor() as cr: + env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {}) + ICP = env['ir.config_parameter'].sudo() + + # 1) Brand neutro + ICP.set_param('skeen.clinic.brand', json.dumps(BRAND, ensure_ascii=False)) + print('brand:', ICP.get_param('skeen.clinic.brand')) + + # 2) Ocultar WACRM para todos (lista vacia = ningun rol) + roles = json.loads(ICP.get_param('skeen.menu_roles') or '{}') + roles['/wacrm/messages'] = [] + roles['/wacrm/leads'] = [] + ICP.set_param('skeen.menu_roles', json.dumps(roles, ensure_ascii=False)) + print('menu_roles wacrm:', roles.get('/wacrm/messages'), roles.get('/wacrm/leads')) + + # 3) Admin: password propio de la instancia + cambio forzado + admin = env['skeen.frontend.user'].sudo().search([('login', '=', 'admin')], limit=1) + admin.set_password(ADMIN_PASSWORD) + admin.write({'must_change_password': True}) + print('admin:', admin.login, 'must_change_password:', admin.must_change_password) + + # 4) Empresa principal renombrada (el backend Odoo la muestra) + company = env['res.company'].sudo().search([], limit=1, order='id') + if company: + company.write({'name': 'Clínica'}) + print('company:', company.name) + + cr.commit() +print('POST-INSTALL OK') diff --git a/odoo-addons/skeen_citas/__manifest__.py b/odoo-addons/skeen_citas/__manifest__.py index 56ac383..df4a2cb 100644 --- a/odoo-addons/skeen_citas/__manifest__.py +++ b/odoo-addons/skeen_citas/__manifest__.py @@ -14,6 +14,8 @@ 'data': [ 'security/ir.model.access.csv', 'data/sequences.xml', + # Menú padre primero: las vistas de abajo cuelgan de él. + 'views/menus.xml', 'views/cita_views.xml', 'views/bloqueo_views.xml', 'views/lista_espera_views.xml', diff --git a/odoo-addons/skeen_citas/views/menu_views.xml b/odoo-addons/skeen_citas/views/menu_views.xml index a1c33dd..fc0a740 100644 --- a/odoo-addons/skeen_citas/views/menu_views.xml +++ b/odoo-addons/skeen_citas/views/menu_views.xml @@ -1,10 +1,7 @@ - - - - - - + - \ No newline at end of file + diff --git a/odoo-addons/skeen_citas/views/menus.xml b/odoo-addons/skeen_citas/views/menus.xml new file mode 100644 index 0000000..2a6606d --- /dev/null +++ b/odoo-addons/skeen_citas/views/menus.xml @@ -0,0 +1,10 @@ + + + + + + + diff --git a/odoo-addons/skeen_inventario/views/inventario_views.xml b/odoo-addons/skeen_inventario/views/inventario_views.xml index 12e30c3..51898fe 100644 --- a/odoo-addons/skeen_inventario/views/inventario_views.xml +++ b/odoo-addons/skeen_inventario/views/inventario_views.xml @@ -133,7 +133,7 @@ - + - + diff --git a/odoo-addons/skeen_pacientes/__manifest__.py b/odoo-addons/skeen_pacientes/__manifest__.py index aa4a167..105d873 100644 --- a/odoo-addons/skeen_pacientes/__manifest__.py +++ b/odoo-addons/skeen_pacientes/__manifest__.py @@ -13,6 +13,10 @@ 'depends': ['base', 'contacts', 'hr'], 'data': [ 'security/ir.model.access.csv', + # El menú raíz se declara primero: las vistas de abajo lo usan como + # parent y en una instalación limpia este módulo carga antes que + # skeen_citas (dueño histórico del xmlid). + 'views/menu_views.xml', 'views/paciente_views.xml', 'views/receta_views.xml', 'views/catalogos_views.xml', diff --git a/odoo-addons/skeen_pacientes/views/catalogos_views.xml b/odoo-addons/skeen_pacientes/views/catalogos_views.xml index a2cfce4..b2593f2 100644 --- a/odoo-addons/skeen_pacientes/views/catalogos_views.xml +++ b/odoo-addons/skeen_pacientes/views/catalogos_views.xml @@ -69,8 +69,8 @@ - - diff --git a/odoo-addons/skeen_pacientes/views/menu_views.xml b/odoo-addons/skeen_pacientes/views/menu_views.xml new file mode 100644 index 0000000..2b08d65 --- /dev/null +++ b/odoo-addons/skeen_pacientes/views/menu_views.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/odoo-addons/skeen_pacientes/views/receta_views.xml b/odoo-addons/skeen_pacientes/views/receta_views.xml index 6c7176e..2ad89b2 100644 --- a/odoo-addons/skeen_pacientes/views/receta_views.xml +++ b/odoo-addons/skeen_pacientes/views/receta_views.xml @@ -39,6 +39,6 @@ - diff --git a/odoo-addons/skeen_ventas/views/menu_views.xml b/odoo-addons/skeen_ventas/views/menu_views.xml index e96e3b6..9457fdb 100644 --- a/odoo-addons/skeen_ventas/views/menu_views.xml +++ b/odoo-addons/skeen_ventas/views/menu_views.xml @@ -1,7 +1,7 @@ - + diff --git a/odoo-addons/skeen_visitas/views/menu_views.xml b/odoo-addons/skeen_visitas/views/menu_views.xml index fc6c3f3..67a8b1e 100644 --- a/odoo-addons/skeen_visitas/views/menu_views.xml +++ b/odoo-addons/skeen_visitas/views/menu_views.xml @@ -1,7 +1,7 @@ - +