Compare commits

...
Author SHA1 Message Date
devops 0fd05dab2a fix(ecommerce): comillas en url() del zoom, rompia con espacios/parentesis en el nombre de archivo 2026-09-20 20:07:32 +00:00
devops 2318e1da7d Merge branch 'docs/adr-0010-cierre-bugs-mobile-imagenes-2026-09-20'
Build and Push Docs Portal / Escaneo de Secretos (Gitleaks) (push) Successful in 31s
Build and Push Docs Portal / Construir y publicar Docs Portal (push) Successful in 6m18s
2026-09-20 19:18:22 +00:00
devops b6b2f15c12 docs(adr-0010): cerrar bug de overflow mobile 320px, actualizar estado de imagenes 2026-09-20 19:17:55 +00:00
devops 7658b9879a chore: promote v1.0.273 to production [skip ci] 2026-09-20 19:10:43 +00:00
devops a1dc145528 chore: release v1.0.273 [skip ci] 2026-09-20 19:10:41 +00:00
devops 4f5a715812 Merge branch 'fix/navbar-departamentos-overflow-320-2026-09-20'
Build and Push Frontend / Escaneo de Secretos (Gitleaks) (push) Successful in 31s
Build and Push Frontend / Construir y Subir Imagen (push) Successful in 11m57s
2026-09-20 18:58:13 +00:00
devops 5ce9c07829 fix(ecommerce): ocultar texto de Departamentos en mobile, 3 shrink-0 desbordaban 320px 2026-09-20 18:57:54 +00:00
devops 428ad83e5b chore: promote v1.0.272 to production [skip ci] 2026-09-20 18:27:46 +00:00
devops 80862bef5b chore: release v1.0.272 [skip ci] 2026-09-20 18:27:44 +00:00
devops 777e47ce16 Merge branch 'fix/footer-newsletter-overflow-320-2026-09-20'
Build and Push Frontend / Escaneo de Secretos (Gitleaks) (push) Successful in 27s
Build and Push Frontend / Construir y Subir Imagen (push) Successful in 17m33s
2026-09-20 18:09:35 +00:00
devops 167ae4057a fix(ecommerce): min-w-0 en Newsletter del footer, forzaba overflow a 320px 2026-09-20 18:09:10 +00:00
devops 51300c234a chore: promote v1.0.271 to production [skip ci] 2026-09-20 17:39:43 +00:00
devops 0c0efe3727 chore: release v1.0.271 [skip ci] 2026-09-20 17:39:42 +00:00
devops e4dafe9017 Merge branch 'ci/automate-staging-to-prod-promotion-2026-09-20'
Build and Push Frontend / Escaneo de Secretos (Gitleaks) (push) Successful in 23s
Build and Push Frontend / Construir y Subir Imagen (push) Successful in 8m47s
2026-09-20 17:30:00 +00:00
devopsandClaude Sonnet 5 fed5dfc1c1 ci(ecommerce): automatizar promocion de Staging a Produccion
Agrega el step "Promover a Produccion (CD)" al mismo job que ya
despliega a Staging en build.yaml -- misma logica de
scripts/promote-to-production.sh (sed/perl sobre el manifest de
Produccion) pero sin el read -rp interactivo, gateado por la misma
verificacion de promocion segura (merge-base --is-ancestor) que ya usa
el step de Staging.

