# Runbook — Recovery

> Última actualización: 2026-04-29 (post Sprint 1+2+3)
> Cómo actuar cuando algo se rompe. Cada escenario es independiente: ir directo a la sección que aplica.

## Convención

- **Síntoma**: cómo se manifiesta el problema (qué ven los humanos / qué dice el monitor).
- **Diagnóstico**: comandos para confirmar la causa.
- **Mitigación**: solución temporal para volver a operativo.
- **Fix**: solución de raíz (cuando aplique).

---

## A) Provisioning

### A1. Un proyecto queda atascado en `Pipeline_Status="Processing"`

**Síntoma**: Airtable muestra el record en `Processing` por más de ~10 min. Cliente no recibe credenciales. Slack `cd-bewpro` no muestra el ✅.

**Diagnóstico** (en VPS1):
```bash
# Buscar el proyecto en logs
grep -A20 "Proyecto:.*<NOMBRE>" /var/log/bewpro-provision.log | tail -50

# Ver si setup_cd_project*.sh quedó corriendo
ps -ef | grep -E 'setup_cd_project|bewpro:new'

# Ver últimas líneas del log de createacct para ese cpanel user
tail -100 /tmp/createacct_<base_name>.log
```

**Mitigación**:
1. Si `setup_cd_project*.sh` murió a mitad: revertir manualmente con `php artisan bewpro:delete <cpanel_user>` desde el VPS donde se intentó.
2. En Airtable: cambiar `Pipeline_Status` de `Processing` → `Required` para que el siguiente cron lo reintente.
3. Si la cuenta cPanel se creó pero el clone falló, podemos saltarnos pasos manualmente — reescribir el comando del setup desde su step actual.

**Fix**: depende de qué falló. Causas vistas históricamente:
- DNS no propagó → `fix-ssl.sh <user>` después de unos minutos.
- composer install murió por OOM → setear `php-fpm` con más memoria o `--no-dev`.
- Airtable rate limit → reducir frecuencia del cron a `*/10`.

---

### A2. Cliente paga en Stripe pero no aparece en Airtable

**Síntoma**: Slack `cd-bewpro` no muestra "💰 Nueva compra". Cliente pregunta por sus credenciales.

**Diagnóstico**:
```bash
# Buscar evento en Stripe dashboard
# Ver logs del webhook receiver (Laravel principal de bewpro.com)
tail -200 storage/logs/laravel.log | grep -i 'stripe\|webhook'
```

**Causas comunes**:
1. Webhook de Stripe no apunta a la URL correcta. Verificar en Stripe Dashboard → Webhooks.
2. `STRIPE_WEBHOOK_SECRET` desincronizado con el secreto del endpoint en Stripe.
3. `AIRTABLE_TOKEN` revocado/expirado.
4. Laravel app de bewpro.com está caída.

**Mitigación**:
1. Si el evento aparece en Stripe pero falló: hacer "Resend" desde Stripe Dashboard.
2. Si no se puede recuperar: crear el record manualmente en Airtable Projects con `Pipeline_Status=Required`.

---

### A3. Provisioning OK pero sitio devuelve 500

**Síntoma**: Email de credenciales llega, cliente entra, pero el dominio responde 500.

**Diagnóstico** (SSH al VPS donde se provisionó):
```bash
su - <user> -c "tail -100 public_html/git-files/<user>/storage/logs/laravel.log"
```

**Causas comunes**:
1. **Permisos**: `storage/` y `bootstrap/cache/` deben ser escribibles por el usuario.
2. **`.env` corrupto**: faltan variables, `APP_KEY` mal formado.
3. **Migraciones no corridas**: tabla `users` no existe.

**Mitigación**:
```bash
su - <user> -c "
  cd public_html/git-files/<user>
  php artisan config:clear && php artisan view:clear
  chmod -R 775 storage bootstrap/cache
"
```

Si persiste: re-ejecutar `bewpro:new` con `--skip-migrate` no-no, el flag correcto sería investigar el log primero.

---

## B) SSL / DNS

### B1. SSL self-signed o cert no emitido

**Síntoma**: el browser muestra "no es seguro" o `ERR_CERT_AUTHORITY_INVALID`.

**Diagnóstico** (en el VPS donde está la cuenta):
```bash
# Verificar emisor actual
echo | openssl s_client -connect <domain>:443 -servername <domain> 2>/dev/null \
  | openssl x509 -noout -issuer

# Verificar que DNS apunta a la IP correcta del VPS
dig +short A <domain>
```

