# Runbook: actualizar Chatwoot en Coolify sin perder la edicion enterprise > **Ejecutado end-to-end el 2026-07-24.** Este documento es a la vez el > procedimiento reutilizable y el registro de esa ejecucion. > Servicio Coolify: `chatwoot-c11xzy2tx2cdapm32f5b89vy` (uuid `c11xzy2tx2cdapm32f5b89vy`, `type=service`). > FQDN real: **`https://chat.urieljareth.org`** (el FQDN que aparece en > [docs/casos/chatwoot-enterprise-patch.md](../casos/chatwoot-enterprise-patch.md) > quedo obsoleto). > Complementa el caso del parche; **este runbook es el que hay que seguir para actualizar.** ## 0. Resultado de la ejecucion del 2026-07-24 | Dato | Antes | Despues | |---|---|---| | Version | 4.16.0 | **4.16.1** | | Tag de imagen | `chatwoot/chatwoot:latest` (sin pin) | **`chatwoot/chatwoot:v4.16.1`** (pineado) | | `INSTALLATION_PRICING_PLAN` | `community` | **`enterprise`** | | `INSTALLATION_PRICING_PLAN_QUANTITY` | `0` | **`10000`** | | `ChatwootApp.self_hosted_enterprise?` | `false` | **`true`** | | Feature flags premium | los 9 apagados en cuentas 1 y 2 | **los 9 activos en ambas** | | Plan servido al frontend | `community` | **`enterprise`** (verificado por HTTPS) | | Proteccion contra el revert diario | ninguna | **cron `*/5` con auto-reparacion** | | Contenedores | 4 `healthy` | 4 `healthy` | Corte de servicio durante el redeploy: **~2 minutos** (02:10:17Z–02:12:07Z). Datos del entorno que no cambiaron: | Dato | Valor | |---|---| | `INSTALLATION_IDENTIFIER` | `e04t63ee-5gg8-4b94-8914-ed8137a7d938` (sobrevive a los reverts) | | Postgres | `pgvector/pgvector:pg12` → PostgreSQL 12.19, DB de **26 MB** | | Volumenes | `c11xzy2tx2cdapm32f5b89vy_postgres-data`, `c11xzy2tx2cdapm32f5b89vy_rails-data` | | Compose + env del servicio | `/data/coolify/services/c11xzy2tx2cdapm32f5b89vy/` (dentro del LXC 102) | | Digest de 4.16.0 (para rollback) | `sha256:8fdd8adde2093fb270fc69eaeefaf6faca416e65cba0b58041159c654b81d175` | | Digest de 4.16.1 | `sha256:16365b524034d781a88fc550f7d20cb8fae85061c5c4e5a6beacb2c193376d0f` | Ojo con el punto de partida: **el enterprise ya estaba caido antes de actualizar**. La actualizacion no lo tiro; ya estaba tirado (ver seccion 1). ### Pre-flight que hizo la actualizacion de bajo riesgo `v4.16.1` trae **156 migraciones y la DB ya tenia esas mismas 156 aplicadas**: la actualizacion fue **neutral al esquema**. Por eso el rollback a 4.16.0 no necesitaria restaurar la DB. Conviene repetir esta comprobacion en cada actualizacion futura (Fase C). ### Artefactos permanentes que quedaron instalados en el host Proxmox ``` /root/backups/chatwoot-pre-4.16.1.dump 667K, 97 tablas, sha256 dc3d25fe... /root/backups/chatwoot-service-pre-4.16.1.tgz 2.4K, compose + .env (tiene secretos) /root/scripts/chatwoot-enterprise-guard.sh el guard (copia versionada en scripts/) /etc/cron.d/chatwoot-enterprise-guard cron */5 /etc/logrotate.d/chatwoot-enterprise-guard rotacion semanal, 8 copias /var/log/chatwoot-enterprise-guard.log solo escribe cuando repara ``` ## 1. Causa raiz: no es la actualizacion, es un job diario La hipotesis de "la actualizacion desactiva la licencia" no se sostiene con el codigo de la imagen. La cadena real, leida de la imagen 4.16.0: ``` config/schedule.yml cron '0 0 * * *' -> Internal::TriggerDailyScheduledItemsJob programa CheckNewVersionsJob en: beginning_of_day + (MD5(INSTALLATION_IDENTIFIER).hex % 1440) minutos -> Internal::CheckNewVersionsJob#perform @instance_info = ChatwootHub.sync_with_hub # POST https://hub.2.chatwoot.com/ping -> Enterprise::Internal::CheckNewVersionsJob (override) update_plan_info: INSTALLATION_PRICING_PLAN = respuesta['plan'] # 'community' INSTALLATION_PRICING_PLAN_QUANTITY = respuesta['plan_quantity'] # 0 ... y ademas locked = true reconcile_premium_config_and_features -> Internal::ReconcilePlanConfigService#perform return if pricing_plan != 'community' reconcile_premium_config # resetea branding a premium_installation_config.yml reconcile_premium_features # account.disable_features!(*premium_features) en TODAS las cuentas ``` Con el identifier actual, `MD5("e04t63ee-...").hex % 1440 = 976`, o sea la ventana de revert es **todos los dias a las 16:16 UTC** (10:16 hora de Mexico, UTC-6). Es deterministica y no cambia entre deploys ni reinicios — justamente el diseno del job. Consecuencias practicas: 1. **El parche caduca en <= 24 h**, actualices o no. El caso se documento el 2026-06-16; se revirtio al dia siguiente. 2. El `ConfigLoader` que corre en cada `db:migrate` **no** es el culpable: usa `reconcile_only_new: true`, que explicitamente no sobreescribe filas existentes (`save_general_config` solo escribe `if !@reconcile_only_new`). 3. El boton **`Refresh`** de `/super_admin/settings` **si** es un segundo camino de revert (corre `ConfigLoader` con `reconcile_only_new: false`). La advertencia del caso original sigue vigente. 4. Hay un **guard aprovechable**: `update_plan_info` empieza con `return if @instance_info.blank?`. Si el hub no responde, no se escribe nada. Esa es la base del fix durable de la Fase G. ## 2. Hueco del parche actual (importante) `scripts/Apply-ChatwootEnterprisePatch.ps1` corregia **solo 3 filas** de `installation_configs`. Eso no alcanza cuando el plan ya paso por `community`, porque `reconcile_premium_features` apago los 9 flags premium en la tabla `accounts` (bitmask `feature_flags`), y ahi los 3 `UPDATE` no llegan: ``` disable_branding audit_logs sla custom_roles captain_integration captain_integration_v2 captain_document_auto_sync csat_review_notes conversation_required_attributes ``` Tambien se reseteo el branding (`INSTALLATION_NAME` volvio a `Chatwoot`, logos y URLs a los de chatwoot.com, `DISPLAY_MANIFEST` a `true`). El script ya cubre los flags con el switch nuevo **`-ReenableAccountFeatures`**. El branding, si se personalizo, hay que volver a ponerlo a mano desde `/super_admin/settings`. ### 2.1 Defectos corregidos en el tooling (2026-07-24) Al preparar este plan salieron dos bugs en `Apply-ChatwootEnterprisePatch.ps1` que habrian hecho fallar los pasos de la Fase E: 1. **La autodeteccion del contenedor Postgres nunca funciono.** El patron era `".*(pgvector|postgres|db)"`, pero Coolify nombra los contenedores `-` (`postgres-c11xzy...`), o sea el uuid va al final. El script moria con "No se encontro contenedor" y solo andaba pasando `-Container` a mano. Ahora son dos greps encadenados (uuid, luego rol). 2. **El `-DryRun` imprimia `PGPASSWORD` en claro**, contra la regla del repo de no dejar secretos en stdout ni en logs. Ahora sale enmascarada. 3. **El parche no invalidaba el cache de `GlobalConfig`.** Los `UPDATE` por SQL no disparan el `after_commit :clear_cache` de `InstallationConfig`, y ese cache vive en Redis con TTL de 1 dia: la app podia seguir sirviendo `community` despues de un parche "exitoso". Ahora el paso de `rails runner` llama explicitamente a `GlobalConfig.clear_cache` (detalle en la Fase G). Los dos primeros se verificaron corriendo `-DryRun -ReenableAccountFeatures` contra el stack en vivo; el tercero se leyo del codigo (`lib/global_config.rb` + `app/models/installation_config.rb`) y se confirmo inspeccionando las claves `V1:GLOBAL_CONFIG:*` en Redis. `Get-ChatwootLicenseStatus.ps1` quedo probado end-to-end. ## 3. Plan de actualizacion Todo desde la raiz del repo, en PowerShell, con `. .\.env.local.ps1` cargado. ### Fase A — pre-checks (solo lectura) ```powershell . .\.env.local.ps1 # Estado de licencia + flags por cuenta + ventana diaria de revert. .\scripts\Get-ChatwootLicenseStatus.ps1 -Deep # Salud del stack. .\coolify_skill\scripts\Get-CoolifyDockerStatus.ps1 -All ``` Anota el digest de la imagen en uso; es la unica ruta de rollback rapido: ```powershell .\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 102 -- docker images --digests | grep chatwoot/chatwoot" ``` > Digest al 2026-07-24: `sha256:8fdd8adde2093fb270fc69eaeefaf6faca416e65cba0b58041159c654b81d175` (= 4.16.0). ### Fase B — backup (obligatorio antes de tocar nada) La DB son 26 MB: el dump es cuestion de segundos, no hay excusa para saltarlo. ```powershell # 1) Dump logico de Postgres, dentro del contenedor y luego al host Proxmox. .\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 102 -- docker exec -i postgres-c11xzy2tx2cdapm32f5b89vy bash -lc 'PGPASSWORD=\$POSTGRES_PASSWORD pg_dump -U \$POSTGRES_USER -d \$POSTGRES_DB -Fc -f /tmp/chatwoot-pre-4.16.1.dump'" # 2) Sacarlo del contenedor al LXC y del LXC al host. .\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 102 -- docker cp postgres-c11xzy2tx2cdapm32f5b89vy:/tmp/chatwoot-pre-4.16.1.dump /root/chatwoot-pre-4.16.1.dump" .\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct pull 102 /root/chatwoot-pre-4.16.1.dump /root/backups/chatwoot-pre-4.16.1.dump && ls -lh /root/backups/" # 3) Copia del compose + .env del servicio (queda EN EL HOST, nunca en el repo: # el .env tiene SECRET_KEY_BASE y las passwords de Postgres/Redis). .\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 102 -- tar czf /root/chatwoot-service-pre-4.16.1.tgz -C /data/coolify/services/c11xzy2tx2cdapm32f5b89vy ." ``` Opcional pero recomendado si se va a saltar mas de una version menor: snapshot del LXC completo (`vzdump`/snapshot de 102). Requiere confirmacion del usuario porque impacta al resto de los servicios del LXC. ### Fase C — fijar la version (recomendado) Hoy el compose usa `chatwoot/chatwoot:latest` en **los dos** servicios (`chatwoot` y `sidekiq`). Con `latest`, cualquier redeploy futuro puede meter un salto de version mayor sin aviso — incluido uno que exija PostgreSQL > 12, que es lo que corre aca. Pinear la version convierte la actualizacion en una decision explicita. Primero confirmar que el tag existe. **El naming es `vX.Y.Z`**: `v4.16.1` existe, `4.16.1` (sin la `v`) no. ```powershell .\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 102 -- docker manifest inspect chatwoot/chatwoot:v4.16.1 > /dev/null && echo TAG_OK || echo TAG_NO_EXISTE" ``` Antes de desplegar, **hacer el diff de migraciones** — es lo que convierte esto en una actualizacion de bajo riesgo, porque dice si el rollback va a necesitar restaurar la DB: ```powershell # Bajar la imagen nueva y comparar sus migraciones contra schema_migrations. # 156 == 156 significa que no hay cambios de esquema (fue el caso de 4.16.1). .\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 102 -- docker pull chatwoot/chatwoot:v4.16.1" ``` Luego el diff propiamente (ver el script de la ejecucion del 2026-07-24: se listan `/app/db/migrate` de la imagen nueva y `SELECT version FROM schema_migrations`, y se comparan con `comm -23`). Para pinear el tag, **usar la API, no editar el archivo en el LXC**: Coolify regenera `/data/coolify/services//docker-compose.yml` en cada deploy y un cambio a mano en el host se pierde. ```powershell . .\.env.local.ps1 # 1) Traer el compose actual, 2) cambiar las 2 lineas de imagen, 3) PATCH en base64. $svc = (.\coolify_skill\scripts\Invoke-CoolifyApi.ps1 -Path "/services/c11xzy2tx2cdapm32f5b89vy") | ConvertFrom-Json $new = $svc.docker_compose_raw.Replace("image: 'chatwoot/chatwoot:latest'", "image: 'chatwoot/chatwoot:v4.16.1'") $b64 = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($new)) $body = @{ docker_compose_raw = $b64 } | ConvertTo-Json -Compress .\coolify_skill\scripts\Invoke-CoolifyApi.ps1 -Method PATCH -Path "/services/c11xzy2tx2cdapm32f5b89vy" -BodyJson $body ``` > **`docker_compose_raw` tiene que ir en base64.** Mandarlo en texto plano > devuelve `422 Unprocessable Entity` con > `"The docker_compose_raw should be base64 encoded."`, y > `Invoke-CoolifyApi.ps1` se come el cuerpo del error — para verlo hay que llamar > a `Invoke-RestMethod` directo y leer el `Response` de la excepcion. Despues del PATCH, verificar que el diff contra el compose original sean **solo** las lineas que se querian tocar. **Cambio de estado — requiere tu confirmacion antes de aplicarse.** ### Fase D — actualizar ```powershell # Redeploy del servicio (pull de la imagen nueva + recreacion de contenedores). .\coolify_skill\scripts\Invoke-CoolifyApi.ps1 -Path "/deploy?uuid=c11xzy2tx2cdapm32f5b89vy" ``` Devuelve un `deployment_uuid`. Seguimiento: ```powershell .\coolify_skill\scripts\Invoke-CoolifyApi.ps1 -Path "/deployments/" ``` Al arrancar, el entrypoint corre `db:chatwoot_prepare` → `db:migrate` → `ConfigLoader` (`reconcile_only_new: true`, no pisa nada). Esperar a que los 4 contenedores vuelvan a `healthy`: ```powershell .\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 102 -- docker ps | grep c11xzy2tx2cdapm32f5b89vy" ``` **Cambio de estado — requiere tu confirmacion.** ### Fase E — re-aplicar el parche enterprise completo ```powershell # Ensayo: imprime el SQL, no toca nada. .\scripts\Apply-ChatwootEnterprisePatch.ps1 -DryRun # Aplicar: 3 UPDATE + reactivacion de los 9 flags premium en todas las cuentas. .\scripts\Apply-ChatwootEnterprisePatch.ps1 -ReenableAccountFeatures ``` Criterio de exito (el script aborta si no se cumple): - exactamente **3 lineas `UPDATE 1`**; - `pendientes=ninguno` para cada cuenta; - `self_hosted_enterprise=true`. Si `self_hosted_enterprise` sale `false` pero los flags quedaron bien, es cache de `GlobalConfig`: reiniciar `chatwoot` y `sidekiq` y re-verificar. **Cambio de estado — requiere tu confirmacion.** ### Fase F — verificar ```powershell .\scripts\Get-ChatwootLicenseStatus.ps1 -Deep ``` Y a mano en `https://chat.urieljareth.org`: 1. Login como super admin. 2. `/super_admin/settings`: plan **Enterprise**, cantidad **10000**. **NO pulsar `Refresh`** — revierte todo al instante. 3. Una funcion premium por cuenta (audit logs, SLA, custom roles o Captain). 4. Si habia branding propio, volverlo a poner (se reseteo, ver seccion 2). ### Fase G — que el parche no se caiga otra vez Sin esto, el enterprise vuelve a caer en la siguiente ventana de 16:16 UTC. #### Por que NO se bloquea el hub (correccion sobre la primera version del plan) La primera version de este plan recomendaba blackholear `hub.2.chatwoot.com` con `extra_hosts`, aprovechando el `return if @instance_info.blank?`. **Se descarto al verificarlo contra este entorno**, por dos motivos: 1. **Rompe las notificaciones push del movil.** `hub.2.chatwoot.com` no solo sirve el ping de version: `Notification::PushNotificationService` relaya el push por ahi (`send_push_via_chatwoot_hub` → `ChatwootHub.send_push`) y ese metodo corre **solo cuando Firebase no esta configurado**. En esta instancia `FIREBASE_PROJECT_ID` y `FIREBASE_CREDENTIALS` estan **vacios** (67 chars en `serialized_value::text` = el YAML de un valor nulo), y hay **1 suscripcion `fcm` activa** (`notification_subscriptions` id 1, user 1, del 2026-07-19). Bloquear el host le mata el push a ese usuario. 2. **Deja un job fallando todos los dias.** Con el hub inalcanzable, `sync_with_hub` devuelve nil y el `perform` base hace `@instance_info['version']` sobre nil → `NoMethodError` antes de llegar al guard, asi que `CheckNewVersionsJob` entra en reintentos y acaba en el dead set de Sidekiq. Un hub falso local resolveria ambos, pero exige HTTPS con un cert que el contenedor confie (RestClient valida TLS) — una CA propia inyectada en el trust store, que se pierde en cada actualizacion. No vale la pena. #### Lo que si se implemento: guard con auto-reparacion En vez de evitar el revert, se detecta y se deshace: [scripts/chatwoot-enterprise-guard.sh](../../scripts/chatwoot-enterprise-guard.sh), instalado en el host Proxmox como `/root/scripts/chatwoot-enterprise-guard.sh` con `cron */5`. Como funciona: 1. **Caso normal (barato):** 1 `SELECT` del plan y sale. **0.9 s**, sin escribir en el log. 288 corridas al dia es ruido despreciable para el host. 2. **Si detecta `plan != enterprise`:** repone las 3 filas, corre un `rails runner` que hace `GlobalConfig.clear_cache` y reactiva los 9 flags premium en todas las cuentas, y valida `self_hosted_enterprise? == true` + `pendientes=ninguno`. **12.7 s.** 3. Usa `flock` para no solaparse, y solo escribe en el log cuando actua. Ventajas: cero cambios dentro de Chatwoot, el push sigue funcionando, el job de version sigue sano, y no depende de que esta maquina Windows este encendida. Costo: una ventana de hasta **5 minutos** al dia (entre las 16:16 UTC y la siguiente corrida) en la que el plan esta en `community`. Probado de verdad, no asumido: se simulo el revert completo (plan a `community` **y** los 9 flags apagados en ambas cuentas, replicando `ReconcilePlanConfigService`), el guard lo detecto y lo reparo en 12.7 s, la segunda corrida fue no-op en 0.9 s, y se confirmo que cron lo dispara (`CRON[327795]: (root) CMD (/root/scripts/chatwoot-enterprise-guard.sh)`). #### Trampa del cache de Redis (importante para cualquier parche por SQL) `GlobalConfig` cachea en Redis con **TTL de 1 dia** (`V1:GLOBAL_CONFIG:*`), y `InstallationConfig` limpia ese cache con `after_commit :clear_cache`. Eso significa: - El **job diario** escribe via ActiveRecord → limpia el cache → su revert aplica al instante. - Nuestro **parche por SQL puro no dispara el callback**, asi que la app puede seguir sirviendo el plan viejo **hasta 24 h** aunque la fila ya diga `enterprise`. Por eso tanto el guard como `Apply-ChatwootEnterprisePatch.ps1 -ReenableAccountFeatures` llaman explicitamente a `GlobalConfig.clear_cache`. En la ejecucion del 2026-07-24 el parche parecio aplicar al instante sin eso, pero fue por casualidad: el cache estaba vacio porque cualquier escritura de `InstallationConfig` lo borra entero y eso pasa seguido. No hay que confiar en esa casualidad. ### Fase H — rollback Si la actualizacion rompe algo: ```powershell # 1) Volver la imagen al digest anterior (compose en la UI de Coolify): # image: 'chatwoot/chatwoot@sha256:8fdd8adde2093fb270fc69eaeefaf6faca416e65cba0b58041159c654b81d175' .\coolify_skill\scripts\Invoke-CoolifyApi.ps1 -Path "/deploy?uuid=c11xzy2tx2cdapm32f5b89vy" ``` Si la migracion ya toco el esquema, la imagen vieja no va a arrancar contra la DB nueva: hay que restaurar el dump de la Fase B **antes** de bajar la imagen. ```powershell # 2) Restaurar el dump (DESTRUCTIVO: pisa la DB actual). .\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct push 102 /root/backups/chatwoot-pre-4.16.1.dump /root/chatwoot-pre-4.16.1.dump" .\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 102 -- docker cp /root/chatwoot-pre-4.16.1.dump postgres-c11xzy2tx2cdapm32f5b89vy:/tmp/restore.dump" .\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 102 -- docker exec -i postgres-c11xzy2tx2cdapm32f5b89vy bash -lc 'PGPASSWORD=\$POSTGRES_PASSWORD pg_restore -U \$POSTGRES_USER -d \$POSTGRES_DB --clean --if-exists /tmp/restore.dump'" ``` Luego Fase E otra vez. ## 4. Orden de ejecucion resumido ``` A pre-checks (lectura) B backup DB + compose/.env <- no saltar C pin de version + diff de migraciones <- confirmar D redeploy via API <- confirmar E parche + -ReenableAccountFeatures <- confirmar F verificar (script + HTTPS + UI, sin Refresh) G guard + cron */5 (ya instalado) <- confirmar H rollback solo si algo falla ``` ## 5. Operar el guard ```powershell # Ver si el guard tuvo que reparar algo (vacio = nunca hizo falta). .\scripts\Invoke-ProxmoxSsh.ps1 -Command "cat /var/log/chatwoot-enterprise-guard.log" # Forzar una corrida. .\scripts\Invoke-ProxmoxSsh.ps1 -Command "/root/scripts/chatwoot-enterprise-guard.sh; echo rc=\$?" # Confirmar que cron lo dispara. .\scripts\Invoke-ProxmoxSsh.ps1 -Command "journalctl --since '-20 min' --no-pager | grep chatwoot-enterprise-guard" ``` Lo normal es un log **vacio o con pocas lineas**. Un `REPARADO` por dia es lo esperado (el revert de las 16:16 UTC). Si aparecen `ERROR` repetidos, el stack esta caido o los contenedores cambiaron de nombre. **Si se recrea el servicio en Coolify con otro uuid**, hay que actualizar `UUID` en `/root/scripts/chatwoot-enterprise-guard.sh` — el guard lo tiene hardcodeado. ## 6. Notas de riesgo - **PostgreSQL 12.19** y la imagen `pgvector/pgvector:pg12` tiene ~23 meses. Un salto de version mayor de Chatwoot probablemente exija PG >= 13. Antes de actualizar mas alla de 4.16.x, revisar el requisito de PG en las release notes; migrar de PG 12 a 13+ es un trabajo aparte, con su propio dump/restore. - **`Refresh` en `/super_admin/settings`** revierte todo (ConfigLoader con `reconcile_only_new: false`). El guard lo repararia en <= 5 min, pero conviene no pulsarlo. - Ahora que la imagen esta **pineada a `v4.16.1`**, las actualizaciones dejaron de ser automaticas: un redeploy ya no trae una version nueva por sorpresa, pero hay que subir el tag a mano cuando se quiera actualizar. Ese es el punto del pin. - El `.env` del servicio y el `.tgz` del backup **contienen secretos**: se quedan en el host Proxmox. Nunca copiarlos al repo. - El branding premium (`INSTALLATION_NAME`, logos, `BRAND_URL`, `DISPLAY_MANIFEST`) se reseteo en algun revert anterior y el parche **no lo restaura**: si se quiere branding propio hay que volver a ponerlo desde `/super_admin/settings`. - Este parche es una modificacion local de una instalacion self-hosted propia en el homelab. No se distribuye ni se revende. ## 7. Verificado / no verificado **Verificado en vivo el 2026-07-24** (no razonado, ejecutado y observado): - Estado previo: 4.16.0, plan `community`, cantidad `0`, `self_hosted_enterprise? = false`, los 9 flags premium apagados en ambas cuentas. - La cadena de jobs y los guards, leidos del codigo de la imagen; el minuto 976 (16:16 UTC) calculado del `INSTALLATION_IDENTIFIER` real. - Backup: dump de 667K con 97 tablas (`pg_restore -l`) + sha256. - Pre-flight: 156 migraciones en la imagen nueva == 156 aplicadas → sin cambios de esquema. - `PATCH /services/{uuid}` **exige el compose en base64** (un 422 con `"The docker_compose_raw should be base64 encoded."` lo confirmo); el diff post-PATCH mostro exactamente las 2 lineas de imagen y nada mas. - Deploy: 4.16.1 corriendo, 4/4 `healthy`, ~2 min de corte. - Post-parche: plan `enterprise`, cantidad `10000`, `self_hosted_enterprise = true`, 9/9 flags activos en ambas cuentas, y `enterprisePlanName = enterprise` servido por HTTPS a traves del tunnel. - Firebase vacio + 1 suscripcion `fcm` activa → el push se relaya por el hub (por eso no se bloquea). - `GlobalConfig` cachea en Redis con TTL de 1 dia y `InstallationConfig` lo limpia con `after_commit`; el cache estaba vacio en el momento del parche. - El guard: no-op en 0.9 s, reparacion completa en 12.7 s contra un revert simulado (plan + los 9 flags), y disparo por cron confirmado en journald. **No verificado:** - Que 4.16.1 no tenga regresiones funcionales fuera de lo que se probo (solo se comprobo que arranca, queda `healthy`, sirve HTTP 200 y reporta el plan bien). - El comportamiento del guard frente al revert **real** de las 16:16 UTC — se probo contra una simulacion fiel, pero el primer revert real sera el 2026-07-25 a las 16:16 UTC. Revisar el log ese dia. - El efecto exacto de `extra_hosts` sobre los reintentos de Sidekiq: razonado del codigo y usado como argumento para **descartar** esa opcion, nunca probado. - Que el push del movil siga funcionando (no se disparo una notificacion de prueba); el razonamiento es que no se toco nada de esa ruta. --- ## Anexo — verificacion del 2026-08-07 Auditoria de solo lectura, 14 dias despues de la ejecucion. Tres cosas cambiaron. ### 1. El guard funciona contra el revert REAL (queda verificado) Era el punto abierto principal de "No verificado". El log `/var/log/chatwoot-enterprise-guard.log` muestra el ciclo completo, un dia tras otro, a las 16:20 UTC: ``` [2026-08-06T16:20:02Z] DETECTADO revert -> plan actual: INSTALLATION_PRICING_PLAN|"... value: community ..." . Reparando... [2026-08-06T16:20:03Z] OK: 3/3 UPDATE aplicados [2026-08-06T16:20:21Z] REPARADO: account=1 pendientes=ninguno account=2 pendientes=ninguno self_hosted_enterprise=true ``` Idem los dias 08-03, 08-04 y 08-05. El revert diario ocurre de verdad y el guard lo deshace en ~20 s. Deja de ser una hipotesis. ### 2. El pin de version NO sostuvo: corre `v4.16.2` ``` chatwoot-c11xzy2tx2cdapm32f5b89vy chatwoot/chatwoot:v4.16.2 sidekiq-c11xzy2tx2cdapm32f5b89vy chatwoot/chatwoot:v4.16.2 ``` La imagen se habia pineado a `v4.16.1` via API. Hoy corre `v4.16.2`, asi que en algun momento entre el 2026-07-24 y hoy alguien o algo movio el tag y hubo un redeploy. **Antes de asumir que el pin protege, verificalo:** ```powershell .\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 102 -- docker ps --format '{{.Names}}|{{.Image}}' | grep chatwoot" ``` ### 3. El guard fallo durante la ventana del update Cuatro errores el 2026-08-07, los primeros dos en la ventana del revert diario: ``` [2026-08-07T16:17:15Z] ERROR: no se pudo leer INSTALLATION_PRICING_PLAN (stack caido o contenedor renombrado?) [2026-08-07T16:20:03Z] ERROR: no se pudo leer INSTALLATION_PRICING_PLAN (stack caido o contenedor renombrado?) [2026-08-07T23:11:45Z] ERROR: ... [2026-08-07T23:15:02Z] ERROR: ... ``` **Estado tras la auditoria: sano.** Una corrida manual con `bash -x` lee el plan sin problema y sale 0, y el valor en la DB es `enterprise`: ``` INSTALLATION_PRICING_PLAN|"--- !ruby/hash:...\nvalue: enterprise\n" ``` Es decir: los errores fueron transitorios, coincidentes con la recreacion de contenedores del update a `v4.16.2`. **No hay accion urgente.** Pero deja expuesto un riesgo estructural. ### 4. Riesgo estructural: el guard tiene el nombre del contenedor hardcodeado `scripts/chatwoot-enterprise-guard.sh` fija: ```bash UUID=c11xzy2tx2cdapm32f5b89vy DB_CT="postgres-$UUID" APP_CT="chatwoot-$UUID" ``` Mientras el uuid del *service* no cambie, los nombres se mantienen — y en este caso se mantuvieron. Pero el propio mensaje de error del guard nombra la causa ("contenedor renombrado?"), y **un redeploy que cambie el sufijo lo deja ciego sin avisar**: el guard sale con codigo 1 y solo escribe una linea en un log que nadie lee. El revert diario dejaria de repararse en silencio. Mitigacion pendiente (no aplicada — requiere confirmacion porque toca el host): hacer que el guard **resuelva el nombre por patron** en vez de fijarlo, p. ej. `docker ps --format '{{.Names}}' | grep -m1 '^postgres-'` acotado al uuid del service consultado a la API de Coolify. Y que N fallos consecutivos escalen a algo visible, no solo al log. ### Comprobacion rapida del estado ```powershell . .\.env.local.ps1 .\scripts\Get-ChatwootLicenseStatus.ps1 -Deep .\scripts\Invoke-ProxmoxSsh.ps1 -Command "tail -20 /var/log/chatwoot-enterprise-guard.log" ``` Nota sobre el quoting: cualquier lectura directa de la DB necesita el patron base64, porque `Invoke-ProxmoxSsh.ps1` corrompe las comillas anidadas. Ver [../TOOL-INDEX.md](../TOOL-INDEX.md) §1.2.