# Despliegue del entorno "dev" (hosting compartido / VPS con panel)

> Este documento describe el despliegue **manual** a un entorno de pruebas
> para que el equipo pueda probar el flujo completo (portal de reserva +
> panel admin) desde un VPS con panel (cPanel/Plesk, terminal web, Redis
> disponible). **No es el plan de producción** — el plan original de F0
> (`docs/helpmi-planificacion.md`) sigue siendo la referencia para
> producción real (Laravel Forge + servidor dedicado), y esa fase (F10)
> sigue sin ejecutarse. Este es un camino distinto y más ligero, pensado
> solo para que compañeros prueben funcionalidad — nunca se apunta a este
> entorno con datos reales de pacientes.

## Qué NO se despliega

- `storage/app/private` (11 GB, adjuntos históricos importados) — no se
  sube al hosting.
- `backup/` (12 GB, DXL/ZIP de Domino) — no se sube al hosting, ni falta
  para que el equipo pruebe el flujo (son datos de origen ya procesados
  localmente).
- El entorno dev arranca **solo con los seeders base** (roles,
  Hauptthemen/Kategorien, Medikamente, Berater, Feiertage) — sin
  Therapeuten/Termine históricos reales.

## 1. Requisitos a verificar en el panel antes de desplegar

- PHP `^8.2` (el proyecto usa PHP 8.3 en local, pero `composer.json` solo
  exige `^8.2`).
- Extensiones PHP: `pdo_mysql`, `gd` o `imagick` (conversiones de
  Media Library), `zip` (exportación XLSX), `bcmath`, `mbstring`,
  `fileinfo`, `ctype`, `xml`, `tokenizer` (estándar Laravel 11).
- MySQL/MariaDB ya provisionado, con base de datos y usuario creados.
- Redis accesible por red (host/puerto/password), **opcional** — si no
  está disponible (caso real de OVH, ver punto 1.1), se usa el driver
  `database` para sesión/cache/colas sin cambios de código. Si sí está
  disponible, el proyecto usa `predis/predis` (cliente PHP puro, sin
  depender de la extensión nativa `phpredis`), así que basta con que sea
  alcanzable por TCP.
- `symlink()` habilitado en PHP (necesario para `storage:link`) —
  algunos paneles lo bloquean, verificarlo antes.
- Acceso a crontab del panel (para el scheduler y el worker de colas, ver
  punto 5).

### 1.1. Caso real: OVH mutualisé (`clusterXXX.hosting.ovh.net`)

Desplegado por primera vez en 2026-08-14 contra un hosting compartido OVH
real. Hallazgos que no se sabían hasta probarlo contra la infraestructura
real (mismo criterio que el resto del proyecto: el `--dry-run`/la teoría no
detecta esto, solo la ejecución real):

- **Sin `composer` ni sistema de módulos** (`module avail` no existe) — se
  instala como `.phar` manual:
  ```bash
  cd ~
  php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
  php composer-setup.php
  php -r "unlink('composer-setup.php');"
  ```
  A partir de ahí, usar `php ~/composer.phar` en vez de `composer` en todos
  los comandos de este documento.
- **Node v10.24.0 preinstalado, sin forma de cambiarlo** (a diferencia de
  PHP, que sí es seleccionable por hosting vía el panel Multisite) — muy
  por debajo del Node 18+ que exige Vite 6. **El build de frontend se hace
  en local** (`npm ci && npm run build`, cualquier máquina con Node 18+) y
  se sube el resultado por `rsync`/`scp` directamente a
  `<proyecto>/public/build/` en el servidor — `public/build` sigue
  ignorado en git (`.gitignore`), esto no cambia ese punto 3, solo lo
  sustituye por una subida manual del artefacto ya compilado:
  ```bash
  # desde tu máquina local, con Node 18+
  npm ci && npm run build
  rsync -avz -e ssh public/build/ usuario@servidor:~/ruta/proyecto/public/build/
  ```
