Compare commits

..
Author SHA1 Message Date
devops 8107f9b6c7 docs(arquitectura): documentar plugins y subagentes instalados en sesion lab 2026-09-20 23:04:20 +00:00
devops 55eb9d8051 Merge branch 'docs/adr-0012-practica-multicloud-2026-09-20'
Build and Push Docs Portal / Escaneo de Secretos (Gitleaks) (push) Successful in 37s
Build and Push Docs Portal / Construir y publicar Docs Portal (push) Successful in 7m47s
2026-09-20 21:03:22 +00:00
devops fb89c26919 docs(adr-0012): practica multicloud GCP/Azure en paralelo al lab ZimaOS 2026-09-20 21:03:02 +00:00
gitea-actions c245f3b67a chore(gitops): deploy Docs Portal v1.0.276 [skip ci] 2026-09-20 20:42:06 +00:00
devops 149f743975 Merge branch 'docs/adr-0010-cierre-zoom-imagenes-manuales-2026-09-20'
Build and Push Docs Portal / Escaneo de Secretos (Gitleaks) (push) Successful in 37s
Build and Push Docs Portal / Construir y publicar Docs Portal (push) Successful in 9m16s
2026-09-20 20:32:18 +00:00
devops cc79d3b089 docs(adr-0010): cerrar bug de zoom con imagenes manuales, url() sin comillas 2026-09-20 20:31:58 +00:00
devops 5bcacdf09d chore: promote v1.0.275 to production [skip ci] 2026-09-20 20:22:40 +00:00
devops 3e5e18c14f chore: release v1.0.275 [skip ci] 2026-09-20 20:22:36 +00:00
devops e75a11d554 Merge branch 'fix/gallery-zoom-url-spaces-2026-09-20'
Build and Push Frontend / Escaneo de Secretos (Gitleaks) (push) Successful in 39s
Build and Push Frontend / Construir y Subir Imagen (push) Successful in 13m30s
2026-09-20 20:07:58 +00:00
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
gitea-actions 5fe18ce3d4 chore(gitops): deploy Docs Portal v1.0.274 [skip ci] 2026-09-20 19:25:20 +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
devopsandClaude Sonnet 5 291db05a5e docs(tmux-host): documentar fix de Remote Control, --continue y patron de swap
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
2026-09-20 15:56:47 +00:00
13 changed files with 669 additions and 26 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
# automaticamente (workloads/ecommerce-staging), no a Produccion
# directo -- decision del usuario para dejar de probar cambios en
# vivo contra Prod. Promover a Produccion (workloads/ecommerce) es
# una accion manual aparte, ver scripts/promote-to-production.sh.
# vivo contra Prod.
# 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
PROD_MANIFEST_FILE: workloads/ecommerce/frontend/patches/deployment.yaml
KUSTOMIZE_DIR: workloads/ecommerce-staging/frontend
IMAGE_NAME: gitea.cruzcloud.net/devops/ecommerce-frontend
@@ -604,6 +611,48 @@ jobs:
# sobrescribir commits ajenos o promocionar una versión antigua.
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
if: always()
shell: bash
@@ -0,0 +1,111 @@
# Plugins y subagentes de Claude Code (sesión "lab")
Inventario de qué plugins/skills se instalaron en la sesión principal
`lab` (no `cloud-practice`, que sigue su propia línea de práctica
multi-cloud — ver [ADR 0012](../decisiones/0012-practica-multicloud-gcp-azure.md))
y qué subagentes propios se configuraron, con la fuente real de cada
uno y por qué se aceptó o se descartó.
## Por qué esto queda documentado como el resto del lab
Dos frentes reales motivaron esto: (a) trabajo visual del storefront
ARI Shopping — ADR 0008 ya costó tres ciclos completos de
implementar-y-descartar una identidad en producción por no verla
renderizada antes de aprobarla; (b) crecimiento de habilidades de
Platform Engineering/DevOps. Un plugin instala instrucciones que
Claude sigue automáticamente, algunos con hooks que se ejecutan sin
pedir confirmación en cada uso — se aplicó la misma disciplina de
verificación que el resto del lab: quién publica (Anthropic vs
comunitario), qué permisos/hooks declara, y popularidad como señal de
confianza, antes de instalar cualquiera que no fuera 100% de Anthropic.
## Plugins instalados
| Plugin | Marketplace | Publica de verdad | Hooks | Notas |
|---|---|---|---|---|
| `frontend-design` | `claude-plugins-official` | **Anthropic** (verificado en `plugin.json`) | Ninguno | Solo skills, sin red externa |
| `context7` | `claude-plugins-official` | **Upstash** (tag `community-managed` — está *hospedado* en el catálogo oficial, no *publicado* por Anthropic) | Sin hooks, pero es un MCP que manda cada consulta al servidor remoto de Upstash (`mcp.context7.com`) | Aceptado con esa salvedad explícita de datos saliendo a un tercero |
| `superpowers` | `claude-plugins-official` | **Jesse Vincent** (`obra/superpowers`, MIT, 288k+ ⭐ en GitHub) — **NO es de Anthropic** pese a estar listado en el catálogo oficial | Sí: `SessionStart` corre un script que inyecta contexto en cada sesión nueva (revisado: solo lee su propio skill local, sin red ni nada destructivo) | Aceptado con esa salvedad — cambia comportamiento de arranque sin pedir confirmación cada vez |
| `gitops-workflows` | `devops-skills` (`ahmedasmar/devops-claude-skills`) | Ahmad Asmar (comunitario) | Ninguno (skill pura, sin ejecución automática) | |
| `k8s-troubleshooter` | `devops-skills` | Ahmad Asmar | Ninguno | |
| `ci-cd` | `devops-skills` | Ahmad Asmar | Ninguno | |
| `monitoring-observability` | `devops-skills` | Ahmad Asmar | Ninguno | |
**Descartados, no instalados:**
- **`iac-terraform`** (del mismo marketplace `devops-skills`) — descartado a propósito, no por falta de calidad. Terraform ya se maneja aparte vía `hashicorp/agent-skills` en la sesión `cloud-practice` (ver ADR 0012 Fase 2); instalarlo acá también duplicaría/confundiría la fuente de verdad.
- **`aws-cost-optimization`** (mismo marketplace) — no se pidió, se dejó afuera para no sumar alcance no solicitado.
- **`theme-factory`** — investigado y descartado: el mismo nombre aparece en al menos 4 fuentes comunitarias distintas y no relacionadas entre sí (`kivilaid`, `shipshitdev`, `composiohq`, etc.), ninguna en el catálogo oficial de Anthropic. Sin una fuente única verificable, no se instaló nada con ese nombre.
- **`ui-theme-designer`** (alternativa considerada del catálogo oficial) — es de **SAP SE**, específico para el SAP Fiori Design System / UI5 Web Components. No tiene nada que ver con el stack real de ARI Shopping (Next.js + Tailwind) — se descartó por no aplicar, no por falta de confianza.
## Subagentes propios (`~/.claude/agents/`)
Tres subagentes con propósito acotado y tools restringidas, definidos
como archivos markdown con frontmatter YAML (formato estándar de
Claude Code — ver [documentación oficial](https://code.claude.com/docs/en/sub-agents)).
### `devsecops-auditor`
Solo lectura y explicación del pipeline DevSecOps real (Gitleaks,
Trivy, Semgrep, Syft, Cosign) en `apps-registry`/`platform-infra`/
`scripts`. `tools: Read, Grep, Glob, Bash` + `disallowedTools: Write,
Edit` — nunca modifica código de producción, su único output es
diagnóstico contra el YAML/config real, citando archivo:línea, nunca
teoría genérica de CI/CD.
### `frontend-critic`
Revisa visualmente un cambio del storefront **antes** de aprobarlo —
formaliza la disciplina "simular antes de implementar" que costó tres
ciclos de descarte en ADR 0008. Regla dura: si no puede ver el
resultado renderizado en esa corrida puntual, el veredicto es "no
puedo aprobar esto sin verlo", nunca un aprobado condicional.
Vía de verificación, en orden de preferencia:
1. Chrome DevTools / Claude in Chrome — **hoy no disponible en esta
sesión** (corre headless dentro de `tmux-host`, sin navegador real
conectado — mismo gotcha ya documentado para Claude in Chrome en
general).
2. Playwright vía Docker en la NAS (el workaround ya validado este
mismo lab, ver `.browser-scratch/README.md`) contra
`shop.cruzcloud.net`/`staging.cruzcloud.net` reales.
Usa la skill `frontend-design` (oficial, ver tabla de arriba) como
marco de criterio de calidad visual.
### `cloud-cost-auditor`
Definido de antemano, a propósito, **sin nada que auditar todavía**.
Necesita los MCP de GCP/Azure que se configuran en la sesión
`cloud-practice` (ADR 0012) para funcionar — no va a tener recursos
reales que revisar hasta que esa línea de trabajo pase de la Fase 1.
Su propio system prompt tiene la regla dura de decir explícitamente
"no tengo nada que auditar todavía" en vez de inventar un análisis si
lo invocan antes de tiempo. No es un bloqueante para nada de lo demás,
solo queda listo para cuando aplique.
## Alcance y persistencia de la instalación
Los 7 plugins quedaron en **`scope: user`** (confirmado con
`claude plugin list`), no a nivel de proyecto de un repo específico.
Los 3 subagentes viven en `~/.claude/agents/` (también scope de
usuario, ubicación estándar según la doc oficial).
**Verificado en vivo que sobrevive a un reinicio del contenedor**,
mismo criterio que el resto de la configuración persistente de
`tmux-host` (ver [Sesión tmux persistente](sesion-tmux-persistente.md)):
`stat -c '%d' /root/.claude` y `stat -c '%d'
/DATA/AppData/k3d-lab-v3/claude-code-home` devuelven el mismo device
ID — es el mismo bind mount ya validado para login/confianza de
carpeta, no un directorio efímero del contenedor.
**Gotcha encontrado en vivo:** tanto los plugins como los subagentes
nuevos **no quedaron activos en la misma sesión donde se instalaron/
crearon** — la doc oficial de subagentes dice que se cargan sin
reinicio, *excepto* la primera vez que se crea el directorio
`~/.claude/agents/` (que no existía antes en este lab). Confirmado en
vivo: invocar `devsecops-auditor` en la misma sesión dio
`Agent type 'devsecops-auditor' not found`. Ambos (plugins y
subagentes) quedan pendientes de confirmar activos recién en el
próximo arranque real de la sesión — no es una falla de instalación,
es el comportamiento documentado.
@@ -230,11 +230,13 @@ falla:
contenedor.
- **Existencia de la sesión tmux `lab`** (`tmux has-session`): si no
existe, **sí se recrea automáticamente** (`tmux new-session -d -s lab
-c <WORKDIR>`, seguido de `tmux send-keys -t lab claude Enter` — ver
gotcha "sesión recreada pero sin Claude Code adentro" más abajo) — es
solo recuperar el propio entorno de trabajo del contenedor, no tocar
infraestructura externa, así que es seguro hacerlo sin intervención
humana. Si `WORKDIR` todavía no está montado (ver gotcha siguiente),
-c <WORKDIR>`, seguido de `tmux send-keys -t lab claude --remote-control
--continue Enter` — ver gotcha "sesión recreada pero sin Claude Code
adentro" más abajo, y los gotchas de Remote Control y `--continue`
agregados el 2026-09-20) — es solo recuperar el propio entorno de
trabajo del contenedor, no tocar infraestructura externa, así que es
seguro hacerlo sin intervención humana. Si `WORKDIR` todavía no está
montado (ver gotcha siguiente),
no se crea la sesión esa vuelta — se reintenta en el próximo ciclo en
vez de crearla mal posicionada.
@@ -301,14 +303,111 @@ en un contenedor descartable aparte (mismos mounts de `/DATA` y
`docker start` (arranque desde cero, no `restart`) recreó la sesión `lab`
con `claude` corriendo adentro sin intervención manual, en ~1 segundo.
## Gotcha: Remote Control nunca se activaba solo (2026-09-20)
Con la sesión `lab` recreándose sola y `claude` corriendo adentro (gotcha
anterior), quedaba un problema distinto y más sutil: la sesión servía
perfecto para trabajar vía `docker exec`/`lab-session.sh`, pero nunca
aparecía como controlable remotamente en claude.ai — como si Remote
Control jamás se hubiera activado.
**Causa raíz (confirmado en vivo 2026-09-20):** Remote Control es
opt-in por lanzamiento, vía el flag `claude --remote-control [nombre]`
de la CLI. Nada en `launch_claude_in_session()` lo pasaba nunca, así
que `remoteEnabled` en `~/.claude.json` quedaba en `false` para siempre
porque nadie lo pedía. No existe un paso separado de confirmación
"¿Activar Remote Control? y/n" más allá del diálogo normal de
confianza del folder (una hipótesis inicial de que sí existía tal paso,
y de que había una clave `remoteControlAtStartup` en `settings.json`,
resultó falsa al revisar directamente los archivos de configuración
reales).
**Cómo se detectó:** lanzar `claude --remote-control` a mano (en una
ventana de tmux en foreground, no vía un `!` de un solo uso que se
mata al terminar) sí registró la sesión y apareció en la lista de
sesiones activas de claude.ai como `zimaos-quizzical` — el hostname del
contenedor (`ZimaOS`) se usa como prefijo por defecto del nombre de
sesión remota.
**Fix:** cambiar la línea `tmux send-keys` en
`launch_claude_in_session()` para lanzar `claude --remote-control` en
vez de `claude` a secas.
**Validado en vivo:** tras reconstruir la imagen y swapear el
contenedor real (ver el gotcha del swap más abajo), `health.log`
registró `'claude --remote-control' lanzado...` al recrear la sesión, y
el pane de tmux mostró `/remote-control is active` con un link de
sesión válido — sin que nadie lo activara a mano.
## Gotcha: cada recreación de la sesión empezaba una conversación en blanco (2026-09-20)
Con Remote Control ya activándose solo, quedaba una fricción menor pero
molesta: cada vez que la sesión `lab` se recreaba (reinicio del NAS,
`docker rm` + recrear, o el propio healthcheck reaccionando a un
crash), `launch_claude_in_session()` arrancaba una conversación
completamente nueva. El contexto de trabajo en curso —no las
credenciales ni la confianza del folder, que sí persisten vía volumen—
se perdía cada vez, sin relación con los límites de cuota (ver la nota
al principio de este doc).
**Fix:** agregar `-c`/`--continue` (retoma la conversación más reciente
en el directorio de trabajo actual) al comando de lanzamiento:
`claude --remote-control --continue`. Si no hay ninguna conversación
previa en `WORKDIR`, el propio CLI arranca una nueva sin error — no
hace falta lógica condicional en el script.
**Validado en vivo — la prueba más fuerte posible:** el swap que
desplegó este mismo cambio (ver gotcha siguiente) mató la sesión de
Claude Code que lo estaba ejecutando. Del otro lado, la *misma
conversación* siguió sin cortarse, con el mismo ID de sesión, sin
ninguna intervención manual — evidencia directa de que `--continue`
efectivamente retoma la conversación y no solo que el contenedor volvió
a levantar.
## Gotcha: actualizar la imagen de `tmux-host` en caliente mata al propio proceso que hace el swap (2026-09-20)
Este contenedor tiene un problema estructural para actualizarse a sí
mismo: el proceso de Claude Code que ejecutaría `docker stop tmux-host
&& docker rm tmux-host && docker run ... tmux-host:nueva-imagen` corre
*dentro* del propio contenedor `tmux-host` — el primer `docker stop`
mata su propia shell a mitad de la secuencia, dejando el swap a medio
terminar (visto en vivo: un primer intento así falló en silencio).
**Fix:** correr la secuencia de swap desde un contenedor *sibling*
efímero, no desde dentro de `tmux-host`:
```bash
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /DATA/AppData/k3d-lab-v3/tmux-host:/tmux-host-cfg \
docker:cli sh /tmux-host-cfg/swap-retry.sh
```
Como este helper solo comparte el socket de Docker (no vive *dentro*
del contenedor `tmux-host`), detener y recrear `tmux-host` no lo afecta
a él ni al proceso que lo lanzó desde el host. El script
(`swap-retry.sh`, junto a sus logs, vive en
`/DATA/AppData/k3d-lab-v3/tmux-host/` — mismo path físico que
`/media/HDD-Storage/AppData/k3d-lab-v3/tmux-host/`, ver el gotcha del
symlink más arriba) deja un log fechado por corrida en `logs/` con el
`docker ps` y un `grep` sobre `healthcheck.sh` dentro de la imagen
nueva, para verificar sin tener que adivinar si el fix realmente quedó
adentro.
**Validado en vivo dos veces (2026-09-20):** el fix de Remote Control y
el de `--continue` se desplegaron cada uno con este mismo mecanismo,
ambos confirmados por `docker ps` (imagen e `CreatedAt` nuevos) y por
`health.log` mostrando la línea de lanzamiento esperada inmediatamente
después del swap.
## Qué sobrevive a qué
| Evento | ¿Sobrevive la sesión tmux (`lab`)? | ¿Sobrevive el login de Claude Code? |
|---|---|---|
| Corte de SSH / cerrar la terminal / cambiar de dispositivo | Sí — es exactamente el caso que este setup resuelve | Sí |
| `docker restart tmux-host` (reinicio del NAS) | **No al toque, pero se auto-recupera solo** — el proceso `tmux server` muere con el contenedor; `healthcheck.sh` la recrea en el próximo ciclo (máx. 30 min) sin intervención, o `lab-session.sh` la crea al instante si alguien se reconecta antes | Sí, vía el volumen montado en `/root/.claude` |
| `docker rm` + recrear el contenedor desde cero | No, pero igual se auto-recupera sola (mismo mecanismo de arriba) | Sí, vía el mismo volumen — no hace falta volver a autenticar ni reconfirmar el diálogo de confianza del folder (ver el mount de `claude-code-home.json`) |
| Falla de red real del host/NAS | N/A | N/A — `healthcheck.sh` la detecta y notifica, pero no la corrige (fuera de su alcance) |
| Evento | ¿Sobrevive la sesión tmux (`lab`)? | ¿Sobrevive el login de Claude Code? | ¿Retoma la misma conversación? |
|---|---|---|---|
| Corte de SSH / cerrar la terminal / cambiar de dispositivo | Sí — es exactamente el caso que este setup resuelve | Sí | Sí — el proceso `claude` ni se entera |
| `docker restart tmux-host` (reinicio del NAS) | **No al toque, pero se auto-recupera solo** — el proceso `tmux server` muere con el contenedor; `healthcheck.sh` la recrea en el próximo ciclo (máx. 30 min) sin intervención, o `lab-session.sh` la crea al instante si alguien se reconecta antes | Sí, vía el volumen montado en `/root/.claude` | Sí, desde el 2026-09-20 — `--continue` retoma la conversación más reciente en `WORKDIR` (ver gotcha arriba) |
| `docker rm` + recrear el contenedor desde cero | No, pero igual se auto-recupera sola (mismo mecanismo de arriba) | Sí, vía el mismo volumen — no hace falta volver a autenticar ni reconfirmar el diálogo de confianza del folder (ver el mount de `claude-code-home.json`) | Sí, mismo mecanismo — validado en vivo durante el swap de imagen del 2026-09-20 |
| Falla de red real del host/NAS | N/A | N/A — `healthcheck.sh` la detecta y notifica, pero no la corrige (fuera de su alcance) | N/A |
## Validación en vivo
@@ -425,3 +425,66 @@ Resultados reales, uno por uno (código de cada prueba en
un dispositivo real.
21b. **Accesibilidad del checkout completo** por teclado (no solo el
buscador) — tab order y foco visible en los 3 pasos.
22. **Importación manual de imágenes no se reflejaba en el storefront —
RESUELTO 2026-09-20, con un giro real durante la verificación.**
Reportado por el usuario 2026-09-20. Causa raíz #1 (caché
multi-pod): `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 #1:** se sacó el `next.revalidate`/`tags` del fetch.
Verificado en vivo por el usuario con una edición real (agregó
varias fotos manuales al producto `minnie-y-mickey-mouse`): las
fotos sí se reflejaron al toque, confirmando el fix #1 — pero
apareció un segundo síntoma real, no relacionado: "el zoom
interactivo no se visualiza" en las fotos subidas a mano, aunque sí
en las de CSV. **Causa raíz #2** (`components/product/product-gallery.tsx`):
el panel/overlay de zoom arma `backgroundImage: \`url(${selected})\``
sin comillas — un nombre de archivo con espacio y paréntesis (ej.
`descarga (4)-xxx.png`, el patrón por defecto de Chrome al descargar
una imagen sin renombrarla, exactamente como quedan las subidas
manuales, a diferencia de las de CSV que llegan con nombres
saneados) rompe la sintaxis de `url()` sin comillas — el navegador
descarta el valor completo en silencio (`backgroundImage: none`).
La lupa (recuadro) sí aparecía porque no depende de la URL; solo el
panel/overlay quedaba en blanco. **Fix #2:** comillas alrededor de
la URL en las dos ocurrencias del patrón. Verificado en vivo contra
las 9 fotos reales del producto (2 de CSV + 7 manuales con
espacio+paréntesis) con Playwright: `computed backgroundImage`
pasó de `none` a `url("...")` correcto en las 7 que fallaban, sin
tocar las 2 que ya andaban bien.
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.
@@ -0,0 +1,277 @@
# ADR 0012 — Práctica multi-cloud: GCP y Azure DevOps en paralelo al lab ZimaOS
**Estado:** aceptada — Fase 1 (Google Workspace) en ejecución en esta
misma sesión; Fases 2-4 quedan documentadas como roadmap, explícitamente
no implementadas hasta que se pidan en una sesión futura.
**Fecha:** 2026-09-20
## Contexto
El lab GitOps de CruzCloud (ZimaOS + k3d + Gitea + Argo CD) ya prueba en
vivo, con rigor de ADR, patrones que después se pueden proponer en
trabajo real con clientes — pero corre íntegro sobre hardware propio,
sin tocar ningún proveedor de nube público. Dos motivaciones concretas
empujan a abrir una segunda línea de práctica en paralelo:
1. **Practicar herramientas que el cliente actual restringe.** Ciertas
plataformas/herramientas de nube no están disponibles en el entorno
de trabajo del cliente actual — la única forma real de ganar
experiencia con ellas es un entorno propio.
2. **Preparar propuestas de arquitectura con evidencia, no solo teoría.**
Poder decir "ya lo corrí, así se ve, esto cuesta X" en una propuesta
a un cliente vale más que una descripción de manual.
3. **Portafolio.** Los mismos ADRs, diagramas y decisiones documentadas
que ya sostiene este lab sirven como muestra de trabajo verificable.
**Alcance confirmado explícitamente por el usuario, no asumido:**
- **Entorno paralelo, no migración.** ZimaOS sigue siendo el lab
principal — nada de lo de acá reemplaza nada de lo ya construido.
- **Cuentas personales únicamente.** Nunca credenciales del cliente
actual ni de ningún cliente futuro — separación total.
- **Costo cero o mínimo**, con alertas de presupuesto como red de
seguridad desde el primer recurso pago que se cree (ver
[Análisis de costo](#analisis-de-costo-explicito) más abajo).
- Mismo rigor documental que el resto del lab: un ADR por decisión
arquitectónica real, diagramas Mermaid, cifras verificadas en vivo o
contra fuente primaria (no contra un blog de terceros sin
corroborar — ver el caso del supuesto MCP "oficial" de Google
Workspace en la [Fase 1](#fase-1-ejecutar-en-esta-sesion-google-workspace)).
## Diagrama del lab actual (ZimaOS)
Versión simplificada, con solo los componentes relevantes para la
comparación de este ADR — el diagrama completo (con Traefik,
`serverlb`, cada `Host` de Ingress, etc.) vive en
[Visión general](../arquitectura/vision-general.md).
```mermaid
flowchart LR
User(["Usuario"]) -->|HTTPS| CF["Cloudflare Tunnel<br/>(sin puertos abiertos)"]
CF --> NPM["Nginx Proxy Manager<br/>enrutamiento por dominio"]
NPM --> K3D["k3d<br/>(cluster K8s local, Docker)"]
subgraph K3D_INT["Dentro de k3d"]
ARGO["Argo CD"] -->|sync| APPS["Apps reales<br/>(ecommerce, docs-portal)"]
GIT["Gitea<br/>(auto-alojado)"] -.->|watch repos| ARGO
end
K3D --> K3D_INT
```
**Costo real:** $0 — hardware propio (NAS ZimaOS), sin proveedor de
nube de por medio.
## Diagrama Fase 1 — entorno paralelo en GCP (VM + k3s)
**Esto NO es GKE administrado.** Es una VM `e2-micro` corriendo k3s
self-managed, elegida específicamente para mantener el costo real en
$0 — GKE (administrado, ver [Fase 4](#fase-4-documentar-no-ejecutar-gke-real-con-spot-auto-apagado))
no tiene una vía 100% gratuita permanente (ver el análisis de costo
más abajo).
```mermaid
flowchart LR
User(["Usuario / Claude"]) -->|SSH / kubectl| VM["Compute Engine e2-micro<br/>Always Free — us-west1/us-central1/us-east1<br/>NO es GKE administrado"]
subgraph VM_INT["Dentro de la VM"]
K3S["k3s<br/>(single-node, self-managed)"]
ARGO2["Argo CD<br/>(instancia propia, no la de ZimaOS)"] -->|sync| APPS2["Manifiestos del nuevo repo<br/>(mismo patrón base+overlay<br/>de platform-templates)"]
end
VM --> VM_INT
GIT_EXISTENTE["Gitea existente<br/>gitea.cruzcloud.net<br/>(reusado, repo nuevo, NO uno nuevo)"] -.->|watch| ARGO2
```
**Por qué reusar el Gitea existente en vez de levantar uno nuevo en
GCP:** el objetivo es practicar el patrón GitOps completo (Kustomize
base+overlay + Argo CD sincronizando) con el mínimo de piezas nuevas —
levantar un segundo Gitea solo sumaría superficie a mantener sin
enseñar nada nuevo sobre GCP en sí. El *cómputo* (la VM, k3s) es lo que
cambia de proveedor; el origen de verdad (Git) se queda donde ya
funciona.
**Costo real:** $0 — `e2-micro` es Always Free **de forma permanente**
(no es el trial de $300/90 días), siempre que la VM viva en
`us-west1`, `us-central1` o `us-east1` y no supere 1 instancia
no-preemptible + 30 GB-mes de disco persistente estándar + 1 GB/mes de
salida hacia Norteamérica — límites reales del programa, verificados
contra la documentación oficial de Google Cloud.[^1]
## Fase futura (no implementada) — GKE real con Spot + auto-apagado
Marcada explícitamente como **fase futura, no implementada** — ver
[Fase 4](#fase-4-documentar-no-ejecutar-gke-real-con-spot-auto-apagado)
para cuándo se decidiría ejecutarla.
```mermaid
flowchart LR
TF["Terraform<br/>(provisiona todo)"] --> GKE["GKE Standard<br/>1 cluster zonal"]
GKE --> NP["Node pool<br/>Spot VMs (e2-small)"]
SCHED["Cloud Scheduler<br/>(cron: apagar/prender)"] -->|invoca| FN["Cloud Run/Functions<br/>job de auto-apagado"]
FN -->|resize node pool<br/>a 0 / de vuelta| NP
style GKE fill:#333,stroke:#f90,color:#fff
style NP fill:#333,stroke:#f90,color:#fff
style SCHED fill:#333,stroke:#f90,color:#fff
style FN fill:#333,stroke:#f90,color:#fff
```
### Mecanismo de auto-apagado: comparación antes de recomendar
Dos mecanismos reales existen para esto; se investigaron ambos antes de
elegir uno, sin asumir cuál es "obviamente" mejor:
| | Escalar node pool a 0 | Destruir/recrear cluster completo (`terraform apply`/`destroy` programado) |
|---|---|---|
| Qué apaga | Solo el cómputo (los nodos) | Todo, incluido el control plane |
| Ahorra el fee de control plane ($0.10/hora) | No — pero **ya está cubierto por el crédito mensual de $74.40** para un único cluster, así que no hay nada real que ahorrar ahí | Sí, pero es un ahorro sobre algo que ya es $0 |
| Tiempo de "volver a prender" | ~1-3 min (crear nodos sobre un cluster que ya existe) | ~5-10 min (crear el cluster entero de cero) |
| Piezas para automatizarlo | Cloud Scheduler → Cloud Function/Run → 1 llamada a la API de GKE (resize node pool) | Cloud Scheduler → runner con Terraform + credenciales + state remoto seguro → `apply`/`destroy` completo |
| Riesgo de drift/config perdida | Bajo — el cluster nunca se borra | Mayor — cualquier cosa hecha fuera de Terraform se pierde en cada destroy |
| Patrón documentado en la industria | Sí, es el patrón estándar (Cloud Scheduler + Pub/Sub + Cloud Function), con casos reales de ~30% de ahorro de cómputo en clusters no-críticos[^2] | Existe, pero es la opción menos común para este caso puntual |
**Recomendación (sin implementar todavía):** escalar el node pool a
0/de vuelta vía Cloud Scheduler + Cloud Function, no destruir/recrear
el cluster completo. Razón concreta: el crédito de $74.40/mes **ya
cubre el fee de control plane de un único cluster exista o no exista
un nodo adentro** — destruir el cluster entero no ahorra nada sobre
ese componente porque ya es $0, y a cambio suma la complejidad real de
manejar Terraform state de forma segura dentro de un cron job
automatizado, más un tiempo de arranque 3-5x más largo. La única razón
real para preferir destruir/recrear sería eliminar *toda* huella en
GCP entre usos (por ejemplo, para no depender de que el crédito
mensual siga existiendo) — no es el caso acá.
## Tabla comparativa
| | Lab actual (ZimaOS) | GCP Fase 1 (VM + k3s) | GCP Fase futura (GKE + Spot) |
|---|---|---|---|
| Cómputo | NAS propio, Docker/k3d | Compute Engine `e2-micro` | GKE Standard, node pool Spot `e2-small` |
| ¿Kubernetes administrado? | No (k3d local) | No (k3s self-managed) | Sí (GKE Standard) |
| Control plane | N/A (local) | N/A (k3s corre en la misma VM) | $0.10/hora — cubierto por crédito de $74.40/mes para 1 cluster |
| Cómputo de nodos | $0 (hardware propio) | $0 (Always Free permanente) | ~$7-10/mes (Spot `e2-small`, sin cubrir por ningún crédito) |
| GitOps | Argo CD + Gitea, ambos autoalojados | Argo CD propio en la VM, mismo Gitea reusado | Igual que Fase 1, sobre GKE |
| Costo mensual real | $0 | $0 | ~$7-10/mes (no $0) |
| Estado | En producción, uso diario | A ejecutar esta sesión (Parte 2/3) | Documentado, no implementado |
## Análisis de costo explícito
**No existe un GKE administrado 100% gratis de forma permanente.** El
crédito de $74.40/mes por cuenta de facturación cubre exactamente el
fee de control plane ($0.10/hora) de **un** cluster zonal Standard o
Autopilot — nunca el cómputo real. GKE Autopilot factura desde el
primer pod que se programa (vCPU/memoria/almacenamiento por pod), sin
ningún equivalente al Always Free de Compute Engine; GKE Standard sí
puede correr sobre un único nodo Spot/preemptible barato (~$7/mes para
un `e2-small`, verificado contra tarifas actuales), pero ese cómputo
nunca es gratis.[^3]
**La única ruta de costo cero real y permanente es Compute Engine
`e2-micro` (Always Free) + k3s self-managed** — exactamente la Fase 1
de este ADR. No es el trial de $300/90 días (que expira y no es
"gratis", es prepago con reloj corriendo); es un beneficio permanente
del programa de nivel gratuito de Google Cloud, activo mientras la VM
cumpla los límites de región/tamaño/tráfico ya descritos arriba.
**Activar cualquier billing account de GCP requiere tarjeta igual**,
incluso para quedarse exclusivamente en recursos Always Free — Google
verifica el medio de pago aunque el cargo real sea $0. Detalle no
obvio, verificado contra documentación oficial: **las alertas de
presupuesto no se pueden configurar mientras la cuenta esté operando
sobre crédito gratuito de prueba** — hace falta pasar la cuenta de
facturación a estado "paga" (el uso real sigue siendo $0 si no se sale
del Always Free) para que la opción de crear un presupuesto/alerta
quede disponible.[^4] **Recomendación:** hacer ese paso — pasar a
cuenta de facturación paga — como parte del setup inicial, y configurar
de inmediato una alerta de presupuesto en **$1-5 USD** como red de
seguridad, antes de crear ningún recurso más allá de lo cubierto por
Always Free.
## Secuencia de fases
Todas documentadas acá; **solo la Fase 1 se ejecuta en esta sesión.**
Las Fases 2-4 no se implementan sin pedido explícito en una sesión
futura — quedan como roadmap, no como trabajo en curso.
### Fase 1 (ejecutar en esta sesión) — Google Workspace
MCP de Google Workspace vía cuenta personal, para las iniciativas ya
en curso. Detalle de la investigación previa a implementar (qué MCP
usar, oficial vs comunitario) en la Parte 3 de esta conversación —
resumen: **no existe un MCP oficial de Google Workspace** publicado
por Google (se verificó explícitamente buscando en los GitHub
`google`/`googleworkspace` oficiales — no aparece ninguno; un solo
blog de terceros afirma que "ya existe uno oficial" sin que ninguna
otra fuente lo corrobore, así que se descarta esa afirmación por no
verificable). `taylorwilsdon/google_workspace_mcp` es la opción
comunitaria más completa y activamente mantenida (120+ herramientas,
OAuth 2.1, +2500 estrellas) — es la que se usa, etiquetada como
comunitaria, no oficial.
### Fase 2 (documentar, no ejecutar) — Terraform aislado
Instalar el marketplace oficial de HashiCorp Agent Skills, y practicar
aprovisionando algo mínimo y desechable en GCP. Nota para cuando se
ejecute: HashiCorp también publica un **MCP Server de Terraform**
propio y oficial (`hashicorp/terraform-mcp-server`, integra con el
Terraform Registry y HCP Terraform) — es una pieza distinta a los
Agent Skills (el MCP es el "tubo" hacia herramientas reales; los
Skills son paquetes de conocimiento/instrucciones) y vale la pena
evaluar si se usan juntos o solo uno de los dos en ese momento.
### Fase 3 (documentar, no ejecutar) — Azure DevOps en paralelo
MCP oficial `microsoft/azure-devops-mcp` (confirmado como el oficial y
activamente mantenido por Microsoft, hay varios forks/alternativas
comunitarias que no son la vía recomendada). Replicar el mismo
pipeline DevSecOps que ya corre en Gitea Actions para este lab
(Gitleaks → Trivy → Semgrep → Syft → Cosign) como Azure Pipelines YAML,
en un proyecto personal — mismo objetivo de práctica que la Fase 1:
tener el patrón ya probado para proponerlo en trabajo con clientes que
usen Azure DevOps.
### Fase 4 (documentar, no ejecutar) — GKE real con Spot + auto-apagado
Solo si se decide conscientemente asumir el costo bajo pero no-cero
(~$7-10/mes, ver el análisis de costo arriba) — vía Terraform, node
pool Spot, y el mecanismo de auto-apagado recomendado en la sección de
arriba (escalar a 0, no destruir/recrear el cluster).
## Opciones consideradas
| Opción | A favor | En contra |
|---|---|---|
| Entorno paralelo en GCP+Azure (elegido) | Practica exactamente lo que el cliente actual restringe; portafolio con evidencia real; ZimaOS sigue intacto | Dos entornos más que mantener; curva de aprendizaje de dos proveedores nuevos a la vez |
| Migrar el lab completo a un solo cloud | Un solo entorno que mantener | Pierde justo lo que se quiere conservar (costo $0, control total de hardware); no es lo que se pidió |
| Solo AWS (el "default" de la industria) | Ecosistema más grande | El cliente actual ya usa/restringe otras plataformas específicas — practicar AWS no cierra esa brecha puntual |
| GKE administrado desde el día 1 (saltear la VM self-managed) | Más parecido a "producción real" desde el inicio | Rompe el requisito de costo cero explícito del usuario — ningún tier de GKE es gratis de forma permanente |
## Decisión
Se abre una línea de práctica multi-cloud paralela al lab ZimaOS, en
cuentas personales, arrancando por Compute Engine `e2-micro` (Always
Free) + k3s self-managed en GCP como Fase 1 de cómputo, y Google
Workspace vía MCP comunitario (`taylorwilsdon/google_workspace_mcp`)
como primer MCP a configurar en esta sesión. Terraform, Azure DevOps y
GKE administrado quedan documentados como Fases 2-4, sin implementar.
## Consecuencias
- **Se gana** un segundo entorno real, documentado con el mismo rigor
de ADR que ya sostiene el lab, listo para mostrar en propuestas a
clientes — sin tocar nunca credenciales de cliente.
- **Se sacrifica/queda como deuda técnica:** la Fase 1 de GCP usa k3s
self-managed, no un Kubernetes administrado real — cualquier lección
específica de operar GKE (autoscaling nativo, node pools, IAM de
GCP) queda pendiente hasta la Fase 4, y esa fase sí tiene costo real
(~$7-10/mes) que hoy no se está pagando.
- **Para revertir:** dar de baja la VM `e2-micro` y (si se llegó a
crear) el node pool de la Fase 4 — ningún componente de este ADR
toca o depende del lab ZimaOS existente, así que no hay nada que
revertir de ese lado.
[^1]: [Compute Engine Free Tier — Google Cloud](https://cloud.google.com/free/docs/compute-getting-started), [GCP Free Tier Complete Guide 2026](https://agentdeals.dev/gcp-free-tier-2026)
[^2]: [Reduce Your GKE Bill with Cloud Scheduler by Scaling to Zero](https://chimbu.medium.com/reduce-your-gke-bill-with-cloud-scheduler-by-scaling-to-zero-during-off-peak-hours-9796bb4f7b8b), [Scaling GKE Standard Clusters to Zero](https://medium.com/@gustavo.romerobenitez_34616/scaling-gke-standard-clusters-to-zero-56bf5270510e)
[^3]: [GKE Pricing Guide 2026 — Sedai](https://sedai.io/blog/google-cloud-kubernetes-cluster-cost), [e2-small pricing — Economize](https://www.economize.cloud/resources/gcp/pricing/compute-engine/e2-small/), [Google Kubernetes Engine pricing — Google Cloud](https://cloud.google.com/kubernetes-engine/pricing)
[^4]: [Google Cloud Free Trial FAQs](https://cloud.google.com/signup-faqs), [Create, edit, or delete budgets and budget alerts — Google Cloud](https://docs.cloud.google.com/billing/docs/how-to/budgets)
+2
View File
@@ -86,6 +86,7 @@ nav:
- Flujo GitOps: arquitectura/gitops-flujo.md
- Agente de monitoreo: arquitectura/agente-monitoreo.md
- Sesión tmux persistente: arquitectura/sesion-tmux-persistente.md
- Plugins y subagentes: arquitectura/plugins-y-subagentes.md
- Glosario: arquitectura/glosario.md
- Decisiones (ADRs):
- "0001 · Por qué k3d": decisiones/0001-por-que-k3d.md
@@ -99,6 +100,7 @@ nav:
- "0009 · Checkout real": decisiones/0009-checkout-real.md
- "0010 · Pago y envío reales": decisiones/0010-pago-envio-reales-y-storefront.md
- "0011 · Ambiente de Staging": decisiones/0011-ambiente-staging.md
- "0012 · Práctica multi-cloud GCP/Azure": decisiones/0012-practica-multicloud-gcp-azure.md
- Plantilla ADR: decisiones/plantilla-adr.md
- Playbooks:
- Crashloop Medusa: playbooks/incidente-crashloop-medusa.md
@@ -25,7 +25,7 @@ spec:
runAsNonRoot: false
containers:
- name: app
image: gitea.cruzcloud.net/devops/docs-portal:v1.0.267
image: gitea.cruzcloud.net/devops/docs-portal:v1.0.276
# El base saca TODAS las capabilities. Probado en vivo y
# revertido: con drop: ALL (aunque se agregue NET_BIND_SERVICE
# de vuelta) el propio entrypoint de nginx:1.27-alpine falla al
@@ -26,11 +26,11 @@ spec:
runAsUser: 1001
containers:
- name: app
image: gitea.cruzcloud.net/devops/ecommerce-frontend:v1.0.266
image: gitea.cruzcloud.net/devops/ecommerce-frontend:v1.0.275
env:
# Ver comentario en workloads/ecommerce/frontend/patches/deployment.yaml
- name: APP_VERSION
value: v1.0.266
value: v1.0.275
- name: HEADLESS_PROVIDER
value: medusa
# Backend COMPARTIDO con Producción a propósito (decisión del
@@ -34,7 +34,16 @@ const SHOP_LINKS = [
];
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 }[] }) {
@@ -261,10 +261,18 @@ function DepartmentsMegaMenu({ tree, isActive }: { tree: CategoryNode[]; isActiv
onClick={() => setOpen((value) => !value)}
aria-expanded={open}
aria-haspopup="true"
aria-label="Departamentos"
className={`flex items-center gap-1.5 ${pillClass(open)}`}
>
<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" : ""}`} />
</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"
style={{
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",
backgroundSize: `${HOVER_ZOOM_SCALE * 100}%`,
backgroundPosition: hoverZoom ? `${hoverZoom.bgX}% ${hoverZoom.bgY}%` : "center",
@@ -217,7 +228,9 @@ export function ProductGallery({ images, name }: { images: string[]; name: strin
top: hoverZoom.panelTop,
width: 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",
backgroundSize: `${HOVER_ZOOM_SCALE * 100}%`,
backgroundPosition: `${hoverZoom.bgX}% ${hoverZoom.bgY}%`,
@@ -25,14 +25,14 @@ spec:
runAsUser: 1001
containers:
- name: app
image: gitea.cruzcloud.net/devops/ecommerce-frontend:v1.0.263
image: gitea.cruzcloud.net/devops/ecommerce-frontend:v1.0.275
env:
# La actualiza el mismo paso del CD (.gitea/workflows/build.yaml)
# que bumpea el tag de la imagen -- el footer la lee
# (process.env.APP_VERSION) para mostrar la versión real
# desplegada en vez de un "v4.1" fijo.
- name: APP_VERSION
value: v1.0.263
value: v1.0.275
- name: HEADLESS_PROVIDER
value: medusa
- name: MEDUSA_BACKEND_URL
@@ -111,14 +111,26 @@ async function medusaFetch<T>(
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, {
headers: publishableKey
? { "x-publishable-api-key": publishableKey }
: undefined,
next: {
revalidate: 60,
tags: ["medusa-products"],
},
});
if (!response.ok) {