Files

581 lines
27 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
`"<uuid>.*(pgvector|postgres|db)"`, pero Coolify nombra los contenedores
`<servicio>-<uuid>` (`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/<uuid>/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/<deployment_uuid>"
```
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.