Decision explicita del usuario 2026-09-20: cero gate humano entre
Staging y Produccion -- la promocion manual deja de ser el camino
normal, scripts/promote-to-production.sh queda solo como herramienta
de excepcion/rollback.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QMP7ULbxbsB5g1vELNAT3D
2026-09-20 17:27:37 +00:00
devops 5773154db9 chore: release v1.0.270 [skip ci] 2026-09-20 17:04:00 +00:00
devopsandClaude Sonnet 5 553b1c7a32 Merge branch 'fix/medusa-fetch-cache-multi-pod-2026-09-20'
Build and Push Frontend / Escaneo de Secretos (Gitleaks) (push) Successful in 26s
Build and Push Frontend / Construir y Subir Imagen (push) Successful in 13m9s
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QMP7ULbxbsB5g1vELNAT3D
2026-09-20 16:50:25 +00:00
devopsandClaude Sonnet 5 d9c39ba2a1 fix(ecommerce): sacar cache de fetch que competia contra force-dynamic
medusaFetch() usaba next:{revalidate:60,tags:["medusa-products"]}, cache
en memoria por-pod que sobrevivia a force-dynamic en las paginas que lo
consumen. Con frontend-deploy en 2 replicas sin cacheHandler compartido,
el POST de /api/revalidate del subscriber de Medusa solo llegaba a uno
de los dos pods, dejando al otro sirviendo datos viejos hasta que el TTL
expirara solo -- reportado por el usuario: ediciones manuales de imagen
no se reflejaban en el storefront, pero la importacion por CSV (mucho
mas lenta) si, porque el TTL tenia tiempo de sobra para expirar en
ambos pods.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QMP7ULbxbsB5g1vELNAT3D
2026-09-20 16:50:16 +00:00
gitea-actions 164223e844 chore(gitops): deploy Docs Portal v1.0.269 [skip ci] 2026-09-20 16:24:54 +00:00
devopsandClaude Sonnet 5 ee327b420c Merge branch 'docs/adr-0010-bugs-imagenes-2026-09-20'
Build and Push Docs Portal / Escaneo de Secretos (Gitleaks) (push) Successful in 26s
Build and Push Docs Portal / Construir y publicar Docs Portal (push) Successful in 6m35s
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QMP7ULbxbsB5g1vELNAT3D
2026-09-20 16:17:55 +00:00
devopsandClaude Sonnet 5 17348e6285 docs(adr-0010): agregar dos bugs reportados al checklist de staging
Importacion manual de imagenes de producto no se refleja en el
storefront (CSV si funciona), y en mobile la visualizacion de imagenes
de producto se corta obligando a scrollear. Ambos reportados por el
usuario 2026-09-20, en investigacion.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QMP7ULbxbsB5g1vELNAT3D
2026-09-20 16:17:47 +00:00
gitea-actions 6c6303cf8e chore(gitops): deploy Docs Portal v1.0.268 [skip ci] 2026-09-20 16:03:40 +00:00
devopsandClaude Sonnet 5 410975f972 Merge branch 'docs/tmux-host-remote-control-continue'
Build and Push Docs Portal / Escaneo de Secretos (Gitleaks) (push) Successful in 31s
Build and Push Docs Portal / Construir y publicar Docs Portal (push) Successful in 6m10s
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QMP7ULbxbsB5g1vELNAT3D
2026-09-20 15:57:01 +00:00
9 changed files with 152 additions and 15 deletions

No files matched your search

