# Incidente: imágenes de producto en blanco en el storefront (proxy host de media.cruzcloud.net mal configurado en NPM) (2026-08)

**Ventana del incidente:** 2026-08-13, resuelto el mismo día
**Servicio afectado:** `media.cruzcloud.net` (Nginx Proxy Manager → MinIO, `minio-svc` en namespace `ecommerce`)
**Impacto:** las imágenes subidas a productos desde el Admin de Medusa no se visualizaban en el detalle de producto del storefront (`shop.cruzcloud.net`) — el resto del producto (título, precio, subtítulo, botón añadir al carrito) se renderizaba bien.

## Síntoma

Al agregar imágenes a un producto desde el Admin de Medusa (sección Media), estas se veían correctamente cargadas en el Admin, pero en la página de detalle de producto del frontend los thumbnails aparecían vacíos/en blanco.

## Diagnóstico

1. **Storage:** confirmado que Medusa usa el provider `@medusajs/medusa/file-s3` apuntando a MinIO interno (`S3_ENDPOINT=http://minio-svc:9000`, bucket `ari-products`), con URLs públicas construidas sobre `S3_FILE_URL=https://media.cruzcloud.net/ari-products` (env en configmap `commerce-config`, namespace `ecommerce`).
2. **API:** el producto traía URLs absolutas válidas, ej. `https://media.cruzcloud.net/ari-products/<archivo>.jpg`.
3. **Prueba directa de la URL:**
   - Contra el ingress interno del cluster (bypaseando el proxy externo, `Host: media.cruzcloud.net` directo a la IP del `k3d-proxy`) → `200 OK`, MinIO sirve el JPEG correctamente.
   - Contra `https://media.cruzcloud.net/...` (la ruta real que usa el navegador del cliente, vía Cloudflare) → `500 Internal Server Error`, `Server: openresty`.

Ese contraste (funciona interno, falla público, con `Server: openresty` — el motor de Nginx Proxy Manager) aisló el problema al proxy externo, no a Medusa/MinIO/frontend.

## Causa raíz

En Nginx Proxy Manager (`/DATA/AppData/nginxproxymanager/`, contenedor `nginxproxymanager`), el proxy host de `media.cruzcloud.net` (`id=13` en la tabla `proxy_host` de `database.sqlite`) estaba mal configurado:

```
forward_host = "http://192.168.68.61/"   -- esquema y barra final metidos dentro del campo (formato inválido)
forward_port = 5005                       -- puerto donde no escucha ningún proceso en el host
```

Los proxy hosts que sí funcionan (`shop.cruzcloud.net` id 23, `commerce.cruzcloud.net` id 28) apuntan correctamente a:

```
forward_host = "192.168.68.61"
forward_port = 90   -- puerto publicado por k3d-lab-cluster-serverlb hacia Traefik
```

Se confirmó con `ss`/`docker ps` en el host que nada escuchaba en el puerto `5005` — este proxy host nunca estuvo apuntando correctamente al cluster, muy probablemente un typo desde que se creó.

## Fix aplicado

1. Backups previos: `database.sqlite.bak-pre-media-fix` y `13.conf.bak-pre-media-fix`.
2. Corrección directa en la base SQLite de NPM:
   ```sql
   UPDATE proxy_host SET forward_host='192.168.68.61', forward_port=90 WHERE id=13;
   ```
3. NPM regeneró automáticamente `nginx/proxy_host/13.conf` con los valores correctos (la app Node de NPM detecta el cambio en la base y reescribe el `.conf`; no hace falta editarlo a mano).
4. Recarga de nginx dentro del contenedor:
   ```
   docker exec nginxproxymanager nginx -t
   docker exec nginxproxymanager nginx -s reload
   ```

Adicionalmente, se seteó el campo `thumbnail` del producto de prueba desde el Admin (estaba en `null`) — Medusa no lo autocompleta a partir del array `images`, y el frontend lo usa como imagen hero en listados/tarjetas de catálogo.

## Validación

Confirmado visualmente en `shop.cruzcloud.net/product/cybertron-sentinel-figura-de-coleccion-test-revalidate` que tanto la galería del detalle como el thumbnail en listados/tarjetas se renderizan correctamente. También se validó que ediciones de descripción/título/precio desde el Admin se reflejan automáticamente en el storefront sin necesidad de restart ni intervención manual, vía el subscriber `revalidate-storefront.ts` (`workloads/commerce-backend/src/subscribers/`) que procesa el evento `product.updated` de Medusa.

## Lecciones aprendidas

**Aislar interno vs. público antes de sospechar de la app.** Medusa, MinIO y el frontend estaban sanos todo el tiempo — la falla estaba en una capa de infraestructura fuera de los repos de GitOps (Nginx Proxy Manager, gestionado por fuera de `apps-registry`/`platform-infra`, sin versionar). Comparar la misma petición por la ruta interna del cluster contra la ruta pública real fue lo que aisló la causa en minutos, sin necesidad de tocar Medusa ni el frontend.

Ver `docs/known-issues.md` para la topología completa Cloudflare Tunnel → Nginx Proxy Manager → k3d y el patrón a seguir si otro subdominio (`*.cruzcloud.net`) presenta el mismo síntoma (`500`/`Server: openresty` público pero sano internamente).