- **`composer.lock` exigía PHP 8.3** en varios paquetes
  (`maennchen/zipstream-php`, `openspout/openspout`,
  `spatie/laravel-translatable`) pese a que `composer.json` declara
  `"php": "^8.2"` — divergencia nunca detectada porque local/CI siempre
  corrieron en PHP 8.3 real. El hosting OVH sí permite seleccionar PHP 8.3
  para el multihost, **pero se descartó deliberadamente** por afectar a
  otros sitios ya alojados en la misma cuenta compartida. Corregido fijando
  `config.platform.php: "8.2.31"` en `composer.json` y regenerando el lock
  con `composer update maennchen/zipstream-php openspout/openspout
  spatie/laravel-translatable maatwebsite/excel filament/actions
  --with-all-dependencies` — resultado verificado con la suite completa
  (Pint + PHPStan + 117 tests, incluida la exportación XLSX que depende de
  `openspout`) en local sobre PHP 8.3 real, sin regresiones.
  **Implicación permanente, no solo para este deploy**: con
  `config.platform.php` fijado, cualquier `composer update`/`install`
  futuro (local, CI, producción) resolverá los paquetes como si el
  intérprete fuera 8.2.31, para que el lock file no vuelva a exigir 8.3+ y
  rompa este despliegue. Revisar/quitar este pin si en algún momento el
  multihost de OVH pasa a PHP 8.3, o si se cambia de hosting.
- **No existe `package-lock.json` en el repo** (nunca se generó/commiteó)
  — se detectó al intentar `npm ci` (falla sin lockfile), hubo que usar
  `npm install`. Pendiente de corregir aparte: generar y commitear el
  lockfile para que las builds sean reproducibles entre máquinas.
- **Sin Redis contratado** — el hosting solo tiene la base de datos MySQL,
  ningún servicio Redis (se comprobó en "Web Cloud Databases" del panel
  OVH, solo aparece la instancia MySQL). Se usa `SESSION_DRIVER=database`,
  `CACHE_STORE=database` y `QUEUE_CONNECTION=database` en su lugar — todos
  ya soportados sin cambios de código salvo uno: la conexión `database` de
  `config/queue.php` tenía `after_commit` fijo en `false`, pero
  `TerminBookingService::book()` despacha `AppointmentCreated` dentro de
  una transacción y necesita que el listener encolado espere al commit
  (mismo motivo por el que la conexión `redis` ya tenía `after_commit =>
  true`, ver CLAUDE.md). Corregido: ahora es
  `env('DB_QUEUE_AFTER_COMMIT', false)` — el valor por defecto no cambia
  (sigue `false` en cualquier otro entorno que no lo active explícitamente),
  y en este `.env` de dev se activa con `DB_QUEUE_AFTER_COMMIT=true`.
  Verificado con la suite completa tras el cambio, sin regresiones.

## 2. Variables de entorno (`.env`, no se commitea)

Partir de `.env.example` y fijar explícitamente:

```env
APP_ENV=dev
APP_DEBUG=false
APP_URL=https://<subdominio-elegido>

DB_CONNECTION=mysql
DB_HOST=...
DB_PORT=3306
DB_DATABASE=...
DB_USERNAME=...
DB_PASSWORD=...

# Sin Redis disponible en este hosting (ver punto 1.1) — driver database
# para todo. DB_QUEUE_AFTER_COMMIT=true es obligatorio, no opcional (ver
# punto 1.1: sin esto, los listeners encolados de F4 pueden ejecutarse
# antes de que la transacción de TerminBookingService::book() confirme).
SESSION_DRIVER=database
CACHE_STORE=database
QUEUE_CONNECTION=database
DB_QUEUE_AFTER_COMMIT=true

FILESYSTEM_DISK=local
CRM_ADAPTER=null
MAIL_MAILER=log
```

Si en algún momento se contrata Redis en OVH, volver a
`SESSION_DRIVER=redis`, `CACHE_STORE=redis`, `QUEUE_CONNECTION=redis` +
`REDIS_CLIENT=predis` (ver `predis/predis` en `composer.json`) y quitar
`DB_QUEUE_AFTER_COMMIT` (la conexión `redis` ya trae `after_commit => true`
fijo).

`APP_DEBUG=false` es obligatorio aunque sea "solo dev": el sitio es
accesible desde una URL pública real, y con `APP_DEBUG=true` cualquier
error expone stack traces con rutas y configuración del servidor.