**Mitigación**:
```bash
/root/scripts/fix-ssl.sh <cpanel_user> [expected_ip]

# Si no existe fix-ssl.sh:
uapi --user=<cpanel_user> SSL set_autossl_excluded_domains domains=
whmapi1 start_autossl_check_for_one_user username=<cpanel_user>
```

**Fix de raíz**:
- Si el DNS apunta a un VPS pero la cuenta vive en el otro: ver `04-risks-and-priorities.md` R9 (drift de IP en setup script).

---

### B2. DNS no propaga

**Síntoma**: step 10 del setup script reporta WARN "DNS no propagó en 3 minutos".

**Diagnóstico**:
```bash
dig +short A <subdomain>.bewpro.com @8.8.8.8
dig +short A <subdomain>.bewpro.com @1.1.1.1
```

**Mitigación**:
1. Verificar zona en Hostinger panel manualmente.
2. Si no existe el record: crear manualmente vía API o panel.
3. Esperar TTL (ttl=3600 en el setup → hasta 1h en peor caso).

---

## C) VPS

### C1. Cae VPS1 (orquestador)

**Síntoma**: hace > 5 min que no entran provisiones nuevas. Cliente que pagó hace 30 min sigue sin sitio.

**Diagnóstico**:
- Hostinger panel → estado del VPS
- Ping 72.61.45.136
- SSH directo

**Impacto**:
- ❌ Provisioning detenido (cron orquestador)
- ❌ Suspensiones detenidas
- ❌ Backups detenidos
- ❌ Email forwarders → Slack detenidos
- ❌ ~36 sitios alojados en VPS1 caídos
- ❌ DNS de bewpro.com responde (Hostinger maneja la zona) pero subdominios de tenants en VPS1 no resuelven a HTTP

**Mitigación inmediata** (sin VPS1):
1. Comunicar en Slack y por email a clientes activos.
2. Stripe sigue cobrando — los webhooks se acumulan en cola de retry de Stripe (3 días).
3. Provisioning manual: ejecutar `setup_cd_project*.sh` directamente en VPS2 si el cliente es urgente.

**Fix**:
1. Levantar VPS1 desde snapshot/backup de Hostinger.
2. Cuando esté arriba: el cron orquestador procesará el backlog de Airtable.
3. Stripe reintentará webhooks pendientes automáticamente.

**Mitigación de fondo (Fase 3)**: replicar orquestador a VPS2 con lock distribuido — ver R6.

---

### C2. Cae VPS2

**Síntoma**: tenants alojados en VPS2 (~21) caídos. Provisioning sigue funcionando porque vive en VPS1.

**Diagnóstico**:
```bash
ssh -p 5633 root@179.43.124.219  # desde VPS1 o local
```

**Mitigación**:
- Levantar desde snapshot/backup.
- El cron de VPS1 no podrá disparar nuevas provisiones a VPS2 hasta que vuelva — `select_target_server()` hace fallback a VPS1, lo que sobrecarga a este último.

---

### C3. Pérdida total de un VPS (sin snapshot recuperable)

**Síntoma**: VPS irrecuperable. Hay que recrear desde cero.

**Recuperación**:
1. **Antes que nada**: confirmar si hay backups externos (no solo snapshots de Hostinger). Sospecha actual: `backup-daily-cpanel.sh` corre pero **no se ha verificado restore** (R8).
2. Provisionar VPS nuevo en el mismo proveedor.
3. Reinstalar cPanel.
4. Restaurar cuentas desde backup tarball.
5. Reapuntar DNS (Hostinger zone) a la IP nueva.
6. Recrear `/root/scripts/.airtable.env` con tokens (rotar por seguridad).
7. Re-clonar scripts desde repo.
8. Reconfigurar cron.

**Tiempo estimado**: 4-12 hs si hay backups; **catastrófico sin ellos**.

---

## D) Airtable / Slack

### D1. AIRTABLE_TOKEN revocado/expirado

**Síntoma**: webhook de Stripe falla con 401. Cron orquestador no encuentra registros (devuelve `{}`).

**Diagnóstico**:
```bash
curl -H "Authorization: Bearer $AIRTABLE_TOKEN" \
  "https://api.airtable.com/v0/appRxvpzqCmNsw2JN/Projects?maxRecords=1"
# Si devuelve 401 → token revocado
```

