Add port 3000 mapping to docker-compose

This commit is contained in:
Erwin
2026-03-27 20:31:23 +00:00
parent a65a973310
commit 978be535d3
4 changed files with 99 additions and 60 deletions

View File

@@ -5,6 +5,8 @@ services:
dockerfile: Dockerfile
container_name: mission-control-ui
restart: unless-stopped
ports:
- "3000:80"
labels:
- "traefik.enable=true"
- "traefik.http.routers.mission-control-ui.rule=Host(`mission-control.danielarroyo.cl`)"

View File

@@ -60,7 +60,7 @@ export function MissionControlDashboard() {
{/* Status Bar */}
<StatusBar
agents={agents}
health={health}
health={health ?? undefined}
lastUpdated={lastUpdated}
loading={loading}
error={error}

View File

@@ -1,26 +1,36 @@
import { useState, useEffect, useCallback, useRef } from 'react';
import { openclawClient } from '../lib/openclaw-api';
import type { Agent, TimelineEvent } from '../types/agent';
import type { Agent, Connection, TimelineEvent } from '../types/agent';
interface UseOpenClawOptions {
pollInterval?: number;
gatewayUrl?: string;
token?: string;
}
interface UseOpenClawReturn {
agents: Agent[];
connections: Connection[];
events: TimelineEvent[];
health: { status: string; uptime: number; sessions: number } | null;
isLoading: boolean;
lastUpdated: Date | null;
loading: boolean;
error: string | null;
lastUpdate: Date | null;
refresh: () => void;
connected: boolean;
refetch: () => void;
}
const POLLING_INTERVAL = 30000;
export function useOpenClaw(options: UseOpenClawOptions = {}): UseOpenClawReturn {
const { pollInterval = 30000 } = options;
export function useOpenClaw(): UseOpenClawReturn {
const [agents, setAgents] = useState<Agent[]>([]);
const [connections] = useState<Connection[]>([]);
const [events, setEvents] = useState<TimelineEvent[]>([]);
const [health, setHealth] = useState<{ status: string; uptime: number; sessions: number } | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [lastUpdate, setLastUpdate] = useState<Date | null>(null);
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
const [connected, setConnected] = useState(false);
const intervalRef = useRef<number | null>(null);
const isMountedRef = useRef(true);
@@ -37,19 +47,21 @@ export function useOpenClaw(): UseOpenClawReturn {
setAgents(agentsData);
setEvents(eventsData);
setHealth(healthData);
setConnected(true);
setError(null);
setLastUpdate(new Date());
setIsLoading(false);
setLastUpdated(new Date());
setLoading(false);
}
} catch (err) {
if (isMountedRef.current) {
setError(err instanceof Error ? err.message : 'Failed to connect to gateway');
setIsLoading(false);
setConnected(false);
setLoading(false);
}
}
}, []);
const refresh = useCallback(() => {
const refetch = useCallback(() => {
fetchData();
}, [fetchData]);
@@ -57,7 +69,7 @@ export function useOpenClaw(): UseOpenClawReturn {
isMountedRef.current = true;
fetchData();
intervalRef.current = window.setInterval(fetchData, POLLING_INTERVAL);
intervalRef.current = window.setInterval(fetchData, pollInterval);
return () => {
isMountedRef.current = false;
@@ -66,15 +78,17 @@ export function useOpenClaw(): UseOpenClawReturn {
intervalRef.current = null;
}
};
}, [fetchData]);
}, [fetchData, pollInterval]);
return {
agents,
connections,
events,
health,
isLoading,
lastUpdated,
loading,
error,
lastUpdate,
refresh,
connected,
refetch,
};
}

View File

