Verificado en vivo contra shop.cruzcloud.net: /store/search reporta
estimatedTotalHits (campo `count`) en 0 cuando limit=0, aunque
facetDistribution si trae los valores correctos (limit>=1 lo reporta
bien). El bucket "Todas"/"Todos" del sidebar de facetas (Fase 3b)
dependia de ese campo -- ahora suma los valores de la propia
facetDistribution, que da el total real sin ese problema.
Fase 3b (ultima) de ADR 0006 Etapa 2: los conteos de categoria/marca/edad
del sidebar de /catalog dejan de ser un snapshot client-side fijo. Ahora
se refetchean en vivo contra /api/search en cada cambio de filtro (query
debounced 300ms), tres llamadas paralelas -- una por dimension, cada una
omitiendo su propio filtro para preservar el patron Amazon de "cuantos
resultados habria si eligiera este otro valor".
- catalog-client.tsx: useMemo client-side originales pasan a ser fallback
(mock/shopify sin searchWithFacets, o cuando la categoria activa es un
grupo raiz sintetico que Meilisearch no puede filtrar).
- ProductFilters gana `limit` opcional; /api/search y el provider medusa
lo reenvian -- las llamadas de solo-conteo piden limit=0 para no
rehidratar precios de productos que no se van a mostrar.
PredictiveSearch pasa de filtrar en memoria el arreglo completo de
productos (prop products, substring match) a un fetch debounced
(250ms, con AbortController) contra la nueva ruta /api/search --
mismo componente visual, solo cambia de donde salen los datos, tal
como pide el ADR 0006 seccion 5.
/api/search (nueva, server-side) es el puente same-origin: el
componente es cliente y no puede pegarle directo al provider
server-side (MEDUSA_BACKEND_URL es DNS interno del cluster, no
resuelve desde el navegador).
CatalogProvider gana un metodo OPCIONAL searchWithFacets? (no cambia
la firma de getProducts/searchProducts) -- mock.ts y shopify.ts
compilan sin tocarlos, siguen sin implementarlo. medusa.ts lo
implementa contra /store/search (Fase 2), reutilizando mapProduct()
ya que la ruta devuelve el mismo shape de producto que /store/products.
searchProducts tambien se repunta a /store/search, dejando de usar el
?q= + re-filtro client-side de fetchProducts/matchesFilters para busqueda
(que sigue sirviendo para navegacion sin busqueda, sin cambios).
Navbar deja de pasarle products a PredictiveSearch (ya no los necesita)
pero los sigue recibiendo el mismo -- collections todavia se deriva de
ahi.
Limpieza: borrado patches/frontend-medusa-env.yaml, confirmado
huerfano (ningun kustomization lo referencia; el patch que si esta
vivo es frontend/patches/deployment.yaml).
Validado: build completo de Docker (target builder) -- npm ci,
next build con typecheck, /api/search aparece en la tabla de rutas
generada. Cero errores nuevos (los warnings de ESLint son
preexistentes en catalog-client.tsx, no tocado en esta fase).
Confirmado en vivo: pasar solo region_id a QueryContext no alcanza --
"Method calculatePrices requires currency_code in the pricing
context" persistia. A diferencia de /store/products nativo (resuelto
por el middleware setPricingContext), esta ruta propia no pasa por esa
cadena, asi que resuelve la moneda real de la region con un
query.graph adicional antes de pedir calculated_price -- no asume COP
fijo por si el catalogo suma otra region/moneda despues.
Mismo error encontrado en el gate de verificacion, ahora en la ruta
propia en vez del job de sync: "Method calculatePrices requires
currency_code in the pricing context" -- query.graph necesita
region_id/currency_code en el contexto para resolver
variants.calculated_price. El storefront ya manda region_id contra
/store/products (medusa.ts, regionId()); esta ruta ahora acepta el
mismo query param y arma el contexto de pricing con QueryContext(),
igual que la ruta oficial /store/meilisearch/products del plugin.
Encontrado en vivo (gate de verificacion de Fase 2): el job de sync
inicial del plugin fallaba en TODOS los productos con "Method
calculatePrices requires currency_code in the pricing context" --
variants.calculated_price necesita region_id/currency_code en el
contexto de query.graph, que el workflow interno del plugin
(sync-products.js) no provee. Las categorias si indexaron bien (21),
solo productos fallaba, por eso numberOfDocuments quedaba en 0 pese a
que el indice y sus settings se crearon correctamente.
variants.prices (lista cruda, sin resolver promociones/reglas de
precio) no requiere ese contexto y alcanza para el filtro/orden por
precio de la busqueda -- no necesitamos el precio final promocional
ahi, esa resolucion ya la hace la ruta /store/search via query.graph
con el contexto real al re-hidratar los productos.
Instala @rokmohar/medusa-plugin-meilisearch@^1.4.1 (linea v1, la unica
compatible con nuestro Medusa 2.18.0 exacto -- confirmado contra
peerDependencies reales de npm: @medusajs/* ^2.15.2, satisfecho por
2.18.0). Conectado via un array `plugins:` nuevo en medusa-config.ts
(primer uso en este repo, no `modules:`), env vars leidos con `?? ""`
en vez de required() -- si Meilisearch esta caido o mal configurado,
el plugin se degrada solo en vez de tumbar el arranque de Medusa
entero.
No se escribe un subscriber propio: se verifico contra el codigo
fuente real del paquete (descargado el tarball exacto de npm,
inspeccionado en /app/node_modules dentro de una imagen Docker de
verdad) que trae sus propios subscribers de product/category/
collection/variant/etc, descubiertos por Medusa en runtime desde
dentro del propio paquete -- confirmado que sobreviven a
`npm prune --omit=dev` y llegan intactos a la imagen final. Tambien
trae un job de indexado inicial de una sola corrida al boot
(numberOfExecutions: 1, no un cron recurrente -- no viola la
restriccion del ADR de "nunca cron").
Nueva ruta Store API propia (src/api/store/search/route.ts): las
rutas /store/meilisearch/* que trae el plugin no devuelven
facetDistribution (verificado leyendo su codigo fuente), asi que esta
ruta pide facets explicitamente via additionalOptions.facets, y
despues re-hidrata los productos via query.graph (mismos campos que
ya usa el storefront) en vez de confiar en precios/inventario
indexados -- mismo patron "safe path" que ya usa la ruta oficial del
plugin para /store/meilisearch/products.
Transformer custom en medusa-config.ts que espeja mapProduct() del
storefront (category/collection/brand/ageRange desde metadata,
disponibilidad desde la primera variante) para que el documento
indexado sea exactamente lo que el storefront ya muestra.
Validado en vivo: build completo de Docker (target builder) corre
limpio -- npm ci, medusa build (typecheck + compile backend/frontend),
npm prune -- cero errores. package-lock.json regenerado con un
contenedor node:22.18.0-bookworm-slim real (mismo base image que el
Dockerfile), no a mano.
Dockerfile: dummy MEILISEARCH_HOST/MEILISEARCH_API_KEY en el stage de
build (insurance barata, no son required() asi que no eran
estrictamente necesarios para que el build pasara, y de hecho paso
sin ellos en la prueba -- se dejan de todas formas por consistencia
con el resto de dummies de esa capa).
Motor de busqueda self-hosted, sin que Medusa lo conozca todavia
(eso es Fase 2). Mismo patron que redis/postgres/minio en este mismo
namespace: StatefulSet + PVC via volumeClaimTemplates en local-path,
nunca Deployment ni emptyDir -- el indice de Meilisearch necesita
sobrevivir a un reinicio del pod para no reindexar todo desde cero.
Imagen getmeili/meilisearch:v1.53.1 (ultimo release estable no-RC,
verificado contra la API de GitHub, no memoria de entrenamiento).
Recursos exactos del ADR (request 100m/256Mi, limit 500m/768Mi).
Sin securityContext explicito -- verificado en vivo que la imagen no
trae un usuario no-root (Alpine minimo, /etc/passwd sin usuario
dedicado), misma convencion que redis.yaml/minio.yaml en este repo,
que tampoco fuerzan runAsNonRoot.
MEILISEARCH_HOST (no secreto) va al ConfigMap commerce-config.
MEILI_MASTER_KEY/MEILISEARCH_API_KEY (mismo valor, dos nombres de env
var -- uno para el servidor, otro para el futuro plugin de Medusa en
Fase 2) documentados en secrets.template.yaml; el valor real ya se
aplico al Secret commerce-secrets en el cluster antes de este merge
(patch puntual, no via create-commerce-secrets.sh completo -- ese
script regenera TODAS las credenciales en cada corrida y hubiera
rotado postgres/redis/minio ya en uso).
Aprobada tras el diagnóstico de memoria del host (swap saturado
resuelto, ~500-700MiB liberados al desactivar monitoring). Agrega
nota de implementación con 3 desviaciones reales encontradas al
verificar el código fuente del plugin en el tag v1.4.1 antes de
escribir código: no hace falta subscriber propio (el plugin trae los
suyos), las rutas del plugin no devuelven facetDistribution (se
agrega una ruta Store API propia), y se conecta vía plugins: no
modules:. Ninguna cambia la decisión, solo el cómo.
commerce.cruzcloud.net (medusa-svc) has no route at "/" — Medusa only
serves /health, /app (admin), /store, etc. lab-agent's generic public
domain check flagged the resulting 404 as an anomaly every cycle even
though the service is healthy.
Adds a Traefik Middleware (redirectRegex) scoped to the commerce host
that 301s "/" to "/app", so the DNS/routing layer owns the entry point
instead of every client (including monitoring) having to know to
append /app manually. Regex includes the host explicitly because the
same Ingress also routes media.cruzcloud.net and shares
router.middlewares at the Ingress-annotation level.
Documenta el cambio a Telegram (silencioso para info, hilos al
recuperarse, log adjunto), la pausa nocturna 06:00-23:00 con su
tensión sin resolver con la ventana de medusa-deploy, la activación
del timer de systemd (60 min, bajado de 15 para reducir costo) con el
gotcha de Environment=HOME= para systemd --user, y el costo mensual
estimado actualizado.
17 corridas/día en vez de 68. Nota importante: el costo por corrida
sube al perder la ventaja del cache tibio de Claude Code (~1h TTL,
corridas espaciadas 60 min lo pierden casi siempre) -- el ahorro real
es ~1.5-2x, no 4x. También documenta el trade-off de latencia de
detección (~70 min en vez de ~25 min hasta detectar un problema).
Timer activo desde 2026-08-26. Documenta el hallazgo en vivo: el
manager de systemd --user de devops arranca con HOME=/DATA (el de
/etc/passwd), no con el override de /etc/profile.d/zz-devops-home.sh
que solo aplica a shells interactivas -- sin Environment=HOME=... en
la unidad, claude -p habría fallado la autenticación en cada corrida
del timer, silenciosamente. Se detectó y corrigió antes de activar,
corriendo el servicio manualmente vía systemctl --user start con el
entorno real como último paso de validación.
Actualiza la sección de Notificaciones: formato con íconos por tipo de
evento, log completo adjunto como documento, nivel "info" silencioso
(disable_notification), y respuestas en hilo cuando un recurso
accionado se recupera solo -- con la limitación conocida de que un
desbloqueo manual (borrando la entrada de state.json) pierde el
message_id y no queda threadeado. Validado en vivo con el canario.
Actualiza docs/arquitectura/agente-monitoreo.md: canal de notificación
cambiado de ntfy a Telegram Bot API (con la evidencia de la prueba de
entrega), horario activo 06:00-23:00 con su justificación de costo, la
tensión sin resolver con la ventana de medusa-deploy (00:00-06:00, no
se solapa con el horario activo del agente), y la estimación de costo
mensual (~US$60-65, basada en las corridas reales medidas).
Agrega docs/arquitectura/agente-monitoreo.md como referencia técnica y
documento de gobernanza del wrapper headless de diagnóstico/corrección
(scripts/lab-agent). Cubre la lista blanca de acciones automáticas
aprobada, por qué Claude corre con --allowedTools "" en vez de un Bash
tool con prefijo (coincidencia por prefijo permite inyección de
comandos, confirmado en vivo), el rediseño de autenticación a login de
suscripción en vez de API key, y la evidencia completa de la prueba
end-to-end del 2026-08-26 (DRY_RUN, ejecución real, rate limit/bloqueo,
2 bugs encontrados y corregidos).
Etapa 1 (auditoria/propuesta, sin codigo) de la Etapa 3 de Fase 5:
motor de busqueda y filtros facetados reales, reemplazando el
filtrado/conteo 100% client-side que dejo la sub-fase 3 de Etapa 2.
Verificado contra documentacion oficial vigente (no memoria de
entrenamiento):
- Medusa no tiene modulo de busqueda propio integrado con
Meilisearch; el Search Module nativo llego en Medusa 2.19, y el
plugin comunitario @rokmohar/medusa-plugin-meilisearch se conecta a
el solo desde su version ^2.0.0 (requiere Medusa >=2.19). Nuestro
backend corre Medusa 2.18.0 -- la version compatible es la linea
^1.4.1 del plugin (Medusa >=2.15 <2.19), documentada como deuda
tecnica explicita si se sube Medusa mas adelante.
- Impacto de recursos evaluado contra el estado real del host (swap
al 100%, mismo tipo de contencion que el incidente de Gitea):
Meilisearch recomendado sobre Typesense por menor huella de RAM y
por ser el unico con integracion Medusa mantenida para nuestra
version -- comparacion mantenida breve porque la diferencia es
clara, sin profundizar mas de lo necesario.
- Diseno de indexacion (campos buscables/filtrables/ordenables),
sincronizacion (mismo patron de subscriber por eventos que
revalidate-storefront.ts, sumando product-category.* al set de
eventos) y UI de facetas/autocompletado (reutiliza catalog-client.tsx
y CatalogProvider.searchProducts ya existentes, sin componentes
nuevos).
Cross-referencia agregada en ADR 0004 (que ya habia identificado
Meilisearch como paso natural) y entrada nueva en mkdocs.yml.
Etapa 2 (implementacion) NO arranca hasta aprobacion explicita de esta
propuesta.
Estado seguia diciendo "propuesta -- pendiente de aprobacion antes de
Etapa 2" aunque las 3 sub-fases (datos/categorias, navegacion,
componentes visuales -- PRs #29-#42) ya estan implementadas y en
produccion desde el 2026-08-25. Actualiza el header de estado y
reemplaza la seccion 6 (antes "Proximos pasos" pendiente de
aprobacion) por el resumen real de lo ejecutado, dejando explicito que
sigue pendiente borrar el usuario admin temporal de la auditoria y que
no hay Etapa 3 definida todavia.
tsconfig tiene noUncheckedIndexedAccess -- promoRoots[promoIndex %
promoRoots.length] tipa como Category | undefined aunque en runtime
siempre es valido (promoRoots.length > 0 ya se verifico antes de este
punto). Extraido a una constante con guard explicito para que TS
angoste el tipo. Sin cambio de comportamiento.
Detectado por el build real en main tras el merge de #41 -- no se
puede correr tsc localmente en esta maquina (ver notas de sesiones
anteriores), asi que esto solo lo valida el CI post-merge.