**Mitigación**:
1. Generar PAT nuevo en airtable.com/create/tokens (mismos scopes: read/write base appRxvpzqCmNsw2JN).
2. Actualizar:
   - `.env` de la app principal de bewpro.com
   - `/root/scripts/.airtable.env` en VPS1
   - `/root/scripts/.airtable.env` en VPS2 (si existe)
3. Restartear queue worker / php-fpm si aplica.

---

### D2. Slack webhook devuelve `invalid_payload` o `404`

**Síntoma**: notificaciones no llegan a un canal. Resto de canales OK.

**Diagnóstico**:
```bash
curl -X POST -H 'Content-Type: application/json' \
  --data '{"text":"test"}' \
  "$SLACK_WEBHOOK_URL"
```

**Causas**:
- App de Slack archivada/eliminada → 404. Recrear webhook.
- Payload mal formateado → ver código de `SlackService.php`.

---

## E) Billing

### E1. Stripe deja de cobrar

**Síntoma**: `bewpro:billing:status` muestra suscripciones que deberían haber renovado pero `Last_Payment_Date` no se actualizó.

**Diagnóstico**:
1. Revisar webhook receiver — ver A2.
2. En Stripe Dashboard → Subscriptions: verificar estado. Si está en `past_due`, ver si el método de pago caducó.

---

### E2. Cliente con pago hecho sigue suspendido

**Síntoma**: cliente paga en Stripe (manual o automático), pero su sitio sigue suspendido en cPanel.

**Diagnóstico**:
- En Airtable Subscriptions: ¿cambió `Status` a `Active`? ¿`Suspended_Date` está limpio?

**Mitigación**:
- Si Airtable está actualizado: el siguiente cron de `process-suspensions.sh` lo reactivará (corre 08:00 diario).
- Si urgente: `whmapi1 unsuspendacct user=<cpanel_user>` directo en el VPS.

---

## F) Acceso operativo

### F1. Perdimos password root del VPS

**Mitigación**:
1. Recuperar desde Hostinger/Donweb panel (rescue mode o reset password).
2. Una vez adentro: regenerar SSH keys, deshabilitar password auth (ver Fase 2 — [05-ssh-access-methodology.md](05-ssh-access-methodology.md)).

### F2. Repo GitHub revocó acceso al deploy key

**Síntoma**: `setup_cd_project*.sh` falla en step 5 (git clone) con "Permission denied (publickey)".

**Mitigación**:
1. Generar deploy key nueva en el VPS:
   ```bash
   ssh-keygen -t ed25519 -f /root/.ssh/cd_system_deploy -N ''
   cat /root/.ssh/cd_system_deploy.pub
   ```
2. Agregar a GitHub: Settings → Deploy keys del repo `cd-system`.
3. Crear `~/.ssh/config` con `Host github.com IdentityFile /root/.ssh/cd_system_deploy`.

---

## H) Backups

### H1. VPS2 cron de backup dice `BACKUPACCTS disabled`

**Síntoma**: log `/usr/local/cpanel/logs/cpbackup/*.log` muestra `info [backup] BACKUPACCTS disabled.` aunque `whmapi1 backup_config_get` devuelve `backupaccts: 1`.

**Diagnóstico**:
```bash
ssh vps2 "cat /var/cpanel/backups/config | grep BACKUPACCTS"
ssh vps2 "ls -la /var/cpanel/backups/config /var/cpanel/backups/config.cache"
ssh vps2 "ls -1t /usr/local/cpanel/logs/cpbackup/*.log | head -1 | xargs tail -20"
```

**Mitigación**:
1. Verificar mtime del config — si fue modificado recientemente, el cache puede estar stale. Esperar al próximo cron 02:00 (timezone VPS2 es `-0300` = Argentina).
2. Forzar cache rebuild: `whmapi1 backup_config_set backupaccts=1` (re-aplicar con misma value para regenerar cache).
3. Verificar resultado en próximo cron daily.

**Fix**: si el problema persiste tras 2 ciclos, **escalar a Donweb support** (es VPS managed por Donweb, ellos tienen visibilidad de cPanel internals).

### H2. Verificar backups en Backblaze B2 (sin web UI)

