# Despliegue de `main` en producción (GoDaddy/cPanel)

Guía operativa para publicar la rama `main` en el hospedaje de producción. La
preparación inicial (dominio, base de datos, `.env` y cron) está en el
[README](../README.md#producción-en-godaddycpanel); aquí se describe **cómo
ejecutar cada despliegue**, qué confirmar antes de cada paso y qué comprobar
después. La puede seguir una persona o un asistente de código al que se le pida
expresamente desplegar
([ver cómo pedirlo](#pedir-el-despliegue-a-un-asistente-de-código)).

Use marcadores como `<ruta-de-php>`, `<ruta-del-clon>`, `<usuario>` y
`<servidor>`. No escriba rutas, servidores ni credenciales reales en este archivo
ni en los reportes.

## Reglas que no se omiten

- Desplegar modifica producción. Solo se hace con una instrucción expresa, y cada
  paso marcado **Confirmar** necesita una aprobación específica antes de
  ejecutarse; la aprobación de un paso no cubre los siguientes.
- No se fusionan ramas ni se reescribe el historial (`push --force`, `reset`,
  `rebase`) para desplegar.
- Las migraciones no se automatizan: requieren respaldo verificado, revisión de
  `migrate:status` y autorización.
- En producción no se ejecutan `migrate:fresh`, `migrate:refresh`, `db:wipe`,
  `composer update` ni `artisan serve`. Tampoco se regenera `APP_KEY` ni se
  reemplazan `.env`, `storage/` o `vendor/`.
- No se muestran ni se copian secretos (`.env`, claves, contraseñas, tokens,
  enlaces de recuperación) ni direcciones de correo completas de médicos.
- Si un paso falla, se detiene el despliegue y se informa el error. No se
  improvisan correcciones directamente en producción.

## Qué significa «desplegar main»

`main` es la rama de producción y el trabajo se integra primero en `develop`. El
despliegue automatizado depende de dos archivos que deben existir **en la rama que
se despliega**: `.cpanel.yml` y `scripts/desplegar.sh`. Si `main` no los contiene,
cPanel no ejecuta las tareas de despliegue. Por eso el release se prepara antes
(pasos 1 y 2) y no se da por hecho que `main` esté al día con `develop`.

`.cpanel.yml` ejecuta `scripts/desplegar.sh`, que comprueba `.env`, instala las
dependencias de producción, reconstruye las cachés de configuración y vistas y
reinicia los workers. No copia `.env`, no toca `storage/` y no aplica migraciones.
Como último paso exige un Super Admin activo (ver
[Super Admin inicial](#super-admin-inicial)): si no existe ninguno lo crea desde el
`.env`, y si no queda uno activo el despliegue termina con error.

## 1. Comprobaciones previas (solo lectura)

Desde un clon local, sin tocar el servidor:

```bash
git fetch origin
git log --oneline -5 origin/main                   # último commit que se publicaría
git rev-list --count origin/main..origin/develop   # commits de develop que aún no están en main
git ls-tree -r --name-only origin/main | grep -E '^(\.cpanel\.yml|scripts/desplegar\.sh)$'
git diff --name-status <commit-desplegado>..origin/main -- database/migrations
```

- El `grep` debe listar `.cpanel.yml` y `scripts/desplegar.sh`. Si no aparecen,
  `main` todavía no contiene la automatización de despliegue.
- `<commit-desplegado>` es el último commit que muestra cPanel en Git Version
  Control (o `git log -1` en el clon del servidor). La última orden lista las
  migraciones que traerá el release y que habrá que revisar en el paso 5.
- Ejecute `composer lint` sobre el commit que se publicará. No necesita base de
  datos ni toca la de trabajo o la de producción. Las pruebas unitarias son de uso
  local y no forman parte del repositorio.

## 2. Llevar el release a `main`

**Confirmar.** Integrar `develop` en `main` es una decisión de release distinta
del despliegue. La hace o la autoriza expresamente quien administra el
repositorio, por ejemplo con un pull request revisado de `develop` a `main`. Un
asistente no debe crearlo ni fusionarlo por iniciativa propia.

Cuando `main` contenga el release, anote su hash corto con
`git rev-parse --short origin/main`: es el commit que se aprueba desplegar.

## 3. Respaldo

**Confirmar.** Antes de aplicar migraciones, y recomendado en cada despliegue:

1. Respalde la base (cPanel > Backup, phpMyAdmin > Exportar o `mysqldump` si hay
   SSH) y copie el `.env` y `storage/app/` a un lugar privado fuera del document
   root.
2. Verifique que el respaldo existe, que su tamaño es razonable y que puede
   restaurarse. Pruebe la restauración en una base distinta, nunca sobre la de
   producción.
3. No guarde respaldos dentro del repositorio ni de `public/`.

## 4. Desplegar el código

**Confirmar** el hash exacto de `main` antes de empezar.

**Vía A: cPanel (no necesita SSH).**

1. En **Git Version Control > Manage** del repositorio, compruebe que la rama activa
   es `main` y que el clon no tiene cambios sin confirmar. cPanel puede negarse a
   desplegar con un árbol modificado o si la rama no incluye `.cpanel.yml`.
2. Pulse **Update from Remote** y compruebe que el último commit coincide con el
   hash aprobado.
3. Pulse **Deploy HEAD Commit** y revise el registro del despliegue. Si falla,
   deténgase: las causas habituales son que falte `.env` o que cPanel no encuentre
   `php` o `composer` (ver `PHP_BIN` y `COMPOSER_BIN` en el README).

**Vía B: SSH (solo si el plan lo ofrece y se autoriza).** Es equivalente a lo que
cPanel ejecuta con `.cpanel.yml`:

```bash
ssh <usuario>@<servidor>
cd <ruta-del-clon>
git status --short                  # debe salir vacío
git fetch origin main
git merge --ff-only origin/main     # solo avance rápido: sin commits de fusión ni reescritura
PHP_BIN=<ruta-de-php> bash scripts/desplegar.sh
```

No escriba contraseñas en comandos ni archivos: use la clave o el agente SSH que
ya tenga configurados la persona responsable.

## 5. Migraciones pendientes

**Confirmar**, solo si hay migraciones pendientes y con el respaldo del paso 3:

```bash
<ruta-de-php> artisan migrate:status    # revise cuáles faltan
<ruta-de-php> artisan migrate --force   # aplica solo las pendientes
```

Compruebe que todas quedan como aplicadas. No use `migrate:rollback`: un cambio de
esquema erróneo se corrige con una migración nueva o restaurando el respaldo, con
autorización. La carga de catálogos (`admin:bootstrap`) también requiere
autorización; ver el README.

Después de migrar, ejecute la verificación estricta del Super Admin. A diferencia
del despliegue, que solo avisa si hay migraciones pendientes, esta también falla en
ese caso:

```bash
<ruta-de-php> artisan admin:asegurar-superusuario --estricto
```

## 6. Verificación posterior

```bash
<ruta-de-php> artisan correo:verificar --produccion   # sin errores
<ruta-de-php> artisan schedule:list                   # debe incluir queue:work --queue=correo y correo:depurar
```

- El registro del despliegue termina con «Despliegue de código completado» después
  de «Verificando el Super Admin...». Sin migraciones aplicadas, esa verificación
  solo avisa; confirme con `admin:asegurar-superusuario --estricto` (paso 5).
- `storage/logs/scheduler.log` muestra ejecuciones recientes: el cron corre cada
  minuto.
- `/`, `/admin/login` y `/portal/login` responden por HTTPS, sin trazas de error
  (`APP_DEBUG=false`), y la pestaña del navegador muestra el ícono institucional.
- `storage/logs/laravel.log` no tiene errores nuevos.
- Con una cuenta de prueba autorizada y un buzón controlado, nunca la de un médico
  real, solicite la recuperación de contraseña. En `mail_deliveries` la entrega
  debe pasar a `sent` y el mensaje debe llegar. Si no, siga la sección siguiente.

## 7. Si algo falla

- **Código.** Publique en `main` un commit de reversión autorizado (sin reescribir
  el historial) y repita el paso 4.
- **Datos o esquema.** Restaure el respaldo verificado con autorización. No use
  `migrate:fresh` ni `migrate:refresh`.
- **Falla en «Verificando el Super Admin...».** El código, las cachés y los workers
  ya están actualizados: solo falla la exigencia del Super Admin. Siga
  [Super Admin inicial](#super-admin-inicial).
- **Informe.** Indique qué paso falló y el mensaje de error, sin secretos ni datos
  personales.

## Super Admin inicial

La cuenta se crea **una sola vez**, en el despliegue inicial. Cada despliegue termina
con `admin:asegurar-superusuario`, que exige un Super Admin activo: lo crea solo si no
existe ninguno y, en los despliegues siguientes, únicamente verifica que haya uno
activo. Nunca reescribe la cuenta, restablece contraseñas, vincula registros
existentes ni reactiva cuentas, y sus mensajes no muestran valores del `.env`.

| Situación | Resultado |
| --- | --- |
| No existe ningún Super Admin y `BOOTSTRAP_SUPER_ADMIN_ENABLED=true` con datos válidos | Se crea la cuenta (persona, usuario y rol) y el despliegue continúa. |
| Ya existe un Super Admin activo | No se crea ni se cambia nada. Las variables `BOOTSTRAP_SUPER_ADMIN_*` ni siquiera se validan. |
| No existe ninguno y la opción está en `false` | El despliegue falla: «la creación automática está desactivada». |
| No existe ninguno y falta un dato o es inválido | Falla indicando los **nombres** de los campos, nunca sus valores. |
| La identidad, el correo o la colegiación ya pertenecen a otro registro | Falla: no se vinculan ni se elevan registros existentes. |
| Existen Super Admin, pero ninguno está activo | Falla: no se reactiva ni se crea otro automáticamente. |
| Hay migraciones pendientes | Solo avisa y omite la verificación; tras migrar se ejecuta con `--estricto`. |

Variables y reglas (se completan solo en el `.env` del servidor). Son obligatorias
`ENABLED`, `FIRST_NAMES`, `LAST_NAMES`, `EMAIL` y `PASSWORD`; las demás son
opcionales y no hace falta declararlas. Para omitirlas debe estar aplicada la
migración `make_people_optional_data_nullable`; en una instalación nueva se aplica
antes del alta inicial, y en una base ya migrada queda pendiente hasta el siguiente
`migrate` (paso 5):

| Variable | Regla |
| --- | --- |
| `BOOTSTRAP_SUPER_ADMIN_ENABLED` | `true` para permitir la creación automática. |
| `..._FIRST_NAMES`, `..._LAST_NAMES` | Obligatorias, hasta 150 caracteres cada una. |
| `..._IDENTITY_NUMBER` | Opcional, hasta 50 caracteres; si se indica, no puede existir ya en `people`. |
| `..._BIRTH_DATE` | Opcional; si se indica, formato `AAAA-MM-DD` y no futura. |
| `..._ADDRESS` | Opcional, hasta 2000 caracteres. |
| `..._PHONE` | Opcional, hasta 50 caracteres. |
| `..._EMAIL` | Obligatorio, válido y único; se guarda en minúsculas y es el usuario de `/admin/login`. |
| `..._PASSWORD` | Obligatoria, de 12 a 72 bytes; se guarda con hash. |
| `..._MEDICAL_LICENSE` | Opcional; si se indica, debe ser única. |

Use comillas si un valor lleva espacios, y comillas simples en la contraseña si
contiene `$`, `#` o espacios: con comillas dobles `$` se interpreta como variable.

**Si el despliegue falla.** Corrija el `.env`; si lo cambió después de que la
configuración quedó en caché, ejecute `<ruta-de-php> artisan config:clear`. Después
repita **Deploy HEAD Commit** o ejecute solo la verificación:

```bash
<ruta-de-php> artisan admin:asegurar-superusuario --estricto
```

**Si la única cuenta Super Admin está desactivada** y nadie puede ingresar a
`/admin` para reactivarla, hágalo directamente en la base, con autorización y
respaldo, y registre el motivo:

```sql
SELECT u.id, u.is_active
FROM users u
JOIN role_user ru ON ru.user_id = u.id
JOIN roles r ON r.id = ru.role_id
WHERE r.name = 'super_admin';

UPDATE users SET is_active = 1 WHERE id = <id-de-la-cuenta>;
```

**La contraseña en el `.env`.** Solo se necesita mientras no exista un Super Admin.
Tras el primer ingreso, cambie la contraseña desde el perfil de la cuenta
(`/perfil`) y puede quitar `BOOTSTRAP_SUPER_ADMIN_PASSWORD` y los demás datos
del `.env`; el siguiente despliegue reconstruye la caché de configuración y deja de
conservarlos. Si el Super Admin faltara más adelante, el despliegue fallaría
pidiendo esos datos, que es lo esperado. Mantenga el `.env` fuera del document root
y con permisos restringidos a la cuenta de cPanel.

## El correo no llega: qué revisar

El estado de cada envío queda en `mail_deliveries`, que nunca guarda el contenido
del mensaje ni la dirección completa:

```sql
SELECT type, status, reason, attempts, created_at
FROM mail_deliveries ORDER BY id DESC LIMIT 10;
```

| Estado y motivo | Qué significa | Qué hacer |
| --- | --- | --- |
| `queued` que no avanza | El mensaje sigue en la cola `correo` porque ningún worker la procesa. | En producción, compruebe el cron con `schedule:run` cada minuto, `schedule:list`, `storage/logs/scheduler.log` y que exista la tabla `jobs`. |
| `sent` | El servidor SMTP aceptó el mensaje. | **En local, el mensaje está en Mailpit (<http://localhost:8025>): nunca llega a Gmail ni a otro buzón real.** En producción revise correo no deseado, SPF, DKIM, DMARC y los registros del proveedor. |
| `failed` | El SMTP lo rechazó tras los reintentos (a 1, 5 y 15 minutos). `reason` indica el tipo de error. | Revise servidor, puerto, cifrado, credenciales y límites del proveedor. `php artisan queue:failed` lista los fallos sin el contenido. |
| `suppressed` / `throttled` | Se alcanzó el límite de envíos por hora al mismo destinatario (`MAIL_MAX_PER_RECIPIENT_HOUR`). | Espere a que venza el límite. |
| `suppressed` / `environment_restricted` | Fuera de producción, el SMTP no es local y el destinatario no está permitido. | Use Mailpit o agréguelo a `MAIL_ALLOWED_RECIPIENTS`. |
| `suppressed` / `configuration` | En producción la configuración no es válida: mailer `log` o `array`, SMTP en la misma máquina, remitente de ejemplo o fuera de `MAIL_ALLOWED_FROM_DOMAINS`, cola síncrona o `APP_URL` sin HTTPS. | Ejecute `correo:verificar --produccion` y corrija el `.env`. |
| `suppressed` / `invalid_recipient` | La dirección de la cuenta no es válida. | Corrija el correo registrado. |
| `suppressed` / `stale` | El mensaje dejó de ser útil antes de enviarse, por ejemplo un código vencido. | Solicite uno nuevo. |
| Ninguna fila nueva | La solicitud pública se descartó antes de enviar el mensaje. La respuesta que ve el usuario es la misma a propósito. | Vea la lista siguiente. |

Sin fila nueva en la recuperación de contraseña, las causas habituales son:

- **Límites de solicitud:** 60 segundos entre solicitudes de la misma cuenta, tres
  por cuenta y diez por IP cada hora. En local, `php artisan cache:clear` reinicia
  los contadores; no lo use en producción para saltarse los límites.
- **Usuario distinto al registrado:** debe coincidir exactamente con la identidad,
  la colegiación o el correo de la cuenta.
- **Cuenta no elegible:** inactiva, sin persona con colegiación asociada o sin el
  rol Médico.

En local, `php artisan correo:verificar` revisa la configuración y
`php artisan correo:probar <destinatario>` envía un mensaje de prueba y explica el
resultado. `correo:probar` no se ejecuta en producción.

## Pedir el despliegue a un asistente de código

Un asistente de código (por ejemplo, Claude Code) puede seguir esta guía si se le
da una instrucción expresa. Plantilla:

```text
Despliega `main` en producción siguiendo docs/despliegue.md.
Empieza solo con las comprobaciones de lectura (sección 1) y muéstrame el
resultado y el plan. No fusiones ramas, no hagas respaldos, no ejecutes
migraciones ni toques el servidor hasta que yo apruebe cada paso marcado
«Confirmar». No muestres secretos ni datos personales.
```

Reparto de tareas:

- **El asistente** ejecuta las comprobaciones de lectura, resume el estado, prepara
  los comandos de cada paso y verifica el resultado cuando se le autoriza.
- **La persona responsable** aprueba cada paso **Confirmar**, decide la integración
  en `main` y realiza las acciones de cPanel que el asistente no pueda hacer. Si el
  asistente va a usar SSH, es quien lo habilita y quien conserva las claves.
- Si falta acceso al servidor, el asistente se detiene en el paso que lo necesita
  y lo informa; no pide contraseñas por el chat ni las guarda en archivos.
