# CLAUDE.md Guía para Claude Code (claude.ai/code) al trabajar en este repositorio. ## Antes de invocar cualquier herramienta 1. Carga los secretos: `. .\.env.local.ps1` (gitignored). Sin esto, todas las llamadas a API lanzan excepción. 2. Lee **[docs/TOOL-INDEX.md](docs/TOOL-INDEX.md)** — es el catálogo canónico de todo lo ejecutable: firma real de cada script, variables de entorno que requiere, y si es solo-lectura o mutante. **Su §1 lista seis gotchas que producen resultados silenciosamente incorrectos.** No los adivines. 3. Para el estado actual del sistema (qué existe, qué versión corre): [docs/proxmox-inventory.md](docs/proxmox-inventory.md). Los seis gotchas, en una línea cada uno (detalle en el índice): - **`-Raw` está invertido entre wrappers.** En Coolify y Cloudflare, sin `-Raw` recibes un *string*, no objetos: filtrar da vacío sin error. - **`Invoke-ProxmoxSsh.ps1` corrompe comillas anidadas.** Para comandos con más de un nivel de comillas, codifica en base64. - **Los nombres de contenedor llevan sufijo uuid y cambian en cada redeploy.** Resuélvelos siempre; nunca los escribas a mano. - **`/applications/*` de la API de Coolify da 404 por el hostname público (Cloudflare), no por Coolify.** Contra el origen (`http://192.168.0.117:8000/api/v1`, `$env:COOLIFY_API_URL_ORIGIN`) la API completa responde; y desde v4.2 los endpoints de estado exigen POST. - **"Verde en Coolify" no es "enrutado en Traefik".** La UI mira `running`; Traefik exige `healthy`. Un servicio nuevo aún arrancando da `503 no available server` sin que nada esté mal configurado. - **Un `502` casi siempre es el puerto.** Coolify saca el puerto de Traefik del `:puerto` del FQDN guardado; sin él cae al `EXPOSE` de la imagen. Si no coinciden, `connection refused`. ## Qué es este repo **No es el código de una aplicación.** Es un toolkit de operaciones: wrappers de PowerShell más documentación de contexto y runbooks que permiten a un agente diagnosticar y administrar un homelab — un host Proxmox local (`192.168.0.200`, nodo `thinkcentre`) y el stack Coolify que corre dentro de su LXC `102`. No hay build, lint ni tests: los "comandos" son los scripts operativos. **Convención de idioma:** la documentación (`docs/`, `README.md`, este archivo) está en español. Los scripts y los `SKILL.md`/`TOOLS.md` de cada skill están en inglés. ## Router de intención → herramienta Lo que pide el usuario, y con qué se resuelve. Para la firma completa de cada script, ve al [índice](docs/TOOL-INDEX.md). | El usuario pide… | Usa | |---|---| | "¿está bien el servidor?", "revisa el Proxmox" | `.\scripts\Test-ProxmoxConnection.ps1` y luego `.\scripts\Get-ProxmoxInventory.ps1` | | "inventario de LXC/VMs", "qué hay corriendo" | `.\scripts\Get-ProxmoxInventory.ps1` | | "¿está corriendo X?", "estado de los contenedores" | `.\coolify_skill\scripts\Get-CoolifyDockerStatus.ps1 -Filter ` | | "logs de X" | `.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 102 -- docker logs --tail 100"` | | "qué apps hay en Coolify", "dame el uuid de X" | `.\coolify_skill\scripts\Invoke-CoolifyApi.ps1 -Path "/resources" -Raw` | | cualquier cosa de la API de Coolify | `.\coolify_skill\scripts\Invoke-CoolifyApi.ps1` — pero revisa primero qué endpoints viven (§1.4 del índice) | | "¿está online el sitio X?" | `.\deploy_skill\scripts\Test-ServiceOnline.ps1 -Fqdn https://x.urieljareth.org` | | "el servicio nuevo está en verde pero da 503 / `no available server`" | `.\coolify_skill\scripts\Test-CoolifyServiceReady.ps1 -Uuid -WaitSeconds 600` — **espera, no redeployes**; ver §1.5 del índice | | "arregla el túnel de Cloudflare", rutas/DNS | `.\scripts\Invoke-CloudflareApi.ps1` + [docs/runbooks/cloudflare-tunnel.md](docs/runbooks/cloudflare-tunnel.md) | | "Chatwoot perdió el enterprise" | diagnostica con `.\scripts\Get-ChatwootLicenseStatus.ps1 -Deep`; repara con `Apply-ChatwootEnterprisePatch.ps1 -ReenableAccountFeatures` | | "que arranque solo tras un apagón" | `.\scripts\Install-CoolifyAutostart.ps1 -VerifyOnly` primero | | "publica este proyecto en Coolify" | **lee §4 del índice antes**. Stack compose → `New-CoolifyService.ps1` (arreglado y verificado el 2026-08-24); app git → flujo UI con Playwright | | "una app de Coolify sigue el `main` de upstream y se rompió" | [docs/casos/firecrawl-stack-minimo.md](docs/casos/firecrawl-stack-minimo.md) — cambiar a imágenes precompiladas y compose propio | | "valida que este proyecto se puede deployar" | `.\deploy_skill\scripts\Test-PreDeployChecklist.ps1 -Path -Strict` | | "estoy construyendo una app para este Coolify" | [docs/AGENTS-coolify-apps.md](docs/AGENTS-coolify-apps.md) (contrato de red, puertos, dominios, volúmenes) | | "crea/lista un repo en Gitea", "sube esto a Gitea" | `.\gitea_skill\scripts\` — ver §5 del índice | | "diagnosticar/iniciar Hermes (LXC 100)", "MiniMax-M3" | `.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct status 100"` + [docs/casos/hermes-minimax-m3-setup.md](docs/casos/hermes-minimax-m3-setup.md) | | "un contenedor no conecta a su base de datos" | ver "Gotcha de red Docker" abajo | ## Arquitectura **Todo llega al host por un único camino SSH.** No hay acceso directo a Docker ni a la red interna: ``` script PowerShell → Invoke-ProxmoxSshCommand (scripts/ProxmoxAgent.ps1) → ssh root@192.168.0.200 → pct exec 102 -- docker ... (cualquier trabajo de Docker/Coolify) ``` - [scripts/ProxmoxAgent.ps1](scripts/ProxmoxAgent.ps1) es la librería compartida. **Hazle dot-source** (`. .\scripts\ProxmoxAgent.ps1`) para obtener `Get-ProxmoxConfig`, `Invoke-ProxmoxSshCommand` e `Invoke-ProxmoxApi`. Todos los demás scripts la consumen en lugar de reimplementar la conexión. - **Resolución de configuración:** `Get-ProxmoxConfig` lee variables de entorno (`PROXMOX_HOST`, `PROXMOX_NODE`, `PROXMOX_SSH_KEY`, `PROXMOX_COOLIFY_LXC`…) y cae a los defaults locales hardcodeados. SSH funciona solo con los defaults; **las llamadas a la API exigen `PROXMOX_API_TOKEN_ID` + `PROXMOX_API_TOKEN_SECRET`** (si faltan, `Invoke-ProxmoxApi` lanza). El ID del LXC de Coolify (`102`) viene de la config: usa `$config.CoolifyLxc`, no lo hardcodees en scripts nuevos. - **Cuatro APIs independientes**, con scripts y variables distintas: Proxmox REST (`curl.exe -k`, header de token PVE), Coolify (`Invoke-RestMethod`, Bearer, base `https://coolify.urieljareth.org/api/v1`), Cloudflare y Gitea. - Docker **no** se administra en el host Proxmox: vive dentro del LXC `102`. Todo comando de contenedor va envuelto en `pct exec 102 -- docker ...`. ## Gotcha de red Docker Cuando una app de Coolify y su base de datos son contenedores hermanos en la misma red Docker, la app debe alcanzar la DB **por su nombre de servicio Docker, no por `localhost`** (Nextcloud, por ejemplo, usa el host `nextcloud-db`). El DNS entre servicios es la causa raíz habitual de los fallos de "conexión a la base de datos" aquí. Compruébalo con: ```powershell .\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 102 -- docker exec getent hosts " ``` ## Reglas de operación (las imponen las skills — cúmplelas) - **Solo-lectura primero.** El default es diagnosticar: list, status, logs, inspect, health checks. - **Confirma antes de cualquier cambio de estado.** Pregunta explícitamente antes de: `pct`/`qm` start/stop/reboot/destroy; `docker` restart/stop/rm/compose up-down; deploys de Coolify o cualquier `POST`/`PUT`/`PATCH`/`DELETE`; escritura de variables de entorno; y todo cambio de firewall, red, storage, volumen, clave o token. Para acciones riesgosas: captura el estado actual y enuncia primero el camino de rollback. - **Nunca escribas secretos en el repo.** Ni tokens, passwords, claves privadas, cookies o secretos de token PVE — no en Markdown, no en scripts, no en logs. Viven solo en `.env.local.ps1` (gitignored) o en el almacén del SO. `.gitignore` bloquea además `ACCESS.md`, `*.key`, `*.pem`, `*.crt`. Al depurar bases de datos, verifica conectividad sin imprimir credenciales. - **Prefiere los scripts del repo** antes que cadenas de comandos manuales largas, y no ejecutes comandos destructivos amplios construidos desde strings generados. ## Chatwoot: el parche enterprise se degrada solo — pero un guard lo auto-repara El parche no se pierde al actualizar. `Internal::CheckNewVersionsJob` hace ping diario a `hub.2.chatwoot.com` (a los `MD5(INSTALLATION_IDENTIFIER).hex % 1440` minutos pasada la medianoche UTC = **16:16 UTC** en esta instalación) y reescribe `INSTALLATION_PRICING_PLAN` con la respuesta del hub; después `ReconcilePlanConfigService` apaga los 9 feature flags premium en **todas** las cuentas. De ahí tres consecuencias: - **El SQL de 3 filas no basta.** Los flags por cuenta viven en `accounts.feature_flags` (bitmask). Usa `.\scripts\Apply-ChatwootEnterprisePatch.ps1 -ReenableAccountFeatures`; comprueba el estado con `.\scripts\Get-ChatwootLicenseStatus.ps1 -Deep`. - **Los `UPDATE` por SQL no invalidan la caché Redis de `GlobalConfig`** (TTL de 1 día, `V1:GLOBAL_CONFIG:*`) porque se saltan el `after_commit :clear_cache` de `InstallationConfig`. Cualquier parche por SQL debe llamar además a `GlobalConfig.clear_cache`. - **No bloquees `hub.2.chatwoot.com`.** Ese mismo host relaya el push móvil (`ChatwootHub.send_push`), activo aquí porque `FIREBASE_*` está vacío. En su lugar, [scripts/chatwoot-enterprise-guard.sh](scripts/chatwoot-enterprise-guard.sh) corre en el host Proxmox desde `/root/scripts/`, agendado por `/etc/cron.d/chatwoot-enterprise-guard` cada 5 minutos, y repara un revert detectado en ~13s (log: `/var/log/chatwoot-enterprise-guard.log`, solo escribe cuando actúa). Nunca pulses `Refresh` en `/super_admin/settings`. > **Estado al 2026-08-07:** corre **`chatwoot/chatwoot:v4.16.2`** (el pin > documentado antes era `v4.16.1`, así que el pin no sostuvo la versión). El plan > está en `enterprise` y una ejecución manual del guard pasa correctamente, pero > el log registra `ERROR: no se pudo leer INSTALLATION_PRICING_PLAN` durante la > ventana del update. El guard tiene el nombre del contenedor hardcodeado > (`postgres-c11xzy2tx2cdapm32f5b89vy`), así que **un redeploy que cambie el > sufijo lo deja ciego**. Análisis completo, registro de ejecución y rollback: > [docs/runbooks/chatwoot-update.md](docs/runbooks/chatwoot-update.md). ## Túnel de Cloudflare El túnel es **gestionado desde el dashboard** (el ingress baja del edge — se ve como `INF Updated to new configuration version=N` en los logs de `cloudflared`). Corrige rutas en el dashboard o vía [scripts/Invoke-CloudflareApi.ps1](scripts/Invoke-CloudflareApi.ps1), **nunca** editando archivos en el host. Los puertos 6001/6002 deben usar `http://`. Ver [docs/runbooks/cloudflare-tunnel.md](docs/runbooks/cloudflare-tunnel.md). ## Dónde vive el contexto | Qué | Dónde | |---|---| | **Catálogo de herramientas + gotchas** | **[docs/TOOL-INDEX.md](docs/TOOL-INDEX.md)** | | Estado verificado del sistema | [docs/proxmox-inventory.md](docs/proxmox-inventory.md) | | Procedimientos vigentes | [docs/runbooks/](docs/runbooks/) — `conexion.md`, `diagnostico.md`, `coolify-docker.md`, `seguridad.md`, `cloudflare-tunnel.md`, `autostart-coolify.md`, `nextcloud.md`, `baserow.md`, `chatwoot-update.md` | | Casos resueltos paso a paso | [docs/casos/](docs/casos/) — `chatwoot-enterprise-patch.md`, `hermes-minimax-m3-setup.md`, `coolify-servicio-nuevo-503-no-available-server.md`, `firecrawl-stack-minimo.md` | | Incidentes archivados (**no fuente de verdad**) | [docs/incidentes/](docs/incidentes/) | | Contrato para *construir* una app deployable | [docs/AGENTS-coolify-apps.md](docs/AGENTS-coolify-apps.md) | | Reglas operativas por dominio | [agent/SKILL.md](agent/SKILL.md), [coolify_skill/SKILL.md](coolify_skill/SKILL.md), [deploy_skill/SKILL.md](deploy_skill/SKILL.md), [gitea_skill/SKILL.md](gitea_skill/SKILL.md) | | Ejemplos ejecutables por dominio | el `TOOLS.md` de cada carpeta `*_skill/` | | Referencia de la API de Coolify | `coolify_skill/references/ops/*.md` — **busca con `rg`, no cargues el árbol** | Para la referencia de la API de Coolify: no leas el árbol completo. Busca en `coolify_skill/references/` con `rg` y abre el único `references/ops/*.md` que coincida.