```bash
ssh vps1-claude '
set -a; source /root/scripts/.airtable.env; set +a
APPKEY=$(whmapi1 backup_destination_list --output=jsonpretty | python3 -c "
import sys,json
d=json.load(sys.stdin)
for x in d[\"data\"][\"destination_list\"]:
    if x[\"type\"]==\"Backblaze\":
        print(x[\"application_key_id\"]+\" \"+x[\"application_key\"]+\" \"+x[\"bucket_id\"])
")
KEYID=$(echo $APPKEY | cut -d" " -f1)
KEY=$(echo $APPKEY | cut -d" " -f2)
BID=$(echo $APPKEY | cut -d" " -f3)
AUTH=$(curl -s "https://api.backblazeb2.com/b2api/v3/b2_authorize_account" -u "$KEYID:$KEY")
TOKEN=$(echo $AUTH | python3 -c "import sys,json;print(json.load(sys.stdin)[\"authorizationToken\"])")
URL=$(echo $AUTH | python3 -c "import sys,json;print(json.load(sys.stdin)[\"apiInfo\"][\"storageApi\"][\"apiUrl\"])")
curl -s -X POST "$URL/b2api/v3/b2_list_file_names" -H "Authorization: $TOKEN" \
  -d "{\"bucketId\":\"$BID\",\"maxFileCount\":50,\"prefix\":\"backups/\"}" \
  | python3 -c "import sys,json,datetime
d=json.load(sys.stdin)
for f in d.get(\"files\",[])[:30]:
    ts=datetime.datetime.utcfromtimestamp(f[\"uploadTimestamp\"]/1000).strftime(\"%Y-%m-%d %H:%M\")
    print(ts, f\"{f[\\\"contentLength\\\"]/1024/1024:.0f}MB\", f[\"fileName\"])"
'
```

### H3. Restore de un tenant desde backup

**Drill parcial validado 2026-04-29**: download B2 + tar verify + DB extract OK.
Drill completo (restorepkg a cuenta sandbox) pendiente — requiere ventana de mantenimiento.

#### Validación que sí está confirmada
- B2 devuelve `taws.tar.gz` (324MB) en 13s.
- `tar -tzf` integridad OK: 64,015 archivos.
- Estructura cPanel correcta (`<user>/{apache_tls,cp,cron,mysql,homedir}/`).
- DB dump extraíble: `tar -xzOf <backup> <user>/mysql/<db>.sql` devuelve header MariaDB válido.

#### Procedimiento end-to-end (cuando se necesite restore real)

```bash
# 1. Listar backups disponibles del user — vía B2 (KEEPLOCAL=0)
ssh vps1-claude "/root/scripts/sync-scripts.sh --check"  # asegurar sync OK
# Usar scripts H2 arriba para listar B2

# 2. Download el .tar.gz desde B2
# (ver script H2 — usar `curl -H "Authorization:" download_url/file/bucket/path/<user>.tar.gz`)

# 3. Verify integridad
tar -tzf /tmp/<user>.tar.gz | wc -l  # debería ser miles de archivos
tar -tzf /tmp/<user>.tar.gz | grep "\.sql$"  # debería listar el DB dump

# 4a. Restore replace (si la cuenta original existe pero está corrupta):
ssh vps1 "/usr/local/cpanel/scripts/restorepkg /tmp/<user>.tar.gz"
# ⚠️ Esto SOBRESCRIBE el user activo — solo para recovery real.

# 4b. Restore a cuenta nueva (drill o testing):
# whmapi1 createacct username=<user>-recover password=...
# extraer manualmente al /home/<user>-recover/ y ajustar paths/DB.
# Más complicado pero non-destructive.

# 5. Validar
ssh vps1 "whmapi1 listaccts --output=json | grep <user>"
ssh vps1 "su - <user> -c 'cd public_html/git-files/<user> && php artisan migrate:status' | tail -10"
ssh vps1 "curl -sf -m 10 https://<user>.bewpro.com/ -o /dev/null -w 'HTTP %{http_code}'"
```

#### Tiempo estimado
- Download B2 → VPS: ~30s por GB.
- restorepkg: 5-10min para tenant ~2GB.
- Validación: 1-2min.
**Total**: ~10-15min por tenant.

---

## I) Workers y queue (R4 — Sprint 2)

### I1. Worker `bewpro-queue` no procesa jobs

**Síntoma**: tabla `jobs` en bewpro22 acumula records sin procesarse. Stripe webhooks generan dispatch pero no se ejecuta nada.

