Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ebd341c060 | ||
|
|
e7349f34c3 | ||
|
|
c94ad5f651 | ||
|
|
ce929188ac | ||
|
|
98e0d331e9 | ||
|
|
d299fc1201 | ||
|
|
72a2715502 | ||
|
|
1ed06edbdd | ||
|
|
ffcadc5dca |
No files matched your search
@@ -57,6 +57,7 @@ docker run -d \
|
|||||||
-v /media/HDD-Storage:/media/HDD-Storage \
|
-v /media/HDD-Storage:/media/HDD-Storage \
|
||||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||||
-v /DATA/AppData/k3d-lab-v3/claude-code-home:/root/.claude \
|
-v /DATA/AppData/k3d-lab-v3/claude-code-home:/root/.claude \
|
||||||
|
-v /DATA/AppData/k3d-lab-v3/claude-code-home.json:/root/.claude.json \
|
||||||
--network host \
|
--network host \
|
||||||
alpine sh -c "apk add --no-cache tmux docker-cli bash curl git kubectl nodejs npm && npm install -g @anthropic-ai/claude-code && git config --global --add safe.directory '*' && tail -f /dev/null"
|
alpine sh -c "apk add --no-cache tmux docker-cli bash curl git kubectl nodejs npm && npm install -g @anthropic-ai/claude-code && git config --global --add safe.directory '*' && tail -f /dev/null"
|
||||||
```
|
```
|
||||||
@@ -67,6 +68,7 @@ docker run -d \
|
|||||||
| `/media/HDD-Storage` | `/media/HDD-Storage` | Ver "Gotcha: el symlink `/DATA/AppData`" abajo — sin esto, los repos no se ven dentro del contenedor pese a montar `/DATA` |
|
| `/media/HDD-Storage` | `/media/HDD-Storage` | Ver "Gotcha: el symlink `/DATA/AppData`" abajo — sin esto, los repos no se ven dentro del contenedor pese a montar `/DATA` |
|
||||||
| `/var/run/docker.sock` | `/var/run/docker.sock` | Seguir controlando Docker y k3d (contenedores, `k3d node`, etc.) desde dentro del contenedor |
|
| `/var/run/docker.sock` | `/var/run/docker.sock` | Seguir controlando Docker y k3d (contenedores, `k3d node`, etc.) desde dentro del contenedor |
|
||||||
| `/DATA/AppData/k3d-lab-v3/claude-code-home` | `/root/.claude` | Persistir login/config de Claude Code — sobrevive no solo a un `docker restart`, sino a recrear el contenedor entero |
|
| `/DATA/AppData/k3d-lab-v3/claude-code-home` | `/root/.claude` | Persistir login/config de Claude Code — sobrevive no solo a un `docker restart`, sino a recrear el contenedor entero |
|
||||||
|
| `/DATA/AppData/k3d-lab-v3/claude-code-home.json` | `/root/.claude.json` | Ver "Gotcha: la confianza del folder no persistía" abajo — es un archivo suelto, no vive dentro de `~/.claude/` |
|
||||||
|
|
||||||
## Gotcha: el symlink `/DATA/AppData`
|
## Gotcha: el symlink `/DATA/AppData`
|
||||||
|
|
||||||
@@ -127,6 +129,40 @@ bajo el volumen de `/root/.claude`) — se perdería si el contenedor se
|
|||||||
recrea. Ponerla en el `sh -c` de arranque la deja recreándose sola
|
recrea. Ponerla en el `sh -c` de arranque la deja recreándose sola
|
||||||
cada vez, sin pasos manuales que recordar.
|
cada vez, sin pasos manuales que recordar.
|
||||||
|
|
||||||
|
## Gotcha: la confianza del folder no persistía
|
||||||
|
|
||||||
|
Al abrir `claude` por primera vez en un directorio, pide confirmar un
|
||||||
|
diálogo de "¿confiás en este folder?" (más aún acá, porque este repo
|
||||||
|
trae un `.claude/settings.local.json` con permisos pre-aprobados). Esa
|
||||||
|
confirmación se guarda en `~/.claude.json` — un **archivo suelto en
|
||||||
|
`/root`**, no dentro de la carpeta `~/.claude/` que ya estaba montada
|
||||||
|
como volumen. Resultado: aunque el login (`~/.claude/.credentials.json`)
|
||||||
|
sí persistía, el diálogo de confianza volvía a aparecer cada vez que
|
||||||
|
se recreaba el contenedor — justo el tipo de "reconfigurar de nuevo"
|
||||||
|
que este setup existe para evitar.
|
||||||
|
|
||||||
|
**Cómo se detectó:** al confirmar el diálogo por primera vez, se
|
||||||
|
comparó `/root/.claude.json` dentro del contenedor contra sí mismo
|
||||||
|
antes y después — mismo tamaño y mismo `mtime`, es decir, la
|
||||||
|
confirmación en pantalla nunca se había registrado. La causa real:
|
||||||
|
la primera vez se movió el selector con las flechas pero nunca se
|
||||||
|
presionó Enter, así que en rigor el diálogo seguía sin confirmar; una
|
||||||
|
vez confirmado *de verdad* (`tmux send-keys` moviendo el cursor a
|
||||||
|
"Yes, I trust this folder" + Enter), el archivo saltó de 2.2 KB a
|
||||||
|
40 KB y apareció la clave `projects` con la ruta del repo adentro.
|
||||||
|
|
||||||
|
**Fix:** copiar ese `.claude.json` ya confirmado a
|
||||||
|
`/DATA/AppData/k3d-lab-v3/claude-code-home.json` en el host, y
|
||||||
|
montarlo **archivo a archivo** (no como carpeta) en
|
||||||
|
`-v .../claude-code-home.json:/root/.claude.json`. A diferencia de
|
||||||
|
los demás volúmenes de este setup, este necesitó sembrarse con
|
||||||
|
contenido real primero — un bind mount de archivo con un origen que
|
||||||
|
no existe todavía en el host termina creando un directorio vacío en
|
||||||
|
su lugar, no un archivo, y rompe el arranque del contenedor.
|
||||||
|
Verificado en vivo: tras recrear el contenedor con este mount, `claude`
|
||||||
|
arrancó directo en la sesión normal, sin volver a pedir el diálogo de
|
||||||
|
confianza.
|
||||||
|
|
||||||
Sobre paquetes: alpine (imagen `alpine` sin pin de versión, la misma
|
Sobre paquetes: alpine (imagen `alpine` sin pin de versión, la misma
|
||||||
que uso este setup) sí trae `kubectl` en sus repos — no hizo falta
|
que uso este setup) sí trae `kubectl` en sus repos — no hizo falta
|
||||||
cambiar de imagen base ni instalarlo por binario aparte, contrario a
|
cambiar de imagen base ni instalarlo por binario aparte, contrario a
|
||||||
@@ -165,7 +201,7 @@ crea. No hay que recordar cuál de los dos casos aplica.
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Corte de SSH / cerrar la terminal / cambiar de dispositivo | Sí — es exactamente el caso que este setup resuelve | Sí |
|
| 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** — el proceso `tmux server` muere con el contenedor; `lab-session.sh` crea una sesión nueva automáticamente | Sí, vía el volumen montado en `/root/.claude` |
|
| `docker restart tmux-host` (reinicio del NAS) | **No** — el proceso `tmux server` muere con el contenedor; `lab-session.sh` crea una sesión nueva automáticamente | Sí, vía el volumen montado en `/root/.claude` |
|
||||||
| `docker rm` + recrear el contenedor desde cero | No | Sí, vía el mismo volumen — no hace falta volver a autenticar |
|
| `docker rm` + recrear el contenedor desde cero | No | 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`) |
|
||||||
|
|
||||||
## Validación en vivo
|
## Validación en vivo
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ spec:
|
|||||||
runAsNonRoot: false
|
runAsNonRoot: false
|
||||||
containers:
|
containers:
|
||||||
- name: app
|
- name: app
|
||||||
image: gitea.cruzcloud.net/devops/docs-portal:v1.0.181
|
image: gitea.cruzcloud.net/devops/docs-portal:v1.0.188
|
||||||
# El base saca TODAS las capabilities. Probado en vivo y
|
# El base saca TODAS las capabilities. Probado en vivo y
|
||||||
# revertido: con drop: ALL (aunque se agregue NET_BIND_SERVICE
|
# revertido: con drop: ALL (aunque se agregue NET_BIND_SERVICE
|
||||||
# de vuelta) el propio entrypoint de nginx:1.27-alpine falla al
|
# de vuelta) el propio entrypoint de nginx:1.27-alpine falla al
|
||||||
|
|||||||
@@ -54,12 +54,12 @@ export function PredictiveSearch() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={submit} className="relative w-full">
|
<form onSubmit={submit} className="relative w-full">
|
||||||
<div className="rounded-full bg-gradient-to-r from-cyan-400 via-violet-500 to-fuchsia-500 p-[2px] shadow-lg shadow-violet-500/10">
|
<div className="flex items-center rounded-full border-2 border-slate-200 bg-white pl-5 pr-1.5 shadow-sm transition focus-within:border-ari-marino focus-within:ring-4 focus-within:ring-ari-coral-claro/30">
|
||||||
<div className="relative rounded-full bg-white">
|
<Input value={query} onChange={(event) => setQuery(event.target.value)} placeholder="Busca juguetes, mochilas, personajes…" className="h-[48px] border-0 bg-transparent p-0 shadow-none focus-visible:ring-0" />
|
||||||
<Search className="pointer-events-none absolute left-5 top-1/2 z-10 size-5 -translate-y-1/2 text-violet-600" />
|
{query && <button type="button" aria-label="Limpiar búsqueda" className="mr-1 shrink-0 rounded-full p-1.5 text-slate-400 hover:bg-slate-100" onClick={() => setQuery("")}><X className="size-4" /></button>}
|
||||||
<Input value={query} onChange={(event) => setQuery(event.target.value)} placeholder="Busca juguetes, mochilas, personajes…" className="h-[52px] border-0 bg-transparent pl-[52px] pr-12 shadow-none focus-visible:ring-violet-300/30" />
|
<button type="submit" aria-label="Buscar" className="grid size-10 shrink-0 place-items-center rounded-full bg-ari-marino text-white transition hover:brightness-110">
|
||||||
{query && <button type="button" aria-label="Limpiar búsqueda" className="absolute right-4 top-1/2 -translate-y-1/2 rounded-full p-1 text-slate-400 hover:bg-slate-100" onClick={() => setQuery("")}><X className="size-4" /></button>}
|
<Search className="size-4.5" />
|
||||||
</div>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{loading && normalized.length >= 2 && matches.length === 0 && (
|
{loading && normalized.length >= 2 && matches.length === 0 && (
|
||||||
<div className="absolute left-0 right-0 top-[calc(100%+10px)] z-50 rounded-2xl border border-slate-200 bg-white p-4 text-center text-sm text-slate-500 shadow-2xl">
|
<div className="absolute left-0 right-0 top-[calc(100%+10px)] z-50 rounded-2xl border border-slate-200 bg-white p-4 text-center text-sm text-slate-500 shadow-2xl">
|
||||||
@@ -69,12 +69,12 @@ export function PredictiveSearch() {
|
|||||||
{matches.length > 0 && (
|
{matches.length > 0 && (
|
||||||
<div className="absolute left-0 right-0 top-[calc(100%+10px)] z-50 overflow-hidden rounded-2xl border border-slate-200 bg-white p-2 shadow-2xl">
|
<div className="absolute left-0 right-0 top-[calc(100%+10px)] z-50 overflow-hidden rounded-2xl border border-slate-200 bg-white p-2 shadow-2xl">
|
||||||
{matches.map((product) => (
|
{matches.map((product) => (
|
||||||
<Link key={product.id} href={`/product/${product.slug}`} prefetch={false} className="grid grid-cols-[54px_1fr] items-center gap-3 rounded-xl p-2 hover:bg-violet-50" onClick={() => setQuery("")}>
|
<Link key={product.id} href={`/product/${product.slug}`} prefetch={false} className="grid grid-cols-[54px_1fr] items-center gap-3 rounded-xl p-2 hover:bg-ari-marino/5" onClick={() => setQuery("")}>
|
||||||
<div className="relative aspect-square overflow-hidden rounded-lg bg-slate-100"><Image src={product.thumbnail} alt="" fill sizes="54px" className="object-cover" /></div>
|
<div className="relative aspect-square overflow-hidden rounded-lg bg-slate-100"><Image src={product.thumbnail} alt="" fill sizes="54px" className="object-cover" /></div>
|
||||||
<div><strong className="line-clamp-1 text-sm">{product.name}</strong><span className="text-xs text-slate-500">{product.category} · {product.collection}</span></div>
|
<div><strong className="line-clamp-1 text-sm">{product.name}</strong><span className="text-xs text-slate-500">{product.category} · {product.collection}</span></div>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
<button type="submit" className="mt-1 w-full rounded-xl bg-slate-950 px-4 py-3 text-sm font-black text-white hover:bg-violet-700">Buscar “{query.trim()}” en todo el catálogo</button>
|
<button type="submit" className="mt-1 w-full rounded-xl bg-ari-marino px-4 py-3 text-sm font-black text-white hover:brightness-110">Buscar “{query.trim()}” en todo el catálogo</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ spec:
|
|||||||
runAsUser: 1001
|
runAsUser: 1001
|
||||||
containers:
|
containers:
|
||||||
- name: app
|
- name: app
|
||||||
image: gitea.cruzcloud.net/devops/ecommerce-frontend:v1.0.185
|
image: gitea.cruzcloud.net/devops/ecommerce-frontend:v1.0.189
|
||||||
env:
|
env:
|
||||||
- name: HEADLESS_PROVIDER
|
- name: HEADLESS_PROVIDER
|
||||||
value: medusa
|
value: medusa
|
||||||
|
|||||||
@@ -42,6 +42,22 @@ const nextConfig: NextConfig = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
// Assets de marca (logo, favicon) cambian con cada iteracion de
|
||||||
|
// identidad visual, a diferencia de las fotos de producto -- el
|
||||||
|
// cache de 7 dias de la regla anterior dejaba servido el logo
|
||||||
|
// viejo en Cloudflare/navegador durante toda una semana despues de
|
||||||
|
// cada deploy (ver ADR 0008). Next.js aplica el ultimo header que
|
||||||
|
// matchee un mismo path, asi que esta regla, al ir despues,
|
||||||
|
// sobreescribe el Cache-Control solo para /images/brand/*.
|
||||||
|
{
|
||||||
|
source: "/images/brand/:path*",
|
||||||
|
headers: [
|
||||||
|
{
|
||||||
|
key: "Cache-Control",
|
||||||
|
value: "public, max-age=300, must-revalidate",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in new issue
Block a user