`MAIL_MAILER=log` de entrada — no se envían emails reales a terapeutas
reales mientras el equipo prueba (los emails generados por F4 quedan en
`storage/logs/laravel.log`). Si más adelante el equipo quiere ver los
emails renderizados, cambiar a Mailtrap sin tocar código.

Sin capa extra de protección delante del login de Filament (decisión
tomada explícitamente) — basta con el login normal + 2FA opcional por
usuario, ya que el entorno no tiene datos reales de pacientes.

## 3. Checklist de despliegue

Si el hosting no trae `composer` ni Node 18+ (caso real de OVH, ver punto
1.1), sustituir `composer` por `php ~/composer.phar` y hacer el build de
Vite en local antes (paso `npm ci && npm run build` + `rsync`), en vez de
`npm ci && npm run build` directo en el servidor:

```bash
composer install --no-dev --optimize-autoloader   # o: php ~/composer.phar install --no-dev --optimize-autoloader
npm ci && npm run build                            # en el servidor si hay Node 18+; si no, en local + rsync (ver 1.1)
cp .env.example .env   # editar según el punto 2
php artisan key:generate
php artisan storage:link
php artisan migrate --force
php artisan db:seed --force
php artisan config:cache
php artisan route:cache
php artisan view:cache
```

Verificar permisos de escritura del usuario de PHP-FPM sobre `storage/` y
`bootstrap/cache/` (estándar en cualquier despliegue Laravel).

## 4. Crear usuarios para el equipo

No existe ningún seeder de usuario admin — se crean a mano por cada
compañero:

```bash
php artisan make:filament-user --name="Nombre" --email="correo@ejemplo.com" --password="..."
```

El comando muestra el ID del usuario creado. Asignar el rol con ese ID
(`permission:assign-role` espera el ID, no el email):

```bash
php artisan permission:assign-role admin <ID>
```

Para probar también la vista de `berater` (con scope reducido), usar el
rol `berater` y además vincular ese `User` a un registro `Berater` ya
sembrado por `BeraterSeeder` (columna `beraters.user_id`) — sin ese
vínculo, `Termin::scopeVisibleTo()` no le mostrará ninguna cita.

No activar `google2fa_enabled` en estos usuarios — el 2FA es opt-in por
usuario (`RequireTwoFactorChallenge` solo lo exige si el flag está
activo), así que dejarlo desactivado evita fricción para entrar a probar.

## 5. Cron del servidor (vía panel cPanel/Plesk)

```cron
* * * * * cd /ruta/al/proyecto && php artisan schedule:run >> /dev/null 2>&1
* * * * * cd /ruta/al/proyecto && php artisan queue:work --stop-when-empty --max-time=55 >> /dev/null 2>&1
```

El scheduler (`bootstrap/app.php` → `withSchedule()`, 3 jobs de F4) se
evalúa cada minuto aunque los jobs sean diarios/mensuales — sin este cron,
nunca se ejecutan. El worker de colas usa `--stop-when-empty --max-time=55`
en vez de un proceso persistente con Supervisor (no garantizado en hosting
compartido clásico) — introduce hasta ~1 minuto de latencia en el envío de
emails/eventos encolados (`SendBookingConfirmation`, `SendNonMemberWarning`),
aceptable para un entorno de pruebas.

## 6. Verificación tras el despliegue

1. Cargar `https://<subdominio>/admin/login`, entrar con un usuario del
   punto 4, confirmar que el dashboard (widgets de próximas citas/stats)
   carga sin error 500 — un 500 aquí normalmente indica `APP_DEBUG` mal
   puesto o falta el build de Vite (`public/build/manifest.json`).
2. Probar el portal público (`/de/buchen` con un token válido) reservando
   una cita de prueba end-to-end.
3. Confirmar el email de confirmación en `storage/logs/laravel.log` (con
   `MAIL_MAILER=log`) en menos de ~1 minuto (verifica que el cron del
   worker de colas está corriendo).
4. Forzar `php artisan schedule:run` a mano una vez y comprobar que no
   lanza excepciones de conexión a Redis/MySQL.

## Fuera de alcance

- No hay pipeline de CI/CD para este despliegue — es un procedimiento
  manual, documentado aquí, no automatizado con GitHub Actions.
- No se toca el plan de producción de F0 (Forge/S3) — este documento
  describe un camino de despliegue distinto, solo para el entorno de
  pruebas del equipo.