**Diagnóstico**:
```bash
ssh vps1-claude "supervisorctl status bewpro-queue:*"
ssh vps1-claude "tail -30 /var/log/bewpro/queue.log"
ssh vps1-claude "su - bewpro22 -c 'cd public_html/bewpro && php artisan tinker --execute=\"echo DB::table(\\\"jobs\\\")->count();\"'"
```

**Mitigación**:
```bash
# Restart worker
ssh vps1-claude "supervisorctl restart bewpro-queue"

# Si supervisor está caído
ssh vps1-claude "systemctl restart supervisord"

# Drenar queue manualmente (one-shot)
ssh vps1-claude "su - bewpro22 -c 'cd public_html/bewpro && /usr/local/bin/php artisan queue:work --stop-when-empty --tries=3'"
```

### I2. Después de cada deploy de bewpro22

```bash
ssh vps1-claude "su - bewpro22 -c 'cd public_html/bewpro && /usr/local/bin/php artisan queue:restart'"
```

El worker termina su job actual y supervisor lo reinicia con código nuevo.

---

## J) Sync VPS↔repo (R1 final — Sprint 2)

### J1. Slack notifica `Sync drift VPS↔repo`

**Síntoma**: cron diario `0 7 * * *` postea drift entre `/root/scripts/` y `infrastructure/scripts/` del repo.

**Diagnóstico**:
```bash
ssh vps1-claude "/root/scripts/sync-scripts.sh --check 2>&1"
```

**Mitigación**:

a) **Si el cambio del VPS es deseado** (alguien editó un script urgente):
   1. SCP del archivo del VPS al repo local: `scp vps1-claude:/root/scripts/<file> infrastructure/scripts/<categoria>/<file>`
   2. Commit + push.

b) **Si el cambio del VPS es accidental / debe revertirse**:
   ```
   ssh vps1-claude "/root/scripts/sync-scripts.sh --apply"
   ```
   Esto sobrescribe el VPS con la versión del repo (con backup `.bak-pre-sync-*` automático).

---

## K) DR — VPS1 caído por completo

Si VPS1 se pierde irrecuperablemente:

1. **Provisionar VPS1 nuevo** en Hostinger.
2. **Restaurar cuentas cPanel** desde backups B2 (ver H2/H3 — bajar tarballs y `restorepkg`).
3. **Reapuntar DNS** de `bewpro.com` y subdominios al IP nuevo (Hostinger panel).
4. **Recrear scripts** vía `git clone` del repo + `sync-scripts.sh --apply` (todos los canónicos están versionados).
5. **Recrear `/root/scripts/.airtable.env`** desde gestor de passwords (template en `infrastructure/env-templates/airtable.env.example`).
6. **Recrear `/root/.ssh/`** (deploy keys GitHub + cross-VPS).
7. **Activar email pipes**: crear cuenta cPanel `lacompany` + apuntar MX `lacompaniadigital.com` al VPS nuevo + activar pipe en `cPanel Email Forwarders`.
8. **Activar cron orquestador**: ya está en `infrastructure/scripts/orchestrator/process-airtable.sh`. Agregar entry a crontab root.
9. **Activar supervisor `bewpro-queue`**: `dnf install supervisor && systemctl enable --now supervisord`. Config en repo.

**Tiempo estimado**: 4-12hs si los backups son recuperables.

**Mitigación temporal mientras se reconstruye**:
- Activar cron `process-airtable.sh` en VPS2 (script ya está sincronizado, solo falta agregar al crontab root).
- Stripe retries el webhook 3 días — no perdemos compras nuevas.

---

## N) Setup CI/CD GitHub Actions (Sprint 4 / E.2)

El workflow `.github/workflows/deploy.yml` deploya bewpro.com en push a `cd-system`.
Necesita 4 secrets en GH repo settings.

### N.1 Generar deploy key dedicada

**No reusar** la key personal de Coke ni la de Claude (separación de concerns).

```bash
# En tu Mac local:
ssh-keygen -t ed25519 \
  -f ~/.ssh/bewpro_gh_deploy \
  -N "" \
  -C "github-actions deploy → bewpro22@vps1"

# La pubkey queda en ~/.ssh/bewpro_gh_deploy.pub
# La privkey queda en ~/.ssh/bewpro_gh_deploy (esta va a GH secret)
```

### N.2 Agregar pubkey a VPS1