+51 -2
View File
@@ -129,9 +129,16 @@ jobs:
# Cambio 2026-09-14: todo push a main promueve a STAGING # Cambio 2026-09-14: todo push a main promueve a STAGING
# automaticamente (workloads/ecommerce-staging), no a Produccion # automaticamente (workloads/ecommerce-staging), no a Produccion
# directo -- decision del usuario para dejar de probar cambios en # directo -- decision del usuario para dejar de probar cambios en
# vivo contra Prod. Promover a Produccion (workloads/ecommerce) es # vivo contra Prod.
# una accion manual aparte, ver scripts/promote-to-production.sh. # Cambio 2026-09-20: la promocion de Staging a Produccion (antes
# scripts/promote-to-production.sh, manual con confirmacion
# interactiva) ahora corre sola en este mismo job -- ver el step
# "Promover a Produccion (CD)" mas abajo, justo despues del de
# Staging. Decision explicita del usuario: cero gate humano entre
# Staging y Produccion. El script manual queda solo como
# herramienta de rollback/excepcion, ya no es el camino normal.
MANIFEST_FILE: workloads/ecommerce-staging/frontend/patches/deployment.yaml MANIFEST_FILE: workloads/ecommerce-staging/frontend/patches/deployment.yaml
PROD_MANIFEST_FILE: workloads/ecommerce/frontend/patches/deployment.yaml
KUSTOMIZE_DIR: workloads/ecommerce-staging/frontend KUSTOMIZE_DIR: workloads/ecommerce-staging/frontend
IMAGE_NAME: gitea.cruzcloud.net/devops/ecommerce-frontend IMAGE_NAME: gitea.cruzcloud.net/devops/ecommerce-frontend
@@ -604,6 +611,48 @@ jobs:
# sobrescribir commits ajenos o promocionar una versión antigua. # sobrescribir commits ajenos o promocionar una versión antigua.
git push origin HEAD:main git push origin HEAD:main
# Automatiza lo que antes era scripts/promote-to-production.sh (paso
# manual con confirmación interactiva, read -rp). Decisión del
# usuario 2026-09-20: cero pasos manuales entre Staging y
# Producción — la misma imagen que acaba de promocionar a Staging
# arriba se promueve sola a Producción, sin gate humano en el
# medio. Mismo sed/perl que el script manual (mismo MANIFEST, otro
# archivo), corriendo como un segundo commit separado justo
# después del de Staging — no se pisan en un solo commit porque
# Staging y Producción son releases conceptualmente distintos
# (permite revertir uno sin tocar el otro).
- name: Promover a Producción (CD)
if: steps.promotion.outputs.promote == 'true'
shell: bash
run: |
set -euo pipefail
VERSION="${{ steps.vars.outputs.VERSION }}"
git config user.name "Cristian Felipe"
git config user.email "[email protected]"
# Re-fetch: el step de Staging de arriba ya pusheó un commit a
# main en este mismo job -- sin este refresh, este checkout
# local queda un commit atrás y el push de abajo se rechaza.
git fetch origin main
git checkout -B main origin/main
sed -i -E "s|(image: ${IMAGE_NAME}:).*|\1${VERSION}|g" "${PROD_MANIFEST_FILE}"
perl -0777 -pi -e "s/(- name: APP_VERSION\n\s*value: ).*/\${1}${VERSION}/" "${PROD_MANIFEST_FILE}"
git add "${PROD_MANIFEST_FILE}"
if git diff --cached --quiet; then
echo "Producción ya está en ${VERSION}."
exit 0
fi
git commit -m "chore: promote ${VERSION} to production [skip ci]"
git push origin HEAD:main
- name: Resumen del pipeline - name: Resumen del pipeline
if: always() if: always()
shell: bash shell: bash
@@ -425,3 +425,49 @@ Resultados reales, uno por uno (código de cada prueba en
un dispositivo real. un dispositivo real.
21b. **Accesibilidad del checkout completo** por teclado (no solo el 21b. **Accesibilidad del checkout completo** por teclado (no solo el
buscador) — tab order y foco visible en los 3 pasos. buscador) — tab order y foco visible en los 3 pasos.
22. **Importación manual de imágenes no se reflejaba en el storefront**
(reportado por el usuario 2026-09-20) — **causa raíz encontrada y
arreglada en código (2026-09-20), pendiente confirmar con una
edición real.** `medusaFetch()` en `lib/headless/providers/medusa.ts`
usaba `next:{revalidate:60,tags:["medusa-products"]}`, caché en
memoria por-pod que sobrevivía a `force-dynamic`. Con
`frontend-deploy` en 2 réplicas sin cacheHandler compartido, el POST
de `/api/revalidate` del subscriber de Medusa solo llegaba a uno de
los dos pods — el otro seguía sirviendo datos viejos hasta que el
TTL expirara solo. Una edición puntual + revisar al toque tenía
~50% de chance de pegarle al pod stale; una importación CSV (mucho
más lenta) le daba tiempo de sobra al TTL para expirar en ambos.
**Fix:** se sacó el `next.revalidate`/`tags` del fetch — ya en
producción. Falta la prueba con una edición manual real (vía
`temp-admin`/`ari-shopping-tienda` en memoria) para confirmar que
el síntoma original desapareció, no solo que la causa técnica tiene
sentido.
23. **En mobile (viewport real de 320px), la página se cortaba y
obligaba a scrollear horizontalmente — RESUELTO 2026-09-20.**
Reportado por el usuario con captura real de `shop.cruzcloud.net`.
**No era la galería de imágenes** (esa clase de bug con `min-w-0`
ya estaba corregida desde la sesión anterior, confirmado en vivo
que seguía sana). Dos causas reales encontradas por separado,
reproducidas con Playwright contra un viewport de 320×480 real:
- **Footer, bloque "Newsletter"** (`components/layout/footer.tsx`):
el párrafo de texto, sin `min-w-0`, le pedía al grid item su
ancho de una sola línea sin wrappear (~335px, medido en vivo)
en vez de dejarse achicar — forzaba la única columna del grid
(`sm:grid-cols-2 lg:grid-cols-4`, sin `grid-cols-1` propio pero
con auto-stacking correcto por debajo de `sm`) a 335px de ancho,
arrastrando la página entera. Mismo bug de fondo que el ya
documentado en la galería, en un grid en vez de un
`overflow-x-auto`.
- **Navbar, fila de "Departamentos"/"Todo"** (`components/layout/navbar.tsx`):
los 3 items de esa fila (botón de menú, "Departamentos", "Todo")
tenían los tres `shrink-0` — ninguno podía achicarse, así que si
los tres no entraban en 320px, desbordaban sí o sí (`min-w-0` no
aplica cuando el ítem no está pensado para achicarse). Fix:
ocultar el texto "Departamentos" en mobile (queda ícono + flecha,
con `aria-label` agregado para no perder accesibilidad), libera
espacio suficiente para que quepan los tres.
Verificado en vivo con Playwright (viewport 320×480 real, no
emulación de dispositivo con DPR) contra staging y producción
después del deploy: `document.documentElement.scrollWidth ===
clientWidth` en home y en página de producto, en ambos ambientes —
sin overflow.
@@ -25,7 +25,7 @@ spec:
runAsNonRoot: false runAsNonRoot: false
containers: containers:
- name: app - name: app
image: gitea.cruzcloud.net/devops/docs-portal:v1.0.267 image: gitea.cruzcloud.net/devops/docs-portal:v1.0.269
# El base saca TODAS las capabilities. Probado en vivo y # El base saca TODAS las capabilities. Probado en vivo y
# revertido: con drop: ALL (aunque se agregue NET_BIND_SERVICE # revertido: con drop: ALL (aunque se agregue NET_BIND_SERVICE
# de vuelta) el propio entrypoint de nginx:1.27-alpine falla al # de vuelta) el propio entrypoint de nginx:1.27-alpine falla al
@@ -26,11 +26,11 @@ spec:
runAsUser: 1001 runAsUser: 1001
containers: containers:
- name: app - name: app
image: gitea.cruzcloud.net/devops/ecommerce-frontend:v1.0.266 image: gitea.cruzcloud.net/devops/ecommerce-frontend:v1.0.273
env: env:
# Ver comentario en workloads/ecommerce/frontend/patches/deployment.yaml # Ver comentario en workloads/ecommerce/frontend/patches/deployment.yaml
- name: APP_VERSION - name: APP_VERSION
value: v1.0.266 value: v1.0.273
- name: HEADLESS_PROVIDER - name: HEADLESS_PROVIDER
value: medusa value: medusa
# Backend COMPARTIDO con Producción a propósito (decisión del # Backend COMPARTIDO con Producción a propósito (decisión del
@@ -34,7 +34,16 @@ const SHOP_LINKS = [
]; ];
export function Footer() { export function Footer() {
return <footer className="bg-ari-marino text-white"><div className="mx-auto grid max-w-7xl gap-10 px-4 py-14 sm:grid-cols-2 lg:grid-cols-4"><div><div className="flex flex-col items-start gap-3"><img src="/images/brand/logo-ari.svg" alt="ARI shopping" className="h-11 w-auto" width={200} height={64} />{/* eslint-disable-line @next/next/no-img-element -- vector propio, next/image no optimiza SVG sin habilitar dangerouslyAllowSVG */}<p className="text-xs text-white/60">Tesoros para cada aventura.</p></div><div className="mt-5 flex gap-3">{SOCIAL_LINKS.map(({ label, href, Icon }) => <a key={label} href={href} target="_blank" rel="noopener noreferrer" aria-label={label} className="text-white/80 transition hover:text-ari-coral-claro"><Icon /></a>)}</div></div><FooterColumn title="Compra" links={SHOP_LINKS} /><FooterColumn title="Ayuda" links={HELP_LINKS} /><div><h3 className="font-black">Newsletter</h3><p className="mt-3 text-sm leading-6 text-white/60">Recibe lanzamientos y promociones especiales.</p><div className="mt-4 flex rounded-full bg-white p-1"><input className="min-w-0 flex-1 bg-transparent px-4 text-sm text-slate-950 outline-none" placeholder="Tu correo" /><button className="rounded-full bg-ari-coral px-4 text-sm font-black text-white">Unirme</button></div></div></div><div className="border-t border-white/10 py-5 text-center text-xs text-white/50">© 2026 ARI Shopping · Frontend headless {appVersion} · GitOps</div></footer>; return <footer className="bg-ari-marino text-white"><div className="mx-auto grid max-w-7xl gap-10 px-4 py-14 sm:grid-cols-2 lg:grid-cols-4"><div><div className="flex flex-col items-start gap-3"><img src="/images/brand/logo-ari.svg" alt="ARI shopping" className="h-11 w-auto" width={200} height={64} />{/* eslint-disable-line @next/next/no-img-element -- vector propio, next/image no optimiza SVG sin habilitar dangerouslyAllowSVG */}<p className="text-xs text-white/60">Tesoros para cada aventura.</p></div><div className="mt-5 flex gap-3">{SOCIAL_LINKS.map(({ label, href, Icon }) => <a key={label} href={href} target="_blank" rel="noopener noreferrer" aria-label={label} className="text-white/80 transition hover:text-ari-coral-claro"><Icon /></a>)}</div></div><FooterColumn title="Compra" links={SHOP_LINKS} /><FooterColumn title="Ayuda" links={HELP_LINKS} />{/* min-w-0: sin esto, un grid item de texto toma como minimo su ancho
max-content (una sola linea, "Recibe lanzamientos y promociones
especiales." sin wrappear, ~335px) en vez de dejarse achicar y
wrappear dentro del track del grid -- confirmado en vivo 2026-09-20
contra un viewport real de 320px: la columna entera del footer (y
con ella, la pagina completa) quedaba forzada a 335px de ancho por
este unico parrafo. Mismo bug de fondo que el ya documentado en
product-gallery.tsx/app/product/[slug]/page.tsx, en un grid en vez
de un overflow-x-auto. */}
<div className="min-w-0"><h3 className="font-black">Newsletter</h3><p className="mt-3 text-sm leading-6 text-white/60">Recibe lanzamientos y promociones especiales.</p><div className="mt-4 flex rounded-full bg-white p-1"><input className="min-w-0 flex-1 bg-transparent px-4 text-sm text-slate-950 outline-none" placeholder="Tu correo" /><button className="rounded-full bg-ari-coral px-4 text-sm font-black text-white">Unirme</button></div></div></div><div className="border-t border-white/10 py-5 text-center text-xs text-white/50">© 2026 ARI Shopping · Frontend headless {appVersion} · GitOps</div></footer>;
} }
function FooterColumn({ title, links }: { title: string; links: { label: string; href: string }[] }) { function FooterColumn({ title, links }: { title: string; links: { label: string; href: string }[] }) {
@@ -261,10 +261,18 @@ function DepartmentsMegaMenu({ tree, isActive }: { tree: CategoryNode[]; isActiv
onClick={() => setOpen((value) => !value)} onClick={() => setOpen((value) => !value)}
aria-expanded={open} aria-expanded={open}
aria-haspopup="true" aria-haspopup="true"
aria-label="Departamentos"
className={`flex items-center gap-1.5 ${pillClass(open)}`} className={`flex items-center gap-1.5 ${pillClass(open)}`}
> >
<LayoutGrid className="size-4" /> <LayoutGrid className="size-4" />
Departamentos {/* Texto oculto en mobile: los 3 items de esta fila (menu, este
boton, "Todo") tienen shrink-0 -- ninguno se achica, asi que si
los tres no entran, desbordan la pagina entera (confirmado en
vivo 2026-09-20 contra 320px real: 14px de overflow horizontal
con el texto visible). No es un caso de min-w-0 faltante (ese
fix asume que el item PUEDE achicarse) -- ocultar la palabra
mas larga de la fila es lo que de verdad libera espacio. */}
<span className="hidden sm:inline">Departamentos</span>
<ChevronDown className={`size-3.5 opacity-70 transition-transform ${open ? "rotate-180" : ""}`} /> <ChevronDown className={`size-3.5 opacity-70 transition-transform ${open ? "rotate-180" : ""}`} />
</button> </button>
@@ -179,7 +179,18 @@ export function ProductGallery({ images, name }: { images: string[]; name: strin
className="pointer-events-none absolute inset-0 bg-slate-50 transition-opacity duration-150 ease-out" className="pointer-events-none absolute inset-0 bg-slate-50 transition-opacity duration-150 ease-out"
style={{ style={{
opacity: hoverZoom && !hoverZoom.showPanel ? 1 : 0, opacity: hoverZoom && !hoverZoom.showPanel ? 1 : 0,
backgroundImage: `url(${selected})`, // Comillas dentro de url() a proposito: sin ellas, un
// espacio o parentesis literal en el nombre de archivo
// (ej. "descarga (4)-xxx.png", el patron por defecto de
// Chrome al descargar una imagen sin renombrarla -- asi
// quedan las subidas manuales, a diferencia de las
// importadas por CSV) rompe la sintaxis de url() sin
// comillas -- el navegador descarta el valor completo en
// silencio (backgroundImage queda "none"). Confirmado en
// vivo 2026-09-20: reportado por el usuario como "el zoom
// interactivo no se visualiza" solo en fotos subidas a
// mano por el admin, nunca en las de CSV.
backgroundImage: `url("${selected}")`,
backgroundRepeat: "no-repeat", backgroundRepeat: "no-repeat",
backgroundSize: `${HOVER_ZOOM_SCALE * 100}%`, backgroundSize: `${HOVER_ZOOM_SCALE * 100}%`,
backgroundPosition: hoverZoom ? `${hoverZoom.bgX}% ${hoverZoom.bgY}%` : "center", backgroundPosition: hoverZoom ? `${hoverZoom.bgX}% ${hoverZoom.bgY}%` : "center",
@@ -217,7 +228,9 @@ export function ProductGallery({ images, name }: { images: string[]; name: strin
top: hoverZoom.panelTop, top: hoverZoom.panelTop,
width: hoverZoom.panelSize, width: hoverZoom.panelSize,
height: hoverZoom.panelSize, height: hoverZoom.panelSize,
backgroundImage: `url(${selected})`, // Ver el comentario en el overlay in-place de arriba -- mismo
// fix, mismo motivo (comillas necesarias dentro de url()).
backgroundImage: `url("${selected}")`,
backgroundRepeat: "no-repeat", backgroundRepeat: "no-repeat",
backgroundSize: `${HOVER_ZOOM_SCALE * 100}%`, backgroundSize: `${HOVER_ZOOM_SCALE * 100}%`,
backgroundPosition: `${hoverZoom.bgX}% ${hoverZoom.bgY}%`, backgroundPosition: `${hoverZoom.bgX}% ${hoverZoom.bgY}%`,
@@ -25,14 +25,14 @@ spec:
runAsUser: 1001 runAsUser: 1001
containers: containers:
- name: app - name: app
image: gitea.cruzcloud.net/devops/ecommerce-frontend:v1.0.263 image: gitea.cruzcloud.net/devops/ecommerce-frontend:v1.0.273
env: env:
# La actualiza el mismo paso del CD (.gitea/workflows/build.yaml) # La actualiza el mismo paso del CD (.gitea/workflows/build.yaml)
# que bumpea el tag de la imagen -- el footer la lee # que bumpea el tag de la imagen -- el footer la lee
# (process.env.APP_VERSION) para mostrar la versión real # (process.env.APP_VERSION) para mostrar la versión real
# desplegada en vez de un "v4.1" fijo. # desplegada en vez de un "v4.1" fijo.
- name: APP_VERSION - name: APP_VERSION
value: v1.0.263 value: v1.0.273
- name: HEADLESS_PROVIDER - name: HEADLESS_PROVIDER
value: medusa value: medusa
- name: MEDUSA_BACKEND_URL - name: MEDUSA_BACKEND_URL
@@ -111,14 +111,26 @@ async function medusaFetch<T>(
const publishableKey = process.env.MEDUSA_PUBLISHABLE_KEY; const publishableKey = process.env.MEDUSA_PUBLISHABLE_KEY;
// Sin next.revalidate/tags a proposito (reportado por el usuario
// 2026-09-20: imagenes editadas a mano en Medusa no se reflejaban en
// el storefront, pero por CSV si). Causa real: frontend-deploy corre 2
// replicas sin cacheHandler compartido -- cada pod tenia su propio
// cache en memoria de este fetch (next.revalidate:60), y el POST de
// /api/revalidate (revalidateTag) del subscriber de Medusa solo le
// llegaba a UNO de los dos pods via el Service, dejando al otro
// sirviendo la respuesta vieja hasta que el TTL expirara solo. Una
// edicion puntual + revisar al toque tenia ~50% de chance de pegarle
// al pod stale; una importacion CSV (minutos de duracion) le daba
// tiempo de sobra al TTL para expirar en ambos pods, disimulando el
// mismo bug. Las paginas que consumen esto ya son force-dynamic /
// dinamicas por diseño (ver comentario en app/page.tsx y
// app/product/[slug]/page.tsx) -- este next.revalidate competia contra
// esa intencion en vez de cumplirla. Sin este bloque, Next 15 no
// cachea el fetch por default dentro de una ruta dinamica.
const response = await fetch(url, { const response = await fetch(url, {
headers: publishableKey headers: publishableKey
? { "x-publishable-api-key": publishableKey } ? { "x-publishable-api-key": publishableKey }
: undefined, : undefined,
next: {
revalidate: 60,
tags: ["medusa-products"],
},
}); });
if (!response.ok) { if (!response.ok) {