v0.1.0 — Repositorio público

This commit is contained in:
RGJorge
2026-05-11 03:02:14 +00:00
parent 77da10bfde
commit 396873ae87
16 changed files with 56 additions and 1341 deletions
+1 -1
View File
@@ -36,7 +36,7 @@ assignees: ''
## Are you willing to wait? ## 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 - [ ] Yes, I'll wait — I just want to flag this
- [ ] I'd contribute a PR if/when PRs open - [ ] I'd contribute a PR if/when PRs open
+5
View File
@@ -11,3 +11,8 @@ data/
.dockerflow-*.db .dockerflow-*.db
.dockerflow-*.db-wal .dockerflow-*.db-wal
.dockerflow-*.db-shm .dockerflow-*.db-shm
# Local notes / marketing — not part of the public repo
linkdin.md
# AI assistant context — internal, not for public repo
CLAUDE.md
-40
View File
@@ -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"`
+1 -1
View File
@@ -41,7 +41,7 @@ Use the [feature request template](.github/ISSUE_TEMPLATE/feature_request.md). T
- Why existing functionality doesn't work - Why existing functionality doesn't work
- A rough sketch of how you'd want it to work in the UI - 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 ## Reporting security vulnerabilities
-350
View File
@@ -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
+39 -3
View File
@@ -1,8 +1,8 @@
# ContainerFlow # 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) [![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) ![Docker Required](https://img.shields.io/badge/Docker-required-blue?logo=docker)
![Bun](https://img.shields.io/badge/runtime-Bun-f9f1e1?logo=bun) ![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) [![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) ![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 ## 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 ## Requisitos
@@ -381,6 +405,18 @@ src/
types.ts — tipos compartidos server/client 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 ## Licencia
Copyright (C) 2026 Jorge Gonzalez D. (RGJorge) Copyright (C) 2026 Jorge Gonzalez D. (RGJorge)
-944
View File
@@ -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<string, Service[]>();
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<Stats[]> {
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<string, string> = {
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 (
<div className={`relative rounded-xl border border-slate-700 ${s.bg} backdrop-blur-sm
shadow-lg shadow-black/30 p-4 min-w-[180px] ring-2 ${s.ring}
transition-all duration-500`}>
<Handle type="target" position={Position.Top} className="!bg-slate-500" />
{/* Status dot + name */}
<div className="flex items-center gap-2 mb-2">
<div className={`w-2.5 h-2.5 rounded-full ${s.dot}
${data.state === "running" ? "animate-pulse" : ""}`} />
<span className="font-bold text-white text-sm">{icon} {data.label}</span>
</div>
{/* Image */}
<div className="text-[11px] text-slate-400 truncate mb-1">{data.image}</div>
{/* Ports */}
{data.ports?.length > 0 && (
<div className="flex gap-1 flex-wrap mt-1.5">
{data.ports.map((p) => (
<span key={p} className="text-[10px] bg-slate-800 text-cyan-400 px-1.5 py-0.5 rounded font-mono">
:{p}
</span>
))}
</div>
)}
{/* Stats bar */}
{data.stats && (
<div className="mt-2.5 space-y-1">
<div className="flex justify-between text-[10px] text-slate-400">
<span>CPU {data.stats.cpu.toFixed(1)}%</span>
<span>MEM {data.stats.mem_mb.toFixed(0)}MB</span>
</div>
<div className="h-1 bg-slate-800 rounded-full overflow-hidden">
<div className="h-full bg-emerald-500/70 rounded-full transition-all duration-500"
style={{ width: `${Math.min(data.stats.cpu, 100)}%` }} />
</div>
</div>
)}
{/* Compose project badge */}
{data.project && (
<div className="mt-2 flex justify-end">
<span className="text-[9px] px-1.5 py-0.5 rounded bg-slate-800 text-slate-400">
{data.project}
</span>
</div>
)}
<Handle type="source" position={Position.Bottom} className="!bg-slate-500" />
</div>
);
}
```
---
## 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<WebSocket>();
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)
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 MiB

After

Width:  |  Height:  |  Size: 5.5 MiB

View File
+1 -1
View File
@@ -211,7 +211,7 @@ export function HeaderBar({
/> />
<div className="flex flex-col items-end"> <div className="flex flex-col items-end">
<span className="text-base font-bold text-white tracking-wide">ContainerFlow</span> <span className="text-base font-bold text-white tracking-wide">ContainerFlow</span>
<span className="text-[9px] text-slate-500 font-mono -mt-1">v0.0.1</span> <span className="text-[9px] text-slate-500 font-mono -mt-1">v{__APP_VERSION__}</span>
</div> </div>
</div> </div>
+1 -1
View File
@@ -122,7 +122,7 @@ export function SettingsPage({ projects, servicesCount, token }: SettingsPagePro
<div className="grid grid-cols-2 gap-4 text-sm"> <div className="grid grid-cols-2 gap-4 text-sm">
<div className="bg-slate-900/50 rounded-lg p-3"> <div className="bg-slate-900/50 rounded-lg p-3">
<span className="text-slate-500 block text-xs mb-1">{t("settings.version")}</span> <span className="text-slate-500 block text-xs mb-1">{t("settings.version")}</span>
<span className="text-slate-200 font-mono">v0.0.1</span> <span className="text-slate-200 font-mono">v{__APP_VERSION__}</span>
</div> </div>
<div className="bg-slate-900/50 rounded-lg p-3"> <div className="bg-slate-900/50 rounded-lg p-3">
<span className="text-slate-500 block text-xs mb-1">{t("settings.mode")}</span> <span className="text-slate-500 block text-xs mb-1">{t("settings.mode")}</span>
+3
View File
@@ -0,0 +1,3 @@
/// <reference types="vite/client" />
declare const __APP_VERSION__: string;
+5
View File
@@ -1,6 +1,7 @@
import { defineConfig } from "vite"; import { defineConfig } from "vite";
import react from "@vitejs/plugin-react"; import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite"; import tailwindcss from "@tailwindcss/vite";
import pkg from "./package.json" with { type: "json" };
export default defineConfig({ export default defineConfig({
plugins: [react(), tailwindcss()], plugins: [react(), tailwindcss()],
@@ -9,6 +10,10 @@ export default defineConfig({
outDir: "../../dist", outDir: "../../dist",
emptyOutDir: true, 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: { server: {
port: 9420, port: 9420,
host: "0.0.0.0", host: "0.0.0.0",