```bash
ssh vps1-claude '
PUBKEY="ssh-ed25519 AAAA... github-actions deploy → bewpro22@vps1"
echo "$PUBKEY" >> /root/.ssh/authorized_keys
chmod 600 /root/.ssh/authorized_keys
'
```

### N.3 Setear secrets en GitHub

GH repo → Settings → Secrets and variables → Actions → New repository secret.

| Secret name | Valor |
|---|---|
| `VPS1_SSH_HOST` | `72.61.45.136` |
| `VPS1_SSH_PORT` | `22` (opcional — default 22) |
| `VPS1_SSH_USER` | `root` (opcional — default root) |
| `VPS1_SSH_KEY` | Contenido completo de `~/.ssh/bewpro_gh_deploy` (incluyendo `-----BEGIN OPENSSH PRIVATE KEY-----` y `-----END OPENSSH PRIVATE KEY-----`) |

```bash
# Para copiar al portapapeles macOS:
pbcopy < ~/.ssh/bewpro_gh_deploy
```

### N.4 Validar

Push a `cd-system` o "Run workflow" manual desde GH Actions tab. El job debería:
1. Verify required secrets ✅
2. Setup SSH key ✅
3. SSH a VPS1 → git pull → composer → migrate → cache:clear → queue:restart → smoke test ✅

Si falla con "Missing required secrets": volver a N.3.
Si falla con "Invalid SSH key": el secret `VPS1_SSH_KEY` no se pegó completo. Re-pegar incluyendo BEGIN/END.

### N.5 Revocar acceso (rotación)

Si hay sospecha de compromiso:
```bash
# 1. Borrar la línea de la pubkey de root@vps1:
ssh vps1-claude "sed -i '/github-actions deploy/d' /root/.ssh/authorized_keys"

# 2. Generar una nueva (paso N.1) e instalar (paso N.2).

# 3. Update GH secret VPS1_SSH_KEY con la nueva privkey.

# 4. Borrar privkey vieja del Mac:
rm ~/.ssh/bewpro_gh_deploy_old
```

---

## L) Rotación de webhooks Slack (R10b)

Las URLs completas de los 6 webhooks Slack estuvieron expuestas con perm world-readable. Si querés rotarlos:

1. Ir a Slack workspace → Settings → Apps → Custom Integrations → Incoming Webhooks.
2. Para cada webhook (cd-bewpro, ventas, proyectos, soporte, contacto, juan, coke):
   - Editar la integration → "Regenerate URL".
   - Copiar el nuevo URL completo.
3. Actualizar el VPS:
   ```bash
   ssh vps1-claude 'vim /home/lacompany/scripts/.slack-webhooks.env'
   # Reemplazar las 6 URLs con las nuevas. Mantener perm 640 owner lacompany:lacompany.
   ```
4. Si `SLACK_WEBHOOK_URL` general también cambió:
   ```bash
   ssh vps1-claude 'vim /root/scripts/.airtable.env'
   # Update SLACK_WEBHOOK_URL line. Perm 600 owner root:root.
   ```
5. Test:
   ```bash
   ssh vps1-claude '
   echo -e "From: test@test.com\nSubject: Test rotation\n\nBody" | sudo -u lacompany php /home/lacompany/scripts/email_to_slack.php coke
   '
   ```

---

## G) Comandos rápidos de diagnóstico

```bash
# Estado de cuentas cPanel (en cualquier VPS)
whmapi1 listaccts | grep 'user:' | wc -l

# Ver SSL de todas las cuentas
for user in $(whmapi1 listaccts | grep 'user:' | awk '{print $2}'); do
  domain=$(grep -A1 "user: $user" <(whmapi1 listaccts) | head -2 | grep domain | awk '{print $2}')
  issuer=$(echo | timeout 5 openssl s_client -connect "${domain}:443" -servername "$domain" 2>/dev/null | openssl x509 -noout -issuer 2>/dev/null || echo 'NO_CERT')
  echo "$user | $domain | $issuer"
done

# Healthcheck HTTP de tenants vivos (desde local)
# (Pendiente — comando bewpro:healthcheck propuesto en Fase 3)

# Ver cola de Airtable Required
curl -s -H "Authorization: Bearer $AIRTABLE_TOKEN" \
  "https://api.airtable.com/v0/appRxvpzqCmNsw2JN/Projects?filterByFormula=%7BPipeline_Status%7D%3D%22Required%22" \
  | python3 -m json.tool
```
