# Nueva instancia white-label (provisioning automatizado) Cómo levantar una clínica/spa nueva sobre el mismo código y la misma VPS, con sus propios secretos, puerto, base de datos, vhost, marca y configuración. Es la versión automatizada de `docs/segunda-instancia.md` (que sigue vigente como referencia del procedimiento manual y del caso `clinica2`). El código de los addons es compartido y read-only (`/root/skeen-odoo/addons`); cada instancia es solo infra + base de datos + configuración. La marca no vive en el código: sale del parámetro `skeen.clinic.brand` (API `/brand`). ## Requisitos previos - VPS con Odoo, PostgreSQL, nginx, Node y el venv `/root/odoo-venv` ya instalados (ver `instalacion.md`). - Al menos una instancia de referencia (`skeen_odoo`) con los módulos `skeen_*` instalados: de ahí se copia la lista de módulos a inicializar. - Un slug, nombre comercial, dominio y puerto libres para la instancia nueva. ## Uso en 2 pasos ```bash cd /root/skeen-proyecto/infra/provision # Paso 1 — SIEMPRE simular primero. No escribe ni ejecuta nada. ./provision_clinica.py \ --slug spa-bella --nombre "Spa Bella" \ --dominio spa.consultoria-as.com --puerto 8071 --db spa_bella_odoo \ --pack spa --demo --dry-run # Paso 2 — ejecución real (coordinada, fuera de horario pico). ./provision_clinica.py \ --slug spa-bella --nombre "Spa Bella" \ --dominio spa.consultoria-as.com --puerto 8071 --db spa_bella_odoo \ --pack spa --demo ``` ## Opciones | Opción | Default | Descripción | |---|---|---| | `--slug` | — | Identificador corto, `^[a-z0-9-]+$`. Nombra conf, unit, vhost y frontend. | | `--nombre` | — | Nombre comercial (marca, empresa, menú raíz). | | `--dominio` | — | Dominio público, p. ej. `spa.consultoria-as.com`. | | `--puerto` | — | Puerto HTTP de Odoo; longpolling usa `puerto+3`. | | `--db` | `_odoo` | Nombre de la base de datos. | | `--db-role` | `` | Rol PostgreSQL. Por defecto se crea uno propio con password nueva; `--db-role skeen` usa el rol compartido (ver abajo). | | `--pack` | `generico` | Giro para glosario y demo: `generico`, `spa`, `clinica`, `derma`. | | `--demo` | off | Siembra servicios, clientes, empleados, citas, paquetes y monedero de ejemplo. | | `--demo-limpia` | off | Corre `demo_limpiar.py` al final (deja la instancia sin datos demo). | | `--dominio-lan` | — | Puerto LAN extra en el vhost (p. ej. `8881` → `http://:8881`). | | `--admin-login` | `admin` | Login del usuario admin del frontend. | | `--pie` | genérico | Línea del pie de página de la marca. | | `--salida` | `/root/backups/-credenciales-.txt` | Archivo de credenciales (chmod 600). | | `--sin-wacrm` / `--con-wacrm` | bloquear | Por defecto el vhost corta `/api/odoo/skeen/frontend/v1/wacrm/` con 403. | | `--skip-build` | off | No recompilar el frontend; reusar `/root/skeen-frontend/dist`. | | `--force` | off | Continuar aunque existan colisiones (puerto/DB/unit/vhost/dirs). | | `--dry-run` | off | Imprime pasos, contenido de archivos y comandos sin ejecutar nada. | ## Qué crea Con `--slug spa-bella --puerto 8071 --db spa_bella_odoo`: | Recurso | Valor | |---|---| | Rol PostgreSQL | `spa_bella` (LOGIN, password aleatoria) | | Base de datos | `spa_bella_odoo` (owner = rol propio) | | Conf Odoo | `/root/spa-bella-odoo.conf` (chmod 600) | | data_dir | `/var/lib/odoo-spa-bella` | | Log | `/tmp/odoo-spa-bella.log` | | Unit systemd | `odoo-spa-bella.service` | | Vhost nginx | `/etc/nginx/sites-available/spa-bella` + symlink en `sites-enabled` | | Frontend | `/var/www/spa-bella-frontend` | | Credenciales | `/root/backups/spa-bella-credenciales-.txt` | ## Orden de ejecución del script 1. **Validaciones**: slug/DB/puerto libres, sin unit/vhost/dir/conf previos. Aborta listando colisiones salvo `--force`. 2. **Secretos propios**: `admin_passwd` (master de Odoo), `db_password` y password del admin del frontend, todos aleatorios. 3. **DB**: crea el rol y `su postgres -c "createdb -O "` (el rol `skeen` no tiene CREATEDB; se crea como `postgres`). 4. **`odoo.conf`** desde plantilla: puerto, db, addons_path, log propio, `without_demo = all`, sin workers. 5. **Unit systemd** desde plantilla + `daemon-reload` + `enable`. 6. **Módulos**: corre `odoo-bin ... --stop-after-init --no-http -i `. 7. **Post-instalación** (`postinstall_clinica.py`): marca, empresa, admin, y siembra tolerante de glosario/sucursal/settings (abajo). 8. **Menú raíz**: actualiza el xmlid `skeen_pacientes.menu_skeen_root` a `{"en_US": ""}` (igual que `clinica2_odoo`). 9. **Vhost nginx** + `nginx -t` + `reload`. 10. **Frontend**: build compartido + `rsync --delete` + logos + `chown`. 11. **Arranque**: `systemctl start`. 12. **Credenciales** y resumen (público, LAN, logs, cómo limpiar). ### Lista de módulos a inicializar Se toma de la instancia de referencia en vez de hardcodearla: ```bash su postgres -c "psql -d skeen_odoo -Atc \ \"SELECT name FROM ir_module_module WHERE state='installed' AND name LIKE 'skeen%'\"" ``` El script consulta eso y ordena los módulos topológicamente con `ir_module_module_dependency` (dependencias primero). Odoo resuelve igual las dependencias, pero el orden explícito evita la trampa documentada en `segunda-instancia.md` (el menú raíz debe existir antes de que las vistas que lo referencian se carguen). ## Post-instalación y el nuevo sistema de configuración `postinstall_clinica.py` corre con el ORM de Odoo y hace: - Brand neutro en `skeen.clinic.brand` (nombre comercial, logos `/brand/logo-blanco.svg` y `/brand/logo-negro.svg`, pie genérico). - `res.company.name` = nombre. - Admin del frontend (`skeen.frontend.user`) con la password generada y `must_change_password = True`. - Oculta WACRM en `skeen.menu_roles` (salvo `--con-wacrm`). Y **solo si existen en runtime** siembra los parámetros/modelos del nuevo sistema de configuración (otro cambio en curso): | Qué | Cómo lo detecta | Qué siembra | |---|---|---| | Glosario | llave `ir.config_parameter` `skeen.glosario` **o** modelo `skeen.glosario` | `pack` elegido | | `skeen.clinic.settings` | el modelo existe | nombre, `url_publica = https://`, horario 08:00–19:00, moneda MXN | | `skeen.sucursal` | el modelo existe | 1 sucursal default con el nombre del negocio | | `skeen.service.category` | el modelo existe | semilla de categorías del pack | Lo que no exista se reporta como `TODO` en la salida y **no aborta** el provisioning. Cuando ese cambio esté desplegado, una corrida nueva ya lo siembra; para una instancia ya creada, se puede re-ejecutar solo el post-install: ```bash cat > /tmp/post.json <<'JSON' {"conf": "/root/spa-bella-odoo.conf", "db": "spa_bella_odoo", "nombre": "Spa Bella", "dominio": "spa.consultoria-as.com", "pack": "spa", "admin_login": "admin", "admin_password": "", "con_wacrm": false, "demo": false} JSON /root/odoo-venv/bin/python /root/skeen-proyecto/infra/provision/postinstall_clinica.py /tmp/post.json ``` ## Datos demo `--demo` siembra ~10 servicios con precios, ~15 clientes, 2–3 empleados con `staff_type`, ~20 citas en la semana actual y algunos paquetes/monedero, todo coherente con el `--pack`. Los registros quedan marcados (`DEMO-` en códigos de servicio, `referred_by='DEMO'`, `[DEMO]` en notas) y sus ids en el parámetro `skeen.demo.ids`. Para borrarlos antes de entregar la instancia a un cliente: ```bash /root/odoo-venv/bin/python /root/skeen-proyecto/infra/provision/demo_limpiar.py \ --conf /root/spa-bella-odoo.conf --db spa_bella_odoo ``` (o pasar `--demo-limpia` en la corrida de provisioning). ## Frontend El build es **compartido**: un solo bundle (`npm run build` en `/root/skeen-frontend`) se copia con `rsync -a --delete` a cada `/var/www/-frontend`. La marca y el giro salen del API, no del build; por eso no hace falta compilar por instancia (usar `--skip-build` cuando ya haya un `dist/` fresco). Los logos genéricos se copian desde `/var/www/clinica2-frontend/brand/` (o `frontend/public/brand/`) a `/brand/`. ## Acceso público (paso manual) Igual que con `clinica2`, el túnel cloudflared es remotamente administrado: hay que agregar el ingress en el dashboard (Zero Trust → Networks → Tunnels): ```yaml - hostname: spa.consultoria-as.com service: http://localhost:80 ``` El vhost ya escucha en el 80 con ese `server_name`. El `--dominio-lan` deja además un acceso LAN permanente. ## Rol compartido vs rol propio (decisión de diseño) El procedimiento manual creaba la DB con `-O skeen` y usaba `db_password = skeen_dev_2026` para todas las instancias. Eso obliga a reutilizar la misma password y no permite "secretos propios por instancia". Por eso el script, por defecto, crea un **rol propio** (``) con password aleatoria y hace a ese rol owner de la DB. No toca el rol `skeen` ni ninguna instancia existente. Si se necesita reproducir el esquema compartido: ```bash ./provision_clinica.py ... --db-role skeen ``` En ese modo el script avisa que `db_password` en la conf debe coincidir con la password real del rol (no la cambia). ## Fallos a medias y limpieza Si algo falla, el script imprime **qué quedó hecho** y el comando para deshacerlo (rol, DB, conf, unit, vhost, frontend, service). Opciones: `--force` para saltar colisiones, o limpiar a mano con la lista de undo que imprime. ## Verificación tras una corrida real ```bash systemctl status odoo- # active (running) tail -50 /tmp/odoo-.log curl -s http://127.0.0.1:/web/health curl -s http://127.0.0.1:/skeen/frontend/v1/brand # nombre comercial nginx -t ``` Y en el navegador: login con el admin del frontend (fuerza cambio de password), verificar el nombre en el menú, el logo y que WACRM esté bloqueado (403).