Nueva fase, separada y con prioridad sobre las Fases 2-4 del ADR 0012:
practica de Apps Script (via clasp, sin MCP -- se verifico que no existe
uno oficial de Google) y AppSheet (documentado desde afuera, sin export
de codigo posible, verificado contra la documentacion oficial) para
iniciativas reales de la empresa con cuenta personal, sin datos ni
credenciales de cliente en ningun repo/doc. Deja explicito lo que queda
en pausa mientras esta fase este activa: swap de tmux-host, Fase 4 del
ADR 0012 (GKE), prueba del skill frontend-design.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QMP7ULbxbsB5g1vELNAT3D
El MCP self-hosted (taylorwilsdon/google_workspace_mcp) quedo abandonado:
limite real en FastMCP's OAuthProxy, no error de configuracion -
redirect_uri se arma desde base_url (no desde GOOGLE_OAUTH_REDIRECT_URI),
y base_url tambien alimenta issuer_url, que mcp/server/auth/routes.py
exige HTTPS-o-localhost sin bypass. Documenta la causa raiz confirmada
leyendo el codigo fuente instalado, la decision del usuario de usar
Connectors nativos en su lugar, y la verificacion en vivo (3 calendarios
reales, razonamiento de fechas correcto).
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QMP7ULbxbsB5g1vELNAT3D
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
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
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
Agrega tres gotchas nuevos a sesion-tmux-persistente.md, mismo estilo
que los existentes:
- Remote Control nunca se activaba solo: faltaba el flag
--remote-control en launch_claude_in_session().
- Cada recreacion empezaba una conversacion en blanco: se agrego
--continue al comando de lanzamiento.
- Actualizar tmux-host en caliente mata al proceso que hace el swap
(corre dentro del propio contenedor): documentado el workaround de
contenedor sibling via docker.sock.
Tambien actualiza la tabla "Que sobrevive a que" con una columna de
continuidad de conversacion, y el comando citado en la seccion de
healthcheck automatico.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QMP7ULbxbsB5g1vELNAT3D
Tras un corte de luz real, la sesion tmux "lab" se recreaba sola pero
Claude Code nunca arrancaba adentro -- distinto al caveat de persistencia
de sesion ya documentado (ese es "el proceso muere, el contenedor sigue
vivo"; esto es "el host completo se apaga y arranca de cero"). Documenta
las dos causas encontradas (claude nunca se lanzaba tras recrear la
sesion; riesgo de carrera con el mount de /media/HDD-Storage) y el fix
aplicado en scripts/tmux-host/healthcheck.sh.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
El importador de commerce-backend definia normalizeTitleForMatch (colapsa
guion largo/comillas tipograficas) pero nunca la usaba en el matching de
producto/categoria/coleccion -- seguia comparando con toLowerCase().trim()
plano, por eso un re-import de "Toy Story 5 - Woody" con guion largo pegado
desde Excel volvio a crear un duplicado en vez de actualizar el existente.
En el storefront, cualquier slug de producto inexistente devolvia HTTP 200
en vez de 404 (confirmado con un slug inventado): el Suspense del navbar
hace que el layout empiece a stream-ear la respuesta con status 200 antes
de que notFound() se ejecute dentro de la pagina. Se mueve la verificacion
a generateMetadata, que se resuelve completo antes de iniciar el stream.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Documenta el ambiente de Staging (staging.cruzcloud.net, promoción
manual, footer con version real), y agrega a notas-sueltas la
correccion de bodega Cali, redes sociales reales, rediseño de galeria
estilo Amazon, la leccion de min-w-0 en CSS Grid, y la remocion
definitiva de la barra flotante de carrito en mobile. De paso, suma a
la nav el ADR 0010 que existía en el repo pero nunca se había
enlazado.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012Y4df3Z7odr9Cg8F2nw6uV
Incluye: footer con version real via APP_VERSION, y remocion de la
barra flotante de "Anadir al carrito" en mobile -- ambos verificados
en Staging antes de promover.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012Y4df3Z7odr9Cg8F2nw6uV
El footer mostraba "v4.1" fijo -- inutil para distinguir a simple
vista si Staging y Produccion estan en la misma version. Ahora lee
process.env.APP_VERSION, que el propio CD (.gitea/workflows/build.yaml)
bumpea junto con el tag de la imagen en cada auto-deploy a Staging.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012Y4df3Z7odr9Cg8F2nw6uV
Pedido del usuario con capturas reales: la barra fija seguía
superponiéndose sobre contenido de abajo al scrollear (ej. sobre la
tarjeta de "También te puede interesar"), incluso después de varias
vueltas ajustando cuándo debía aparecer/ocultarse. En vez de seguir
parchando esa lógica, se saca la barra flotante por completo -- queda
un solo botón "Añadir al carrito", visible en su lugar normal dentro
de la página también en mobile (sin duplicar), igual que la mayoría de
tiendas reales.
Se borra components/product/mobile-add-to-cart.tsx (ya sin uso) y el
id="product-buy-button"/id="recommendations-section" que solo existían
para el IntersectionObserver de ese componente.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012Y4df3Z7odr9Cg8F2nw6uV
Decisión del usuario 2026-09-14: dejar de validar cambios directo
contra shop.cruzcloud.net (producción real).
- workloads/ecommerce-staging/: overlay del frontend, mismo Dockerfile
e imagen que Producción, backend compartido (Medusa/Postgres/
Meilisearch de "ecommerce" via FQDN cross-namespace
medusa-svc.ecommerce.svc.cluster.local -- decisión del usuario, evita
duplicar el consumo de la NAS con un backend propio para Staging).
1 réplica en vez de 2, resources mas chicos.
- apps/ecommerce-staging-app.yaml + ecommerce-staging-governance-app.yaml:
Applications de Argo CD para el workload y la governance (en
platform-infra), mismo patrón que ecommerce-app/ecommerce-governance.
- Ingress en staging.cruzcloud.net -- cubierto por el wildcard DNS
*.cruzcloud.net que ya existe en Cloudflare, sin cambios de DNS.
- .gitea/workflows/build.yaml: MANIFEST_FILE/KUSTOMIZE_DIR pasan a
apuntar a Staging -- todo push a main promueve ahí automáticamente
desde ahora. Promover a Producción (workloads/ecommerce, sin cambios
en este commit) queda como acción manual aparte, ver
scripts/promote-to-production.sh.
Secrets commerce-storefront y gitea-registry-secret ya copiados a mano
al namespace ecommerce-staging (no versionados, mismo criterio que el
resto de los secrets reales del repo).
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012Y4df3Z7odr9Cg8F2nw6uV
- Imagen principal del producto pasa de object-cover a object-contain
(mismo motivo que las miniaturas del commit anterior): la foto
vertical de Buzz Lightyear (933x1500) aparecía sin cabeza al
seleccionarla, reportado en vivo con captura real.
- Descripción del producto baja de 16px a 14px (text-sm leading-6,
mismo tamaño que los párrafos secundarios de /envios y /cambios) --
seguía viéndose grande al lado del resto de la columna.
- El buy-box mostraba los mismos 2 productos que ya aparecían en el
carrusel "También te puede gustar" de abajo (ambos usaban el mismo
cálculo de categoría/colección). Ahora el buy-box sugiere 1 producto
de una categoría DISTINTA -- cross-sell real en vez de duplicado.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012Y4df3Z7odr9Cg8F2nw6uV
Bug real encontrado armando la prueba automatizada que valida el
commit anterior (cd72962): condicionar el ocultamiento a que el borde
superior de "También te puede gustar" cruce el TOPE del viewport
(top <= 0) no es alcanzable en páginas sin suficiente contenido/footer
debajo -- el scroll máximo del documento nunca llega a ese punto, y el
bug original (barra pegada hasta el fondo) seguía reproduciéndose en
la práctica, aunque el código "parecía" correcto.
Se compara ahora contra la ALTURA del viewport (top < innerHeight): la
barra se oculta apenas la sección empieza a asomar desde abajo, y
sigue oculta el resto del scroll sin depender de cuánto contenido haya
más abajo.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012Y4df3Z7odr9Cg8F2nw6uV
- Miniaturas pasan de object-cover a object-contain: fotos con
relación de aspecto muy vertical (confirmado en vivo: 933x1500 en
este catálogo) perdían cabeza o pies al forzar el recorte cuadrado.
Reportado por el usuario viendo las miniaturas de un producto real.
- La descripción del producto bajaba de 18px (text-lg, mismo tamaño
que un párrafo de apertura de página completa como /envios) a 16px
(text-base) -- se veía desproporcionada al lado del resto de la
columna de compra en el layout de 3 columnas.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012Y4df3Z7odr9Cg8F2nw6uV
La barra de "Añadir al carrito" quedaba pegada abajo en mobile hasta
el final de la página (incluso sobre el footer), mucho después de que
el usuario ya pasó "También te puede gustar". Se agrega un tercer
sentinel (#recommendations-section) observado por IntersectionObserver:
una vez que su borde superior cruza el tope del viewport, la barra se
oculta y no vuelve a aparecer bajando más.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012Y4df3Z7odr9Cg8F2nw6uV
Root cause real del bug reportado por el usuario (imagen de producto
viéndose gigante/cortada en Chrome y Edge Android) -- no era la imagen:
al mover "También te puede gustar" DENTRO del grid de la página (para
el layout de 2 filas del commit anterior), su carrusel horizontal
(overflow-x-auto interno, sin flex-wrap) empezó a contar como item de
ese grid. Sin min-w-0, el ancho mínimo intrínseco de TODAS las cards
del carrusel sin wrappear se usa para dimensionar la columna
compartida del grid -- en mobile (grid de una sola columna), eso
empujaba TODO (imagen, título, specs) a ~800px de ancho en un viewport
real de ~380px. El navegador termina "zoomeando" el layout para que
entre, dando la sensación de que todo se ve a tamaño real / cortado.
Confirmado en vivo con bisección real: sin el fix, el grid medía 783px
en un viewport de 380px; agregando min-w-0 al contenedor del carrusel
vuelve a 380px. Se agrega min-w-0 también a los otros 3 contenedores
del grid (galería, info, buy-box) y a la tira de miniaturas dentro de
ProductGallery (mismo patrón de overflow-x-auto) como defensa
preventiva -- práctica estándar para items de grid/flex con scroll
horizontal interno.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012Y4df3Z7odr9Cg8F2nw6uV
Reportado en vivo por el usuario: la foto principal (y el modal de
zoom interactivo) se ven borrosas en mobile, especificamente en
Chrome y Edge de Android.
Investigado a fondo antes de tocar nada: no es un archivo corrupto ni
un problema de nuestro cache/backend -- confirmado con
fetch()+createImageBitmap() (que evita <img>/srcset por completo) que
el archivo real decodifica perfecto a resolución completa (1200x1200).
El bug está en cómo Chromium elige el candidato del srcset con
sizes="100vw" + fill a DPR alto (2.6+, cualquier Android real):
currentSrc reporta la URL de un candidato grande, pero
naturalWidth/Height terminan reflejando un candidato mucho más chico
-- reproducido igual en Chromium headless, y consistente con lo
reportado en Chrome y Edge Android reales (mismo motor).
No hay forma de arreglar el algoritmo de selección de Chromium desde
acá. La foto principal y el modal de zoom pasan a unoptimized: al
quedar un solo recurso real por foto (sin candidatos de por medio),
no hay entre qué elegir mal. Costo: la foto pesa el archivo completo
en vez de una versión redimensionada -- aceptable para dos imágenes
puntuales donde la nitidez importa.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012Y4df3Z7odr9Cg8F2nw6uV
El buy-box (precio/carrito/sugeridos) es sticky y suele terminar
siendo la columna más alta del grid de producto -- al ser un grid de
una sola fila, el carrusel de recomendaciones de abajo (sección
aparte) solo empezaba una vez terminaba la columna MÁS alta,
dejando un hueco vacío enorme debajo de galería+descripción cuando
esas dos son más cortas que el buy-box.
Se reestructura a un grid de 2 filas desde xl: el buy-box ahora ocupa
ambas filas (row-span-2, sigue sticky), y "También te puede gustar"
pasa a ser la 2da fila pero solo debajo de galería+descripción
(col-span-2) -- corre en paralelo al buy-box en vez de esperarlo.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012Y4df3Z7odr9Cg8F2nw6uV
- "Detalles del producto" (marca, edad, categoría, colección) pasa de
acordeón colapsado a una mini tabla siempre visible -- mismos datos
reales de siempre, solo que ya no hace falta un click para verlos.
- Buy-box: nueva franja de confianza real con enlaces a /envios y
/cambios (5 días hábiles, envío de vuelta gratis) -- mismas
políticas ya publicadas en el footer, no texto inventado.
- "También te puede interesar" ahora muestra hasta 2 sugerencias en
vez de 1, mismo cálculo real por categoría/colección que ya
alimentaba el carrusel de abajo.
Investigado de paso (sin fix): la discrepancia de stock reportada
("Quedan 1" vs available:false) no es un bug de ProductPurchase -- la
página de producto usa /store/products de Medusa (en vivo, correcto),
mientras que /store/search (Meilisearch) devuelve inventario
desactualizado para el mismo producto. Eso vive en commerce-backend,
fuera de alcance de este cambio de frontend.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012Y4df3Z7odr9Cg8F2nw6uV
- max-w de la página de producto pasa de 1280px a 1600px -- aprovecha
pantallas grandes en vez de dejar margenes vacíos enormes.
- Nueva 3ra columna desde xl: buy-box propio (tarjeta con borde,
sticky) para precio/variantes/carrito, separado del texto
descriptivo. Por debajo de xl cae al flujo normal de la 2da columna
sin duplicar el componente (mismo <ProductPurchase>, un solo
id="product-buy-button" en el DOM).
- Dentro del buy-box: un producto sugerido real de la misma
categoría/colección (mismo cálculo que ya alimenta el carrusel de
abajo, no un dato inventado).
- MobileAddToCart: la barra fija ahora también requiere haber
scrolleado más de 80px además de que el botón real no esté en
pantalla -- antes aparecía inmediatamente sobre la vista inicial del
producto (imagen + variantes) apenas cargaba la página.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012Y4df3Z7odr9Cg8F2nw6uV
- El panel de zoom lateral ahora calcula tamaño/posición contra el
viewport real (window.innerWidth/innerHeight) en cada mousemove, en
vez de un breakpoint fijo (lg:block) con ancho fijo -- a resoluciones
altas con poco espacio a la derecha de la imagen terminaba
saliéndose de la pantalla. Si no entra un panel de al menos
PANEL_MIN_SIZE, se cae automáticamente al overlay in-place (antes
atado a "por debajo de lg", ahora a "no hay espacio real").
- La barra fija de "Añadir al carrito" en mobile quedaba fija desde el
primer render, tapando permanentemente parte de la imagen del
producto. Ahora el botón real (ProductPurchase) también se muestra
en mobile, y la barra sticky solo aparece cuando ese botón sale del
viewport (IntersectionObserver) -- mismo patrón que el "Buy Now"
sticky de Amazon.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012Y4df3Z7odr9Cg8F2nw6uV
Sin cambio de comportamiento -- solo hooks estables para la suite de
Playwright en .browser-scratch/checks.mjs (el selector genérico
div[aria-hidden] dejó de alcanzar para distinguir la lupa, el overlay
in-place y el panel lateral, que ahora coexisten en el DOM).
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012Y4df3Z7odr9Cg8F2nw6uV
El zoom in-place anterior (overlay de fondo dentro de la misma caja)
seguía sintiéndose confuso: el usuario esperaba el patrón clásico de
e-commerce (Amazon) -- un recuadro semitransparente ("lupa") que marca
qué región de la foto se está mirando, y un panel aparte, más grande,
que muestra esa región ampliada. Sin scroll ni superposición dentro de
la misma caja.
- Lupa: recuadro que sigue al cursor sobre la imagen principal,
clampeado para no salirse de los bordes (visible en lg+).
- bgX/bgY ahora se calculan por fracción de recorrido de la lupa
(posición del borde de la lupa entre 0 y el ancho disponible), no por
posición cruda del cursor -- mapeo correcto incluso cuando la lupa
queda pegada a un borde.
- Panel: a la derecha de la imagen (position absolute, se superpone al
contenido de al lado igual que en Amazon), mismo mecanismo de
background-image con la foto completa que ya evitaba el problema de
object-cover recortando partes de la imagen.
- Por debajo de lg (sin espacio para un panel aparte) se conserva el
overlay in-place como fallback.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012Y4df3Z7odr9Cg8F2nw6uV
El hover-zoom escalaba (transform:scale) la misma <Image> ya recortada
por object-cover -- para fotos verticales (ej. figura de acción de pie)
eso dejaba partes del producto (la cabeza) fuera del recorte desde el
propio render, así que ningún nivel de zoom las mostraba. Se reemplaza
por un overlay con background-image usando la foto completa
(background-size en un solo valor preserva el aspect ratio real) y
background-position siguiendo el cursor, que sí permite recorrer toda
la imagen.
De paso: las miniaturas ahora cambian la imagen principal también con
hover (onMouseEnter), no solo con click.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012Y4df3Z7odr9Cg8F2nw6uV
- ENVIOCLICK_ORIGIN_DANE_CODE en configmap.yaml estaba cargado con el
código DANE de Bogotá (11001000) por error; corregido a Santiago de
Cali, Valle del Cauca (76001000). Toda cotización real de envío hecha
antes de esta fecha se calculó desde el origen equivocado.
- Corregido también el fallback stale en medusa-config.ts
(originDaneCode) que seguía apuntando a Bogotá.
- Actualizado el texto de /envios y /contacto (misma dirección física,
Cra 100 #42-65, solo cambia la ciudad).
- Footer: Instagram y Facebook reales enlazados (verificados en vivo
contra los perfiles antes de linkear), y "Preguntas frecuentes" /
"Cambios" ya apuntan a contenido real en vez de /catalog.
- Nuevas páginas /cambios (5 días hábiles, envío de devolución a cargo
de ARI Shopping) y /preguntas-frecuentes.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012Y4df3Z7odr9Cg8F2nw6uV
El footer "Ayuda" no tenía contenido real -- los 4 links apuntaban a
/catalog. Se agregan dos páginas nuevas con información real:
- /envios: transportadoras reales de EnvíoClick (Coordinadora, Envía,
TCC, Rappi, Pibox -- confirmadas en una cotización real, ver ADR
0010 §2) y cómo se calcula el costo.
- /contacto: WhatsApp real (+57 301 613 8155, mismo número ya usado en
el fallback de checkout) y correo [email protected] (reenviado
por Cloudflare Email Routing -- configuración manual de DNS pendiente
del lado del usuario, fuera de alcance de este cambio).
"Preguntas frecuentes" y "Cambios" quedan sin contenido real definido
todavía, apuntando a /catalog como antes.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01THZFwPzrwv6VAbuwmaf5vR
Checkout con carrito vacío, fallback WhatsApp, filtros de catálogo,
búsqueda sin resultados (bug encontrado y corregido en otro commit),
slug de producto inválido (hallazgo pendiente de investigar: responde
200 en vez de 404), zoom interactivo, wishlist, otros viewports,
página 404 genérica, accesibilidad básica del buscador, y registro/
login con email -- todo verificado con Chromium real vía el harness de
.browser-scratch/.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01THZFwPzrwv6VAbuwmaf5vR
Encontrado probando en vivo con navegador real (Docker + Playwright):
una búsqueda de 2+ caracteres sin coincidencias no mostraba nada -- ni
"Buscando…" ni ningún mensaje. El usuario quedaba sin feedback de que
su búsqueda de verdad corrió. Se agrega un estado vacío con el botón
de buscar en todo el catálogo (esa búsqueda completa puede usar
criterios más amplios que el autocompletado).
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01THZFwPzrwv6VAbuwmaf5vR
Cubre el resto de la superficie del storefront (checkout edge cases,
filtros de catalogo, variantes de producto, cuenta/wishlist,
accesibilidad, otros viewports) agrupado por riesgo, no solo lo ya
probado.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01THZFwPzrwv6VAbuwmaf5vR
Cierra la mayor parte del checklist de pruebas reales que había quedado
pendiente: pago PSE hasta la redirección al banco, envío calculado
visible, banner y buscador mobile -- todo verificado con Chromium real
(Docker + Playwright en la NAS, ver .browser-scratch/README.md). Deja
documentados los 2 bugs reales que esa prueba encontró (ya corregidos
en otro commit) y un checklist ampliado para la próxima sesión.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01THZFwPzrwv6VAbuwmaf5vR
Encontrado probando en vivo con navegador real (Docker + Playwright
contra shop.cruzcloud.net):
- app/page.tsx: un producto featured pero sin stock (available: false)
salía primero en el hero banner con botón "Comprar ahora" hacia una
página donde en realidad no se puede comprar.
- pse-banks/route.ts: el sandbox real de ePayco (bank.getBanks())
incluye un primer elemento placeholder (bankCode "0", "A
continuación seleccione su banco...") mezclado con los bancos reales,
mostrado como opción seleccionable. Se filtra explícitamente.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01THZFwPzrwv6VAbuwmaf5vR
Trivy bloqueó el build del frontend en el pipeline de CI: Next.js
15.5.10 tiene un RCE no autenticado en la API de optimización de
imágenes vía archivos AVIF (CVE-2026-75604 / GHSA-2xp9-vwfh-vxw4),
corregido en 15.5.24. Bump mínimo dentro del mismo minor, sin otros
cambios de dependencias.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01THZFwPzrwv6VAbuwmaf5vR
- hero-carousel.tsx: slides derivados de productos featured/nuevos reales
(imagen, nombre, precio, link, badge de % OFF) en vez de un array
hardcodeado; fallback estático solo si la tienda no tiene productos
activos.
- navbar.tsx: en mobile/tablet el buscador pasa a una segunda fila a
todo el ancho debajo de logo+iconos (antes competía por espacio en una
sola fila y quedaba recortado); desktop sin cambios.
- docs: ADR 0010 documentando el pivote de pago PayU→Rapyd(descartado)→
ePayco, la integración de EnvíoClick y checklist de pruebas reales
pendientes desde el storefront; puntero agregado desde ADR 0009.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01THZFwPzrwv6VAbuwmaf5vR
Placeholders reemplazados por los datos reales que dio el usuario --
dirección de la bodega (Cra 100 #42-65), email y teléfono de contacto.
Estos valores aparecen como remitente en las guías reales de EnvíoClick.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01THZFwPzrwv6VAbuwmaf5vR
ADR 0009 §2 ya había anticipado esto: la shipping option actual es un
placeholder de negocio ($15.000 COP fijo), no un cálculo real por
peso/volumen -- y el modelo de direcciones del checkout (IDs DIVIPOLA
en vez de texto libre en lib/data/colombia.ts) ya se había construido
pensando en esta integración. Este commit la implementa.
Investigación en vivo (no solo documentación -- misma disciplina que
ya costó varios rehechos con las pasarelas de pago): la SPA de docs
públicas (apidoc.envioclickpro.com.co) no renderiza sin sesión, pero
está generada con apiDoc, así que se bajó y leyó directo su
api_data.json/api_project.json (spec completa real), y se probó
/quotation en vivo con la API key real del usuario -- 200 OK con
tarifas reales de PIBOX/COORDINADORA/ENVIA/RAPPI/TCC. Esto corrigió
varios datos mal asumidos de un plugin de WordPress desactualizado
usado como referencia inicial (API v1): la URL base real es
api.envioclickpro.com.co/api/v2 (no api.envioclickpro.com a secas), y
el header de auth real es "Authorization" a secas, no
"AuthorizationKey".
Hallazgo crítico evitado leyendo el código fuente de
@medusajs/utils/dist/common/define-config.js: defineConfig() ya
registra por defecto un módulo de fulfillment con el provider
manual_manual, aunque medusa-config.ts no lo mencione explícitamente
-- pero transformModules() indexa por serviceName y el último que
llega pisa al anterior (no mezcla providers). Agregar un bloque propio
de fulfillment sin incluir también manual_manual ahí habría roto en
silencio el envío manual que ya usa la shipping option existente.
Verificado cargando la config compilada real: ambos providers quedan
registrados juntos.
- src/modules/envioclick-fulfillment/: AbstractFulfillmentProviderService.
calculatePrice() cotiza en vivo durante el checkout -- Medusa advierte
explícitamente que un error acá bloquea el checkout completo, así que
NUNCA se deja propagar una excepción: cualquier falla (red, dirección
sin id DIVIPOLA, EnvíoClick caído) cae al precio fijo de $15.000 que
ya se usaba, para no romper la compra. createFulfillment() vuelve a
cotizar en el momento de generar la guía real en vez de reusar el
idRate mostrado en checkout (más correcto: CalculatedShippingOptionPrice
no tiene forma de persistir ese id entre llamadas, confirmado leyendo
el tipo real, y un idRate viejo podría estar vencido si el fulfillment
pasa horas/días después). cancelFulfillment() sí cancela de verdad
(cancellation/batch/order existe y está confirmado, a diferencia del
refund de ePayco que quedó sin implementar).
- Solo 1 paquete por cotización/guía (límite real de la API, confirmado:
"min 1, max 1") -- el carrito se agrega a un solo paquete. Como hoy
NINGÚN producto tiene peso/dimensiones cargados (confirmado en vivo:
18 productos activos, 0 con weight), se usa un paquete de fallback
conservador completo en vez de mezclar datos reales con inventados a
medias -- la cotización no va a ser precisa hasta que se carguen esos
datos (tarea de datos, no de código, fuera de alcance de este commit).
- Frontend: cambio chico pero necesario en checkout-client.tsx/
medusa-cart.ts -- el checkout mandaba el nombre de la ciudad a Medusa,
nunca el id DIVIPOLA (aunque el form sí lo tenía disponible). Hay
municipios con el mismo nombre en distintos departamentos de Colombia,
así que el nombre solo no alcanza para identificar la ciudad sin
ambigüedad -- se guarda el id real en la metadata de la dirección
(Medusa sí tiene ese campo), que el fulfillment provider lee para
derivar el código DANE real que pide EnvíoClick (5 dígitos + "000" de
relleno, confirmado contra su propio ejemplo: "11001000" = Bogotá).
Pendiente de verificar con una llamada real (no se quiso generar una
guía de prueba en la cuenta real del usuario sin avisar): /shipment_sandbox
y cancellation/batch/order, verificados contra la spec oficial real
pero no probados en vivo todavía.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01THZFwPzrwv6VAbuwmaf5vR