2636 lines
118 KiB
TypeScript
2636 lines
118 KiB
TypeScript
import { BrowserFrame } from "@/components/Browser/BrowserFrame";
|
|
import { useState, useEffect, type ElementType } from "react";
|
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import {
|
|
Database,
|
|
Plus,
|
|
Trash2,
|
|
Play,
|
|
Settings,
|
|
BookOpen,
|
|
Link,
|
|
CheckCircle,
|
|
XCircle,
|
|
Loader2,
|
|
Clock,
|
|
Zap,
|
|
Package,
|
|
ExternalLink,
|
|
Search,
|
|
Brain,
|
|
Code,
|
|
Network,
|
|
Boxes,
|
|
ArrowRight,
|
|
MessageCircle,
|
|
BarChart3,
|
|
Compass,
|
|
Handshake,
|
|
Factory,
|
|
Headphones,
|
|
Calculator,
|
|
Layers,
|
|
Home,
|
|
Bot,
|
|
Globe,
|
|
Workflow,
|
|
Palette,
|
|
Building2,
|
|
Users,
|
|
Crown,
|
|
UserCog,
|
|
DollarSign,
|
|
Edit,
|
|
ChevronDown,
|
|
ChevronRight,
|
|
Receipt,
|
|
Wallet,
|
|
Hash,
|
|
Plug,
|
|
Share2,
|
|
ShoppingCart,
|
|
FileText,
|
|
Store,
|
|
GraduationCap,
|
|
Server,
|
|
Terminal,
|
|
Layout,
|
|
Blocks,
|
|
ThumbsUp,
|
|
Shield,
|
|
HardDrive,
|
|
Wrench,
|
|
ClipboardCheck,
|
|
PenTool,
|
|
MapPin,
|
|
Github,
|
|
Activity,
|
|
Gauge,
|
|
Signal,
|
|
Square,
|
|
AlertCircle,
|
|
RefreshCw,
|
|
Cpu,
|
|
Sparkles,
|
|
ArrowDown,
|
|
} from "lucide-react";
|
|
import { useLocation } from "wouter";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Switch } from "@/components/ui/switch";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogDescription,
|
|
} from "@/components/ui/dialog";
|
|
|
|
interface ErpConnection {
|
|
id: number;
|
|
name: string;
|
|
type: string;
|
|
baseUrl: string;
|
|
isActive: string;
|
|
createdAt: string;
|
|
}
|
|
|
|
interface AgentTask {
|
|
id: number;
|
|
name: string;
|
|
type: string;
|
|
schedule: string | null;
|
|
erpConnectionId: number | null;
|
|
config: string | null;
|
|
status: string | null;
|
|
lastRun: string | null;
|
|
nextRun: string | null;
|
|
createdAt: string;
|
|
}
|
|
|
|
interface KnowledgeBaseEntry {
|
|
id: number;
|
|
title: string;
|
|
content: string;
|
|
author: string;
|
|
category: string;
|
|
source: string | null;
|
|
createdAt: string;
|
|
}
|
|
|
|
interface LibraryPackage {
|
|
name: string;
|
|
version: string;
|
|
type: string;
|
|
category: string;
|
|
}
|
|
|
|
interface LibrariesData {
|
|
nodejs: {
|
|
dependencies: LibraryPackage[];
|
|
devDependencies: LibraryPackage[];
|
|
total: number;
|
|
};
|
|
python: {
|
|
dependencies: LibraryPackage[];
|
|
total: number;
|
|
note?: string;
|
|
};
|
|
}
|
|
|
|
async function fetchConnections(): Promise<ErpConnection[]> {
|
|
const response = await fetch("/api/soe/connections", { credentials: "include" });
|
|
if (!response.ok) throw new Error("Failed to fetch connections");
|
|
return response.json();
|
|
}
|
|
|
|
async function fetchTasks(): Promise<AgentTask[]> {
|
|
const response = await fetch("/api/soe/tasks", { credentials: "include" });
|
|
if (!response.ok) throw new Error("Failed to fetch tasks");
|
|
return response.json();
|
|
}
|
|
|
|
async function fetchKnowledgeBase(): Promise<KnowledgeBaseEntry[]> {
|
|
const response = await fetch("/api/knowledge-base", { credentials: "include" });
|
|
if (!response.ok) throw new Error("Failed to fetch knowledge base");
|
|
return response.json();
|
|
}
|
|
|
|
async function fetchLibraries(): Promise<LibrariesData> {
|
|
const response = await fetch("/api/admin/libraries", { credentials: "include" });
|
|
if (!response.ok) throw new Error("Failed to fetch libraries");
|
|
return response.json();
|
|
}
|
|
|
|
export default function Admin() {
|
|
const queryClient = useQueryClient();
|
|
const [activeTab, setActiveTab] = useState("connections");
|
|
|
|
const [newConnection, setNewConnection] = useState({
|
|
name: "",
|
|
type: "arcadia_plus",
|
|
baseUrl: "",
|
|
apiKey: "",
|
|
apiSecret: "",
|
|
});
|
|
|
|
const [newTask, setNewTask] = useState({
|
|
name: "",
|
|
type: "financial_analysis",
|
|
schedule: "",
|
|
erpConnectionId: "",
|
|
});
|
|
|
|
const [newKbEntry, setNewKbEntry] = useState({
|
|
title: "",
|
|
content: "",
|
|
author: "",
|
|
category: "tributacao",
|
|
source: "",
|
|
});
|
|
|
|
const { data: connections = [], isLoading: loadingConnections } = useQuery({
|
|
queryKey: ["erp-connections"],
|
|
queryFn: fetchConnections,
|
|
});
|
|
|
|
const { data: tasks = [], isLoading: loadingTasks } = useQuery({
|
|
queryKey: ["erp-tasks"],
|
|
queryFn: fetchTasks,
|
|
});
|
|
|
|
const { data: knowledgeBase = [], isLoading: loadingKb } = useQuery({
|
|
queryKey: ["knowledge-base"],
|
|
queryFn: fetchKnowledgeBase,
|
|
});
|
|
|
|
const { data: libraries, isLoading: loadingLibraries } = useQuery({
|
|
queryKey: ["admin-libraries"],
|
|
queryFn: fetchLibraries,
|
|
});
|
|
|
|
const [librarySearch, setLibrarySearch] = useState("");
|
|
const [libraryFilter, setLibraryFilter] = useState("all");
|
|
|
|
const createConnectionMutation = useMutation({
|
|
mutationFn: async (data: typeof newConnection) => {
|
|
const response = await fetch("/api/soe/connections", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(data),
|
|
credentials: "include",
|
|
});
|
|
if (!response.ok) throw new Error("Failed to create connection");
|
|
return response.json();
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["erp-connections"] });
|
|
setNewConnection({ name: "", type: "arcadia_plus", baseUrl: "", apiKey: "", apiSecret: "" });
|
|
},
|
|
});
|
|
|
|
const testConnectionMutation = useMutation({
|
|
mutationFn: async (id: number) => {
|
|
const response = await fetch(`/api/soe/connections/${id}/test`, {
|
|
method: "POST",
|
|
credentials: "include",
|
|
});
|
|
return response.json();
|
|
},
|
|
});
|
|
|
|
const deleteConnectionMutation = useMutation({
|
|
mutationFn: async (id: number) => {
|
|
await fetch(`/api/soe/connections/${id}`, {
|
|
method: "DELETE",
|
|
credentials: "include",
|
|
});
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["erp-connections"] });
|
|
},
|
|
});
|
|
|
|
const createTaskMutation = useMutation({
|
|
mutationFn: async (data: typeof newTask) => {
|
|
const response = await fetch("/api/soe/tasks", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
...data,
|
|
erpConnectionId: data.erpConnectionId ? parseInt(data.erpConnectionId) : null,
|
|
}),
|
|
credentials: "include",
|
|
});
|
|
if (!response.ok) throw new Error("Failed to create task");
|
|
return response.json();
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["erp-tasks"] });
|
|
setNewTask({ name: "", type: "financial_analysis", schedule: "", erpConnectionId: "" });
|
|
},
|
|
});
|
|
|
|
const executeTaskMutation = useMutation({
|
|
mutationFn: async (id: number) => {
|
|
const response = await fetch(`/api/soe/tasks/${id}/execute`, {
|
|
method: "POST",
|
|
credentials: "include",
|
|
});
|
|
return response.json();
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["erp-tasks"] });
|
|
},
|
|
});
|
|
|
|
const deleteTaskMutation = useMutation({
|
|
mutationFn: async (id: number) => {
|
|
await fetch(`/api/soe/tasks/${id}`, {
|
|
method: "DELETE",
|
|
credentials: "include",
|
|
});
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["erp-tasks"] });
|
|
},
|
|
});
|
|
|
|
const createKbMutation = useMutation({
|
|
mutationFn: async (data: typeof newKbEntry) => {
|
|
const response = await fetch("/api/knowledge-base", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(data),
|
|
credentials: "include",
|
|
});
|
|
if (!response.ok) throw new Error("Failed to create knowledge base entry");
|
|
return response.json();
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["knowledge-base"] });
|
|
setNewKbEntry({ title: "", content: "", author: "", category: "tributacao", source: "" });
|
|
},
|
|
});
|
|
|
|
const deleteKbMutation = useMutation({
|
|
mutationFn: async (id: number) => {
|
|
await fetch(`/api/knowledge-base/${id}`, {
|
|
method: "DELETE",
|
|
credentials: "include",
|
|
});
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["knowledge-base"] });
|
|
},
|
|
});
|
|
|
|
const taskTypes = [
|
|
{ value: "financial_analysis", label: "Análise Financeira" },
|
|
{ value: "inventory_monitoring", label: "Monitoramento de Estoque" },
|
|
{ value: "sales_report", label: "Relatório de Vendas" },
|
|
{ value: "payables_alert", label: "Alertas de Contas a Pagar" },
|
|
{ value: "receivables_alert", label: "Alertas de Contas a Receber" },
|
|
];
|
|
|
|
const kbCategories = [
|
|
{ value: "tributacao", label: "Tributação" },
|
|
{ value: "juridico", label: "Jurídico" },
|
|
{ value: "contabil", label: "Contábil" },
|
|
{ value: "financeiro", label: "Financeiro" },
|
|
{ value: "processos", label: "Processos" },
|
|
{ value: "politicas", label: "Políticas" },
|
|
];
|
|
|
|
return (
|
|
<BrowserFrame>
|
|
<div className="h-full w-full bg-slate-50 p-6 overflow-auto">
|
|
<div className="max-w-6xl mx-auto">
|
|
<div className="flex items-center gap-3 mb-6">
|
|
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-primary to-blue-600 flex items-center justify-center">
|
|
<Settings className="w-6 h-6 text-white" />
|
|
</div>
|
|
<div>
|
|
<h1 className="text-2xl font-semibold text-slate-800">Administração</h1>
|
|
<p className="text-slate-500 text-sm">Gerencie conexões SOE, tarefas e base de conhecimento</p>
|
|
</div>
|
|
</div>
|
|
|
|
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
|
<TabsList className="mb-6">
|
|
<TabsTrigger value="connections" className="flex items-center gap-2">
|
|
<Database className="w-4 h-4" />
|
|
Conexões SOE
|
|
</TabsTrigger>
|
|
<TabsTrigger value="tasks" className="flex items-center gap-2">
|
|
<Zap className="w-4 h-4" />
|
|
Tarefas Autônomas
|
|
</TabsTrigger>
|
|
<TabsTrigger value="knowledge" className="flex items-center gap-2">
|
|
<BookOpen className="w-4 h-4" />
|
|
Base de Conhecimento
|
|
</TabsTrigger>
|
|
<TabsTrigger value="libraries" className="flex items-center gap-2" data-testid="tab-libraries">
|
|
<Package className="w-4 h-4" />
|
|
Bibliotecas
|
|
</TabsTrigger>
|
|
<TabsTrigger value="modules" className="flex items-center gap-2" data-testid="tab-modules">
|
|
<Boxes className="w-4 h-4" />
|
|
Módulos
|
|
</TabsTrigger>
|
|
<TabsTrigger value="machine-house" className="flex items-center gap-2" data-testid="tab-machine-house">
|
|
<Factory className="w-4 h-4" />
|
|
Casa de Máquinas
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="connections">
|
|
<div className="grid gap-6 md:grid-cols-2">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-lg">Nova Conexão Motor</CardTitle>
|
|
<CardDescription>Conecte ao Arcádia Plus ou ERPNext</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div>
|
|
<Label>Nome da Conexão</Label>
|
|
<Input
|
|
value={newConnection.name}
|
|
onChange={(e) => setNewConnection({ ...newConnection, name: e.target.value })}
|
|
placeholder="Ex: Produção Arcadia Plus"
|
|
data-testid="input-connection-name"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label>Tipo de Motor</Label>
|
|
<Select
|
|
value={newConnection.type}
|
|
onValueChange={(value) => setNewConnection({ ...newConnection, type: value })}
|
|
>
|
|
<SelectTrigger data-testid="select-erp-type">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="arcadia_plus">Arcadia Plus (REST API)</SelectItem>
|
|
<SelectItem value="arcadia_next">ERPNext (Frappe)</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div>
|
|
<Label>URL Base da API</Label>
|
|
<Input
|
|
value={newConnection.baseUrl}
|
|
onChange={(e) => setNewConnection({ ...newConnection, baseUrl: e.target.value })}
|
|
placeholder="https://api.arcadiaplus.com.br"
|
|
data-testid="input-base-url"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label>API Key</Label>
|
|
<Input
|
|
type="password"
|
|
value={newConnection.apiKey}
|
|
onChange={(e) => setNewConnection({ ...newConnection, apiKey: e.target.value })}
|
|
placeholder="Sua chave de API"
|
|
data-testid="input-api-key"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label>API Secret</Label>
|
|
<Input
|
|
type="password"
|
|
value={newConnection.apiSecret}
|
|
onChange={(e) => setNewConnection({ ...newConnection, apiSecret: e.target.value })}
|
|
placeholder="Seu secret"
|
|
data-testid="input-api-secret"
|
|
/>
|
|
</div>
|
|
<Button
|
|
onClick={() => createConnectionMutation.mutate(newConnection)}
|
|
disabled={createConnectionMutation.isPending || !newConnection.name || !newConnection.baseUrl}
|
|
className="w-full"
|
|
data-testid="button-create-connection"
|
|
>
|
|
{createConnectionMutation.isPending ? (
|
|
<Loader2 className="w-4 h-4 animate-spin mr-2" />
|
|
) : (
|
|
<Plus className="w-4 h-4 mr-2" />
|
|
)}
|
|
Criar Conexão
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-lg">Conexões Ativas</CardTitle>
|
|
<CardDescription>{connections.length} conexões configuradas</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<ScrollArea className="h-[400px]">
|
|
{loadingConnections ? (
|
|
<div className="flex items-center justify-center py-8">
|
|
<Loader2 className="w-6 h-6 animate-spin text-primary" />
|
|
</div>
|
|
) : connections.length === 0 ? (
|
|
<div className="text-center py-8 text-slate-400">
|
|
Nenhuma conexão configurada
|
|
</div>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{connections.map((conn) => (
|
|
<div
|
|
key={conn.id}
|
|
className="p-4 bg-slate-50 rounded-lg border"
|
|
data-testid={`connection-${conn.id}`}
|
|
>
|
|
<div className="flex items-center justify-between mb-2">
|
|
<div className="flex items-center gap-2">
|
|
<Link className="w-4 h-4 text-primary" />
|
|
<span className="font-medium">{conn.name}</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => testConnectionMutation.mutate(conn.id)}
|
|
disabled={testConnectionMutation.isPending}
|
|
data-testid={`test-connection-${conn.id}`}
|
|
>
|
|
{testConnectionMutation.isPending ? (
|
|
<Loader2 className="w-3 h-3 animate-spin" />
|
|
) : (
|
|
<Play className="w-3 h-3" />
|
|
)}
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => deleteConnectionMutation.mutate(conn.id)}
|
|
data-testid={`delete-connection-${conn.id}`}
|
|
>
|
|
<Trash2 className="w-3 h-3 text-red-500" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
<div className="text-xs text-slate-500 space-y-1">
|
|
<div>Tipo: {conn.type === "arcadia_plus" ? "Arcádia Plus" : "ERPNext"}</div>
|
|
<div className="truncate">URL: {conn.baseUrl}</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</ScrollArea>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="tasks">
|
|
<div className="grid gap-6 md:grid-cols-2">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-lg">Nova Tarefa Autônoma</CardTitle>
|
|
<CardDescription>Configure tarefas que o agente executará automaticamente</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div>
|
|
<Label>Nome da Tarefa</Label>
|
|
<Input
|
|
value={newTask.name}
|
|
onChange={(e) => setNewTask({ ...newTask, name: e.target.value })}
|
|
placeholder="Ex: Análise Diária de Balanço"
|
|
data-testid="input-task-name"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label>Tipo de Tarefa</Label>
|
|
<Select
|
|
value={newTask.type}
|
|
onValueChange={(value) => setNewTask({ ...newTask, type: value })}
|
|
>
|
|
<SelectTrigger data-testid="select-task-type">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{taskTypes.map((type) => (
|
|
<SelectItem key={type.value} value={type.value}>
|
|
{type.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div>
|
|
<Label>Conexão Motor</Label>
|
|
<Select
|
|
value={newTask.erpConnectionId}
|
|
onValueChange={(value) => setNewTask({ ...newTask, erpConnectionId: value })}
|
|
>
|
|
<SelectTrigger data-testid="select-task-connection">
|
|
<SelectValue placeholder="Selecione uma conexão" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{connections.map((conn) => (
|
|
<SelectItem key={conn.id} value={conn.id.toString()}>
|
|
{conn.name}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div>
|
|
<Label>Agendamento (opcional)</Label>
|
|
<Input
|
|
value={newTask.schedule}
|
|
onChange={(e) => setNewTask({ ...newTask, schedule: e.target.value })}
|
|
placeholder="Ex: 0 8 * * * (diário às 8h)"
|
|
data-testid="input-task-schedule"
|
|
/>
|
|
</div>
|
|
<Button
|
|
onClick={() => createTaskMutation.mutate(newTask)}
|
|
disabled={createTaskMutation.isPending || !newTask.name}
|
|
className="w-full"
|
|
data-testid="button-create-task"
|
|
>
|
|
{createTaskMutation.isPending ? (
|
|
<Loader2 className="w-4 h-4 animate-spin mr-2" />
|
|
) : (
|
|
<Plus className="w-4 h-4 mr-2" />
|
|
)}
|
|
Criar Tarefa
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-lg">Tarefas Configuradas</CardTitle>
|
|
<CardDescription>{tasks.length} tarefas ativas</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<ScrollArea className="h-[400px]">
|
|
{loadingTasks ? (
|
|
<div className="flex items-center justify-center py-8">
|
|
<Loader2 className="w-6 h-6 animate-spin text-primary" />
|
|
</div>
|
|
) : tasks.length === 0 ? (
|
|
<div className="text-center py-8 text-slate-400">
|
|
Nenhuma tarefa configurada
|
|
</div>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{tasks.map((task) => (
|
|
<div
|
|
key={task.id}
|
|
className="p-4 bg-slate-50 rounded-lg border"
|
|
data-testid={`task-${task.id}`}
|
|
>
|
|
<div className="flex items-center justify-between mb-2">
|
|
<div className="flex items-center gap-2">
|
|
<Zap className="w-4 h-4 text-amber-500" />
|
|
<span className="font-medium">{task.name}</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => executeTaskMutation.mutate(task.id)}
|
|
disabled={executeTaskMutation.isPending}
|
|
data-testid={`execute-task-${task.id}`}
|
|
>
|
|
{executeTaskMutation.isPending ? (
|
|
<Loader2 className="w-3 h-3 animate-spin" />
|
|
) : (
|
|
<Play className="w-3 h-3" />
|
|
)}
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => deleteTaskMutation.mutate(task.id)}
|
|
data-testid={`delete-task-${task.id}`}
|
|
>
|
|
<Trash2 className="w-3 h-3 text-red-500" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
<div className="text-xs text-slate-500 space-y-1">
|
|
<div>Tipo: {taskTypes.find(t => t.value === task.type)?.label || task.type}</div>
|
|
{task.lastRun && (
|
|
<div className="flex items-center gap-1">
|
|
<Clock className="w-3 h-3" />
|
|
Última execução: {new Date(task.lastRun).toLocaleString("pt-BR")}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</ScrollArea>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="knowledge">
|
|
<div className="grid gap-6 md:grid-cols-2">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-lg">Novo Documento</CardTitle>
|
|
<CardDescription>Adicione conhecimento à Inteligência Arcádia Business</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div>
|
|
<Label>Título</Label>
|
|
<Input
|
|
value={newKbEntry.title}
|
|
onChange={(e) => setNewKbEntry({ ...newKbEntry, title: e.target.value })}
|
|
placeholder="Ex: Guia de ICMS para Comércio Eletrônico"
|
|
data-testid="input-kb-title"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label>Autor</Label>
|
|
<Input
|
|
value={newKbEntry.author}
|
|
onChange={(e) => setNewKbEntry({ ...newKbEntry, author: e.target.value })}
|
|
placeholder="Ex: Dr. João Silva"
|
|
data-testid="input-kb-author"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label>Categoria</Label>
|
|
<Select
|
|
value={newKbEntry.category}
|
|
onValueChange={(value) => setNewKbEntry({ ...newKbEntry, category: value })}
|
|
>
|
|
<SelectTrigger data-testid="select-kb-category">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{kbCategories.map((cat) => (
|
|
<SelectItem key={cat.value} value={cat.value}>
|
|
{cat.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div>
|
|
<Label>Fonte (opcional)</Label>
|
|
<Input
|
|
value={newKbEntry.source}
|
|
onChange={(e) => setNewKbEntry({ ...newKbEntry, source: e.target.value })}
|
|
placeholder="Ex: Decreto 1234/2024"
|
|
data-testid="input-kb-source"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label>Conteúdo</Label>
|
|
<Textarea
|
|
value={newKbEntry.content}
|
|
onChange={(e) => setNewKbEntry({ ...newKbEntry, content: e.target.value })}
|
|
placeholder="Conteúdo completo do documento..."
|
|
rows={8}
|
|
data-testid="input-kb-content"
|
|
/>
|
|
</div>
|
|
<Button
|
|
onClick={() => createKbMutation.mutate(newKbEntry)}
|
|
disabled={createKbMutation.isPending || !newKbEntry.title || !newKbEntry.content || !newKbEntry.author}
|
|
className="w-full"
|
|
data-testid="button-create-kb"
|
|
>
|
|
{createKbMutation.isPending ? (
|
|
<Loader2 className="w-4 h-4 animate-spin mr-2" />
|
|
) : (
|
|
<Plus className="w-4 h-4 mr-2" />
|
|
)}
|
|
Adicionar Documento
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-lg">Inteligência Arcádia Business</CardTitle>
|
|
<CardDescription>{knowledgeBase.length} documentos na base</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<ScrollArea className="h-[500px]">
|
|
{loadingKb ? (
|
|
<div className="flex items-center justify-center py-8">
|
|
<Loader2 className="w-6 h-6 animate-spin text-primary" />
|
|
</div>
|
|
) : knowledgeBase.length === 0 ? (
|
|
<div className="text-center py-8 text-slate-400">
|
|
Nenhum documento na base
|
|
</div>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{knowledgeBase.map((entry) => (
|
|
<div
|
|
key={entry.id}
|
|
className="p-4 bg-slate-50 rounded-lg border"
|
|
data-testid={`kb-entry-${entry.id}`}
|
|
>
|
|
<div className="flex items-center justify-between mb-2">
|
|
<div className="flex items-center gap-2">
|
|
<BookOpen className="w-4 h-4 text-primary" />
|
|
<span className="font-medium">{entry.title}</span>
|
|
</div>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => deleteKbMutation.mutate(entry.id)}
|
|
data-testid={`delete-kb-${entry.id}`}
|
|
>
|
|
<Trash2 className="w-3 h-3 text-red-500" />
|
|
</Button>
|
|
</div>
|
|
<div className="text-xs text-slate-500 space-y-1">
|
|
<div>Autor: {entry.author}</div>
|
|
<div>Categoria: {kbCategories.find(c => c.value === entry.category)?.label || entry.category}</div>
|
|
{entry.source && <div>Fonte: {entry.source}</div>}
|
|
<div className="mt-2 text-slate-600 line-clamp-2">{entry.content}</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</ScrollArea>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="libraries">
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h2 className="text-lg font-semibold">Bibliotecas do Sistema</h2>
|
|
<p className="text-sm text-slate-500">
|
|
{libraries?.nodejs?.total ?? 0} pacotes Node.js instalados
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-400" />
|
|
<Input
|
|
placeholder="Buscar biblioteca..."
|
|
value={librarySearch}
|
|
onChange={(e) => setLibrarySearch(e.target.value)}
|
|
className="pl-9 w-64"
|
|
data-testid="input-library-search"
|
|
/>
|
|
</div>
|
|
<Select value={libraryFilter} onValueChange={setLibraryFilter}>
|
|
<SelectTrigger className="w-40" data-testid="select-library-filter">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">Todas</SelectItem>
|
|
<SelectItem value="production">Produção</SelectItem>
|
|
<SelectItem value="development">Desenvolvimento</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid gap-6 md:grid-cols-2">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-lg flex items-center gap-2">
|
|
<Package className="h-5 w-5 text-green-600" />
|
|
Node.js - Produção
|
|
</CardTitle>
|
|
<CardDescription>
|
|
{libraries?.nodejs?.dependencies?.length ?? 0} dependências de produção
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<ScrollArea className="h-[400px]">
|
|
{loadingLibraries ? (
|
|
<div className="flex items-center justify-center py-8">
|
|
<Loader2 className="w-6 h-6 animate-spin text-primary" />
|
|
</div>
|
|
) : (
|
|
<div className="space-y-2">
|
|
{(libraries?.nodejs?.dependencies ?? [])
|
|
.filter(pkg =>
|
|
librarySearch === "" ||
|
|
pkg.name.toLowerCase().includes(librarySearch.toLowerCase())
|
|
)
|
|
.filter(pkg => libraryFilter === "all" || libraryFilter === "production")
|
|
.map((pkg) => (
|
|
<div
|
|
key={pkg.name}
|
|
className="flex items-center justify-between p-3 bg-slate-50 rounded-lg border hover:bg-slate-100 transition-colors"
|
|
data-testid={`lib-${pkg.name}`}
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<Package className="h-4 w-4 text-slate-500" />
|
|
<div>
|
|
<div className="font-medium text-sm">{pkg.name}</div>
|
|
<div className="text-xs text-slate-500">{pkg.category}</div>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Badge variant="secondary" className="text-xs">
|
|
v{pkg.version}
|
|
</Badge>
|
|
<a
|
|
href={`https://www.npmjs.com/package/${pkg.name}`}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-slate-400 hover:text-primary"
|
|
>
|
|
<ExternalLink className="h-4 w-4" />
|
|
</a>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</ScrollArea>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-lg flex items-center gap-2">
|
|
<Package className="h-5 w-5 text-blue-600" />
|
|
Node.js - Desenvolvimento
|
|
</CardTitle>
|
|
<CardDescription>
|
|
{libraries?.nodejs?.devDependencies?.length ?? 0} dependências de desenvolvimento
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<ScrollArea className="h-[400px]">
|
|
{loadingLibraries ? (
|
|
<div className="flex items-center justify-center py-8">
|
|
<Loader2 className="w-6 h-6 animate-spin text-primary" />
|
|
</div>
|
|
) : (
|
|
<div className="space-y-2">
|
|
{(libraries?.nodejs?.devDependencies ?? [])
|
|
.filter(pkg =>
|
|
librarySearch === "" ||
|
|
pkg.name.toLowerCase().includes(librarySearch.toLowerCase())
|
|
)
|
|
.filter(pkg => libraryFilter === "all" || libraryFilter === "development")
|
|
.map((pkg) => (
|
|
<div
|
|
key={pkg.name}
|
|
className="flex items-center justify-between p-3 bg-slate-50 rounded-lg border hover:bg-slate-100 transition-colors"
|
|
data-testid={`lib-dev-${pkg.name}`}
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<Package className="h-4 w-4 text-slate-500" />
|
|
<div>
|
|
<div className="font-medium text-sm">{pkg.name}</div>
|
|
<div className="text-xs text-slate-500">{pkg.category}</div>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Badge variant="secondary" className="text-xs bg-blue-100 text-blue-600">
|
|
v{pkg.version}
|
|
</Badge>
|
|
<a
|
|
href={`https://www.npmjs.com/package/${pkg.name}`}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-slate-400 hover:text-primary"
|
|
>
|
|
<ExternalLink className="h-4 w-4" />
|
|
</a>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</ScrollArea>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-lg flex items-center gap-2">
|
|
<Package className="h-5 w-5 text-yellow-600" />
|
|
Python Microservices
|
|
</CardTitle>
|
|
<CardDescription>
|
|
{libraries?.python?.note ?? "Bibliotecas Python para processamento avançado"}
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-center py-8 text-slate-400">
|
|
<Package className="h-12 w-12 mx-auto mb-3 opacity-50" />
|
|
<p className="font-medium">Planejado para próximas versões</p>
|
|
<p className="text-sm mt-2">
|
|
Microserviços Python para: Blockchain/Web3, Cálculos Financeiros, ML/AI, RPA Contábil
|
|
</p>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="modules">
|
|
<ModulesSection />
|
|
</TabsContent>
|
|
|
|
<TabsContent value="machine-house">
|
|
<MachineHouseSection />
|
|
</TabsContent>
|
|
</Tabs>
|
|
</div>
|
|
</div>
|
|
</BrowserFrame>
|
|
);
|
|
}
|
|
|
|
type ModuleCategory = {
|
|
id: string;
|
|
name: string;
|
|
modules: SystemModule[];
|
|
};
|
|
|
|
type SystemModule = {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
icon: ElementType;
|
|
color: string;
|
|
path: string;
|
|
status: "active" | "coming_soon";
|
|
features: string[];
|
|
};
|
|
|
|
function ModulesSection() {
|
|
const [, setLocation] = useLocation();
|
|
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
|
|
const [searchQuery, setSearchQuery] = useState("");
|
|
const queryClient = useQueryClient();
|
|
|
|
const { data: tenantModules } = useQuery({
|
|
queryKey: ["tenant-modules"],
|
|
queryFn: async () => {
|
|
const res = await fetch("/api/soe/tenant/modules", { credentials: "include" });
|
|
if (!res.ok) throw new Error("Failed");
|
|
return res.json();
|
|
}
|
|
});
|
|
|
|
const toggleModule = useMutation({
|
|
mutationFn: async ({ moduleKey, enabled }: { moduleKey: string; enabled: boolean }) => {
|
|
const currentFeatures = tenantModules?.features || {};
|
|
const updatedFeatures = { ...currentFeatures, [moduleKey]: enabled };
|
|
const res = await fetch("/api/soe/tenant/modules", {
|
|
method: "PUT",
|
|
headers: { "Content-Type": "application/json" },
|
|
credentials: "include",
|
|
body: JSON.stringify({ features: updatedFeatures })
|
|
});
|
|
if (!res.ok) throw new Error("Failed");
|
|
return res.json();
|
|
},
|
|
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["tenant-modules"] })
|
|
});
|
|
|
|
const moduleToFeatureKey: Record<string, string> = {
|
|
'agent': 'manus', 'scientist': 'manus', 'knowledge': 'manus',
|
|
'automations': 'manus', 'mcp': 'centralApis', 'a2a': 'centralApis',
|
|
'xos-inbox': 'xosCrm', 'chat': 'comunidades', 'communities': 'comunidades',
|
|
'xos': 'xosCrm', 'nps': 'crm',
|
|
'crm': 'crm', 'compass': 'compass', 'production': 'production',
|
|
'support': 'support', 'valuation': 'bi',
|
|
'fisco': 'fisco', 'contabil': 'fisco', 'people': 'erp',
|
|
'soe': 'erp', 'financeiro': 'erp',
|
|
'retail': 'retail', 'plus': 'plus', 'marketplace': 'erp',
|
|
'insights': 'bi', 'central-apis': 'centralApis',
|
|
'ide': 'ide', 'canvas': 'ide', 'api-hub': 'centralApis',
|
|
'api-tester': 'centralApis', 'doctype-builder': 'ide',
|
|
'page-builder': 'ide', 'dev-center': 'ide', 'engineering': 'erp',
|
|
'lms': 'biblioteca',
|
|
'field-ops': 'erp', 'quality': 'erp', 'technical': 'retail',
|
|
'suppliers': 'erp',
|
|
'admin': 'erp', 'home': 'erp', 'migration': 'erp', 'super-admin': 'erp'
|
|
};
|
|
|
|
const categories: ModuleCategory[] = [
|
|
{
|
|
id: "intelligence",
|
|
name: "🧠 Inteligência & IA",
|
|
modules: [
|
|
{
|
|
id: "agent",
|
|
name: "Arcádia Agent",
|
|
description: "Assistente de IA conversacional com acesso a todas as ferramentas e conhecimento do sistema",
|
|
icon: Bot,
|
|
color: "from-violet-500 to-purple-600",
|
|
path: "/agent",
|
|
status: "active",
|
|
features: ["Chat IA", "Ferramentas integradas", "Contexto do negócio", "Execução de tarefas"]
|
|
},
|
|
{
|
|
id: "scientist",
|
|
name: "Módulo Cientista",
|
|
description: "Inteligência Central com auto-programação - gera código automaticamente, detecta padrões e aprende com o uso",
|
|
icon: Brain,
|
|
color: "from-purple-500 to-pink-500",
|
|
path: "/scientist",
|
|
status: "active",
|
|
features: ["Geração de código", "Detecção de padrões", "Sugestões automáticas", "Aprendizado contínuo"]
|
|
},
|
|
{
|
|
id: "knowledge",
|
|
name: "Grafo de Conhecimento",
|
|
description: "Todos os dados do negócio conectados em um grafo navegável com busca semântica",
|
|
icon: Network,
|
|
color: "from-emerald-500 to-teal-500",
|
|
path: "/knowledge",
|
|
status: "active",
|
|
features: ["Nós e relações", "Busca semântica", "Visualização", "ChromaDB embeddings"]
|
|
},
|
|
{
|
|
id: "automations",
|
|
name: "Automações",
|
|
description: "Workflows automatizados, agendamentos e execução autônoma de tarefas",
|
|
icon: Workflow,
|
|
color: "from-amber-500 to-orange-500",
|
|
path: "/automations",
|
|
status: "active",
|
|
features: ["Workflows BPMN", "Agendamentos", "Triggers", "Aprovações"]
|
|
},
|
|
{
|
|
id: "mcp",
|
|
name: "MCP Server",
|
|
description: "Model Context Protocol - expõe 56 ferramentas do Arcádia para agentes externos via API",
|
|
icon: Plug,
|
|
color: "from-cyan-500 to-blue-600",
|
|
path: "/api-hub?module=mcp",
|
|
status: "active",
|
|
features: ["56 ferramentas", "JSON-RPC", "Descoberta automática", "Integração com IA"]
|
|
},
|
|
{
|
|
id: "a2a",
|
|
name: "A2A Protocol",
|
|
description: "Agent to Agent Protocol - comunicação bidirecional entre agentes de IA com streaming",
|
|
icon: Share2,
|
|
color: "from-pink-500 to-violet-600",
|
|
path: "/api-hub?module=a2a",
|
|
status: "active",
|
|
features: ["Agent Card", "Multi-turn", "SSE Streaming", "Artefatos"]
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: "communication",
|
|
name: "💬 Comunicação",
|
|
modules: [
|
|
{
|
|
id: "xos-inbox",
|
|
name: "XOS Inbox",
|
|
description: "Central Omnichannel de Atendimento - WhatsApp, Email e Chat Web unificados",
|
|
icon: MessageCircle,
|
|
color: "from-green-500 to-emerald-600",
|
|
path: "/xos/inbox",
|
|
status: "active",
|
|
features: ["WhatsApp Business", "Email IMAP/SMTP", "Filas de Atendimento", "Notas Internas", "Mensagens Rápidas"]
|
|
},
|
|
{
|
|
id: "chat",
|
|
name: "Chat Interno (Equipe)",
|
|
description: "Comunicação em tempo real entre membros da equipe interna",
|
|
icon: MessageCircle,
|
|
color: "from-blue-400 to-blue-600",
|
|
path: "/chat",
|
|
status: "active",
|
|
features: ["Mensagens instantâneas", "Status online", "Notificações", "Histórico"]
|
|
},
|
|
{
|
|
id: "communities",
|
|
name: "Comunidades",
|
|
description: "Servidores estilo Discord com canais temáticos, presença online e chat em tempo real para equipes",
|
|
icon: Hash,
|
|
color: "from-indigo-500 to-purple-600",
|
|
path: "/communities",
|
|
status: "active",
|
|
features: ["Servidores", "Canais", "Presença online", "Chat em tempo real", "Permissões por cargo"]
|
|
},
|
|
{
|
|
id: "xos",
|
|
name: "XOS Central",
|
|
description: "Experience Operating System - Central de Marketing, Vendas e Atendimento Omnichannel",
|
|
icon: Layers,
|
|
color: "from-blue-600 to-indigo-600",
|
|
path: "/xos",
|
|
status: "active",
|
|
features: ["CRM", "Inbox", "Tickets", "Campanhas", "Automações", "Sites"]
|
|
},
|
|
{
|
|
id: "nps",
|
|
name: "Pesquisa NPS",
|
|
description: "Net Promoter Score e pesquisas de satisfação do cliente",
|
|
icon: ThumbsUp,
|
|
color: "from-emerald-600 to-teal-600",
|
|
path: "/nps",
|
|
status: "active",
|
|
features: ["NPS", "CSAT", "Pesquisas", "Relatórios"]
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: "business",
|
|
name: "💼 Negócios & Gestão",
|
|
modules: [
|
|
{
|
|
id: "crm",
|
|
name: "Arcádia CRM",
|
|
description: "Gestão completa de clientes, parceiros, leads e pipeline de vendas",
|
|
icon: Handshake,
|
|
color: "from-pink-500 to-rose-500",
|
|
path: "/crm",
|
|
status: "active",
|
|
features: ["Clientes", "Parceiros", "Pipeline", "Contratos"]
|
|
},
|
|
{
|
|
id: "compass",
|
|
name: "Process Compass",
|
|
description: "Gestão de projetos e tarefas com metodologias ágeis e Kanban",
|
|
icon: Compass,
|
|
color: "from-indigo-500 to-blue-600",
|
|
path: "/compass",
|
|
status: "active",
|
|
features: ["Projetos", "Tarefas", "Sprints", "Kanban"]
|
|
},
|
|
{
|
|
id: "production",
|
|
name: "Produção",
|
|
description: "Gestão de squads, sprints e entregas da equipe de desenvolvimento",
|
|
icon: Factory,
|
|
color: "from-slate-500 to-zinc-600",
|
|
path: "/production",
|
|
status: "active",
|
|
features: ["Squads", "Sprints", "Story Points", "Burndown"]
|
|
},
|
|
{
|
|
id: "support",
|
|
name: "Suporte",
|
|
description: "Sistema de tickets de suporte e atendimento ao cliente",
|
|
icon: Headphones,
|
|
color: "from-cyan-500 to-sky-500",
|
|
path: "/support",
|
|
status: "active",
|
|
features: ["Tickets", "SLA", "Base de conhecimento", "Relatórios"]
|
|
},
|
|
{
|
|
id: "valuation",
|
|
name: "Valuation",
|
|
description: "Ferramentas de avaliação financeira e análise de valor empresarial",
|
|
icon: Calculator,
|
|
color: "from-yellow-500 to-amber-500",
|
|
path: "/valuation",
|
|
status: "active",
|
|
features: ["DCF", "Múltiplos", "Comparáveis", "Relatórios"]
|
|
},
|
|
{
|
|
id: "fisco",
|
|
name: "Arcádia Fisco",
|
|
description: "Motor fiscal centralizado para compliance tributário brasileiro - NF-e, NFC-e, NCM, CFOP e Reforma Tributária IBS/CBS",
|
|
icon: Receipt,
|
|
color: "from-emerald-600 to-emerald-800",
|
|
path: "/fisco",
|
|
status: "active",
|
|
features: ["NCM/CEST", "Grupos Tributários", "NF-e/NFC-e", "IBS/CBS", "Certificados A1"]
|
|
},
|
|
{
|
|
id: "contabil",
|
|
name: "Arcádia Contábil",
|
|
description: "Motor contábil centralizado com plano de contas, lançamentos, centros de custo, DRE, Balanço e SPED ECD",
|
|
icon: Calculator,
|
|
color: "from-green-500 to-teal-600",
|
|
path: "/contabil",
|
|
status: "active",
|
|
features: ["Plano de Contas", "Lançamentos", "DRE", "Balanço", "SPED ECD"]
|
|
},
|
|
{
|
|
id: "people",
|
|
name: "Arcádia People",
|
|
description: "Motor de RH centralizado com folha de pagamento, INSS/IRRF 2024, férias, rescisão e eSocial",
|
|
icon: Users,
|
|
color: "from-blue-500 to-indigo-600",
|
|
path: "/people",
|
|
status: "active",
|
|
features: ["Funcionários", "Folha de Pagamento", "Férias", "eSocial", "FGTS"]
|
|
},
|
|
{
|
|
id: "soe",
|
|
name: "Arcádia SOE",
|
|
description: "Sistema Operacional Empresarial - Kernel de negócios com clientes, fornecedores, produtos, pedidos",
|
|
icon: Building2,
|
|
color: "from-purple-500 to-violet-600",
|
|
path: "/soe",
|
|
status: "active",
|
|
features: ["Clientes", "Fornecedores", "Produtos", "Vendas", "Compras"]
|
|
},
|
|
{
|
|
id: "financeiro",
|
|
name: "Arcádia Financeiro",
|
|
description: "Gestão financeira completa: contas a pagar/receber, fluxo de caixa, controle bancário e meios de pagamento",
|
|
icon: Wallet,
|
|
color: "from-green-600 to-emerald-700",
|
|
path: "/financeiro",
|
|
status: "active",
|
|
features: ["Contas a Pagar", "Contas a Receber", "Fluxo de Caixa", "Bancos", "Transferências"]
|
|
},
|
|
{
|
|
id: "retail",
|
|
name: "Arcádia Retail",
|
|
description: "PDV, vendas e gestão de lojas com frente de caixa moderna",
|
|
icon: Store,
|
|
color: "from-orange-500 to-red-500",
|
|
path: "/retail",
|
|
status: "active",
|
|
features: ["PDV", "Vendas", "Caixa", "Estoque", "Clientes"]
|
|
},
|
|
{
|
|
id: "plus",
|
|
name: "Arcádia Plus",
|
|
description: "ERP completo em Laravel com NF-e, NFC-e, PDV, Cardápio Digital e integrações com iFood, Mercado Livre e WooCommerce",
|
|
icon: HardDrive,
|
|
color: "from-violet-600 to-purple-600",
|
|
path: "/plus",
|
|
status: "active",
|
|
features: ["NF-e/NFC-e", "PDV", "Cardápio Digital", "iFood", "Mercado Livre"]
|
|
},
|
|
{
|
|
id: "marketplace",
|
|
name: "Marketplace",
|
|
description: "Loja de módulos, templates e extensões do Arcádia Suite",
|
|
icon: ShoppingCart,
|
|
color: "from-purple-500 to-pink-500",
|
|
path: "/marketplace",
|
|
status: "active",
|
|
features: ["Módulos", "Templates", "Extensões", "Integrações"]
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: "analytics",
|
|
name: "📊 Dados & Analytics",
|
|
modules: [
|
|
{
|
|
id: "insights",
|
|
name: "Arcádia Insights",
|
|
description: "Business Intelligence com dashboards, relatórios e análise de dados",
|
|
icon: BarChart3,
|
|
color: "from-blue-500 to-indigo-600",
|
|
path: "/insights",
|
|
status: "active",
|
|
features: ["Dashboards", "Gráficos", "SQL Editor", "Datasets"]
|
|
},
|
|
{
|
|
id: "central-apis",
|
|
name: "Central de APIs",
|
|
description: "Catálogo de integrações com SEFAZ, bancos, marketplaces e mais",
|
|
icon: Globe,
|
|
color: "from-teal-500 to-cyan-600",
|
|
path: "/central-apis",
|
|
status: "active",
|
|
features: ["SEFAZ NFe/NFSe", "Open Banking", "Marketplaces", "Documentação"]
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: "development",
|
|
name: "🛠️ Desenvolvimento",
|
|
modules: [
|
|
{
|
|
id: "ide",
|
|
name: "IDE Integrada",
|
|
description: "Ambiente de desenvolvimento completo com Monaco Editor, suportando 4 modos: Auto-Code, Full-Code, Low-Code e No-Code",
|
|
icon: Code,
|
|
color: "from-cyan-500 to-blue-500",
|
|
path: "/ide",
|
|
status: "active",
|
|
features: ["Monaco Editor", "Terminal integrado", "Gerenciador de arquivos", "Execução Python/Node.js"]
|
|
},
|
|
{
|
|
id: "canvas",
|
|
name: "Canvas",
|
|
description: "Editor visual para criação de fluxogramas, diagramas e workflows",
|
|
icon: Palette,
|
|
color: "from-rose-400 to-pink-500",
|
|
path: "/canvas",
|
|
status: "active",
|
|
features: ["Drag & Drop", "Formas", "Conectores", "Exportação"]
|
|
},
|
|
{
|
|
id: "api-hub",
|
|
name: "API Hub",
|
|
description: "Central de APIs com documentação, playground e testes",
|
|
icon: Server,
|
|
color: "from-blue-600 to-indigo-600",
|
|
path: "/api-hub",
|
|
status: "active",
|
|
features: ["Documentação", "Playground", "MCP", "A2A"]
|
|
},
|
|
{
|
|
id: "api-tester",
|
|
name: "API Tester",
|
|
description: "Testador de APIs com requisições HTTP e WebSocket",
|
|
icon: Terminal,
|
|
color: "from-green-600 to-emerald-600",
|
|
path: "/api-tester",
|
|
status: "active",
|
|
features: ["REST", "WebSocket", "Headers", "Body"]
|
|
},
|
|
{
|
|
id: "doctype-builder",
|
|
name: "DocType Builder",
|
|
description: "Construtor visual de tipos de documento e formulários",
|
|
icon: Blocks,
|
|
color: "from-orange-600 to-amber-600",
|
|
path: "/doctype-builder",
|
|
status: "active",
|
|
features: ["DocTypes", "Campos", "Validações", "Layouts"]
|
|
},
|
|
{
|
|
id: "page-builder",
|
|
name: "Page Builder",
|
|
description: "Construtor visual de páginas e interfaces",
|
|
icon: Layout,
|
|
color: "from-pink-600 to-rose-600",
|
|
path: "/page-builder",
|
|
status: "active",
|
|
features: ["Páginas", "Componentes", "Templates", "Responsivo"]
|
|
},
|
|
{
|
|
id: "dev-center",
|
|
name: "Centro de Desenvolvimento",
|
|
description: "Crie, teste e publique funcionalidades com agentes de IA autônomos. Inclui preview ao vivo e deploy.",
|
|
icon: Code,
|
|
color: "from-purple-600 to-indigo-700",
|
|
path: "/dev-center",
|
|
status: "active",
|
|
features: ["Desenvolver", "Preview", "Publicar", "Analisar Repos"]
|
|
},
|
|
{
|
|
id: "engineering",
|
|
name: "Engineering Hub",
|
|
description: "Hub de engenharia e projetos técnicos ambientais",
|
|
icon: PenTool,
|
|
color: "from-indigo-600 to-blue-600",
|
|
path: "/engineering",
|
|
status: "active",
|
|
features: ["Projetos", "Laudos", "Amostras", "ISO 17025"]
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: "education",
|
|
name: "🎓 Educação",
|
|
modules: [
|
|
{
|
|
id: "lms",
|
|
name: "Arcádia LMS",
|
|
description: "Sistema de Gestão de Aprendizagem com cursos, trilhas e certificações",
|
|
icon: GraduationCap,
|
|
color: "from-indigo-500 to-purple-500",
|
|
path: "/lms",
|
|
status: "active",
|
|
features: ["Cursos", "Trilhas", "Certificados", "Gamificação"]
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: "operations",
|
|
name: "🔧 Operações",
|
|
modules: [
|
|
{
|
|
id: "field-ops",
|
|
name: "Operações de Campo",
|
|
description: "Gestão de equipes em campo com formulários digitais e GPS",
|
|
icon: MapPin,
|
|
color: "from-lime-500 to-green-500",
|
|
path: "/field-ops",
|
|
status: "active",
|
|
features: ["Formulários", "GPS", "Fotos", "Assinaturas"]
|
|
},
|
|
{
|
|
id: "quality",
|
|
name: "Qualidade ISO",
|
|
description: "Gestão da qualidade e conformidade ISO 17025 para laboratórios",
|
|
icon: ClipboardCheck,
|
|
color: "from-teal-500 to-cyan-500",
|
|
path: "/quality",
|
|
status: "active",
|
|
features: ["Amostras", "Laudos", "RNC", "Auditorias"]
|
|
},
|
|
{
|
|
id: "technical",
|
|
name: "Assistência Técnica",
|
|
description: "Ordens de serviço, manutenção e gestão de técnicos",
|
|
icon: Wrench,
|
|
color: "from-slate-600 to-gray-600",
|
|
path: "/technical",
|
|
status: "active",
|
|
features: ["Ordens de Serviço", "Técnicos", "Peças", "Garantia"]
|
|
},
|
|
{
|
|
id: "suppliers",
|
|
name: "Portal Fornecedores",
|
|
description: "Gestão de fornecedores, homologação e pedidos de compra",
|
|
icon: Package,
|
|
color: "from-amber-500 to-yellow-500",
|
|
path: "/suppliers",
|
|
status: "active",
|
|
features: ["Fornecedores", "Homologação", "Compras", "Cotações"]
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: "admin",
|
|
name: "⚙️ Administração",
|
|
modules: [
|
|
{
|
|
id: "admin",
|
|
name: "Painel Admin",
|
|
description: "Configurações do sistema, usuários, permissões e integrações SOE",
|
|
icon: Settings,
|
|
color: "from-gray-500 to-slate-600",
|
|
path: "/admin",
|
|
status: "active",
|
|
features: ["Usuários", "Permissões", "Conexões SOE", "Bibliotecas"]
|
|
},
|
|
{
|
|
id: "home",
|
|
name: "Dashboard Home",
|
|
description: "Visão geral do sistema com atalhos rápidos e atividades recentes",
|
|
icon: Home,
|
|
color: "from-blue-400 to-sky-500",
|
|
path: "/",
|
|
status: "active",
|
|
features: ["Atalhos", "Atividades", "Notas rápidas", "Favoritos"]
|
|
},
|
|
{
|
|
id: "migration",
|
|
name: "Migração XOS",
|
|
description: "Importação de dados de sistemas legados - MongoDB, CSV, JSON para Plus/Retail",
|
|
icon: Database,
|
|
color: "from-cyan-600 to-teal-600",
|
|
path: "/migration",
|
|
status: "active",
|
|
features: ["Upload", "Análise", "Mapeamento", "Importação", "Multi-tenant"]
|
|
},
|
|
{
|
|
id: "super-admin",
|
|
name: "Super Admin",
|
|
description: "Painel master com gestão de tenants, planos e configurações globais",
|
|
icon: Shield,
|
|
color: "from-red-600 to-rose-600",
|
|
path: "/super-admin",
|
|
status: "active",
|
|
features: ["Tenants", "Planos", "Usuários", "Configurações"]
|
|
}
|
|
]
|
|
}
|
|
];
|
|
|
|
const allModules = categories.flatMap(cat => cat.modules);
|
|
const activeModules = allModules.filter(m => m.status === "active");
|
|
|
|
const filteredCategories = categories.map(cat => ({
|
|
...cat,
|
|
modules: cat.modules.filter(m => {
|
|
const matchesSearch = !searchQuery ||
|
|
m.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
m.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
m.features.some(f => f.toLowerCase().includes(searchQuery.toLowerCase()));
|
|
const matchesCategory = !selectedCategory || cat.id === selectedCategory;
|
|
return matchesSearch && matchesCategory;
|
|
})
|
|
})).filter(cat => cat.modules.length > 0);
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h2 className="text-xl font-bold">Módulos do Sistema</h2>
|
|
<p className="text-muted-foreground text-sm">
|
|
Todos os módulos do Arcádia Suite em um só lugar
|
|
</p>
|
|
</div>
|
|
<Badge variant="outline" className="text-emerald-600 border-emerald-500">
|
|
{activeModules.length} ativos
|
|
</Badge>
|
|
</div>
|
|
|
|
<div className="flex flex-col md:flex-row gap-4">
|
|
<div className="relative flex-1">
|
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-slate-400 h-4 w-4" />
|
|
<Input
|
|
placeholder="Buscar módulos..."
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
className="pl-10"
|
|
data-testid="input-search-modules"
|
|
/>
|
|
</div>
|
|
<Select value={selectedCategory || "all"} onValueChange={(v) => setSelectedCategory(v === "all" ? null : v)}>
|
|
<SelectTrigger className="w-full md:w-[220px]" data-testid="select-category-filter">
|
|
<SelectValue placeholder="Todas as categorias" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">Todas as categorias</SelectItem>
|
|
{categories.map(cat => (
|
|
<SelectItem key={cat.id} value={cat.id}>{cat.name}</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="space-y-8">
|
|
{filteredCategories.map((category) => (
|
|
<div key={category.id}>
|
|
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
|
{category.name}
|
|
<Badge variant="secondary" className="text-xs">{category.modules.length}</Badge>
|
|
</h3>
|
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
{category.modules.map((module) => {
|
|
const IconComponent = module.icon;
|
|
return (
|
|
<Card
|
|
key={module.id}
|
|
className="hover:shadow-lg transition-shadow cursor-pointer group"
|
|
onClick={() => setLocation(module.path)}
|
|
data-testid={`module-card-${module.id}`}
|
|
>
|
|
<CardHeader className="pb-3">
|
|
<div className="flex items-start justify-between">
|
|
<div className={`p-3 rounded-xl bg-gradient-to-br ${module.color}`}>
|
|
<IconComponent className="w-5 h-5 text-white" />
|
|
</div>
|
|
{(() => {
|
|
const featureKey = moduleToFeatureKey[module.id];
|
|
const isEnabled = featureKey ? (tenantModules?.features?.[featureKey] ?? false) : true;
|
|
const coreModules = ['admin', 'home', 'soe'];
|
|
const isCore = coreModules.includes(module.id);
|
|
return !isCore ? (
|
|
<Switch
|
|
checked={isEnabled}
|
|
onCheckedChange={(checked) => {
|
|
if (featureKey) {
|
|
toggleModule.mutate({ moduleKey: featureKey, enabled: checked });
|
|
}
|
|
}}
|
|
onClick={(e) => e.stopPropagation()}
|
|
data-testid={`switch-module-${module.id}`}
|
|
/>
|
|
) : (
|
|
<Badge variant="outline" className="text-emerald-600 border-emerald-500 text-xs">Core</Badge>
|
|
);
|
|
})()}
|
|
</div>
|
|
<CardTitle className="text-base mt-3 group-hover:text-primary transition-colors">
|
|
{module.name}
|
|
</CardTitle>
|
|
<CardDescription className="text-xs line-clamp-2">
|
|
{module.description}
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="pt-0">
|
|
<div className="flex flex-wrap gap-1 mb-3">
|
|
{module.features.slice(0, 3).map((feature, i) => (
|
|
<Badge key={i} variant="secondary" className="text-[10px] px-1.5 py-0">
|
|
{feature}
|
|
</Badge>
|
|
))}
|
|
{module.features.length > 3 && (
|
|
<Badge variant="outline" className="text-[10px] px-1.5 py-0">
|
|
+{module.features.length - 3}
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
<Button
|
|
className="w-full h-8 text-xs group-hover:bg-primary group-hover:text-white transition-colors"
|
|
variant="outline"
|
|
size="sm"
|
|
data-testid={`button-open-${module.id}`}
|
|
>
|
|
Acessar
|
|
<ArrowRight className="w-3 h-3 ml-1" />
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{filteredCategories.length === 0 && (
|
|
<div className="text-center py-12">
|
|
<Boxes className="h-12 w-12 mx-auto text-slate-300 mb-4" />
|
|
<h3 className="text-lg font-medium text-slate-700">Nenhum módulo encontrado</h3>
|
|
<p className="text-slate-500 text-sm">Tente ajustar os filtros ou termos de busca</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface MHEngineStatus {
|
|
name: string;
|
|
displayName: string;
|
|
type: string;
|
|
port: number;
|
|
category: string;
|
|
description: string;
|
|
status: "online" | "offline" | "error";
|
|
responseTime?: number;
|
|
details?: any;
|
|
error?: string;
|
|
}
|
|
|
|
interface MHAgentStatus {
|
|
name: string;
|
|
running: boolean;
|
|
processedTasks?: number;
|
|
errorCount?: number;
|
|
}
|
|
|
|
interface MHEngineRoomData {
|
|
engines: MHEngineStatus[];
|
|
agents: MHAgentStatus[];
|
|
summary: {
|
|
total_engines: number;
|
|
online_engines: number;
|
|
offline_engines: number;
|
|
health_pct: number;
|
|
total_agents: number;
|
|
running_agents: number;
|
|
};
|
|
timestamp: string;
|
|
}
|
|
|
|
const MH_ENGINE_ICONS: Record<string, ElementType> = {
|
|
"manus-ia": Brain,
|
|
"plus": ShoppingCart,
|
|
"contabil": Calculator,
|
|
"fisco": FileText,
|
|
"bi-engine": BarChart3,
|
|
"automation-engine": Zap,
|
|
};
|
|
|
|
const MH_CATEGORY_COLORS: Record<string, string> = {
|
|
erp: "bg-blue-50 text-blue-600 border-blue-200",
|
|
fiscal: "bg-amber-50 text-amber-600 border-amber-200",
|
|
data: "bg-emerald-50 text-emerald-600 border-emerald-200",
|
|
automation: "bg-purple-50 text-purple-600 border-purple-200",
|
|
intelligence: "bg-cyan-50 text-cyan-600 border-cyan-200",
|
|
};
|
|
|
|
function MHEngineCard({ engine, onRefresh, onClick }: { engine: MHEngineStatus; onRefresh: () => void; onClick?: () => void }) {
|
|
const EngineIcon = MH_ENGINE_ICONS[engine.name] || Server;
|
|
const catColor = MH_CATEGORY_COLORS[engine.category] || "bg-gray-50 text-gray-600 border-gray-200";
|
|
const [showLogs, setShowLogs] = useState(false);
|
|
const [logs, setLogs] = useState<string[]>([]);
|
|
const [logsLoading, setLogsLoading] = useState(false);
|
|
const [actionLoading, setActionLoading] = useState<string | null>(null);
|
|
const [serviceInfo, setServiceInfo] = useState<any>(null);
|
|
const isClickable = !!onClick;
|
|
|
|
const MANAGED_ENGINES = ["contabil", "bi-engine", "automation-engine"];
|
|
const isManaged = MANAGED_ENGINES.includes(engine.name);
|
|
|
|
const handleAction = async (action: "restart" | "stop" | "start") => {
|
|
setActionLoading(action);
|
|
try {
|
|
const res = await fetch(`/api/engine-room/engine/${engine.name}/${action}`, {
|
|
method: "POST",
|
|
credentials: "include",
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) {
|
|
alert(data.error || `Erro ao ${action}`);
|
|
}
|
|
setTimeout(onRefresh, 3000);
|
|
} catch (err) {
|
|
alert(`Erro de conexão ao ${action}`);
|
|
} finally {
|
|
setTimeout(() => setActionLoading(null), 2000);
|
|
}
|
|
};
|
|
|
|
const fetchLogs = async () => {
|
|
setLogsLoading(true);
|
|
try {
|
|
const [logsRes, infoRes] = await Promise.all([
|
|
fetch(`/api/engine-room/engine/${engine.name}/logs?lines=80`, { credentials: "include" }),
|
|
fetch(`/api/engine-room/engine/${engine.name}/info`, { credentials: "include" }),
|
|
]);
|
|
if (logsRes.ok) {
|
|
const data = await logsRes.json();
|
|
setLogs(data.logs || []);
|
|
}
|
|
if (infoRes.ok) {
|
|
const data = await infoRes.json();
|
|
setServiceInfo(data);
|
|
}
|
|
} catch {}
|
|
setLogsLoading(false);
|
|
};
|
|
|
|
const toggleLogs = () => {
|
|
if (!showLogs) fetchLogs();
|
|
setShowLogs(!showLogs);
|
|
};
|
|
|
|
const formatUptime = (seconds: number) => {
|
|
if (seconds < 60) return `${seconds}s`;
|
|
if (seconds < 3600) return `${Math.floor(seconds / 60)}m`;
|
|
return `${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m`;
|
|
};
|
|
|
|
return (
|
|
<Card
|
|
data-testid={`engine-card-${engine.name}`}
|
|
className={`hover:shadow-md transition-all ${isClickable ? "cursor-pointer hover:shadow-lg hover:border-violet-300 ring-0 hover:ring-1 hover:ring-violet-200" : ""}`}
|
|
onClick={isClickable ? onClick : undefined}
|
|
>
|
|
<CardContent className="p-4">
|
|
<div className="flex items-start justify-between mb-3">
|
|
<div className="flex items-center gap-3">
|
|
<div className={`p-2 rounded-lg border ${catColor}`}>
|
|
<EngineIcon className="w-5 h-5" />
|
|
</div>
|
|
<div>
|
|
<h3 className="font-semibold text-sm">{engine.displayName}</h3>
|
|
<p className="text-xs text-muted-foreground">{engine.description}</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
{isClickable && (
|
|
<Badge variant="outline" className="border-violet-300 text-violet-600 bg-violet-50 text-[10px]">
|
|
Ver estrutura
|
|
</Badge>
|
|
)}
|
|
<Badge variant={engine.status === "online" ? "default" : "destructive"} className={engine.status === "online" ? "bg-emerald-100 text-emerald-700 border-emerald-200" : engine.status === "error" ? "bg-amber-100 text-amber-700 border-amber-200" : ""}>
|
|
<div className={`w-2 h-2 rounded-full mr-1.5 ${engine.status === "online" ? "bg-emerald-500" : engine.status === "error" ? "bg-amber-500" : "bg-red-500"}`} />
|
|
{engine.status === "online" ? "Online" : engine.status === "error" ? "Erro" : "Offline"}
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-3 gap-2 mt-3">
|
|
<div className="text-center p-2 rounded bg-slate-50">
|
|
<p className="text-[10px] text-muted-foreground uppercase">Tipo</p>
|
|
<p className="text-xs font-medium">{engine.type.toUpperCase()}</p>
|
|
</div>
|
|
<div className="text-center p-2 rounded bg-slate-50">
|
|
<p className="text-[10px] text-muted-foreground uppercase">Porta</p>
|
|
<p className="text-xs font-medium">{engine.port}</p>
|
|
</div>
|
|
<div className="text-center p-2 rounded bg-slate-50">
|
|
<p className="text-[10px] text-muted-foreground uppercase">Resposta</p>
|
|
<p className="text-xs font-medium">
|
|
{engine.responseTime ? `${engine.responseTime}ms` : "---"}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{engine.details && engine.status === "online" && (
|
|
<div className="mt-3 p-2 rounded bg-slate-50 border">
|
|
<p className="text-[10px] text-muted-foreground uppercase mb-1">Detalhes</p>
|
|
{engine.details.version && (
|
|
<p className="text-xs text-muted-foreground">Versão: <span className="font-medium text-foreground">{engine.details.version}</span></p>
|
|
)}
|
|
{engine.details.database && (
|
|
<p className="text-xs text-muted-foreground">DB: <span className={`font-medium ${engine.details.database === "connected" ? "text-emerald-600" : "text-red-600"}`}>{engine.details.database}</span></p>
|
|
)}
|
|
{engine.details.cache && (
|
|
<p className="text-xs text-muted-foreground">Cache: <span className="font-medium text-foreground">{engine.details.cache.entries} entradas, {engine.details.cache.hit_rate}% hit</span></p>
|
|
)}
|
|
{engine.details.scheduler && (
|
|
<p className="text-xs text-muted-foreground">Scheduler: <span className={`font-medium ${engine.details.scheduler.is_running ? "text-emerald-600" : "text-muted-foreground"}`}>{engine.details.scheduler.is_running ? "Ativo" : "Parado"}</span> ({engine.details.scheduler.active_entries} entradas)</p>
|
|
)}
|
|
{engine.details.workflows && engine.details.workflows.total_workflows !== undefined && (
|
|
<p className="text-xs text-muted-foreground">Workflows: <span className="font-medium text-foreground">{engine.details.workflows.total_workflows} registrados, {engine.details.workflows.total_executions} execuções</span></p>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{engine.error && (
|
|
<div className="mt-3 p-2 rounded bg-red-50 border border-red-200">
|
|
<p className="text-xs text-red-600">{engine.error}</p>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex items-center gap-2 mt-3 pt-3 border-t">
|
|
{isManaged && (
|
|
<>
|
|
{engine.status === "online" || engine.status === "error" ? (
|
|
<>
|
|
<Button
|
|
data-testid={`button-restart-${engine.name}`}
|
|
variant="outline"
|
|
size="sm"
|
|
className="flex-1 text-xs border-amber-300 text-amber-700 hover:bg-amber-50"
|
|
onClick={() => handleAction("restart")}
|
|
disabled={actionLoading !== null}
|
|
>
|
|
{actionLoading === "restart" ? <Loader2 className="w-3 h-3 mr-1 animate-spin" /> : <RefreshCw className="w-3 h-3 mr-1" />}
|
|
Reiniciar
|
|
</Button>
|
|
<Button
|
|
data-testid={`button-stop-${engine.name}`}
|
|
variant="outline"
|
|
size="sm"
|
|
className="flex-1 text-xs border-red-300 text-red-700 hover:bg-red-50"
|
|
onClick={() => handleAction("stop")}
|
|
disabled={actionLoading !== null}
|
|
>
|
|
{actionLoading === "stop" ? <Loader2 className="w-3 h-3 mr-1 animate-spin" /> : <Square className="w-3 h-3 mr-1" />}
|
|
Parar
|
|
</Button>
|
|
</>
|
|
) : (
|
|
<Button
|
|
data-testid={`button-start-${engine.name}`}
|
|
variant="outline"
|
|
size="sm"
|
|
className="flex-1 text-xs border-emerald-300 text-emerald-700 hover:bg-emerald-50"
|
|
onClick={() => handleAction("start")}
|
|
disabled={actionLoading !== null}
|
|
>
|
|
{actionLoading === "start" ? <Loader2 className="w-3 h-3 mr-1 animate-spin" /> : <Play className="w-3 h-3 mr-1" />}
|
|
Iniciar
|
|
</Button>
|
|
)}
|
|
<Button
|
|
data-testid={`button-logs-${engine.name}`}
|
|
variant="outline"
|
|
size="sm"
|
|
className="text-xs"
|
|
onClick={toggleLogs}
|
|
>
|
|
<FileText className="w-3 h-3 mr-1" />
|
|
Logs
|
|
</Button>
|
|
</>
|
|
)}
|
|
{!isManaged && engine.name !== "plus" && (
|
|
<span className="text-xs text-muted-foreground italic">Motor externo - sem controle direto</span>
|
|
)}
|
|
{!isManaged && engine.name === "plus" && (
|
|
<Button
|
|
data-testid="button-open-plus-panel"
|
|
variant="outline"
|
|
size="sm"
|
|
className="flex-1 text-xs border-blue-300 text-blue-700 hover:bg-blue-50"
|
|
onClick={() => window.open('/plus', '_blank')}
|
|
>
|
|
<ExternalLink className="w-3 h-3 mr-1" />
|
|
Abrir Painel Plus
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
{showLogs && isManaged && (
|
|
<div className="mt-3 border rounded-lg overflow-hidden">
|
|
{serviceInfo && (
|
|
<div className="p-2 bg-slate-100 border-b flex items-center gap-4 text-xs">
|
|
<span className="text-muted-foreground">Uptime: <span className="font-medium text-foreground">{formatUptime(serviceInfo.uptime || 0)}</span></span>
|
|
<span className="text-muted-foreground">Restarts: <span className="font-medium text-foreground">{serviceInfo.restartCount || 0}</span></span>
|
|
<span className="text-muted-foreground">Status: <span className={`font-medium ${serviceInfo.status === "running" ? "text-emerald-600" : "text-red-600"}`}>{serviceInfo.status}</span></span>
|
|
<Button variant="ghost" size="sm" className="ml-auto h-6 text-xs" onClick={fetchLogs} disabled={logsLoading}>
|
|
<RefreshCw className={`w-3 h-3 mr-1 ${logsLoading ? "animate-spin" : ""}`} />
|
|
Atualizar
|
|
</Button>
|
|
</div>
|
|
)}
|
|
<div className="bg-gray-900 text-gray-200 p-3 max-h-48 overflow-y-auto font-mono text-[11px] leading-relaxed">
|
|
{logsLoading ? (
|
|
<div className="flex items-center gap-2 text-gray-400">
|
|
<Loader2 className="w-3 h-3 animate-spin" /> Carregando logs...
|
|
</div>
|
|
) : logs.length > 0 ? (
|
|
logs.map((line, i) => (
|
|
<div key={i} className={`${line.includes("[stderr]") ? "text-red-400" : line.includes("[stdout]") ? "text-green-300" : "text-gray-300"}`}>
|
|
{line}
|
|
</div>
|
|
))
|
|
) : (
|
|
<span className="text-gray-500">Nenhum log disponível</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
function MachineHouseSection() {
|
|
const queryClient = useQueryClient();
|
|
const [mhTab, setMhTab] = useState("overview");
|
|
const [manusOpen, setManusOpen] = useState(false);
|
|
|
|
const { data: engineData, isLoading: engineLoading, isRefetching, isError } = useQuery<MHEngineRoomData>({
|
|
queryKey: ["/api/engine-room/status"],
|
|
refetchInterval: 15000,
|
|
});
|
|
|
|
const { data: biMetrics } = useQuery<any>({
|
|
queryKey: ["/api/bi-engine/metrics"],
|
|
enabled: mhTab === "bi",
|
|
refetchInterval: 10000,
|
|
});
|
|
|
|
const { data: manusMetrics } = useQuery<any>({
|
|
queryKey: ["/api/manus/health"],
|
|
enabled: manusOpen,
|
|
refetchInterval: 10000,
|
|
});
|
|
|
|
const { data: autoMetrics } = useQuery<any>({
|
|
queryKey: ["/api/automation-engine/metrics"],
|
|
enabled: mhTab === "automation",
|
|
refetchInterval: 10000,
|
|
});
|
|
|
|
const handleRefresh = () => {
|
|
queryClient.invalidateQueries({ queryKey: ["/api/engine-room/status"] });
|
|
};
|
|
|
|
const handleStartAgents = async () => {
|
|
try {
|
|
await fetch("/api/engine-room/agents/start", { method: "POST", credentials: "include" });
|
|
queryClient.invalidateQueries({ queryKey: ["/api/engine-room/status"] });
|
|
} catch {}
|
|
};
|
|
|
|
const handleStopAgents = async () => {
|
|
try {
|
|
await fetch("/api/engine-room/agents/stop", { method: "POST", credentials: "include" });
|
|
queryClient.invalidateQueries({ queryKey: ["/api/engine-room/status"] });
|
|
} catch {}
|
|
};
|
|
|
|
const handleOpenPlus = (path: string = '') => {
|
|
window.open(`/plus${path}`, '_blank');
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<Card>
|
|
<CardHeader>
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Cpu className="w-5 h-5 text-orange-600" />
|
|
Casa de Máquinas
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Painel de controle de todos os motores e agentes do sistema
|
|
</CardDescription>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
{engineData && (
|
|
<span className="text-xs text-muted-foreground">
|
|
Atualizado: {new Date(engineData.timestamp).toLocaleTimeString("pt-BR")}
|
|
</span>
|
|
)}
|
|
<Button
|
|
data-testid="button-refresh-engines"
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={handleRefresh}
|
|
disabled={isRefetching}
|
|
>
|
|
<RefreshCw className={`w-4 h-4 mr-1 ${isRefetching ? "animate-spin" : ""}`} />
|
|
Atualizar
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="space-y-6">
|
|
{engineLoading ? (
|
|
<div className="flex items-center justify-center h-32">
|
|
<Loader2 className="w-6 h-6 animate-spin text-orange-500" />
|
|
</div>
|
|
) : engineData ? (
|
|
<>
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
|
<Card className="bg-emerald-50 border-emerald-200">
|
|
<CardContent className="p-3 flex items-center gap-3">
|
|
<div className="p-2 rounded-lg bg-emerald-100">
|
|
<Server className="w-4 h-4 text-emerald-600" />
|
|
</div>
|
|
<div>
|
|
<p className="text-xl font-bold text-emerald-700">{engineData.summary.online_engines}/{engineData.summary.total_engines}</p>
|
|
<p className="text-xs text-emerald-600">Motores Online</p>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
<Card className="bg-cyan-50 border-cyan-200">
|
|
<CardContent className="p-3 flex items-center gap-3">
|
|
<div className="p-2 rounded-lg bg-cyan-100">
|
|
<Activity className="w-4 h-4 text-cyan-600" />
|
|
</div>
|
|
<div>
|
|
<p className="text-xl font-bold text-cyan-700">{engineData.summary.health_pct}%</p>
|
|
<p className="text-xs text-cyan-600">Saúde Geral</p>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
<Card className="bg-purple-50 border-purple-200">
|
|
<CardContent className="p-3 flex items-center gap-3">
|
|
<div className="p-2 rounded-lg bg-purple-100">
|
|
<Bot className="w-4 h-4 text-purple-600" />
|
|
</div>
|
|
<div>
|
|
<p className="text-xl font-bold text-purple-700">{engineData.summary.running_agents}/{engineData.summary.total_agents}</p>
|
|
<p className="text-xs text-purple-600">Agentes Ativos</p>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
<Card className={engineData.summary.offline_engines > 0 ? "bg-red-50 border-red-200" : "bg-slate-50 border-slate-200"}>
|
|
<CardContent className="p-3 flex items-center gap-3">
|
|
<div className={`p-2 rounded-lg ${engineData.summary.offline_engines > 0 ? "bg-red-100" : "bg-slate-100"}`}>
|
|
<XCircle className={`w-4 h-4 ${engineData.summary.offline_engines > 0 ? "text-red-600" : "text-slate-400"}`} />
|
|
</div>
|
|
<div>
|
|
<p className={`text-xl font-bold ${engineData.summary.offline_engines > 0 ? "text-red-700" : "text-slate-500"}`}>{engineData.summary.offline_engines}</p>
|
|
<p className={`text-xs ${engineData.summary.offline_engines > 0 ? "text-red-600" : "text-slate-400"}`}>Motores Offline</p>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
<Tabs value={mhTab} onValueChange={setMhTab}>
|
|
<TabsList className="mb-4">
|
|
<TabsTrigger value="overview" data-testid="mh-tab-overview">
|
|
<Server className="w-4 h-4 mr-1.5" /> Visão Geral
|
|
</TabsTrigger>
|
|
<TabsTrigger value="bi" data-testid="mh-tab-bi">
|
|
<BarChart3 className="w-4 h-4 mr-1.5" /> Motor BI
|
|
</TabsTrigger>
|
|
<TabsTrigger value="automation" data-testid="mh-tab-automation">
|
|
<Zap className="w-4 h-4 mr-1.5" /> Motor Automação
|
|
</TabsTrigger>
|
|
<TabsTrigger value="agents" data-testid="mh-tab-agents">
|
|
<Bot className="w-4 h-4 mr-1.5" /> Agentes XOS
|
|
</TabsTrigger>
|
|
<TabsTrigger value="plus" data-testid="mh-tab-plus">
|
|
<ShoppingCart className="w-4 h-4 mr-1.5" /> Arcádia Plus
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="overview">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
{engineData.engines.map((engine) => (
|
|
<MHEngineCard
|
|
key={engine.name}
|
|
engine={engine}
|
|
onRefresh={handleRefresh}
|
|
onClick={engine.name === "manus-ia" ? () => setManusOpen(true) : undefined}
|
|
/>
|
|
))}
|
|
</div>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="bi">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
{engineData.engines.filter(e => e.name === "bi-engine").map(e => (
|
|
<MHEngineCard key={e.name} engine={e} onRefresh={handleRefresh} />
|
|
))}
|
|
{biMetrics && (
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
|
<Gauge className="w-4 h-4 text-emerald-600" /> Métricas do Motor BI
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-3">
|
|
{biMetrics.cache && (
|
|
<div className="p-3 rounded bg-slate-50 border">
|
|
<p className="text-xs text-muted-foreground uppercase mb-2">Cache</p>
|
|
<div className="grid grid-cols-3 gap-2">
|
|
<div className="text-center">
|
|
<p className="text-lg font-bold">{biMetrics.cache.entries}</p>
|
|
<p className="text-[10px] text-muted-foreground">Entradas</p>
|
|
</div>
|
|
<div className="text-center">
|
|
<p className="text-lg font-bold text-emerald-600">{biMetrics.cache.hit_rate}%</p>
|
|
<p className="text-[10px] text-muted-foreground">Hit Rate</p>
|
|
</div>
|
|
<div className="text-center">
|
|
<p className="text-lg font-bold">{biMetrics.cache.hits + biMetrics.cache.misses}</p>
|
|
<p className="text-[10px] text-muted-foreground">Requisições</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
{biMetrics.limits && (
|
|
<div className="p-3 rounded bg-slate-50 border">
|
|
<p className="text-xs text-muted-foreground uppercase mb-2">Limites</p>
|
|
<p className="text-xs text-muted-foreground">Max linhas: <span className="font-medium text-foreground">{biMetrics.limits.max_rows?.toLocaleString()}</span></p>
|
|
<p className="text-xs text-muted-foreground">Timeout: <span className="font-medium text-foreground">{biMetrics.limits.query_timeout_ms?.toLocaleString()}ms</span></p>
|
|
<p className="text-xs text-muted-foreground">Cache TTL: <span className="font-medium text-foreground">{biMetrics.limits.cache_ttl_seconds}s</span></p>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
<div className="mt-4 p-4 rounded-lg bg-slate-50 border">
|
|
<h3 className="text-sm font-medium mb-2 flex items-center gap-2">
|
|
<Signal className="w-4 h-4 text-emerald-600" /> Capacidades do Motor BI
|
|
</h3>
|
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
|
|
{["SQL Query (read-only)", "Chart Data Generation", "Micro-BI API", "Data Analysis (Pandas)", "Aggregation Engine", "Query Cache"].map(cap => (
|
|
<div key={cap} className="flex items-center gap-2 p-2 rounded bg-white border">
|
|
<CheckCircle className="w-3.5 h-3.5 text-emerald-500" />
|
|
<span className="text-xs">{cap}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="automation">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
{engineData.engines.filter(e => e.name === "automation-engine").map(e => (
|
|
<MHEngineCard key={e.name} engine={e} onRefresh={handleRefresh} />
|
|
))}
|
|
{autoMetrics && (
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
|
<Gauge className="w-4 h-4 text-purple-600" /> Métricas do Motor Automação
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-3">
|
|
{autoMetrics.scheduler && (
|
|
<div className="p-3 rounded bg-slate-50 border">
|
|
<p className="text-xs text-muted-foreground uppercase mb-2">Scheduler</p>
|
|
<div className="grid grid-cols-3 gap-2">
|
|
<div className="text-center">
|
|
<p className="text-lg font-bold">{autoMetrics.scheduler.total_entries}</p>
|
|
<p className="text-[10px] text-muted-foreground">Entradas</p>
|
|
</div>
|
|
<div className="text-center">
|
|
<p className="text-lg font-bold">{autoMetrics.scheduler.active_entries}</p>
|
|
<p className="text-[10px] text-muted-foreground">Ativas</p>
|
|
</div>
|
|
<div className="text-center">
|
|
<p className={`text-lg font-bold ${autoMetrics.scheduler.is_running ? "text-emerald-600" : "text-muted-foreground"}`}>
|
|
{autoMetrics.scheduler.is_running ? "ON" : "OFF"}
|
|
</p>
|
|
<p className="text-[10px] text-muted-foreground">Status</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
{autoMetrics.event_bus && (
|
|
<div className="p-3 rounded bg-slate-50 border">
|
|
<p className="text-xs text-muted-foreground uppercase mb-2">Event Bus</p>
|
|
<p className="text-xs text-muted-foreground">Tipos de evento: <span className="font-medium text-foreground">{autoMetrics.event_bus.total_event_types}</span></p>
|
|
<p className="text-xs text-muted-foreground">Subscribers: <span className="font-medium text-foreground">{autoMetrics.event_bus.total_subscribers}</span></p>
|
|
<p className="text-xs text-muted-foreground">Histórico: <span className="font-medium text-foreground">{autoMetrics.event_bus.history_size} eventos</span></p>
|
|
</div>
|
|
)}
|
|
{autoMetrics.workflows && (
|
|
<div className="p-3 rounded bg-slate-50 border">
|
|
<p className="text-xs text-muted-foreground uppercase mb-2">Workflows</p>
|
|
<p className="text-xs text-muted-foreground">Registrados: <span className="font-medium text-foreground">{autoMetrics.workflows.total_workflows}</span></p>
|
|
<p className="text-xs text-muted-foreground">Execuções: <span className="font-medium text-foreground">{autoMetrics.workflows.total_executions}</span></p>
|
|
<p className="text-xs text-muted-foreground">Taxa sucesso: <span className="font-medium text-emerald-600">{autoMetrics.workflows.success_rate}%</span></p>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
<div className="mt-4 p-4 rounded-lg bg-slate-50 border">
|
|
<h3 className="text-sm font-medium mb-2 flex items-center gap-2">
|
|
<Workflow className="w-4 h-4 text-purple-600" /> Capacidades do Motor Automação
|
|
</h3>
|
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
|
|
{["Cron Scheduler", "Event Bus", "Workflow Executor", "HTTP Actions", "SQL Queries (read-only)", "Transform & Filter"].map(cap => (
|
|
<div key={cap} className="flex items-center gap-2 p-2 rounded bg-white border">
|
|
<CheckCircle className="w-3.5 h-3.5 text-purple-500" />
|
|
<span className="text-xs">{cap}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="agents">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h3 className="text-sm font-medium flex items-center gap-2">
|
|
<Bot className="w-4 h-4 text-cyan-600" /> Agentes Autônomos XOS
|
|
</h3>
|
|
<div className="flex gap-2">
|
|
<Button
|
|
data-testid="button-start-agents"
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={handleStartAgents}
|
|
className="border-emerald-300 text-emerald-700 hover:bg-emerald-50"
|
|
>
|
|
<Play className="w-3.5 h-3.5 mr-1" /> Iniciar Todos
|
|
</Button>
|
|
<Button
|
|
data-testid="button-stop-agents"
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={handleStopAgents}
|
|
className="border-red-300 text-red-700 hover:bg-red-50"
|
|
>
|
|
<Square className="w-3.5 h-3.5 mr-1" /> Parar Todos
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
{engineData.agents.length > 0 ? (
|
|
engineData.agents.map((agent) => (
|
|
<div key={agent.name} data-testid={`agent-card-${agent.name}`} className="flex items-center justify-between p-3 rounded-lg bg-white border hover:shadow-sm transition-all">
|
|
<div className="flex items-center gap-3">
|
|
<div className={`p-1.5 rounded-lg ${agent.running ? "bg-emerald-100" : "bg-slate-100"}`}>
|
|
<Bot className={`w-4 h-4 ${agent.running ? "text-emerald-600" : "text-slate-400"}`} />
|
|
</div>
|
|
<div>
|
|
<p className="text-sm font-medium capitalize">{agent.name}</p>
|
|
<p className="text-xs text-muted-foreground">Agente XOS</p>
|
|
</div>
|
|
</div>
|
|
<Badge variant="outline" className={agent.running ? "border-emerald-300 text-emerald-700 bg-emerald-50" : "border-slate-300 text-slate-500 bg-slate-50"}>
|
|
{agent.running ? "Ativo" : "Parado"}
|
|
</Badge>
|
|
</div>
|
|
))
|
|
) : (
|
|
<div className="col-span-2 text-center py-8 text-muted-foreground">
|
|
<Bot className="w-8 h-8 mx-auto mb-2 opacity-50" />
|
|
<p className="text-sm">Nenhum agente registrado</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="plus">
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between p-4 bg-slate-50 rounded-lg border">
|
|
<div className="flex items-center gap-3">
|
|
<div className={`w-3 h-3 rounded-full ${
|
|
engineData.engines.find(e => e.name === "plus")?.status === "online" ? "bg-emerald-500" : "bg-red-500"
|
|
}`} />
|
|
<div>
|
|
<h4 className="font-medium">Status do Engine Plus</h4>
|
|
<p className="text-sm text-muted-foreground">
|
|
{engineData.engines.find(e => e.name === "plus")?.status === "online" ? "Plus está rodando na porta 8080" : "Plus offline"}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<Button onClick={() => handleOpenPlus()} variant="outline" size="sm">
|
|
<ExternalLink className="w-4 h-4 mr-2" />
|
|
Abrir Painel Plus
|
|
</Button>
|
|
</div>
|
|
|
|
<div>
|
|
<h4 className="font-medium mb-3">Módulos do Engine</h4>
|
|
<div className="grid gap-3 md:grid-cols-2 lg:grid-cols-3">
|
|
<div data-testid="link-plus-nfe" onClick={() => handleOpenPlus('/nfe')} className="flex items-center gap-3 p-3 bg-white border rounded-lg hover:border-red-300 hover:bg-red-50 transition-colors cursor-pointer">
|
|
<div className="p-2 bg-red-100 rounded">
|
|
<Receipt className="w-5 h-5 text-red-600" />
|
|
</div>
|
|
<div>
|
|
<p className="font-medium text-sm">Motor Fiscal</p>
|
|
<p className="text-xs text-muted-foreground">NF-e, NFC-e, CT-e, MDF-e</p>
|
|
</div>
|
|
<ExternalLink className="w-4 h-4 text-slate-400 ml-auto" />
|
|
</div>
|
|
<div data-testid="link-plus-caixa" onClick={() => handleOpenPlus('/caixa')} className="flex items-center gap-3 p-3 bg-white border rounded-lg hover:border-blue-300 hover:bg-blue-50 transition-colors cursor-pointer">
|
|
<div className="p-2 bg-blue-100 rounded">
|
|
<ShoppingCart className="w-5 h-5 text-blue-600" />
|
|
</div>
|
|
<div>
|
|
<p className="font-medium text-sm">PDV / Caixa</p>
|
|
<p className="text-xs text-muted-foreground">Ponto de venda</p>
|
|
</div>
|
|
<ExternalLink className="w-4 h-4 text-slate-400 ml-auto" />
|
|
</div>
|
|
<div data-testid="link-plus-vendas" onClick={() => handleOpenPlus('/vendas')} className="flex items-center gap-3 p-3 bg-white border rounded-lg hover:border-emerald-300 hover:bg-emerald-50 transition-colors cursor-pointer">
|
|
<div className="p-2 bg-emerald-100 rounded">
|
|
<DollarSign className="w-5 h-5 text-emerald-600" />
|
|
</div>
|
|
<div>
|
|
<p className="font-medium text-sm">Vendas</p>
|
|
<p className="text-xs text-muted-foreground">Pedidos e orçamentos</p>
|
|
</div>
|
|
<ExternalLink className="w-4 h-4 text-slate-400 ml-auto" />
|
|
</div>
|
|
<div data-testid="link-plus-produtos" onClick={() => handleOpenPlus('/produtos')} className="flex items-center gap-3 p-3 bg-white border rounded-lg hover:border-orange-300 hover:bg-orange-50 transition-colors cursor-pointer">
|
|
<div className="p-2 bg-orange-100 rounded">
|
|
<Package className="w-5 h-5 text-orange-600" />
|
|
</div>
|
|
<div>
|
|
<p className="font-medium text-sm">Produtos</p>
|
|
<p className="text-xs text-muted-foreground">Estoque, lotes, IMEI</p>
|
|
</div>
|
|
<ExternalLink className="w-4 h-4 text-slate-400 ml-auto" />
|
|
</div>
|
|
<div data-testid="link-plus-clientes" onClick={() => handleOpenPlus('/clientes')} className="flex items-center gap-3 p-3 bg-white border rounded-lg hover:border-purple-300 hover:bg-purple-50 transition-colors cursor-pointer">
|
|
<div className="p-2 bg-purple-100 rounded">
|
|
<Users className="w-5 h-5 text-purple-600" />
|
|
</div>
|
|
<div>
|
|
<p className="font-medium text-sm">Clientes</p>
|
|
<p className="text-xs text-muted-foreground">Cadastro de clientes</p>
|
|
</div>
|
|
<ExternalLink className="w-4 h-4 text-slate-400 ml-auto" />
|
|
</div>
|
|
<div data-testid="link-plus-fornecedores" onClick={() => handleOpenPlus('/fornecedores')} className="flex items-center gap-3 p-3 bg-white border rounded-lg hover:border-teal-300 hover:bg-teal-50 transition-colors cursor-pointer">
|
|
<div className="p-2 bg-teal-100 rounded">
|
|
<Building2 className="w-5 h-5 text-teal-600" />
|
|
</div>
|
|
<div>
|
|
<p className="font-medium text-sm">Fornecedores</p>
|
|
<p className="text-xs text-muted-foreground">Cadastro de fornecedores</p>
|
|
</div>
|
|
<ExternalLink className="w-4 h-4 text-slate-400 ml-auto" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</TabsContent>
|
|
</Tabs>
|
|
|
|
<div className="p-4 rounded-lg bg-slate-50 border">
|
|
<h3 className="text-sm font-medium mb-3 flex items-center gap-2">
|
|
<HardDrive className="w-4 h-4 text-orange-600" /> Arquitetura dos Motores
|
|
</h3>
|
|
<div className="font-mono text-xs text-muted-foreground space-y-1 bg-white p-4 rounded-lg border">
|
|
<p>Express (5000) <--proxy--> Plus/Laravel (8080)</p>
|
|
<p>Express (5000) <--proxy--> Contábil/FastAPI (8003)</p>
|
|
<p>Express (5000) <--proxy--> Fisco/FastAPI (8002)</p>
|
|
<p>Express (5000) <--proxy--> BI Engine/FastAPI (8004)</p>
|
|
<p>Express (5000) <--proxy--> Automation/FastAPI (8005)</p>
|
|
<p className="text-orange-600 mt-2">Casa de Máquinas: /api/engine-room/*</p>
|
|
</div>
|
|
</div>
|
|
</>
|
|
) : isError ? (
|
|
<div className="text-center py-8 text-muted-foreground" data-testid="text-engine-error">
|
|
<AlertCircle className="w-8 h-8 mx-auto mb-2 text-red-400" />
|
|
<p className="text-sm font-medium text-red-600">Erro ao carregar status dos motores</p>
|
|
<p className="text-xs mt-1">Verifique se a API /api/engine-room/status está acessível</p>
|
|
<Button variant="outline" size="sm" className="mt-3" onClick={handleRefresh}>
|
|
<RefreshCw className="w-4 h-4 mr-1" /> Tentar novamente
|
|
</Button>
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-8 text-muted-foreground">
|
|
<AlertCircle className="w-8 h-8 mx-auto mb-2 opacity-50" />
|
|
<p className="text-sm">Não foi possível carregar o status dos motores</p>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Dialog open={manusOpen} onOpenChange={setManusOpen}>
|
|
<DialogContent className="max-w-4xl max-h-[85vh] overflow-y-auto">
|
|
<DialogHeader>
|
|
<div className="flex items-center gap-3">
|
|
<div className="p-3 rounded-xl bg-gradient-to-br from-violet-100 to-purple-100 border border-violet-200">
|
|
<Brain className="w-7 h-7 text-violet-600" />
|
|
</div>
|
|
<div>
|
|
<DialogTitle className="text-xl">Manus IA - Cérebro Central</DialogTitle>
|
|
<DialogDescription>
|
|
Arquitetura do motor de inteligência que alimenta todos os agentes
|
|
</DialogDescription>
|
|
</div>
|
|
</div>
|
|
</DialogHeader>
|
|
|
|
<div className="grid grid-cols-4 gap-3 mt-2">
|
|
<div className="text-center p-3 rounded-lg bg-violet-50 border border-violet-200">
|
|
<p className="text-xl font-bold text-violet-600">{manusMetrics?.model || "GPT-4o"}</p>
|
|
<p className="text-[10px] text-muted-foreground uppercase mt-1">Modelo</p>
|
|
</div>
|
|
<div className="text-center p-3 rounded-lg bg-slate-50 border">
|
|
<p className="text-xl font-bold">{manusMetrics?.metrics?.totalCalls || 0}</p>
|
|
<p className="text-[10px] text-muted-foreground uppercase mt-1">Chamadas IA</p>
|
|
</div>
|
|
<div className="text-center p-3 rounded-lg bg-emerald-50 border border-emerald-200">
|
|
<p className="text-xl font-bold text-emerald-600">{((manusMetrics?.metrics?.totalTokens || 0) / 1000).toFixed(1)}k</p>
|
|
<p className="text-[10px] text-muted-foreground uppercase mt-1">Tokens</p>
|
|
</div>
|
|
<div className="text-center p-3 rounded-lg bg-cyan-50 border border-cyan-200">
|
|
<p className="text-xl font-bold text-cyan-600">
|
|
{manusMetrics?.metrics?.uptime
|
|
? `${Math.floor(manusMetrics.metrics.uptime / 3600)}h ${Math.floor((manusMetrics.metrics.uptime % 3600) / 60)}m`
|
|
: "---"}
|
|
</p>
|
|
<p className="text-[10px] text-muted-foreground uppercase mt-1">Uptime</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-4">
|
|
<h3 className="text-sm font-semibold mb-3 flex items-center gap-2">
|
|
<Layers className="w-4 h-4 text-violet-600" />
|
|
Arquitetura: Fluxo de Inteligência
|
|
</h3>
|
|
<div className="relative p-4 rounded-xl bg-slate-50 border">
|
|
<div className="flex flex-col items-center gap-2">
|
|
<div className="w-full p-3 rounded-lg bg-gradient-to-r from-violet-100 to-purple-100 border border-violet-200 text-center">
|
|
<div className="flex items-center justify-center gap-2 mb-1">
|
|
<Brain className="w-5 h-5 text-violet-600" />
|
|
<span className="text-sm font-bold text-violet-700">ManusIntelligence (Singleton)</span>
|
|
</div>
|
|
<p className="text-[10px] text-muted-foreground">GPT-4o + ToolManager + Context Enrichment</p>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-1 text-muted-foreground">
|
|
<ArrowDown className="w-4 h-4" />
|
|
<span className="text-[10px]">generate() / think()</span>
|
|
<ArrowDown className="w-4 h-4" />
|
|
</div>
|
|
|
|
<div className="w-full p-3 rounded-lg bg-amber-50 border border-amber-200 text-center">
|
|
<span className="text-xs font-medium text-amber-700">enrichWithContext()</span>
|
|
<p className="text-[10px] text-muted-foreground mt-1">ToolManager.search_code → Contexto Semântico Automático</p>
|
|
</div>
|
|
|
|
<ArrowDown className="w-4 h-4 text-muted-foreground" />
|
|
|
|
<div className="grid grid-cols-3 gap-2 w-full">
|
|
{[
|
|
{ name: "Architect", role: "Design & Arquitetura", icon: Layers, color: "text-blue-600", bg: "bg-blue-50 border-blue-200" },
|
|
{ name: "Generator", role: "Geração de Código", icon: Code, color: "text-green-600", bg: "bg-green-50 border-green-200" },
|
|
{ name: "Validator", role: "Validação TypeScript", icon: Shield, color: "text-amber-600", bg: "bg-amber-50 border-amber-200" },
|
|
{ name: "Executor", role: "Execução & Staging", icon: Terminal, color: "text-red-600", bg: "bg-red-50 border-red-200" },
|
|
{ name: "Researcher", role: "Pesquisa & Contexto", icon: Search, color: "text-cyan-600", bg: "bg-cyan-50 border-cyan-200" },
|
|
{ name: "Evolution", role: "Aprendizado Evolutivo", icon: Sparkles, color: "text-purple-600", bg: "bg-purple-50 border-purple-200" },
|
|
].map((agent) => (
|
|
<div key={agent.name} className={`p-2.5 rounded-lg border ${agent.bg} text-center`}>
|
|
<agent.icon className={`w-4 h-4 ${agent.color} mx-auto mb-1`} />
|
|
<p className={`text-xs font-semibold ${agent.color}`}>{agent.name}</p>
|
|
<p className="text-[9px] text-muted-foreground mt-0.5">{agent.role}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-4">
|
|
<h3 className="text-sm font-semibold mb-3 flex items-center gap-2">
|
|
<Zap className="w-4 h-4 text-amber-600" />
|
|
Ferramentas Disponíveis ({manusMetrics?.capabilities?.tools || 56})
|
|
</h3>
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
|
|
{[
|
|
{ name: "Busca Semântica", count: 8, icon: Search, color: "text-violet-600" },
|
|
{ name: "Arquivos", count: 12, icon: FileText, color: "text-blue-600" },
|
|
{ name: "Comandos Shell", count: 6, icon: Terminal, color: "text-green-600" },
|
|
{ name: "Web Research", count: 5, icon: Globe, color: "text-cyan-600" },
|
|
{ name: "Knowledge Graph", count: 8, icon: Network, color: "text-amber-600" },
|
|
{ name: "ERP & Database", count: 10, icon: Database, color: "text-emerald-600" },
|
|
{ name: "Análise de Código", count: 7, icon: Code, color: "text-pink-600" },
|
|
].map((cat) => (
|
|
<div key={cat.name} className="p-3 rounded-lg bg-slate-50 border hover:border-slate-300 transition-all">
|
|
<div className="flex items-center gap-2 mb-1.5">
|
|
<cat.icon className={`w-3.5 h-3.5 ${cat.color}`} />
|
|
<span className={`text-xs font-medium ${cat.color}`}>{cat.count}</span>
|
|
</div>
|
|
<p className="text-[10px] text-muted-foreground">{cat.name}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-4">
|
|
<h3 className="text-sm font-semibold mb-3 flex items-center gap-2">
|
|
<CheckCircle className="w-4 h-4 text-emerald-600" />
|
|
Capacidades Ativas
|
|
</h3>
|
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
|
|
{[
|
|
"Cérebro Central (GPT-4o)",
|
|
"Enriquecimento de Contexto",
|
|
"Busca Semântica de Código",
|
|
"Knowledge Graph",
|
|
"Pipeline Autônomo de Dev",
|
|
"Orquestração de 6 Agentes",
|
|
"Leitura/Escrita de Arquivos",
|
|
"Execução de Comandos Shell",
|
|
"Web Research",
|
|
"Análise de Código",
|
|
"Validação TypeScript",
|
|
"Memória Evolutiva",
|
|
].map((cap) => (
|
|
<div key={cap} className="flex items-center gap-2 p-2 rounded-lg bg-slate-50 border">
|
|
<CheckCircle className="w-3.5 h-3.5 text-emerald-500 shrink-0" />
|
|
<span className="text-xs">{cap}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-4 p-4 rounded-xl bg-slate-50 border">
|
|
<h3 className="text-sm font-semibold mb-3 flex items-center gap-2">
|
|
<Network className="w-4 h-4 text-violet-600" />
|
|
Diagrama de Fluxo
|
|
</h3>
|
|
<div className="font-mono text-[11px] text-slate-600 space-y-0.5 bg-white p-4 rounded-lg border">
|
|
<p className="text-violet-600">{"┌──────────────────────────────────────────────────────────┐"}</p>
|
|
<p className="text-violet-600">{"│ MANUS INTELLIGENCE (Singleton GPT-4o) │"}</p>
|
|
<p className="text-violet-600">{"├──────────────────────────────────────────────────────────┤"}</p>
|
|
<p>{"│ .generate(prompt) → enrichWithContext() → OpenAI │"}</p>
|
|
<p>{"│ .think(prompt) → enrichWithContext() → OpenAI │"}</p>
|
|
<p>{"│ .getMetrics() → calls, tokens, errors, uptime │"}</p>
|
|
<p className="text-violet-600">{"├──────────────────────────────────────────────────────────┤"}</p>
|
|
<p className="text-amber-600">{"│ ToolManager.executeTool('search_code', query) │"}</p>
|
|
<p className="text-amber-600">{"│ → Contexto semântico injetado automaticamente │"}</p>
|
|
<p className="text-violet-600">{"├──────────────────────────────────────────────────────────┤"}</p>
|
|
<p className="text-blue-600">{"│ Architect ──────┐ │"}</p>
|
|
<p className="text-green-600">{"│ Generator ──────┤ │"}</p>
|
|
<p className="text-amber-600">{"│ Validator ──────┤── Todos via manusIntelligence ──► │"}</p>
|
|
<p className="text-red-600">{"│ Executor ──────┤ │"}</p>
|
|
<p className="text-cyan-600">{"│ Researcher ──────┤ │"}</p>
|
|
<p className="text-purple-600">{"│ Evolution ──────┘ │"}</p>
|
|
<p className="text-violet-600">{"├──────────────────────────────────────────────────────────┤"}</p>
|
|
<p className="text-emerald-600">{"│ /api/manus/health → Status, métricas, capacidades │"}</p>
|
|
<p className="text-violet-600">{"└──────────────────────────────────────────────────────────┘"}</p>
|
|
</div>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
);
|
|
}
|