diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index dda1aee..9e3bbd3 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -36,7 +36,7 @@ assignees: '' ## Are you willing to wait? -ContainerFlow is currently maintainer-driven (no external PRs accepted yet). Features will land based on roadmap priority. See [monitoreo.md](../../monitoreo.md) for what's planned. +ContainerFlow is currently maintainer-driven (no external PRs accepted yet). Features will land based on roadmap priority. See [roadmap](../../docs/roadmap.md) for what's planned. - [ ] Yes, I'll wait — I just want to flag this - [ ] I'd contribute a PR if/when PRs open diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml.disabled similarity index 100% rename from .github/workflows/ci.yml rename to .github/workflows/ci.yml.disabled diff --git a/.gitignore b/.gitignore index c2e0e86..36dd034 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,8 @@ data/ .dockerflow-*.db .dockerflow-*.db-wal .dockerflow-*.db-shm + +# Local notes / marketing — not part of the public repo +linkdin.md +# AI assistant context — internal, not for public repo +CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 4d88423..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,40 +0,0 @@ -# Dockerflow (ContainerFlow) - -## Nomenclatura UI - -- **Nodo** — tarjeta de servicio en el canvas (`ServiceNode.tsx`) -- **Panel** — panel lateral izquierdo con detalles del servicio (`DetailPanel.tsx`) -- **Canvas** — mesa de trabajo donde se ven los nodos y conexiones (ReactFlow) - -## Stack - -- **Frontend:** React + ReactFlow + Tailwind CSS -- **Backend:** Hono + Bun -- **Docker:** dockerode para comunicacion con Docker API - -## Estructura - -- `src/client/` — frontend React - - `nodes/` — componentes de nodos (ServiceNode, GroupNode) - - `panels/` — paneles (DetailPanel) - - `hooks/` — hooks (useDocker) - - `engine/` — layout - - `components/` — componentes generales -- `src/server/` — backend Hono - - `index.ts` — servidor principal, WebSocket, API REST - - `docker.ts` — interaccion con Docker - - `watcher.ts` — polling de stats y eventos -- `src/shared/` — tipos compartidos - -## Comandos - -- `bun run dev` — desarrollo (servidor + cliente) -- `bun run build` — build de produccion - -## i18n (Internacionalizacion) - -- Todo texto visible en la UI debe usar el sistema de traducciones (`useT()` hook de `src/client/i18n.tsx`) -- Al agregar texto nuevo, agregar la key en ambos diccionarios (en + es) en `i18n.tsx` -- Keys usan formato `seccion.descripcion` (ej. `"settings.save"`, `"actions.restart"`) -- Nunca hardcodear strings de UI directamente en JSX -- El idioma se persiste en `localStorage("df:lang")`, default `"en"` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8db54ad..9eefb6a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -41,7 +41,7 @@ Use the [feature request template](.github/ISSUE_TEMPLATE/feature_request.md). T - Why existing functionality doesn't work - A rough sketch of how you'd want it to work in the UI -We prioritize features that align with the [roadmap](monitoreo.md). +We prioritize features that align with the [roadmap](docs/roadmap.md). ## Reporting security vulnerabilities diff --git a/PUBLICAR.md b/PUBLICAR.md deleted file mode 100644 index 17e4362..0000000 --- a/PUBLICAR.md +++ /dev/null @@ -1,350 +0,0 @@ -# Roadmap para publicar DockerFlow como Open Source - -Estado actual del proyecto: **v0.1.0** | ~1,774 lineas de codigo | 0 tests | 0 CI/CD | Sin licencia formal - ---- - -## Fase 1 — Fundamentos legales y limpieza - -> Sin esto, nadie puede usar tu codigo legalmente ni contribuir con confianza. - -### 1.1 Crear archivo LICENSE - -- [ ] Crear `LICENSE` en la raiz con el texto completo de MIT -- [ ] El README ya dice "MIT" al final, pero sin el archivo no tiene validez legal -- [ ] Opciones alternativas si cambias de opinion: - - **MIT** — maxima adopcion, cualquiera puede hacer lo que quiera - - **Apache 2.0** — como MIT pero protege contra demandas de patentes - - **GPL v3** — obliga a que los forks tambien sean open source - -### 1.2 Auditar secretos en el historial de git - -- [ ] Verificar que `.env` nunca fue commiteado (HECHO: confirmado limpio) -- [ ] Verificar que no hay tokens, passwords o claves hardcodeados en el codigo -- [ ] Buscar en el historial: `git log -p --all -S 'AUTH_TOKEN' -- '*.ts'` -- [ ] Buscar en el historial: `git log -p --all -S 'password' -- '*.ts'` -- [ ] Si se encuentra algo comprometido, considerar `git filter-branch` o `bfg` para limpiar - -### 1.3 Eliminar archivos internos del repo publico - -- [ ] Eliminar `tareas/completadas/` — son notas internas de desarrollo, no aportan al usuario final -- [ ] Eliminar `docker-project.md` — documento de planificacion interna -- [ ] Decidir sobre `PLAN-MULTI-HOST.md` — puede quedarse como roadmap publico o moverse a GitHub Issues/Projects -- [ ] Eliminar `.claude/settings.local.json` si contiene paths locales -- [ ] Actualizar `.gitignore` para excluir `tareas/` y documentos internos futuros - -### 1.4 Limpiar configuracion local - -- [ ] Verificar que `.dockerflow-positions.json` esta en `.gitignore` (esta) -- [ ] Verificar que `.env` esta en `.gitignore` (esta) -- [ ] Agregar a `.gitignore`: `PUBLICAR.md`, `tareas/`, `docker-project.md` - ---- - -## Fase 2 — Documentacion para la comunidad - -> La documentacion es la primera impresion. Un proyecto sin docs claras no recibe contribuciones. - -### 2.1 README en ingles (idioma principal) - -- [ ] Crear `README.md` en ingles como version principal -- [ ] Mover el README actual a `README.es.md` y linkear desde el principal -- [ ] Incluir en el README: - - [ ] **Hero section**: nombre, descripcion de una linea, badges (license, version, bun) - - [ ] **Screenshot/GIF** del dashboard funcionando (esto es CRITICO para adopcion) - - [ ] **Quick start** en 4 lineas o menos - - [ ] **Features** con iconos o emojis descriptivos - - [ ] **Configuration** (tabla de env vars) - - [ ] **Container actions** (start, stop, restart, rebuild, remove) - - [ ] **MCP integration** (esto es diferenciador, destacarlo) - - [ ] **Tech stack** (tabla limpia) - - [ ] **Contributing** link - - [ ] **License** badge + link - -### 2.2 Captura de pantalla / GIF del dashboard - -- [ ] Levantar el dashboard con containers de ejemplo -- [ ] Grabar un GIF de ~10 segundos mostrando: - - Vista general con servicios conectados - - Metricas en tiempo real (CPU/MEM) - - Acciones sobre containers -- [ ] Herramientas recomendadas: `peek` (Linux), `gifski`, o `Kap` (macOS) -- [ ] Guardar en `docs/assets/demo.gif` y referenciar desde README -- [ ] Alternativa: screenshot estatico como fallback - -### 2.3 CONTRIBUTING.md - -- [ ] Crear `CONTRIBUTING.md` con: - - [ ] Requisitos: Bun >= 1.0, Docker corriendo - - [ ] Setup del entorno de desarrollo (`bun install && bun run dev`) - - [ ] Estructura del proyecto (breve, linkear a README) - - [ ] Convenciones de codigo (TypeScript estricto, sin `any`, imports absolutos) - - [ ] Proceso de PRs: fork → branch → PR con descripcion - - [ ] Issues: como reportar bugs, como proponer features - - [ ] Commits: formato convencional (`feat:`, `fix:`, `docs:`) - -### 2.4 CODE_OF_CONDUCT.md - -- [ ] Adoptar Contributor Covenant v2.1 (estandar de la industria) -- [ ] Copiar de https://www.contributor-covenant.org/ -- [ ] Personalizar email de contacto - -### 2.5 CHANGELOG.md - -- [ ] Crear `CHANGELOG.md` siguiendo formato Keep a Changelog -- [ ] Documentar retroactivamente las versiones existentes: - - v0.0.1 — Setup inicial, descubrimiento Docker - - v0.0.2 — WebSocket, metricas en tiempo real - - v0.0.3 — Nodos visuales, layout React Flow - - v0.0.4 — Filtro de proyectos, autenticacion - - v0.0.5 — MCP server, logs, acciones de containers, polish -- [ ] De aqui en adelante, actualizar con cada release - ---- - -## Fase 3 — Calidad de codigo - -> Da confianza a los contribuidores y previene regresiones. - -### 3.1 Configurar linter + formatter - -- [ ] Instalar Biome (rapido, todo-en-uno, compatible con Bun): - ```bash - bun add -d @biomejs/biome - bunx biome init - ``` -- [ ] Configurar reglas en `biome.json`: - - Formatter: tabs/spaces, ancho de linea - - Linter: reglas recomendadas de TypeScript + React - - Organizar imports automaticamente -- [ ] Agregar scripts a `package.json`: - ```json - "lint": "biome check src/", - "lint:fix": "biome check --write src/", - "format": "biome format --write src/" - ``` -- [ ] Ejecutar `bun run lint:fix` una vez para normalizar todo el codigo -- [ ] Commit con mensaje: `chore: configure biome linter and format codebase` - -### 3.2 Agregar tests minimos - -- [ ] Usar `bun:test` (ya viene con Bun, zero config) -- [ ] Tests prioritarios: - - [ ] `src/server/__tests__/docker.test.ts` — parseo de conexiones, deteccion de tipos - - [ ] `src/server/__tests__/watcher.test.ts` — polling de stats, eventos Docker - - [ ] `src/shared/__tests__/types.test.ts` — validacion de tipos con Zod si aplica - - [ ] `src/client/engine/__tests__/layout.test.ts` — calculo de layout basico -- [ ] Agregar script: `"test": "bun test"` -- [ ] Meta inicial: cubrir la logica de negocio del server (docker.ts, watcher.ts) -- [ ] No hace falta 100% coverage, pero si que lo critico este cubierto - -### 3.3 Type checking estricto - -- [ ] Verificar que `bun run build` no genera errores de TypeScript -- [ ] Agregar script: `"typecheck": "tsc --noEmit"` -- [ ] Corregir cualquier error que aparezca - ---- - -## Fase 4 — CI/CD con GitHub Actions - -> Automatiza la verificacion. Cada PR debe pasar lint + tests + build. - -### 4.1 Workflow de CI basico - -- [ ] Crear `.github/workflows/ci.yml`: - ```yaml - name: CI - on: [push, pull_request] - jobs: - check: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: oven-sh/setup-bun@v2 - with: - bun-version: latest - - run: bun install - - run: bun run lint - - run: bun run typecheck - - run: bun run test - - run: bun run build - ``` -- [ ] Verificar que pasa en la primera ejecucion -- [ ] Agregar badge de CI al README - -### 4.2 (Opcional) Release automatizado - -- [ ] Configurar workflow de release al pushear tags: - ```yaml - on: - push: - tags: ['v*'] - ``` -- [ ] Generar GitHub Release con changelog automatico -- [ ] Considerar `changesets` o `release-please` para automatizar versiones - ---- - -## Fase 5 — Distribucion y Docker - -> Facilitar que la gente lo pruebe sin clonar el repo. - -### 5.1 Dockerfile - -- [ ] Crear `Dockerfile` multi-stage: - ```dockerfile - # Build - FROM oven/bun:1 AS builder - WORKDIR /app - COPY package.json bun.lock* ./ - RUN bun install --frozen-lockfile - COPY . . - RUN bun run build - - # Run - FROM oven/bun:1-slim - WORKDIR /app - COPY --from=builder /app/dist ./dist - COPY --from=builder /app/src/server ./src/server - COPY --from=builder /app/node_modules ./node_modules - COPY --from=builder /app/package.json . - EXPOSE 9470 - CMD ["bun", "run", "start"] - ``` -- [ ] Crear `.dockerignore` (node_modules, .git, tareas, etc.) -- [ ] Testear localmente: `docker build -t dockerflow . && docker run -v /var/run/docker.sock:/var/run/docker.sock -p 9470:9470 dockerflow` - -### 5.2 docker-compose.yml de ejemplo - -- [ ] Crear `docker-compose.yml` para que los usuarios levanten con un comando: - ```yaml - services: - dockerflow: - image: ghcr.io/rgjorge/dockerflow:latest - ports: - - "9470:9470" - volumes: - - /var/run/docker.sock:/var/run/docker.sock:ro - environment: - - AUTH_TOKEN=${AUTH_TOKEN:-} - ``` - -### 5.3 Publicar imagen en GitHub Container Registry - -- [ ] Crear workflow `.github/workflows/docker.yml` para build + push automatico -- [ ] Publicar en `ghcr.io/rgjorge/dockerflow` -- [ ] Tags: `latest`, `v0.1.0`, `v0.1`, `v0` -- [ ] Documentar en README el one-liner de Docker - ---- - -## Fase 6 — Preparacion del repositorio - -> Detalles finales antes de hacer el repo publico. - -### 6.1 GitHub repo settings - -- [ ] Descripcion del repo: "Real-time Docker architecture visualization dashboard" -- [ ] Topics: `docker`, `monitoring`, `dashboard`, `visualization`, `devtools`, `bun`, `react`, `mcp` -- [ ] Website: URL del repo o demo si la hay -- [ ] Habilitar Issues -- [ ] Habilitar Discussions (opcional, bueno para comunidad) -- [ ] Configurar branch protection en `main`: - - Require PR reviews - - Require status checks (CI) - - No force push - -### 6.2 Issue templates - -- [ ] Crear `.github/ISSUE_TEMPLATE/bug_report.md` -- [ ] Crear `.github/ISSUE_TEMPLATE/feature_request.md` -- [ ] Crear `.github/PULL_REQUEST_TEMPLATE.md` - -### 6.3 Issues iniciales como roadmap publico - -- [ ] Crear issues con label `good first issue` para atraer contribuidores: - - "Add dark/light theme toggle" - - "Support podman as alternative to Docker" - - "Add container restart/stop actions from UI" - - "Export dashboard as PNG/SVG" -- [ ] Crear issues con label `enhancement` del roadmap: - - "Multi-host Docker monitoring (TCP/TLS)" - - "Container health check visualization" - - "Custom node colors/icons per service type" -- [ ] Convertir `PLAN-MULTI-HOST.md` en un issue detallado - -### 6.4 Crear GitHub Release v0.1.0 - -- [ ] Tag: `v0.1.0` -- [ ] Titulo: "DockerFlow v0.1.0 — Initial Public Release" -- [ ] Body: features principales, screenshot, instrucciones de instalacion -- [ ] Esto reemplaza los tags internos v0.0.x - ---- - -## Fase 7 — Lanzamiento y difusion - -> El codigo listo no sirve si nadie lo ve. - -### 7.1 Preparar assets de lanzamiento - -- [ ] GIF/screenshot de alta calidad del dashboard -- [ ] Descripcion corta (1 parrafo) para copiar/pegar en redes -- [ ] Lista de features destacadas (3-5 bullet points) - -### 7.2 Publicar en comunidades - -- [ ] **Reddit**: r/selfhosted, r/docker, r/devops, r/opensource - - Titulo sugerido: "I built a real-time Docker architecture visualizer with live metrics" - - Incluir GIF y link al repo -- [ ] **Hacker News**: Show HN post - - Titulo: "Show HN: ContainerFlow – Real-time Docker architecture visualization" -- [ ] **Twitter/X**: Thread con GIF y features -- [ ] **Dev.to**: Articulo sobre como se construyo -- [ ] **Discord**: Servidores de Docker, Bun, React -- [ ] **Product Hunt**: Si quieres traccion con publico mas amplio - -### 7.3 Post-lanzamiento - -- [ ] Monitorear issues y PRs las primeras 48-72 horas -- [ ] Responder rapidamente a las primeras contribuciones (esto define la cultura) -- [ ] Agregar un "Star History" badge al README despues de ganar traccion -- [ ] Considerar crear un sitio web/landing page si hay interes - ---- - -## Orden de ejecucion recomendado - -| Prioridad | Tarea | Esfuerzo | Impacto | -|-----------|-------|----------|---------| -| 1 | Licencia MIT | 5 min | Critico | -| 2 | Limpiar archivos internos | 10 min | Alto | -| 3 | Screenshot/GIF del dashboard | 20 min | Critico | -| 4 | README en ingles | 1-2 hrs | Critico | -| 5 | CONTRIBUTING + CODE_OF_CONDUCT | 30 min | Alto | -| 6 | Biome linter + format | 30 min | Medio | -| 7 | Tests minimos (bun:test) | 2-3 hrs | Alto | -| 8 | GitHub Actions CI | 30 min | Alto | -| 9 | Dockerfile + compose | 1 hr | Alto | -| 10 | CHANGELOG retroactivo | 30 min | Medio | -| 11 | Issue templates + good first issues | 30 min | Medio | -| 12 | GitHub Release v0.1.0 | 15 min | Alto | -| 13 | Difusion en comunidades | 1-2 hrs | Critico | - -**Tiempo total estimado: 8-12 horas de trabajo** - ---- - -## Checklist final antes de hacer publico - -- [ ] `LICENSE` existe y es MIT -- [ ] No hay secretos en el codigo ni en el historial de git -- [ ] No hay archivos internos/personales en el repo -- [ ] README en ingles con screenshot/GIF -- [ ] CONTRIBUTING.md existe -- [ ] Al menos 1 test pasa -- [ ] `bun run build` funciona sin errores -- [ ] CI pasa en verde -- [ ] GitHub Release creada -- [ ] Listo para compartir el link diff --git a/README.md b/README.md index 21dba87..2d89d87 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # ContainerFlow -[![CI](https://github.com/RGJorge/containerflow/actions/workflows/ci.yml/badge.svg)](https://github.com/RGJorge/containerflow/actions/workflows/ci.yml) +![Tests](https://img.shields.io/badge/tests-37%20passing-brightgreen) [![License: AGPL-3.0](https://img.shields.io/badge/License-AGPL--3.0-blue.svg)](https://www.gnu.org/licenses/agpl-3.0) -![Version](https://img.shields.io/badge/version-v0.1.0-green) +[![Release](https://img.shields.io/github/v/tag/RGJorge/containerflow?label=version&color=green)](https://github.com/RGJorge/containerflow/tags) ![Docker Required](https://img.shields.io/badge/Docker-required-blue?logo=docker) ![Bun](https://img.shields.io/badge/runtime-Bun-f9f1e1?logo=bun) [![Last Commit](https://img.shields.io/github/last-commit/RGJorge/containerflow)](https://github.com/RGJorge/containerflow/commits/main) @@ -11,9 +11,33 @@ Real-time Docker architecture visualizer. Displays services, connections and met ![ContainerFlow demo](docs/demo.gif) +## Por qué ContainerFlow + +Las herramientas existentes te muestran números. ContainerFlow además: + +- **Visualiza arquitectura** — grafo interactivo con conexiones (app→db, app→cache, proxy→app) detectadas automáticamente, no solo una lista plana +- **Detecta config sub-óptima** — banners cuando un container corre sin límite de memoria, sin límite de CPU, o sin `restart: unless-stopped`. Te enseña buenas prácticas mientras lo usas +- **Mide memoria real** — resta page cache (active + inactive), no solo inactive como `docker stats`. Tu DB con buffers Postgres no muestra 98% falso +- **Multi-usuario seguro** — variable `ALLOWED_PATHS` para servidores compartidos: ves todo, solo tocas lo tuyo +- **80 MB de RAM, startup en 500ms** — Bun + Hono. Pesa una fracción de Portainer y arranca antes que Grafana + +## Quick start + +```bash +git clone https://github.com/RGJorge/containerflow.git +cd containerflow +cp .env.example .env +docker compose up -d +``` + +Abre `http://localhost:9470`. Listo. + +Para desarrollo nativo (hot reload): `bun install && bun run dev`. + ## Documentación -- **[docker-containerflow.md](./docker-containerflow.md)** — Guía rápida de Docker explicado para usar ContainerFlow: qué hace cada acción (Start, Stop, Restart, Recreate, Rebuild, Remove, Exec), restart policies, resource limits, volúmenes, healthchecks y preguntas frecuentes. +- **[docs/docker-guide.md](./docs/docker-guide.md)** — Guía rápida de Docker explicado para usar ContainerFlow: qué hace cada acción (Start, Stop, Restart, Recreate, Rebuild, Remove, Exec), restart policies, resource limits, volúmenes, healthchecks y preguntas frecuentes. +- **[docs/roadmap.md](./docs/roadmap.md)** — Roadmap del proyecto: qué está completo, qué viene, qué se descartó y por qué. ## Requisitos @@ -381,6 +405,18 @@ src/ types.ts — tipos compartidos server/client ``` +## Comunidad y contribuciones + +ContainerFlow está en desarrollo activo (`v0.x`). + +- 🐛 **Bug?** Abre un [issue](https://github.com/RGJorge/containerflow/issues/new?template=bug_report.md) +- 💡 **Idea?** Abre un [feature request](https://github.com/RGJorge/containerflow/issues/new?template=feature_request.md) +- 🔒 **Vulnerabilidad de seguridad?** Reporta privadamente — ver [SECURITY.md](SECURITY.md) +- 📜 **Code of Conduct** — ver [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) +- 🛠 **Quiero contribuir código** — ver [CONTRIBUTING.md](CONTRIBUTING.md). Actualmente solo aceptamos issues; PRs se abrirán cuando el proyecto madure. + +Si ContainerFlow te resulta útil, una ⭐ en GitHub ayuda a la visibilidad del proyecto. + ## Licencia Copyright (C) 2026 Jorge Gonzalez D. (RGJorge) diff --git a/docker-project.md b/docker-project.md deleted file mode 100644 index d9882b3..0000000 --- a/docker-project.md +++ /dev/null @@ -1,944 +0,0 @@ -# Alteonx DockerFlow - -Herramienta open source para visualizar arquitecturas Docker en tiempo real. - -``` -git clone github.com/user/alteonx-dockerflow -cd alteonx-dockerflow -bun install -claude mcp add alteonx-dockerflow -- bun run src/mcp.ts - -# Desde Claude Code: -> "arranca el visualizer" -> "Listo, abre http://localhost:9470" -``` - ---- - -## Stack - -| Capa | Tecnología | Por qué | -|---|---|---| -| **Runtime** | Bun | 3x más rápido que Node, WebSocket nativo, bundler incluido, menos RAM | -| **Server** | Hono | 14KB, ultra rápido, soporte nativo Bun, middleware mínimo | -| **Frontend** | React 19 + @xyflow/react 12 | Librería de grafos más madura, nodos custom, edges custom, minimap | -| **Styling** | Tailwind v4 | Utility-first, tree-shaking agresivo, sin runtime | -| **Real-time** | Bun WebSocket | Nativo en Bun, zero dependencias, más rápido que socket.io | -| **Animaciones** | CSS transitions + keyframes | Sin librerías extra, GPU-accelerated, zero overhead | -| **Docker API** | dockerode | Estándar de facto, tipado, streams | -| **MCP** | @modelcontextprotocol/sdk | SDK oficial, stdio transport | -| **Build** | Vite 6 | HMR instantáneo, tree-shaking, build optimizado | - -### Recursos estimados -| Recurso | Valor | -|---|---| -| **RAM** | ~30-50 MB | -| **CPU** | <1% idle, ~2% durante polling | -| **Disco** | ~80 MB (node_modules), ~2 MB build | -| **Bundle** | ~150 KB gzip | -| **Startup** | <500ms con Bun | - -### Dependencias totales (mínimas) -```json -{ - "dependencies": { - "hono": "^4", - "dockerode": "^4", - "@modelcontextprotocol/sdk": "^1.12", - "zod": "^3", - "yaml": "^2" - }, - "devDependencies": { - "react": "^19", - "react-dom": "^19", - "@xyflow/react": "^12", - "@dagrejs/dagre": "^1", - "@vitejs/plugin-react": "^4", - "tailwindcss": "^4", - "vite": "^6", - "typescript": "^5" - } -} -``` - ---- - -## Arquitectura - -``` -┌──────────────────────────────────────────────────────────────────┐ -│ Docker Socket │ -│ /var/run/docker.sock │ -└──────────┬──────────────────────────────┬───────────────────────┘ - │ │ - ▼ ▼ -┌──────────────────────┐ ┌──────────────────────────┐ -│ Hono Server (:9470)│ │ MCP Server (stdio) │ -│ │ │ │ -│ GET / │ │ Tools: │ -│ → serve SPA │ │ • start_dashboard │ -│ │ │ • list_services │ -│ WS /ws │ │ • get_stats │ -│ → push eventos │ │ • get_logs │ -│ → push stats │ │ • restart_service │ -│ │ │ • inspect_service │ -│ Docker watcher: │ │ • get_networks │ -│ • listContainers │ │ │ -│ • getEvents stream │ │ Resources: │ -│ • stats polling │ │ • docker://services │ -│ │ │ │ -│ Para: humanos 👀 │ │ │ -└──────────────────────┘ │ │ - │ Para: Claude Code 🤖 │ - └──────────────────────────┘ -``` - ---- - -## Estructura del proyecto - -``` -alteonx-dockerflow/ -├── src/ -│ ├── server/ -│ │ ├── index.ts # Hono server + WebSocket -│ │ ├── docker.ts # Docker API wrapper (dockerode) -│ │ └── watcher.ts # Docker events stream + stats polling -│ ├── mcp/ -│ │ └── index.ts # MCP server (stdio) -│ ├── client/ -│ │ ├── App.tsx # React Flow canvas -│ │ ├── main.tsx # Entry point -│ │ ├── nodes/ -│ │ │ ├── ServiceNode.tsx # Nodo de container -│ │ │ └── GroupNode.tsx # Nodo grupo (compose project) -│ │ ├── panels/ -│ │ │ └── DetailPanel.tsx # Panel lateral de detalles -│ │ ├── hooks/ -│ │ │ ├── useDocker.ts # WebSocket hook -│ │ │ └── useStatsStore.ts# Stats por nodo (useSyncExternalStore) -│ │ └── engine/ -│ │ └── layout.ts # Auto-layout -│ └── shared/ -│ └── types.ts # Tipos compartidos server/client -├── package.json -├── tsconfig.json -├── vite.config.ts -└── README.md -``` - ---- - -## Cómo funciona (para cualquier proyecto) - -### 1. Auto-discovery (zero config) - -El dashboard lee el Docker socket y automáticamente: - -```ts -// src/server/docker.ts -import Docker from "dockerode"; - -const docker = new Docker({ socketPath: "/var/run/docker.sock" }); - -export async function discoverServices() { - const containers = await docker.listContainers({ all: true }); - - return containers.map((c) => ({ - id: c.Id.slice(0, 12), - name: c.Labels["com.docker.compose.service"] || c.Names[0].replace("/", ""), - image: c.Image, - state: c.State, - status: c.Status, - ports: c.Ports.filter((p) => p.PublicPort).map((p) => ({ - host: p.PublicPort, - container: p.PrivatePort, - })), - networks: Object.keys(c.NetworkSettings.Networks), - project: c.Labels["com.docker.compose.project"] || "standalone", - compose_file: c.Labels["com.docker.compose.project.config_files"] || "", - })); -} - -export async function discoverConnections() { - // Servicios en la misma network = conectados - const networks = await docker.listNetworks(); - const connections: { from: string; to: string; network: string }[] = []; - - for (const net of networks) { - const info = await docker.getNetwork(net.Id).inspect(); - const members = Object.values(info.Containers || {}).map((c: any) => c.Name); - - // Cada par de containers en la misma red = edge - for (let i = 0; i < members.length; i++) { - for (let j = i + 1; j < members.length; j++) { - connections.push({ - from: members[i], - to: members[j], - network: net.Name, - }); - } - } - } - - return connections; -} -``` - -**Resultado:** sin configurar nada, el usuario ve todos sus containers como nodos y las conexiones de red como edges. Funciona con cualquier proyecto Docker. - -### 2. Agrupación visual (subgraphs automáticos) - -Cada container es su propio nodo (cuadro) con imagen, estado, puertos, stats. Los subgraphs son bordes visuales que agrupan containers del mismo compose file o proyecto. - -**Docker expone 2 labels para agrupar:** -| Label | Qué es | Ejemplo | -|---|---|---| -| `com.docker.compose.project` | Nombre del proyecto (directorio) | `ninjasagacw` | -| `com.docker.compose.project.config_files` | Qué compose file lo levantó | `docker-compose.infra.yml` | - -**Auto-detección del nivel de agrupación:** -```ts -function detectGrouping(services: Service[]): "project" | "compose_file" { - const projects = new Set(services.map((s) => s.project)); - // Múltiples proyectos → agrupar por proyecto - if (projects.size > 1) return "project"; - // 1 solo proyecto con múltiples compose files → agrupar por compose file - return "compose_file"; -} -``` - -**Caso 1: Un proyecto, múltiples compose files** (como NinjaSaga): -``` -┌─ infra.yml ──────────────────────────────────────────────┐ -│ │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ -│ │ 🐘 db │ │ 📡 collector │ │ ⚡ redis │ │ -│ │ postgres:17 │ │ ./backend │ │ redis:7 │ │ -│ │ CPU 0.3% │ │ CPU 1.2% │ │ CPU 0.1% │ │ -│ │ MEM 45MB │ │ MEM 82MB │ │ MEM 12MB │ │ -│ └──────────────┘ └──────────────┘ └──────────────┘ │ -│ │ -│ ┌──────────────┐ ┌──────────────┐ │ -│ │ ⚙️ celery- │ │ ⏰ celery- │ │ -│ │ worker │ │ beat │ │ -│ │ CPU 0.5% │ │ CPU 0.1% │ │ -│ │ MEM 65MB │ │ MEM 40MB │ │ -│ └──────────────┘ └──────────────┘ │ -└───────────────────────────────────────────────────────────┘ - -┌─ dev.yml ────────────────────────────────────────────────┐ -│ │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ -│ │ 🔧 backend │ │ 🔑 auth │ │ ⚙️ celery │ │ -│ │ -dev │ │ -dev │ │ -dev │ │ -│ │ :4020 │ │ :4021 │ │ │ │ -│ │ CPU 0.8% │ │ CPU 0.3% │ │ CPU 0.2% │ │ -│ │ MEM 120MB │ │ MEM 95MB │ │ MEM 60MB │ │ -│ └──────────────┘ └──────────────┘ └──────────────┘ │ -│ │ -│ ┌──────────────┐ │ -│ │ 🖥️ frontend │ │ -│ │ -dev │ │ -│ │ :4030 │ │ -│ │ CPU 0.5% │ │ -│ │ MEM 180MB │ │ -│ └──────────────┘ │ -└───────────────────────────────────────────────────────────┘ - -Edges cruzan entre subgraphs: - backend-dev ──→ db (postgres) - backend-dev ──→ redis (cache) - frontend-dev ──→ backend-dev (proxy /data) - collector ──→ db (snapshot) - collector ──→ redis (invalidate) - celery-worker ──→ redis (broker) -``` - -**Caso 2: Múltiples proyectos** (usuario con varios repos): -``` -┌─ mi-saas ──────────────────┐ ┌─ monitoring ──────────────────┐ -│ │ │ │ -│ ┌────────┐ ┌────────┐ │ │ ┌──────────┐ ┌────────────┐ │ -│ │ 🚀 api │ │ 🐘 db │ │ │ │ 📊 grafana│ │ 📈 prometheus│ -│ │ :3000 │ │ │ │ │ │ :3001 │ │ │ │ -│ └────────┘ └────────┘ │ │ └──────────┘ └────────────┘ │ -│ │ │ │ -│ ┌────────┐ ┌────────┐ │ │ ┌──────────┐ │ -│ │ ⚡redis│ │ 🌐 web │ │ │ │ 📋 loki │ │ -│ │ │ │ :8080 │ │ │ │ │ │ -│ └────────┘ └────────┘ │ │ └──────────┘ │ -└──────────────────────────────┘ └──────────────────────────────┘ -``` - -**Implementación en React Flow:** -```tsx -// Los subgraphs se renderizan como nodos "group" de React Flow -function buildGroupNodes(services: Service[], groupBy: "project" | "compose_file") { - const groups = new Map(); - - for (const svc of services) { - const key = groupBy === "project" ? svc.project : svc.compose_file; - const label = groupBy === "compose_file" - ? key.replace("docker-compose.", "").replace(".yml", "") // "infra", "dev", "prod" - : key; // "mi-saas", "monitoring" - if (!groups.has(label)) groups.set(label, []); - groups.get(label)!.push(svc); - } - - const nodes = []; - - for (const [groupLabel, svcs] of groups) { - // Nodo grupo (subgraph visual) - nodes.push({ - id: `group-${groupLabel}`, - type: "group", - data: { label: groupLabel }, - position: { x: 0, y: 0 }, - style: { - border: "1px dashed #334155", - borderRadius: 16, - padding: 24, - background: "rgba(30, 41, 59, 0.3)", - }, - }); - - // Nodos hijos dentro del grupo - for (const svc of svcs) { - nodes.push({ - id: svc.name, - type: "service", - data: { ...svc, label: svc.name }, - parentId: `group-${groupLabel}`, // ← lo pone dentro del subgraph - extent: "parent", - position: { x: 0, y: 0 }, // dagre calcula la posición - }); - } - } - - return nodes; -} -``` - -### 3. Smart edge detection (heurísticas) - -Además de redes, detecta relaciones por convención: - -```ts -// src/server/docker.ts -export function inferEdgeType(from: Service, to: Service): EdgeType | null { - const toImage = to.image.toLowerCase(); - const fromEnv = from.env || {}; - - // Detectar DB connections - if (toImage.includes("postgres") || toImage.includes("mysql") || toImage.includes("mongo")) { - // Buscar en env vars del "from" si referencia al "to" - for (const [key, val] of Object.entries(fromEnv)) { - if (key.includes("DATABASE") || key.includes("DB_HOST") || key.includes("MONGO")) { - return { type: "database", label: key.split("_")[0].toLowerCase() }; - } - } - return { type: "database", label: "db" }; - } - - // Detectar Redis connections - if (toImage.includes("redis")) { - return { type: "cache", label: "redis" }; - } - - // Detectar RabbitMQ / message brokers - if (toImage.includes("rabbit") || toImage.includes("kafka")) { - return { type: "broker", label: "messages" }; - } - - // Detectar nginx/traefik → upstream - if (fromImage.includes("nginx") || fromImage.includes("traefik")) { - return { type: "proxy", label: "upstream" }; - } - - return null; -} -``` - -### 3. Docker Events (automático, zero config) - -```ts -// src/server/watcher.ts -import Docker from "dockerode"; - -const docker = new Docker({ socketPath: "/var/run/docker.sock" }); - -export function watchDockerEvents(onEvent: (e: DockerEvent) => void) { - docker.getEvents({}, (err, stream) => { - if (err || !stream) return; - stream.on("data", (chunk) => { - try { - const event = JSON.parse(chunk.toString()); - if (event.Type !== "container") return; - - onEvent({ - type: "docker", - action: event.Action, // start, stop, die, restart, health_status - service: event.Actor?.Attributes?.["com.docker.compose.service"] - || event.Actor?.Attributes?.name - || "unknown", - time: event.time, - }); - } catch {} - }); - }); -} -``` - -Cada start/stop/restart se ve como un pulso animado (flash) en el nodo. - -### 4. Stats polling - -```ts -// src/server/watcher.ts -export async function pollStats(services: Service[]): Promise { - const running = services.filter((s) => s.state === "running"); - const results: Stats[] = []; - - for (const svc of running) { - try { - const container = docker.getContainer(svc.id); - const raw = await container.stats({ stream: false }); - - const cpuDelta = raw.cpu_stats.cpu_usage.total_usage - raw.precpu_stats.cpu_usage.total_usage; - const sysDelta = raw.cpu_stats.system_cpu_usage - raw.precpu_stats.system_cpu_usage; - - results.push({ - service: svc.name, - cpu: sysDelta > 0 ? (cpuDelta / sysDelta) * (raw.cpu_stats.online_cpus || 1) * 100 : 0, - mem_mb: (raw.memory_stats.usage || 0) / 1024 / 1024, - mem_percent: ((raw.memory_stats.usage || 0) / (raw.memory_stats.limit || 1)) * 100, - net_rx_mb: Object.values(raw.networks || {}).reduce((a: number, n: any) => a + (n.rx_bytes || 0), 0) / 1024 / 1024, - net_tx_mb: Object.values(raw.networks || {}).reduce((a: number, n: any) => a + (n.tx_bytes || 0), 0) / 1024 / 1024, - }); - } catch {} - } - - return results; -} -``` - ---- - ---- - -## Nodo visual (ServiceNode) - -```tsx -// src/client/nodes/ServiceNode.tsx -import { Handle, Position } from "@xyflow/react"; - -const stateStyles = { - running: { ring: "ring-emerald-500/50", dot: "bg-emerald-500", bg: "bg-emerald-500/10" }, - exited: { ring: "ring-red-500/50", dot: "bg-red-500", bg: "bg-red-500/10" }, - paused: { ring: "ring-amber-500/50", dot: "bg-amber-500", bg: "bg-amber-500/10" }, -}; - -const imageIcons: Record = { - postgres: "🐘", redis: "⚡", nginx: "🔀", node: "💚", python: "🐍", - mongo: "🍃", mysql: "🐬", rabbitmq: "🐰", certbot: "📜", -}; - -function guessIcon(image: string): string { - for (const [key, icon] of Object.entries(imageIcons)) { - if (image.toLowerCase().includes(key)) return icon; - } - return "📦"; -} - -export function ServiceNode({ data }: { data: ServiceNodeData }) { - const s = stateStyles[data.state] || stateStyles.exited; - const icon = guessIcon(data.image); - - return ( -
- - - {/* Status dot + name */} -
-
- {icon} {data.label} -
- - {/* Image */} -
{data.image}
- - {/* Ports */} - {data.ports?.length > 0 && ( -
- {data.ports.map((p) => ( - - :{p} - - ))} -
- )} - - {/* Stats bar */} - {data.stats && ( -
-
- CPU {data.stats.cpu.toFixed(1)}% - MEM {data.stats.mem_mb.toFixed(0)}MB -
-
-
-
-
- )} - - {/* Compose project badge */} - {data.project && ( -
- - {data.project} - -
- )} - - -
- ); -} -``` - ---- - -## Server (Hono + Bun WebSocket) - -```ts -// src/server/index.ts -import { Hono } from "hono"; -import { serveStatic } from "hono/bun"; -import { discoverServices, discoverConnections } from "./docker"; -import { watchDockerEvents, pollStats } from "./watcher"; - -const app = new Hono(); - -// Serve frontend -app.use("/*", serveStatic({ root: "./dist" })); - -// REST endpoints (para MCP y fallback) -app.get("/api/services", async (c) => c.json(await discoverServices())); -app.get("/api/connections", async (c) => c.json(await discoverConnections())); -// WebSocket (Bun native) -const clients = new Set(); - -const PORT = parseInt(process.env.PORT || "9470"); -const AUTH_TOKEN = process.env.AUTH_TOKEN || ""; -const HOST = AUTH_TOKEN ? "0.0.0.0" : "127.0.0.1"; - -Bun.serve({ - hostname: HOST, - port: PORT, - fetch: app.fetch, - websocket: { - open(ws) { clients.add(ws); }, - close(ws) { clients.delete(ws); }, - message(ws, msg) { - // Handle subscribe_logs, unsubscribe_logs, etc. - }, - }, -}); - -function broadcast(type: string, data: any) { - const msg = JSON.stringify({ type, data }); - for (const ws of clients) ws.send(msg); -} - -// Docker events → broadcast -watchDockerEvents((event) => broadcast("docker_event", event)); - -// Stats polling cada 3s -setInterval(async () => { - const services = await discoverServices(); - const connections = await discoverConnections(); - const stats = await pollStats(services); - broadcast("services", services); - broadcast("connections", connections); - broadcast("stats", stats); -}, 3000); - -console.log(`Alteonx DockerFlow running on http://${HOST}:${PORT}`); -``` - ---- - -## MCP Server - -```ts -// src/mcp/index.ts -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import Docker from "dockerode"; -import { z } from "zod"; -import { execSync } from "child_process"; - -const docker = new Docker({ socketPath: "/var/run/docker.sock" }); -const server = new McpServer({ name: "alteonx-dockerflow", version: "1.0.0" }); - -// ── Dashboard control ── - -server.tool("start_dashboard", - "Start the Alteonx DockerFlow dashboard and return the URL", - { port: z.number().default(9470) }, - async ({ port }) => { - // Arranca el server en background - execSync(`bun run src/server/index.ts &`, { stdio: "ignore" }); - return { content: [{ type: "text", text: `Dashboard running at http://localhost:${port}` }] }; - } -); - -// ── Docker tools ── - -server.tool("list_services", - "List all Docker containers with status, ports, image", - {}, - async () => { - const containers = await docker.listContainers({ all: true }); - const services = containers.map((c) => ({ - name: c.Labels["com.docker.compose.service"] || c.Names[0]?.replace("/", ""), - state: c.State, - status: c.Status, - image: c.Image, - ports: c.Ports.filter((p) => p.PublicPort).map((p) => `${p.PublicPort}:${p.PrivatePort}`), - project: c.Labels["com.docker.compose.project"] || "", - })); - return { content: [{ type: "text", text: JSON.stringify(services, null, 2) }] }; - } -); - -server.tool("get_stats", - "Get CPU and memory stats for a container", - { container: z.string().describe("Container name or ID") }, - async ({ container }) => { - const c = docker.getContainer(container); - const raw = await c.stats({ stream: false }); - const cpuDelta = raw.cpu_stats.cpu_usage.total_usage - raw.precpu_stats.cpu_usage.total_usage; - const sysDelta = raw.cpu_stats.system_cpu_usage - raw.precpu_stats.system_cpu_usage; - return { - content: [{ - type: "text", - text: JSON.stringify({ - cpu_percent: (sysDelta > 0 ? (cpuDelta / sysDelta) * (raw.cpu_stats.online_cpus || 1) * 100 : 0).toFixed(2), - mem_mb: ((raw.memory_stats.usage || 0) / 1024 / 1024).toFixed(1), - mem_percent: (((raw.memory_stats.usage || 0) / (raw.memory_stats.limit || 1)) * 100).toFixed(1), - }, null, 2), - }], - }; - } -); - -server.tool("get_logs", - "Get recent logs from a container", - { - container: z.string().describe("Container name or ID"), - tail: z.number().default(50).describe("Number of lines"), - }, - async ({ container, tail }) => { - const logs = await docker.getContainer(container).logs({ - stdout: true, stderr: true, tail, timestamps: true, - }); - return { content: [{ type: "text", text: logs.toString() }] }; - } -); - -server.tool("restart_service", - "Restart a Docker container", - { container: z.string() }, - async ({ container }) => { - await docker.getContainer(container).restart(); - return { content: [{ type: "text", text: `Restarted: ${container}` }] }; - } -); - -server.tool("inspect_service", - "Get detailed info about a container (filters secrets from env)", - { container: z.string() }, - async ({ container }) => { - const info = await docker.getContainer(container).inspect(); - return { - content: [{ - type: "text", - text: JSON.stringify({ - name: info.Name, - state: info.State, - image: info.Config.Image, - cmd: info.Config.Cmd, - env: info.Config.Env?.filter((e) => - !e.match(/PASSWORD|SECRET|KEY|TOKEN|PRIVATE/i) - ), - networks: Object.keys(info.NetworkSettings.Networks || {}), - mounts: info.Mounts?.map((m) => ({ - type: m.Type, src: m.Source, dst: m.Destination, - })), - ports: info.NetworkSettings.Ports, - }, null, 2), - }], - }; - } -); - -server.tool("get_networks", - "List Docker networks and connected containers", - {}, - async () => { - const networks = await docker.listNetworks(); - const result = []; - for (const net of networks) { - const info = await docker.getNetwork(net.Id).inspect(); - result.push({ - name: net.Name, - driver: net.Driver, - containers: Object.values(info.Containers || {}).map((c: any) => c.Name), - }); - } - return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; - } -); - -// ── Resources ── - -server.resource("services", "docker://services", async (uri) => ({ - contents: [{ - uri: uri.href, - mimeType: "application/json", - text: JSON.stringify(await docker.listContainers({ all: true }), null, 2), - }], -})); - -// ── Start ── - -const transport = new StdioServerTransport(); -await server.connect(transport); -``` - -### Setup en Claude Code -```bash -# Instalar -git clone github.com/user/alteonx-dockerflow -cd alteonx-dockerflow -bun install - -# Agregar MCP -claude mcp add alteonx-dockerflow -- bun run src/mcp/index.ts - -# Usar -> "arranca el dashboard" → start_dashboard -> "qué containers tengo?" → list_services -> "cuánta RAM usa el backend?" → get_stats -> "muéstrame los logs de redis" → get_logs -``` - ---- - -## Seguridad - -### Comportamiento por defecto (seguro sin configurar nada) - -| `AUTH_TOKEN` | Bind | Acceso | Uso | -|---|---|---|---| -| No definido | `127.0.0.1:9470` | Solo local | Dev en tu máquina | -| Definido | `0.0.0.0:9470` | Remoto (con token) | SSH a servidor, equipo | - -```ts -// src/server/index.ts -const AUTH_TOKEN = process.env.AUTH_TOKEN || ""; -const HOST = AUTH_TOKEN ? "0.0.0.0" : "127.0.0.1"; - -Bun.serve({ - hostname: HOST, - port: 9470, - // ... -}); -``` - -### Cómo funciona el auth - -**Sin token (default):** bind a `127.0.0.1`, solo accesible desde tu máquina. No pide nada. - -**Con token:** bind a `0.0.0.0`, el dashboard pide el token al entrar: - -``` -┌─────────────────────────────────────────┐ -│ │ -│ 🔒 Alteonx DockerFlow │ -│ │ -│ ┌─────────────────────────────────┐ │ -│ │ Token: •••••••••• │ │ -│ └─────────────────────────────────┘ │ -│ [ Entrar ] │ -│ │ -└─────────────────────────────────────────┘ -``` - -- El token se guarda en `localStorage` (no lo pide cada vez) -- Toda request HTTP y conexión WebSocket valida el token -- Token inválido → 401 Unauthorized - -```ts -// Middleware de auth -app.use("*", async (c, next) => { - if (!AUTH_TOKEN) return next(); // sin token = sin auth - - // Skip para la página de login - if (c.req.path === "/" || c.req.path === "/auth") return next(); - - const token = c.req.header("Authorization")?.replace("Bearer ", ""); - if (token !== AUTH_TOKEN) return c.json({ error: "Unauthorized" }, 401); - - return next(); -}); - -// WebSocket auth -websocket: { - open(ws) { - if (AUTH_TOKEN && ws.data.token !== AUTH_TOKEN) { - ws.close(1008, "Unauthorized"); - return; - } - clients.add(ws); - }, -} -``` - -### Setup remoto (SSH) -```bash -# En el servidor -AUTH_TOKEN=mi-clave-super-segura bun run src/server/index.ts - -# Desde tu máquina -# Abrir http://servidor:9470 → pide token → listo -``` - -### Qué se protege - -| Recurso | Sin auth | Con auth | -|---|---|---| -| Dashboard visual | Accesible (localhost) | Requiere token | -| WebSocket (stats, eventos) | Accesible (localhost) | Requiere token | -| API `/api/services` | Accesible (localhost) | Requiere token | -| MCP Server | Siempre local (stdio) | N/A (no pasa por HTTP) | -| Docker socket | Read-only (`:ro`) | Read-only (`:ro`) | - -### Qué NO expone nunca -- Variables de entorno con `PASSWORD`, `SECRET`, `KEY`, `TOKEN`, `PRIVATE` se filtran automáticamente en `inspect_service` -- El Docker socket se monta como read-only (`:ro`) — no puede crear/eliminar containers desde el dashboard -- El MCP sí puede hacer `restart_service` porque corre local y tiene approval flow de Claude Code - ---- - -## Colores - -| Tipo | Color | Uso | -|---|---|---| -| Nodo running | `#22c55e` verde | Borde + pulso animado | -| Nodo stopped | `#ef4444` rojo | Borde estático | -| Nodo paused | `#f59e0b` amarillo | Borde estático | -| Docker event start | `#22c55e` verde | Flash en nodo | -| Docker event stop | `#ef4444` rojo | Flash en nodo | -| Docker event restart | `#f59e0b` amarillo | Flash en nodo | - ---- - -## Filtrado de proyectos - -### Por CLI (qué containers carga el backend) - -| Modo | Comando | Qué carga | -|---|---|---| -| **Auto** (default) | `bunx alteonx-dockerflow` | Solo containers del proyecto actual (detecta por directorio) | -| **Multi** | `bunx alteonx-dockerflow --projects ninjasagacw,tonal` | Containers de proyectos específicos | -| **All** | `bunx alteonx-dockerflow --all` | Todo lo que esté corriendo | - -```ts -// src/server/index.ts -const args = process.argv.slice(2); -const ALL = args.includes("--all"); -const PROJECTS = args.find((a) => a.startsWith("--projects="))?.split("=")[1]?.split(",") - || [path.basename(process.cwd())]; // default: nombre del directorio actual - -function filterServices(services: Service[]): Service[] { - if (ALL) return services; - return services.filter((s) => PROJECTS.includes(s.project)); -} -``` - -### Por frontend (filtro visual en vivo) - -El backend siempre envía el campo `project` en cada servicio. El frontend tiene un dropdown con checkboxes para filtrar sin reiniciar: - -``` -┌──────────────────────────────────────────────────────┐ -│ Alteonx DockerFlow [Proyecto ▼] [⚙️] │ -│ ☑ ninjasagacw │ -│ ☑ tonal │ -│ ☑ megalabs │ -│ ────────── │ -│ ☑ Mostrar todos │ -└──────────────────────────────────────────────────────┘ -``` - -- Checkboxes por proyecto, filtra los nodos en vivo -- Selección se guarda en `localStorage` -- Si usaste `--all` ves todos los proyectos disponibles para filtrar -- Si usaste modo auto, solo ves el proyecto actual (sin dropdown) - -### Para apagar -- `Ctrl+C` en la terminal -- Desde MCP: `> "apaga el dashboard"` → tool `stop_dashboard` - ---- - -## Dificultad para el usuario final - -| Paso | Dificultad | Tiempo | -|---|---|---| -| `git clone` + `bun install` | Trivial | 30s | -| `claude mcp add` | Trivial | 10s | -| "arranca el dashboard" | Trivial | 5s | -| Ver arquitectura + stats | Automático | 0s (auto-discovery) | - -**Requisitos del usuario:** -- Docker instalado y corriendo -- Bun instalado (`curl -fsSL https://bun.sh/install | bash`) -- Claude Code con MCP (opcional, puede usar sin MCP también) - ---- - -## Roadmap - -### Fase 1 — MVP (auto-discovery + stats + agrupación) -- [ ] Server Hono + Bun WebSocket -- [ ] Auto-discovery de containers desde Docker socket -- [ ] Smart edge detection (networks + heurísticas) -- [ ] **Agrupación visual por compose project/file** (subgraph con borde + label) -- [ ] **Filtrado por proyecto**: CLI (`--all`, `--projects`, auto) + dropdown en frontend -- [ ] ServiceNode con estado, imagen, puertos, stats -- [ ] Auto-layout con dagre (respetando grupos) -- [ ] Stats polling (CPU/MEM) cada 3s -- [ ] Docker events (start/stop/restart) como pulso en nodos -- [ ] Dark theme, minimap, zoom, pan - - -### Fase 3 — Polish -- [ ] Click en nodo → panel lateral con logs en vivo -- [ ] Notificaciones y alertas -- [ ] Responsive (funcione en tablet) -- [ ] Export PNG/SVG del grafo actual -- [ ] Customizar posiciones de nodos (drag + guardar layout) - -### Fase 4 — Avanzado -- [ ] Health check status en nodos (healthy/unhealthy/starting) -- [ ] Métricas históricas (SQLite para mini-gráficas en cada nodo) -- [ ] Alertas cuando un container se cae (webhook/push) -- [ ] Multi-host (Docker Swarm / remote Docker sockets) diff --git a/docs/demo.gif b/docs/demo.gif index ad448e7..5e63c88 100644 Binary files a/docs/demo.gif and b/docs/demo.gif differ diff --git a/docker-containerflow.md b/docs/docker-guide.md similarity index 100% rename from docker-containerflow.md rename to docs/docker-guide.md diff --git a/PLAN-MULTI-HOST.md b/docs/multi-host-plan.md similarity index 100% rename from PLAN-MULTI-HOST.md rename to docs/multi-host-plan.md diff --git a/monitoreo.md b/docs/roadmap.md similarity index 100% rename from monitoreo.md rename to docs/roadmap.md diff --git a/src/client/components/HeaderBar.tsx b/src/client/components/HeaderBar.tsx index e4ee08a..a8dcdf4 100644 --- a/src/client/components/HeaderBar.tsx +++ b/src/client/components/HeaderBar.tsx @@ -211,7 +211,7 @@ export function HeaderBar({ />
ContainerFlow - v0.0.1 + v{__APP_VERSION__}
diff --git a/src/client/pages/SettingsPage.tsx b/src/client/pages/SettingsPage.tsx index dfbb3ce..1b1d192 100644 --- a/src/client/pages/SettingsPage.tsx +++ b/src/client/pages/SettingsPage.tsx @@ -122,7 +122,7 @@ export function SettingsPage({ projects, servicesCount, token }: SettingsPagePro
{t("settings.version")} - v0.0.1 + v{__APP_VERSION__}
{t("settings.mode")} diff --git a/src/client/vite-env.d.ts b/src/client/vite-env.d.ts new file mode 100644 index 0000000..dbb4c62 --- /dev/null +++ b/src/client/vite-env.d.ts @@ -0,0 +1,3 @@ +/// + +declare const __APP_VERSION__: string; diff --git a/vite.config.ts b/vite.config.ts index 37f9315..f47dffc 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,6 +1,7 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import tailwindcss from "@tailwindcss/vite"; +import pkg from "./package.json" with { type: "json" }; export default defineConfig({ plugins: [react(), tailwindcss()], @@ -9,6 +10,10 @@ export default defineConfig({ outDir: "../../dist", emptyOutDir: true, }, + define: { + // Inject app version from package.json so UI never drifts from the source of truth + __APP_VERSION__: JSON.stringify(pkg.version), + }, server: { port: 9420, host: "0.0.0.0",