@@ -1,4 +1,4 @@
import type { Agent, Connection, TimelineEvent } from '../types/agent';
import type { Agent, TimelineEvent, AgentRole, AgentStatus } from '../types/agent';
const GATEWAY_URL = import.meta.env.VITE_OPENCLAW_GATEWAY_URL || 'http://127.0.0.1:18789';
const GATEWAY_TOKEN = import.meta.env.VITE_OPENCLAW_GATEWAY_TOKEN || '';
@@ -19,6 +19,35 @@ interface GatewayAgentsResponse {
}>;
}
const roleMap: Record<string, AgentRole> = {
orchestrator: 'orchestrator',
architect: 'architect',
coder: 'developer',
developer: 'developer',
designer: 'designer',
reviewer: 'reviewer',
assistant: 'assistant',
docs: 'assistant',
};
const statusMap: Record<string, AgentStatus> = {
active: 'active',
busy: 'thinking',
thinking: 'thinking',
blocked: 'blocked',
idle: 'idle',
completed: 'completed',
};
const agentPositions = [
{ id: 'erwin', x: 10, y: 38 },
{ id: 'bulma', x: 36, y: 12 },
{ id: 'rocket', x: 36, y: 64 },
{ id: 'sherlock', x: 66, y: 24 },
{ id: 'hiro', x: 66, y: 64 },
{ id: 'claudia', x: 66, y: 50 },
];
export class OpenClawGatewayClient {
private baseUrl: string;
private token: string;
@@ -53,16 +82,25 @@ export class OpenClawGatewayClient {
async fetchAgents(): Promise<Agent[]> {
try {
const data = await this.fetch<GatewayAgentsResponse>('/api/agents');
return data.agents.map((agent, index) => ({
return data.agents.map((agent) => {
const pos = agentPositions.find(p => p.id === agent.id) || { x: 50, y: 50 };
const role = roleMap[agent.id] || 'assistant';
const status = statusMap[agent.status] || 'idle';
return {
id: agent.id,
name: agent.name,
role: this.getRoleFromAgentId(agent.id),
status: agent.status === 'active' ? 'active' : agent.status === 'busy' ? 'busy' : 'idle',
queue: 0,
icon: this.getIconFromIndex(index),
x: this.getAgentX(index),
y: this.getAgentY(index),
}));
role,
status,
position: { x: pos.x, y: pos.y },
connections: this.getDefaultConnections(agent.id),
stats: {
tasksCompleted: 0,
tasksInProgress: 0,
tasksBlocked: 0,
},
};
});
} catch {
return this.getDefaultAgents();
}
@@ -77,41 +115,26 @@ export class OpenClawGatewayClient {
}
}
private getRoleFromAgentId(id: string): string {
const roles: Record<string, string> = {
orchestrator: 'Coordina flujo y delegación',
architect: 'Define solución y tareas',
coder: 'Implementa cambios',
reviewer: 'Revisa calidad y riesgos',
docs: 'Documenta decisiones y cambios',
assistant: 'Asistente general',
private getDefaultConnections(agentId: string): string[] {
const connectionsMap: Record<string, string[]> = {
erwin: ['bulma', 'rocket', 'sherlock', 'claudia'],
bulma: ['rocket', 'sherlock'],
rocket: ['sherlock', 'claudia'],
sherlock: ['claudia', 'erwin'],
hiro: ['erwin'],
claudia: ['erwin'],
};
return roles[id] || 'Agente';
}
private getIconFromIndex(index: number) {
const icons = ['Workflow', 'Layers3', 'Bot', 'ShieldCheck', 'FileText', 'Activity'];
return icons[index % icons.length];
}
private getAgentX(index: number): number {
const positions = [10, 36, 36, 66, 66, 66];
return positions[index] || 50;
}
private getAgentY(index: number): number {
const positions = [38, 12, 64, 24, 64, 50];
return positions[index] || 50;
return connectionsMap[agentId] || [];
}
private getDefaultAgents(): Agent[] {
return [
{ id: 'erwin', name: 'Erwin', role: 'Orquestador', status: 'active', queue: 0, icon: 'Workflow', x: 10, y: 38 },
{ id: 'bulma', name: 'Bulma', role: 'Arquitecto', status: 'active', queue: 0, icon: 'Layers3', x: 36, y: 12 },
{ id: 'rocket', name: 'Rocket', role: 'Desarrollador', status: 'busy', queue: 0, icon: 'Bot', x: 36, y: 64 },
{ id: 'sherlock', name: 'Sherlock', role: 'Revisor', status: 'idle', queue: 0, icon: 'ShieldCheck', x: 66, y: 24 },
{ id: 'hiro', name: 'Hiro', role: 'Diseñador', status: 'idle', queue: 0, icon: 'FileText', x: 66, y: 64 },
{ id: 'claudia', name: 'Claudia', role: 'Asistente', status: 'idle', queue: 0, icon: 'Activity', x: 66, y: 50 },
{ id: 'erwin', name: 'Erwin', role: 'orchestrator', status: 'active', position: { x: 10, y: 38 }, connections: ['bulma', 'rocket'], stats: { tasksCompleted: 0, tasksInProgress: 0, tasksBlocked: 0 } },
{ id: 'bulma', name: 'Bulma', role: 'architect', status: 'active', position: { x: 36, y: 12 }, connections: ['rocket'], stats: { tasksCompleted: 0, tasksInProgress: 0, tasksBlocked: 0 } },
{ id: 'rocket', name: 'Rocket', role: 'developer', status: 'thinking', position: { x: 36, y: 64 }, connections: ['sherlock'], stats: { tasksCompleted: 0, tasksInProgress: 0, tasksBlocked: 0 } },
{ id: 'sherlock', name: 'Sherlock', role: 'reviewer', status: 'idle', position: { x: 66, y: 24 }, connections: ['claudia'], stats: { tasksCompleted: 0, tasksInProgress: 0, tasksBlocked: 0 } },
{ id: 'hiro', name: 'Hiro', role: 'designer', status: 'idle', position: { x: 66, y: 64 }, connections: [], stats: { tasksCompleted: 0, tasksInProgress: 0, tasksBlocked: 0 } },
{ id: 'claudia', name: 'Claudia', role: 'assistant', status: 'idle', position: { x: 66, y: 50 }, connections: ['erwin'], stats: { tasksCompleted: 0, tasksInProgress: 0, tasksBlocked: 0 } },
];
}
}