diff --git a/README.md b/README.md index 16ad2e2..21dba87 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,8 @@ bun run start - **Ejecutar comandos** — terminal inline (`docker exec`) desde el DetailPanel con output, sin abrir SSH ni terminal externa - **Toast de errores** — cuando una accion falla (rebuild que rompe, exec con exit code != 0, etc.) aparece un toast top-right con el error completo, copiable al clipboard - **Control de acceso por path** — variable `ALLOWED_PATHS` permite restringir acciones a containers cuyo compose file este bajo rutas especificas. Ideal para servidores compartidos: ves todo, solo tocas lo tuyo. Los containers fuera de las rutas aparecen con candado +- **Recomendaciones de configuracion Docker** — banners de aviso en el DetailPanel cuando un container tiene config sub-optima: sin limite de memoria, sin limite de CPU, sin restart policy (`unless-stopped` recomendado). Ayuda al usuario a adoptar mejores practicas de Docker sin tener que recordarlas +- **Volumenes y mounts** — DetailPanel lista cada mount del container: tipo (bind / volume / tmpfs), source en el host, destination en el container, modo rw/ro. Util para debugging ("donde estan mis datos?", "es read-only?", "es persistente?") - **Filtro de proyectos** — dropdown para mostrar/ocultar proyectos, persiste entre sesiones - **Autenticacion** — pantalla de login con AUTH_TOKEN para acceso remoto seguro - **Leyenda de conexiones** — colores por tipo: Database (azul), Cache (rojo), Broker (naranja), Proxy (verde) @@ -109,6 +111,54 @@ bun run start - **Umbrales por contenedor** — overrides personalizados de CPU/MEM (con fallback a umbrales globales) y toggle de notificaciones por servicio - **Pagina de settings** — configuracion de la aplicacion (auth, Discord, hosts Docker) +## Mejores prácticas + +ContainerFlow no solo monitorea: detecta configuración sub-óptima de Docker y la marca con un banner ámbar en el DetailPanel del container afectado. La idea es ayudarte a adoptar buenas prácticas sin tener que recordarlas tú. + +### Recomendaciones activas (warnings automáticos) + +| Detección | Por qué importa | Cómo se ve en ContainerFlow | +|---|---|---| +| **Sin `memory_limit`** | Un container sin tope de RAM puede acaparar toda la memoria del host y tumbar a los demás (incluido el daemon). El kernel hace OOM kill aleatorio bajo presión. | Banner: "Sin límite de memoria configurado en Docker" | +| **Sin `cpu_quota`** | Similar al de memoria — un container puede saturar todos los núcleos. En multi-tenant esto es crítico, en single-tenant degrada la responsividad del host. | Banner: "Sin límite de CPU configurado en Docker" | +| **`restart: no` o vacío** | Si el proceso muere, el container queda muerto. En producción casi siempre quieres `unless-stopped` (reinicia si crashea, **NO** si lo paraste manualmente). | Banner: "Restart policy: none — el contenedor no se reiniciará automáticamente si se detiene" | + +### Configuración recomendada (template) + +```yaml +# docker-compose.yml — buenas prácticas +services: + mi-app: + image: mi-app:latest + restart: unless-stopped # ← reinicia tras crashes, respeta stops manuales + deploy: + resources: + limits: + cpus: "0.5" # ← máximo medio núcleo + memory: 256M # ← tope absoluto, evita OOM del host + healthcheck: # ← detecta apps "vivas pero rotas" + test: ["CMD", "curl", "-f", "http://localhost:8000/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s +``` + +### Por qué ContainerFlow hace esto + +La mayoría de tutoriales de Docker no mencionan estas configuraciones porque "funciona sin ellas". Pero en producción son la diferencia entre: + +- **Sin límites**: un memory leak en un servicio tumba a TODO el servidor +- **Con límites**: el container se mata a sí mismo, el resto sigue vivo, las restart policies lo reviven + +ContainerFlow te lo recuerda visualmente cada vez que abres el DetailPanel — no es spam, es contexto educativo solo donde aplica. + +### En roadmap + +- **Healthcheck recommendations**: detectar containers sin `HEALTHCHECK` y sugerir uno contextual según la imagen (postgres → `pg_isready`, redis → `redis-cli ping`, http app → `curl /health`, etc.) +- **Mounts no persistentes**: warning cuando una DB usa `tmpfs` o bind a directorio efímero +- **Versión latest**: warning cuando un container usa `image:latest` (no reproducible) + ## Monitoreo e historial ContainerFlow guarda un historial de métricas y notifica eventos importantes a Discord. diff --git a/monitoreo.md b/monitoreo.md index d524a11..53fcfef 100644 --- a/monitoreo.md +++ b/monitoreo.md @@ -1,34 +1,46 @@ # ContainerFlow — Roadmap de Monitoreo -## Fase 1: Acciones básicas -- [ ] Stop / Start / Restart desde el DetailPanel -- [ ] Confirmación antes de ejecutar acciones destructivas (stop/restart) -- [ ] Feedback visual del estado de la acción (loading, success, error) -- [ ] Rebuild (docker compose up --build) por servicio +> Actualizado tras integración de eventos persistentes, notificaciones in-app, +> control de acceso por path y corrección de cálculo de memoria real. -## Fase 2: Notificaciones -- [ ] Webhooks configurables (Discord, Slack) -- [ ] PWA — Service Worker + manifest -- [ ] Push notifications cuando un contenedor cae o health check falla -- [ ] Panel de configuración de notificaciones en la UI +## Fase 1: Acciones básicas ✅ COMPLETA +- [x] Stop / Start / Restart desde el DetailPanel +- [x] Confirmación antes de ejecutar acciones destructivas (stop/restart/remove/rebuild/recreate) +- [x] Feedback visual del estado de la acción (loading, success, error) — toast top-right con copy/expand +- [x] Rebuild (docker compose up --build) por servicio +- [x] **Bonus**: Recreate (docker compose up --force-recreate) para aplicar cambios de compose sin rebuild -## Fase 3: Historial y métricas -- [ ] SQLite para persistir stats (CPU, RAM, network I/O) -- [ ] Gráficas temporales de CPU/RAM por servicio (últimas 1h, 6h, 24h, 7d) -- [ ] Dashboard de métricas agregadas -- [ ] Retención configurable (auto-limpiar datos viejos) +## Fase 2: Notificaciones ✅ MAYORMENTE COMPLETA +- [x] Webhooks configurables — **Discord implementado**, Slack pendiente (mismo patrón aplicaría) +- [ ] PWA — Service Worker + manifest (no implementado, depende de habilitar PWA web notifications) +- [x] Push notifications cuando un contenedor cae o health check falla — vía Discord + in-app +- [x] Panel de configuración de notificaciones en la UI (SettingsPage → Discord section) +- [x] **Bonus**: Notificaciones in-app (SQLite persistente, bell del header con read/unread, tab dedicado en Monitoring) +- [x] **Bonus**: Debounce de 15s para detectar restart vs stop+start +- [x] **Bonus**: Cooldown configurable + "Still Down" reminder -## Fase 4: Alertas -- [ ] Reglas de alertas (ej: CPU > 80% por 5 min) -- [ ] Historial de alertas disparadas -- [ ] Integración con notificaciones (Fase 2) -- [ ] Alertas por health check fallido +## Fase 3: Historial y métricas ✅ COMPLETA +- [x] SQLite para persistir stats — CPU/RAM (network I/O pendiente) +- [x] Gráficas temporales de CPU/RAM por servicio (1h, 6h, 24h, 7d) con buckets agregados +- [x] Dashboard de métricas agregadas — MonitoringTotalsCard (CPU/MEM total por filtro) +- [x] Retención configurable — 7d default, auto-limpieza horaria + VACUUM +- [x] **Bonus**: Filtro per-servicio en monitoring (override del range global) +- [x] **Bonus**: Filtros por proyecto/servicio con botón reset +- [x] **Bonus**: Cálculo de memoria real (resta page cache active+inactive, no solo inactive como `docker stats`) + +## Fase 4: Alertas ✅ MAYORMENTE COMPLETA +- [ ] Reglas de alertas con duración (ej: CPU > 80% **por 5 min**) — parcialmente: alertas inmediatas al cruzar threshold, no soporte de duración sostenida +- [x] Historial de alertas disparadas — notifications_log en SQLite, tab Notificaciones +- [x] Integración con notificaciones (Fase 2) — Discord + in-app +- [x] Alertas por health check fallido — vía `health_status` event +- [x] **Bonus**: Thresholds por contenedor con override del global (CPU% y MEM%) +- [x] **Bonus**: Color amber en progress bars cuando supera threshold (dashboard ServiceNode + DetailPanel) ## Fase 5: Info avanzada -- [ ] Volumes/Mounts por contenedor -- [ ] Terminal interactiva (docker exec) desde la UI -- [ ] Network inspector (tráfico entre servicios) -- [ ] Image layers y tamaño +- [ ] Volumes/Mounts por contenedor en DetailPanel (no implementado) +- [x] Terminal interactiva (docker exec) desde la UI — botón Exec en DetailPanel +- [ ] Network inspector (tráfico entre servicios) — parcial: conexiones visualizadas en grafo, no hay info de tráfico +- [ ] Image layers y tamaño (no implementado) ## Fase 6: Deployment (futuro) - [ ] GitHub integration (webhook + clone + build) @@ -36,4 +48,53 @@ - [ ] Deploy management (docker-compose dinámico) - [ ] Domain routing automático (Traefik/Caddy) - [ ] Rollbacks (mantener imágenes anteriores) -- [ ] Env var editing + rebuild desde la UI +- [ ] Env var editing + rebuild desde la UI — parcial: env vars visibles en DetailPanel, edición pendiente + +--- + +## Features adicionales no contempladas en el roadmap original + +### Seguridad / Multi-usuario +- [x] `ALLOWED_PATHS` env var para limitar acciones a paths específicos +- [x] `ALLOW_NON_COMPOSE` para containers `docker run` directo +- [x] Lock icon visual en nodos sin acceso de acción +- [x] Sección "Seguridad" expandida en README (privilegios del container, multi-user, ALLOWED_PATHS) + +### UX / DX +- [x] Tooltips descriptivos en acciones (DetailPanel + NodeContextMenu) +- [x] Confirmación dialogs con (?) que muestra detalle del comando +- [x] Acción labels siempre en inglés (Restart/Stop/Recreate — match docker commands) +- [x] Hot stats al cargar página (`/api/init` devuelve último snapshot cacheado) +- [x] Polling paralelo (Promise.all) — primera ronda en ~3s vs ~90s antes +- [x] Tabs en Monitoring (Historial / Eventos / Notificaciones) +- [x] Click en notificación/evento → abre DetailPanel del container en tab Stats +- [x] DATA_DIR auto-creado en `./data/` (en vez de raíz del proyecto) + +### Documentación +- [x] `docker-containerflow.md` — guía Docker explicada para usuarios +- [x] `.env.example` con secciones documentadas +- [x] README con sección Monitoreo e historial + Seguridad expandida + +--- + +## Pendientes prioritarios (próxima iteración sugerida) + +1. **Alertas con duración** ("CPU > 80% por 5 min") — Fase 4 pendiente + - Requiere ventana deslizante de samples + state machine por threshold +2. ~~Volumes/Mounts en DetailPanel~~ — ✅ HECHO +3. **Recomendaciones de healthcheck** — Nueva fase, "value add" para usuarios + - Detecta containers sin healthcheck (`service.health_status === ""`) + - Sugiere healthcheck contextual según la imagen (postgres → pg_isready, redis → redis-cli ping, etc.) + - Banner/widget en dashboard: "N containers sin healthcheck" + - Tooltip en DetailPanel con ejemplo copiable + - Opción de notificación diaria de resumen ("Tienes X containers sin healthcheck") +4. **Network I/O en stats history** — extiende `pollStats` con `networks.eth0.rx_bytes/tx_bytes` +5. **PWA + Service Worker** — para web push notifications cuando la pestaña no está abierta +6. **Image layers y tamaño** — vía `docker image inspect` + UI en DetailPanel + +## Cosas que cambiaron sustancialmente desde el roadmap original + +- **"Notificaciones"** evolucionó de "solo Discord" a "Discord + in-app + persistente + read/unread" +- **"Historial"** evolucionó de "CPU/RAM por servicio" a "+ totales agregados + per-service override + reset filtros" +- **Acciones** evolucionó de "stop/start/restart/rebuild" a "+ recreate + exec + remove con confirmación" +- **Seguridad** se volvió un eje propio (ALLOWED_PATHS) que no estaba en el roadmap diff --git a/src/client/hooks/processing.test.ts b/src/client/hooks/processing.test.ts index 1ba079a..19c5a35 100644 --- a/src/client/hooks/processing.test.ts +++ b/src/client/hooks/processing.test.ts @@ -24,6 +24,7 @@ function makeSvc(overrides: Partial = {}): Service { exit_code: 0, restart_count: 0, oom_killed: false, + mounts: [], ...overrides, }; } diff --git a/src/client/i18n.tsx b/src/client/i18n.tsx index ca52104..e04a8bc 100644 --- a/src/client/i18n.tsx +++ b/src/client/i18n.tsx @@ -141,6 +141,7 @@ const en = { "detail.memory": "Memory", "detail.noStats": "No stats available", "detail.cpuHistory": "Usage History", + "detail.mounts": "Volumes & Mounts", "detail.memoryHistory": "Memory History", "detail.noHistory": "No historical data available", "detail.loadingHistory": "Loading history...", @@ -399,6 +400,7 @@ const es: Record = { "detail.memory": "Memoria", "detail.noStats": "No hay estad\u00edsticas disponibles", "detail.cpuHistory": "Historial de Consumo", + "detail.mounts": "Volúmenes y Mounts", "detail.memoryHistory": "Historial de Memoria", "detail.noHistory": "No hay datos hist\u00f3ricos disponibles", "detail.loadingHistory": "Cargando historial...", diff --git a/src/client/panels/DetailPanel.tsx b/src/client/panels/DetailPanel.tsx index a89c451..3e7e6e0 100644 --- a/src/client/panels/DetailPanel.tsx +++ b/src/client/panels/DetailPanel.tsx @@ -723,6 +723,39 @@ export function DetailPanel({ service, stats, logLines, token, closing, locked, )} + {/* Volumes / Mounts */} + {service.mounts && service.mounts.length > 0 && ( +
+ {t("detail.mounts")} +
+ {service.mounts.map((m, i) => { + const typeColor = m.type === "volume" ? "text-emerald-400 bg-emerald-500/10" + : m.type === "bind" ? "text-cyan-400 bg-cyan-500/10" + : "text-amber-400 bg-amber-500/10"; + return ( +
+
+ {m.type} + {m.destination} + + {m.rw ? "rw" : "ro"} + +
+ {m.name && ( +
+ name: {m.name} +
+ )} +
+ {m.source} +
+
+ ); + })} +
+
+ )} + {/* Connected services */} {connectedSvcs.length > 0 && (
diff --git a/src/server/docker.test.ts b/src/server/docker.test.ts index 6293a2a..e5edac6 100644 --- a/src/server/docker.test.ts +++ b/src/server/docker.test.ts @@ -24,6 +24,7 @@ function makeSvc(overrides: Partial = {}): Service { exit_code: 0, restart_count: 0, oom_killed: false, + mounts: [], ...overrides, }; } diff --git a/src/server/docker.ts b/src/server/docker.ts index f8ad3ff..bdd0769 100644 --- a/src/server/docker.ts +++ b/src/server/docker.ts @@ -82,6 +82,15 @@ export async function discoverServices(all: boolean, projects: string[]): Promis exit_code: exitCode, restart_count: restartCount, oom_killed: oomKilled, + mounts: (info?.Mounts || []).map((m: any) => ({ + type: m.Type || "bind", + source: m.Source || "", + destination: m.Destination || "", + name: m.Name || undefined, + driver: m.Driver || undefined, + mode: m.Mode || "", + rw: m.RW !== false, + })), }; }); diff --git a/src/shared/types.ts b/src/shared/types.ts index 094e59b..b6ff55f 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -1,3 +1,20 @@ +export interface ContainerMount { + /** "bind" = host directory · "volume" = named docker volume · "tmpfs" = in-memory */ + type: "bind" | "volume" | "tmpfs" | string; + /** Path on the host (or volume backend path) */ + source: string; + /** Path inside the container */ + destination: string; + /** Volume name (only for type=volume) */ + name?: string; + /** Volume driver (only for type=volume) */ + driver?: string; + /** Mode string, e.g. "rw", "ro", "rprivate" */ + mode: string; + /** true = read-write, false = read-only */ + rw: boolean; +} + export interface Service { id: string; uid: string; @@ -19,6 +36,7 @@ export interface Service { exit_code: number; restart_count: number; oom_killed: boolean; + mounts: ContainerMount[]; } export interface Connection {