From 37a81952c642ed0375e04e4fff79e2278d743b92 Mon Sep 17 00:00:00 2001 From: RGJorge Date: Sat, 2 May 2026 03:58:56 +0000 Subject: [PATCH] v0.0.14 --- CLAUDE.md | 3 +- PUBLICAR.md | 17 +- docker-project.md | 358 ++++--------------------------------------- mejoras.md | 2 +- src/client/App.tsx | 11 -- src/client/index.css | 1 + 6 files changed, 38 insertions(+), 354 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 95715c7..b681cf1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -18,13 +18,12 @@ - `nodes/` — componentes de nodos (ServiceNode, GroupNode) - `panels/` — paneles (DetailPanel) - `hooks/` — hooks (useDocker) - - `engine/` — layout y particulas + - `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 - - `flows.ts` — flujos de particulas - `src/shared/` — tipos compartidos ## Comandos diff --git a/PUBLICAR.md b/PUBLICAR.md index 399f983..c501fde 100644 --- a/PUBLICAR.md +++ b/PUBLICAR.md @@ -55,7 +55,7 @@ Estado actual del proyecto: **v0.1.0** | ~1,774 lineas de codigo | 0 tests | 0 C - [ ] **Quick start** en 4 lineas o menos - [ ] **Features** con iconos o emojis descriptivos - [ ] **Configuration** (tabla de env vars) - - [ ] **Flow simulation** con ejemplo YAML + - [ ] **Container actions** (start, stop, restart, rebuild, remove) - [ ] **MCP integration** (esto es diferenciador, destacarlo) - [ ] **Tech stack** (tabla limpia) - [ ] **Contributing** link @@ -66,8 +66,8 @@ Estado actual del proyecto: **v0.1.0** | ~1,774 lineas de codigo | 0 tests | 0 C - [ ] Levantar el dashboard con containers de ejemplo - [ ] Grabar un GIF de ~10 segundos mostrando: - Vista general con servicios conectados - - Particulas animadas fluyendo - - Metricas en tiempo real + - 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 @@ -97,7 +97,7 @@ Estado actual del proyecto: **v0.1.0** | ~1,774 lineas de codigo | 0 tests | 0 C - 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 — Flujos animados, MCP server, logs, polish + - v0.0.5 — MCP server, logs, acciones de containers, polish - [ ] De aqui en adelante, actualizar con cada release --- @@ -131,12 +131,11 @@ Estado actual del proyecto: **v0.1.0** | ~1,774 lineas de codigo | 0 tests | 0 C - [ ] 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__/flows.test.ts` — parseo de flows.yaml, validacion + - [ ] `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 - - [ ] `src/client/engine/__tests__/particles.test.ts` — motor de particulas - [ ] Agregar script: `"test": "bun test"` -- [ ] Meta inicial: cubrir la logica de negocio del server (docker.ts, flows.ts) +- [ ] 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 @@ -297,10 +296,10 @@ Estado actual del proyecto: **v0.1.0** | ~1,774 lineas de codigo | 0 tests | 0 C ### 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 animated data flows" + - 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: DockerFlow – Real-time Docker architecture visualization" + - Titulo: "Show HN: Flowteon – 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 diff --git a/docker-project.md b/docker-project.md index 382dbe1..2299906 100644 --- a/docker-project.md +++ b/docker-project.md @@ -1,6 +1,6 @@ # Alteonx DockerFlow -Herramienta open source para visualizar arquitecturas Docker en tiempo real con partículas animadas que muestran el flujo de datos entre servicios. +Herramienta open source para visualizar arquitecturas Docker en tiempo real. ``` git clone github.com/user/alteonx-dockerflow @@ -24,7 +24,7 @@ claude mcp add alteonx-dockerflow -- bun run src/mcp.ts | **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** | SVG animateMotion + CSS | Sin librerías extra, GPU-accelerated, zero overhead | +| **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 | @@ -81,13 +81,13 @@ claude mcp add alteonx-dockerflow -- bun run src/mcp.ts │ WS /ws │ │ • get_stats │ │ → push eventos │ │ • get_logs │ │ → push stats │ │ • restart_service │ -│ │ │ • list_flows │ -│ Docker watcher: │ │ • simulate_flow │ -│ • listContainers │ │ • create_flow │ -│ • getEvents stream │ │ │ -│ • stats polling │ │ Resources: │ -│ │ │ • docker://services │ -│ Para: humanos 👀 │ │ • docker://flows │ +│ │ │ • inspect_service │ +│ Docker watcher: │ │ • get_networks │ +│ • listContainers │ │ │ +│ • getEvents stream │ │ Resources: │ +│ • stats polling │ │ • docker://services │ +│ │ │ │ +│ Para: humanos 👀 │ │ │ └──────────────────────┘ │ │ │ Para: Claude Code 🤖 │ └──────────────────────────┘ @@ -103,8 +103,7 @@ alteonx-dockerflow/ │ ├── server/ │ │ ├── index.ts # Hono server + WebSocket │ │ ├── docker.ts # Docker API wrapper (dockerode) -│ │ ├── watcher.ts # Docker events stream + stats polling -│ │ └── flows.ts # Flow engine (partículas) +│ │ └── watcher.ts # Docker events stream + stats polling │ ├── mcp/ │ │ └── index.ts # MCP server (stdio) │ ├── client/ @@ -112,22 +111,16 @@ alteonx-dockerflow/ │ │ ├── main.tsx # Entry point │ │ ├── nodes/ │ │ │ ├── ServiceNode.tsx # Nodo de container -│ │ │ └── ExternalNode.tsx# Nodo externo (APIs, users) -│ │ ├── edges/ -│ │ │ └── ParticleEdge.tsx# Edge con partículas animadas +│ │ │ └── GroupNode.tsx # Nodo grupo (compose project) │ │ ├── panels/ -│ │ │ ├── FlowPanel.tsx # Panel de control de flows -│ │ │ ├── StatsPanel.tsx # Panel de stats CPU/MEM -│ │ │ └── LogPanel.tsx # Panel de logs +│ │ │ └── DetailPanel.tsx # Panel lateral de detalles │ │ ├── hooks/ │ │ │ ├── useDocker.ts # WebSocket hook -│ │ │ └── useFlows.ts # Flow state management +│ │ │ └── useStatsStore.ts# Stats por nodo (useSyncExternalStore) │ │ └── engine/ -│ │ ├── particles.ts # Motor de partículas -│ │ └── layout.ts # Auto-layout con dagre +│ │ └── layout.ts # Auto-layout │ └── shared/ │ └── types.ts # Tipos compartidos server/client -├── flows.yaml # Config de flows (opcional) ├── package.json ├── tsconfig.json ├── vite.config.ts @@ -397,7 +390,7 @@ export function watchDockerEvents(onEvent: (e: DockerEvent) => void) { } ``` -**Partículas gratis:** cada start/stop/restart se ve como un pulso animado en el nodo. +Cada start/stop/restart se ve como un pulso animado (flash) en el nodo. ### 4. Stats polling @@ -432,185 +425,6 @@ export async function pollStats(services: Service[]): Promise { --- -## Flows (simulaciones visuales) - -Los flows son recorridos animados que muestran cómo viaja una request o evento por la arquitectura. Son **puramente visuales** y sirven para entender y presentar tu sistema. - -### Dos modos - -**Modo simulación (MVP, funciona para todos):** -- El usuario define un path: "request pasa por nginx → backend → redis → db" -- Click en "simular" → partícula recorre esa ruta con animación -- Como un diagrama de secuencia pero animado y en el grafo real -- No necesita conectar nada, solo definir el camino - -**Modo eventos reales (avanzado, opcional):** -- Requiere un probe (middleware) en los servicios -- El probe emite eventos a Redis/WebSocket -- El dashboard escucha y dispara partículas automáticamente -- Para fase 2+, no MVP - -### Configuración de flows — `flows.yaml` - -```yaml -# flows.yaml — Define recorridos visuales en tu arquitectura -# Cada flow es un camino que una operación recorre entre servicios - -flows: - # Ejemplo genérico: request HTTP - api_request: - name: "API Request" - description: "Request del usuario al backend" - color: "#3b82f6" # azul - speed: 0.8 # segundos por edge - path: [nginx, backend, redis, db, backend, nginx] - - # Ejemplo: background job - background_job: - name: "Background Job" - description: "Tarea programada del worker" - color: "#ec4899" # rosa - speed: 1.0 - path: [redis, worker, db, worker] - - # Ejemplo: cache flow - cache_hit: - name: "Cache Hit" - description: "Request servida desde cache" - color: "#22c55e" # verde - speed: 0.5 - path: [nginx, backend, redis, backend, nginx] - - cache_miss: - name: "Cache Miss" - description: "Cache miss, va a DB" - color: "#f59e0b" # amarillo - speed: 0.8 - path: [nginx, backend, redis, db, redis, backend, nginx] - -# Los nombres de servicios deben coincidir con el nombre del container -# (com.docker.compose.service label o container name) - -settings: - particle_size: 5 # radio en px - trail: true # estela detrás de la partícula - trail_opacity: 0.3 - glow: true # efecto neón - max_particles: 50 # máximo simultáneas - auto_simulate: false # simular automáticamente en idle - auto_simulate_interval: 5 # segundos entre simulaciones auto -``` - -**Zero config posible:** sin `flows.yaml` el dashboard muestra la arquitectura estática con stats. Los flows son un addon visual opcional. - -### Motor de partículas - -```tsx -// src/client/engine/particles.ts - -interface Particle { - id: string; - flowName: string; - color: string; - path: string[]; // lista de service names - currentStep: number; // índice actual en path - progress: number; // 0-1 progreso en el edge actual - label?: string; -} - -class ParticleEngine { - particles: Particle[] = []; - private maxParticles = 50; - - spawn(flow: Flow, label?: string) { - if (this.particles.length >= this.maxParticles) return; - this.particles.push({ - id: crypto.randomUUID(), - flowName: flow.name, - color: flow.color, - path: flow.path, - currentStep: 0, - progress: 0, - label, - }); - } - - tick(deltaMs: number, speed: number) { - const step = deltaMs / (speed * 1000); - this.particles = this.particles.filter((p) => { - p.progress += step; - if (p.progress >= 1) { - p.currentStep++; - p.progress = 0; - // Llegó al final → flash en nodo destino → eliminar - if (p.currentStep >= p.path.length - 1) return false; - } - return true; - }); - } -} -``` - -### Partícula visual (SVG sobre React Flow edge) - -```tsx -// src/client/edges/ParticleEdge.tsx -import { BaseEdge, getSmoothStepPath, type EdgeProps } from "@xyflow/react"; - -export function ParticleEdge({ id, sourceX, sourceY, targetX, targetY, ...props }: EdgeProps) { - const [path] = getSmoothStepPath({ sourceX, sourceY, targetX, targetY }); - const particles = useParticlesForEdge(id); // del engine - - return ( - <> - - - - - - - - - {particles.map((p) => ( - - {/* Estela */} - - - - {/* Partícula principal */} - - - - {/* Label */} - {p.label && ( - - - {p.label} - - )} - - ))} - - - ); -} -``` - -### Panel de control de flows - -``` -┌──────────────────────────────────────────────────────────────┐ -│ Flows [⚙️] │ -├──────────────────────────────────────────────────────────────┤ -│ ● API Request 0/min [ ON ] [ ▶ Simular ] │ -│ ● Cache Hit 0/min [ ON ] [ ▶ Simular ] │ -│ ● Cache Miss 0/min [ ON ] [ ▶ Simular ] │ -│ ● Background Job 0/min [ ON ] [ ▶ Simular ] │ -├──────────────────────────────────────────────────────────────┤ -│ [ ▶ Simular todos ] [ ⏸ Pausar ] [ + Nuevo flow ] │ -└──────────────────────────────────────────────────────────────┘ -``` - --- ## Nodo visual (ServiceNode) @@ -707,18 +521,8 @@ import { Hono } from "hono"; import { serveStatic } from "hono/bun"; import { discoverServices, discoverConnections } from "./docker"; import { watchDockerEvents, pollStats } from "./watcher"; -import { FlowEngine } from "./flows"; -import { parse } from "yaml"; -import { readFileSync, existsSync } from "fs"; const app = new Hono(); -const flowEngine = new FlowEngine(); - -// Load flows if exists -if (existsSync("flows.yaml")) { - const config = parse(readFileSync("flows.yaml", "utf-8")); - flowEngine.loadFlows(config.flows || []); -} // Serve frontend app.use("/*", serveStatic({ root: "./dist" })); @@ -726,12 +530,6 @@ 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())); -app.get("/api/flows", (c) => c.json(flowEngine.getFlows())); -app.post("/api/flows/:name/simulate", (c) => { - flowEngine.simulate(c.req.param("name")); - return c.json({ ok: true }); -}); - // WebSocket (Bun native) const clients = new Set(); @@ -747,8 +545,7 @@ Bun.serve({ open(ws) { clients.add(ws); }, close(ws) { clients.delete(ws); }, message(ws, msg) { - const data = JSON.parse(msg.toString()); - if (data.type === "simulate") flowEngine.simulate(data.flow); + // Handle subscribe_logs, unsubscribe_logs, etc. }, }, }); @@ -771,12 +568,6 @@ setInterval(async () => { broadcast("stats", stats); }, 3000); -// Particle engine tick (60fps) -setInterval(() => { - const particles = flowEngine.tick(16); - if (particles.length > 0) broadcast("particles", particles); -}, 16); - console.log(`Alteonx DockerFlow running on http://${HOST}:${PORT}`); ``` @@ -915,73 +706,6 @@ server.tool("get_networks", } ); -// ── Flow tools ── - -server.tool("list_flows", - "List configured flow simulations", - {}, - async () => { - // Read flows.yaml - const { readFileSync, existsSync } = await import("fs"); - const { parse } = await import("yaml"); - if (!existsSync("flows.yaml")) { - return { content: [{ type: "text", text: "No flows.yaml found. Create one to define flow simulations." }] }; - } - const config = parse(readFileSync("flows.yaml", "utf-8")); - const flows = Object.entries(config.flows || {}).map(([key, f]: [string, any]) => ({ - id: key, - name: f.name, - color: f.color, - path: f.path.join(" → "), - description: f.description || "", - })); - return { content: [{ type: "text", text: JSON.stringify(flows, null, 2) }] }; - } -); - -server.tool("simulate_flow", - "Trigger a flow simulation on the dashboard", - { flow: z.string().describe("Flow ID from flows.yaml") }, - async ({ flow }) => { - // POST to dashboard API - try { - await fetch(`http://localhost:9470/api/flows/${flow}/simulate`, { method: "POST" }); - return { content: [{ type: "text", text: `Simulation triggered: ${flow}` }] }; - } catch { - return { content: [{ type: "text", text: "Dashboard not running. Use start_dashboard first." }] }; - } - } -); - -server.tool("create_flow", - "Add a new flow simulation to flows.yaml", - { - id: z.string().describe("Unique flow ID (snake_case)"), - name: z.string().describe("Display name"), - color: z.string().describe("Hex color for the particle"), - path: z.array(z.string()).describe("Ordered list of service names the flow traverses"), - description: z.string().optional(), - }, - async ({ id, name, color, path, description }) => { - const { readFileSync, writeFileSync, existsSync } = await import("fs"); - const { parse, stringify } = await import("yaml"); - - const file = "flows.yaml"; - const config = existsSync(file) ? parse(readFileSync(file, "utf-8")) : { flows: {}, settings: {} }; - - config.flows[id] = { - name, - description: description || "", - color, - speed: 0.8, - path, - }; - - writeFileSync(file, stringify(config)); - return { content: [{ type: "text", text: `Flow "${name}" created: ${path.join(" → ")}` }] }; - } -); - // ── Resources ── server.resource("services", "docker://services", async (uri) => ({ @@ -992,17 +716,6 @@ server.resource("services", "docker://services", async (uri) => ({ }], })); -server.resource("flows", "docker://flows", async (uri) => { - const { readFileSync, existsSync } = await import("fs"); - return { - contents: [{ - uri: uri.href, - mimeType: "text/yaml", - text: existsSync("flows.yaml") ? readFileSync("flows.yaml", "utf-8") : "# No flows configured", - }], - }; -}); - // ── Start ── const transport = new StdioServerTransport(); @@ -1023,8 +736,6 @@ claude mcp add alteonx-dockerflow -- bun run src/mcp/index.ts > "arranca el dashboard" → start_dashboard > "qué containers tengo?" → list_services > "cuánta RAM usa el backend?" → get_stats -> "crea un flow para mi login endpoint" → create_flow -> "simula el login flow" → simulate_flow > "muéstrame los logs de redis" → get_logs ``` @@ -1130,15 +841,12 @@ AUTH_TOKEN=mi-clave-super-segura bun run src/server/index.ts | Tipo | Color | Uso | |---|---|---| -| HTTP request | `#3b82f6` azul | Partículas de requests | -| Cache | `#f59e0b` amarillo | Redis hit/miss/invalidate | -| Database | `#8b5cf6` violeta | Queries a PostgreSQL/MySQL | -| Push/WebSocket | `#22c55e` verde | Notificaciones, eventos real-time | -| Email/external | `#ef4444` rojo | Envíos de email, llamadas API externas | -| Background job | `#ec4899` rosa | Celery, cron, workers | -| Docker event | `#64748b` gris | Container start/stop/restart | | 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 | --- @@ -1199,8 +907,6 @@ El backend siempre envía el campo `project` en cada servicio. El frontend tiene | `claude mcp add` | Trivial | 10s | | "arranca el dashboard" | Trivial | 5s | | Ver arquitectura + stats | Automático | 0s (auto-discovery) | -| Definir flows custom | Fácil (YAML o via MCP) | 2-5 min | -| Conectar probes reales | Intermedio (middleware) | 15-30 min | **Requisitos del usuario:** - Docker instalado y corriendo @@ -1223,31 +929,21 @@ El backend siempre envía el campo `project` en cada servicio. El frontend tiene - [ ] Docker events (start/stop/restart) como pulso en nodos - [ ] Dark theme, minimap, zoom, pan -### Fase 2 — Flows (partículas) -- [ ] Flow engine con partículas SVG animadas -- [ ] flows.yaml parser -- [ ] Panel de control de flows (toggle, simulate) -- [ ] Efecto glow/neón en partículas -- [ ] Trail (estela) detrás de cada partícula - -### Fase 3 — MCP -- [ ] MCP server con tools de Docker + flows +### Fase 2 — MCP +- [ ] MCP server con tools de Docker - [ ] start_dashboard, list_services, get_stats, get_logs, restart -- [ ] list_flows, simulate_flow, create_flow -- [ ] Resources: docker://services, docker://flows +- [ ] inspect_service, get_networks +- [ ] Resources: docker://services - [ ] README con instrucciones de setup -### Fase 4 — Polish +### Fase 3 — Polish - [ ] Click en nodo → panel lateral con logs en vivo -- [ ] Modo "demo" / auto-simulate para presentaciones +- [ ] Notificaciones y alertas - [ ] Responsive (funcione en tablet) - [ ] Export PNG/SVG del grafo actual - [ ] Customizar posiciones de nodos (drag + guardar layout) -### Fase 5 — Avanzado -- [ ] Probe middleware genérico (npm package para Express/Fastify/Hono) -- [ ] Probe middleware genérico (pip package para FastAPI/Django) -- [ ] Eventos reales → partículas automáticas +### 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) diff --git a/mejoras.md b/mejoras.md index 737f815..d9f1bff 100644 --- a/mejoras.md +++ b/mejoras.md @@ -42,7 +42,7 @@ | Ejemplo | URL | Aplicacion en Flowteon | |---------|-----|------------------------| -| Animating Edges | https://reactflow.dev/examples/edges/animating-edges | Mejorar animaciones de particulas en edges | +| Animating Edges | https://reactflow.dev/examples/edges/animating-edges | Animaciones en edges para indicar actividad | | Custom Edges | https://reactflow.dev/examples/edges/custom-edges | Ya usamos OffsetEdge custom | | Edge Label Renderer | https://reactflow.dev/examples/edges/edge-label-renderer | Mostrar tipo de conexion (db, cache, proxy) como label en el edge | | Floating Edges | https://reactflow.dev/examples/edges/floating-edges | Handles que se mueven con el edge en vez de posicion fija | diff --git a/src/client/App.tsx b/src/client/App.tsx index b14a712..b6f985a 100644 --- a/src/client/App.tsx +++ b/src/client/App.tsx @@ -436,17 +436,7 @@ function Dashboard({ token }: { token: string }) { totalStats={totalStats} /> - {/* Loading skeleton */} - {services.length === 0 && ( -
- {[1, 2, 3].map((i) => ( -
- ))} -
- )} - {/* Canvas — inset */} - {services.length > 0 && (
{ reactFlowRef.current = instance; }} @@ -540,7 +530,6 @@ function Dashboard({ token }: { token: string }) { /> )}
- )}
); diff --git a/src/client/index.css b/src/client/index.css index 7d0c712..226f30d 100644 --- a/src/client/index.css +++ b/src/client/index.css @@ -18,6 +18,7 @@ overflow: hidden !important; } + .react-flow__controls { border: 1px solid #1e293b !important; border-radius: 8px !important;