Merge origin/main into Servidor

# Conflicts:
#	Dockerfile
#	client/src/pages/DevCenter.tsx
#	client/src/pages/DevelopmentModule.tsx
#	server/routes.ts
This commit is contained in:
Jonas Pacheco 2026-03-27 14:34:58 -03:00
commit f3322ab075
139 changed files with 20062 additions and 7141 deletions

20
.env Normal file
View File

@ -0,0 +1,20 @@
PGPASSWORD=afefb610090de1646944b537f3054715ad5755665c90036c2bcd258c9ab52dd8
SESSION_SECRET=092a26dcbebd6f7f5e7b9e020bdc4d4e966286c9780ef72e3426f437eb8d608b
SSO_SECRET=fbb87d1c5a14a6623d4e3dcc2b679bd68c46e59afb48dd6abd42b95cac945856
LITELLM_API_KEY=e396b827cbeb5e77d85cce4eb9da0521114f57a8e799f7aa8febad08d776c07e
WEBUI_SECRET_KEY=76c7fc044f9e8d6e5eb60b67c82832b67e84739da4827ce1fa7c101ae1a055df
DOMAIN=suite.onboardbi.com.br
OLLAMA_BASE_URL=http://ollama-ia1upsekrad96at5hq97e4qa:11434
LLMFIT_BASE_URL=http://llmfit:8000
LLMFIT_API_KEY=key-arcadia
# ── Superset BI ───────────────────────────────────────────────
SUPERSET_SECRET_KEY=421274b5ba360778a5398d528116a45ea962d1197e3dfc99f36a372ff1025a63
SUPERSET_ADMIN_USERNAME=admin
SUPERSET_ADMIN_PASSWORD=arcadia1337
SUPERSET_ADMIN_EMAIL=engenharia@ometas.com.br
COMPOSE_PROFILES=bi
SUPERSET_HOST=superset
SUPERSET_PORT=8088

View File

@ -1,70 +1,110 @@
# ============================================== # ─── Arcádia Suite — Variáveis de Ambiente ────────────────────────────────────
# Arcádia Suite - Variáveis de Ambiente # Copie para .env e preencha os valores.
# ============================================== # NUNCA commite o arquivo .env real.
# Copie este arquivo para .env e preencha os valores
# ---- Banco de Dados PostgreSQL ---- # ── Banco de dados ────────────────────────────────────────────────────────────
DATABASE_URL=postgresql://arcadia:SuaSenhaSegura@db:5432/arcadia DATABASE_URL=postgresql://arcadia:arcadia123@localhost:5432/arcadia
PGHOST=db PGHOST=localhost
PGPORT=5432 PGPORT=5432
PGUSER=arcadia PGUSER=arcadia
PGPASSWORD=SuaSenhaSegura PGPASSWORD=arcadia123
PGDATABASE=arcadia PGDATABASE=arcadia
# ---- Servidor ---- # ── Aplicação ─────────────────────────────────────────────────────────────────
NODE_ENV=production NODE_ENV=development
PORT=5000 PORT=5000
SESSION_SECRET=gere-uma-chave-secreta-aleatoria-aqui SESSION_SECRET=troque-por-string-aleatoria-segura-em-producao
SSO_SECRET=arcadia-sso-secret-2024-plus-integration-key-secure
# ---- SSO / Multi-tenant ---- # ── Docker Mode (desativa spawn de processos filhos) ─────────────────────────
SSO_SECRET=gere-outra-chave-secreta-aqui # Em produção com Docker, defina como "true"
DOCKER_MODE=false
# ---- Senha do Gerente (Retail) ---- # ── URLs dos microserviços Python ─────────────────────────────────────────────
MANAGER_PASSWORD=sua-senha-de-gerente # Em Docker: use nome do serviço (ex: http://contabil:8003)
# Em desenvolvimento local: use localhost
CONTABIL_PYTHON_URL=http://localhost:8003
BI_PYTHON_URL=http://localhost:8004
AUTOMATION_PYTHON_URL=http://localhost:8005
FISCO_PYTHON_URL=http://localhost:8002
PYTHON_SERVICE_URL=http://localhost:8001
# ---- OpenAI (opcional - para funcionalidades de IA) ---- # ── IA — OpenAI ───────────────────────────────────────────────────────────────
OPENAI_API_KEY=sk-... # Deixe vazio se usar apenas Ollama (soberania total)
AI_INTEGRATIONS_OPENAI_API_KEY=sk-... OPENAI_API_KEY=
AI_INTEGRATIONS_OPENAI_BASE_URL=https://api.openai.com/v1
# ---- ERPNext (opcional - integração ERP) ---- # ── IA — LiteLLM (gateway unificado — ÚNICA porta de entrada para LLMs) ──────
ERPNEXT_URL=https://seu-erpnext.com # Em Docker: http://litellm:4000 | Em dev local: http://localhost:4000
LITELLM_BASE_URL=http://localhost:4000
LITELLM_API_KEY=arcadia-internal
# ── IA — Manus Agent (aponta para LiteLLM como gateway) ──────────────────────
# Em Docker: AI_INTEGRATIONS_OPENAI_BASE_URL=http://litellm:4000/v1
# Em dev local: http://localhost:4000/v1
AI_INTEGRATIONS_OPENAI_BASE_URL=http://localhost:4000/v1
AI_INTEGRATIONS_OPENAI_API_KEY=arcadia-internal
# ── IA — Ollama (LLMs locais — soberania total) ───────────────────────────────
# Se Ollama está no host (fora do Docker): OLLAMA_BASE_URL=http://localhost:11434
# Se Ollama está em outro servidor: OLLAMA_BASE_URL=http://IP_DO_SERVIDOR:11434
OLLAMA_BASE_URL=http://localhost:11434
# ── IA — LLMFit (modelos fine-tuned locais — habilitar quando disponível) ─────
# LLMFit turbocharge: modelos treinados com dados do seu negócio
# Deixe vazio para desabilitar (LiteLLM cai para Ollama automaticamente)
LLMFIT_BASE_URL=
# ── IA — Providers externos (opt-in — soberania: dados não saem sem configurar)
# Deixe vazio para operação 100% soberana (apenas Ollama + LLMFit)
OPENAI_API_KEY=
ANTHROPIC_API_KEY=
GROQ_API_KEY=
# ── Open WebUI ────────────────────────────────────────────────────────────────
WEBUI_SECRET_KEY=troque-por-string-aleatoria-segura
# ── Arcádia Plus (Laravel/Fiscal) ─────────────────────────────────────────────
PLUS_URL=http://localhost:8080
PLUS_PORT=8080
PLUS_API_TOKEN=
# ── Superset (BI avançado — Arcádia Insights) ────────────────────────────────
SUPERSET_HOST=superset
SUPERSET_PORT=8088
SUPERSET_SECRET_KEY=troque-por-string-aleatoria-segura # openssl rand -hex 32
SUPERSET_ADMIN_USER=admin
SUPERSET_ADMIN_EMAIL=admin@arcadia.app
SUPERSET_ADMIN_PASSWORD=troque-senha-forte-em-prod
# URL do banco Arcádia para o Superset acessar os dados (idealmente read-only)
ARCADIA_DATABASE_URL=postgresql://arcadia:arcadia123@db:5432/arcadia
# ── Redis ─────────────────────────────────────────────────────────────────────
REDIS_URL=redis://localhost:6379
# ── Domínio (produção) ────────────────────────────────────────────────────────
DOMAIN=seudominio.com.br
# ── ERPNext ────────────────────────────────────────────────────────────────────
# Modo 1 — ERPNext em container Docker (perfil erpnext)
# Sobe com: docker compose --profile erpnext up
# Proxy ativado automaticamente em DOCKER_MODE=true
ERPNEXT_PROXY_ENABLED=false # true para forçar proxy mesmo fora do Docker
ERPNEXT_CONTAINER_HOST=erpnext # nome do serviço no docker-compose
ERPNEXT_CONTAINER_PORT=8080
ERPNEXT_DB_ROOT_PASSWORD=erpnext-root-2026 # senha root do MariaDB
ERPNEXT_DB_PASSWORD=frappe2026 # senha do usuário frappe
ERPNEXT_ADMIN_PASSWORD=admin2026 # senha do Administrator no site
# Modo 2 — ERPNext externo (instância existente)
# Em Docker: ERPNEXT_URL=http://erpnext:8080 (aponta pro container)
# Externo: ERPNEXT_URL=https://meuerpnext.com
ERPNEXT_URL=
ERPNEXT_API_KEY= ERPNEXT_API_KEY=
ERPNEXT_API_SECRET= ERPNEXT_API_SECRET=
# ---- GitHub (opcional - integração DevCenter) ---- # GitHub (para sync de código)
GITHUB_TOKEN=ghp_... GITHUB_TOKEN=
GITHUB_OWNER=seu-usuario GITHUB_REPO=
GITHUB_REPO=seu-repositorio
GITHUB_DEFAULT_BRANCH=main
# ---- Arcádia Plus - ERP Laravel (opcional) ---- # WhatsApp (Baileys — sessões salvas no banco)
PLUS_URL=http://localhost:8080 # Nenhuma variável adicional necessária — configurado via interface
PLUS_API_TOKEN=
# ---- Serviços Python (opcional) ----
PYTHON_SERVICE_URL=http://localhost:8001
FISCO_PYTHON_URL=http://localhost:8002
FISCO_PORT=8002
CONTABIL_PYTHON_URL=http://localhost:8003
CONTABIL_PORT=8003
PEOPLE_PYTHON_URL=http://localhost:8004
PEOPLE_PORT=8004
# ---- Protocolos MCP/A2A (opcional) ----
PROTOCOL_API_KEYS=sua-api-key-aqui
# ---- Apache Superset — BI Visual (opcional, ativar com --profile bi) ----
# Gerar chave: openssl rand -hex 32
SUPERSET_SECRET_KEY=gere-com-openssl-rand-hex-32
SUPERSET_ADMIN_USER=admin
SUPERSET_ADMIN_EMAIL=admin@arcadia.app
SUPERSET_ADMIN_PASSWORD=troque-em-producao
# Domínio público do Superset (apenas prod/Traefik)
SUPERSET_DOMAIN=bi.seudominio.com.br
# Remover do .env se existirem (não são mais necessários):
# METABASE_HOST
# METABASE_PORT
# METASET_ADMIN_EMAIL
# METASET_ADMIN_PASSWORD

19
.gitignore vendored
View File

@ -1,9 +1,6 @@
node_modules/ node_modules/
dist/ dist/
.DS_Store .DS_Store
.env
.env.local
.env.production
# Replit specific # Replit specific
.cache/ .cache/
@ -63,18 +60,4 @@ attached_assets/
# Package lock (regenerated on install) # Package lock (regenerated on install)
package-lock.json package-lock.json
.claude/
# Documentation files (except essential)
MAPA_*.md
AUDITORIA_*.md
AVALIACAO_*.md
DOCUMENTATION.md
HOSPEDAGEM.md
PLANO_*.md
RELATORIO_*.md
REQUISITOS_*.md
*.pdf
*.txt
!README.md
!replit.md
.aider*

View File

@ -0,0 +1,475 @@
# Fase 4: OpenClaw Embutido — Plano de Implementação
**Fase:** 4
**Data Início:** 2026-03-26
**Prazo:** 2026-04-08 (Semanas 7-8)
**Status:** 🔄 Sprint 3 Pendente (Sprint 1 e 2 concluídos)
---
## 1. OBJETIVO DA FASE
Implementar **detecção proativa de padrões** e **sugestão emergente de skills**, transformando comportamentos repetitivos do usuário em automações reutilizáveis através de um widget conversacional flutuante.
### Entregável Final
✅ Skills emergentes funcionando end-to-end:
- Usuário executa ação repetida → PatternDetector detecta → OpenClawWidget sugere → Blackboard gera DRAFT → Dev Center aprova → Skill disponível
---
## 2. ARQUITETURA
```
FLUXO OPENCCLAW:
1. USER INTERACTION
└─ Usuário usa Arcádia
└─ Learning API registra evento (já existe)
└─ Neo4j armazena interação
2. PATTERN DETECTION (NOVO)
└─ PatternDetector analisa Learning API
└─ Identifica: 3+ ocorrências em 30 dias
└─ Calcula confiança (threshold 0.8+)
└─ Dispara evento no Socket.IO
3. WIDGET SUGGESTION (NOVO)
└─ OpenClawWidget fica "ouvindo"
└─ Recebe evento de padrão detectado
└─ Exibe notificação flutuante
└─ Modal SkillSuggestion mostra preview
4. SKILL GENERATION (NOVO)
└─ Usuário confirma
└─ OpenClawEngine chama Blackboard
└─ Blackboard gera código (DRAFT)
└─ Armazena em database
5. APPROVAL FLOW (EXISTENTE - Dev Center)
└─ Skill DRAFT aparece em Dev Center
└─ Admin/Lead revisa e aprova
└─ Publica no tenant
└─ Skill fica disponível para reutilização
```
### Componentes para Implementar
```
BACKEND:
server/modules/openclaw/
├── PatternDetector.ts ← [NOVO] Detecta padrões
├── SkillEmergence.ts ← [NOVO] Coordena criação
├── OpenClawEngine.ts ← [NOVO] Lógica central
├── config/
│ └── arcadia.config.yaml ← [NOVO] Configuração
FRONTEND:
client/src/components/openclaw/
├── OpenClawWidget.tsx ← [NOVO] Widget flutuante
├── PatternDetector.ts ← [NOVO] Hook de detecção
├── SkillSuggestion.tsx ← [NOVO] Modal de sugestão
└── useAgentEmergence.ts ← [NOVO] Lógica de emergência
```
---
## 3. DEPENDÊNCIAS VERIFICADAS
| Componente | Status | Localização |
|-----------|--------|-----------|
| **Skills Engine** | ✅ Pronto | `server/skills/engine.ts` |
| **Blackboard** | ✅ Pronto | `server/blackboard/service.ts` |
| **Dev Center** | ✅ Pronto | `client/src/pages/DevCenter.tsx` |
| **Learning API** | ✅ Existe | `/api/learning/*` |
| **Neo4j (KG)** | ✅ Running | Docker |
| **Socket.IO** | ✅ Configurado | `server/socket-io.ts` |
| **PostgreSQL** | ✅ Rodando | Docker |
| **Scientist (MiroFlow)** | ✅ Existe | `/api/manus/scientist/*` |
**Conclusão:** Todas as dependências estão prontas ✅
---
## 4. TASKS DETALHADAS
### SPRINT 1 (26-27/03): PatternDetector Backend — ✅ COMPLETO
#### Task 1.1: Criar PatternDetector.ts ✅
**Arquivo:** `server/modules/openclaw/PatternDetector.ts`
**Objetivo:** Analisar eventos de usuário e detectar padrões
```typescript
// Pseudocódigo da estrutura
export class PatternDetector {
// Config
min_occurrences: 3;
time_window_days: 30;
confidence_threshold: 0.8;
// Métodos
detectPattern(userId: string, action: string): Pattern | null
calculateConfidence(interactions: Interaction[]): number
storePattern(pattern: Pattern): Promise<void>
emitEvent(pattern: Pattern): void
}
```
**Entrada:** Eventos do Learning API (`/api/learning/interactions`)
**Saída:** Pattern objects armazenados em Neo4j + Socket.IO event
**Dependência:** Learning API funcionando ✅
#### Task 1.2: Criar OpenClawEngine.ts ✅
**Arquivo:** `server/modules/openclaw/OpenClawEngine.ts`
**Objetivo:** Orquestração central de OpenClaw
```typescript
export class OpenClawEngine {
patternDetector: PatternDetector;
skillEmergence: SkillEmergence;
// Métodos
async suggestSkill(pattern: Pattern): Promise<SkillSuggestion>
async onUserConfirmation(suggestion: SkillSuggestion): Promise<Skill>
async checkPatternsPeriodicly(): Promise<void>
}
```
**Entrada:** Padrões detectados
**Saída:** Sugestões + Skills gerados
**Evento:** Socket.IO broadcast `/openclaw/pattern-detected`
#### Task 1.3: Criar SkillEmergence.ts ✅
**Arquivo:** `server/modules/openclaw/SkillEmergence.ts`
**Objetivo:** Integração com Blackboard para codegen
```typescript
export class SkillEmergence {
async generateSkillDraft(pattern: Pattern): Promise<SkillDraft> {
// 1. Montar prompt baseado no padrão
// 2. Chamar POST /api/blackboard/generate-skill
// 3. Receber código gerado
// 4. Salvar como DRAFT no database
// 5. Notificar Dev Center
// 6. Retornar skill DRAFT
}
}
```
**Integração:** POST `/api/blackboard/generate-skill`
**Database:** skills table (com status: 'draft')
#### Task 1.4: Criar rotas OpenClaw ✅
**Arquivo:** `server/modules/openclaw/routes.ts`
**Rotas:**
```
POST /api/openclaw/detect-patterns ← Trigger manual de análise
GET /api/openclaw/patterns ← Listar padrões detectados
POST /api/openclaw/confirm-skill ← Usuário confirma sugestão
GET /api/openclaw/suggestions ← Histórico de sugestões
```
#### Task 1.5: Integrar em server/index.ts ✅
**Arquivo:** `server/index.ts`
**Ação:** Importar rotas OpenClaw e carregá-las
```typescript
import openclawRoutes from "./modules/openclaw/routes";
app.use("/api/openclaw", openclawRoutes);
```
### SPRINT 2 (28-29/03): Frontend Widgets — ✅ COMPLETO (commit 5231166)
#### Task 2.1: Criar OpenClawWidget.tsx ✅
**Arquivo:** `client/src/components/openclaw/OpenClawWidget.tsx`
**Objetivo:** Widget flutuante que recebe notificações de padrões
```typescript
export function OpenClawWidget() {
// Escutar Socket.IO event: /openclaw/pattern-detected
// Exibir notificação flutuante (canto inferior direito)
// Mostrar padrão detectado
// Botão "Ver Sugestão" abre modal
// Botão "Ignorar" descarta
return (
<div className="fixed bottom-4 right-4">
{/* Widget flutuante */}
</div>
);
}
```
**Socket.IO:** Escuta evento `/openclaw/pattern-detected`
**Trigger:** Modal SkillSuggestion
#### Task 2.2: Criar SkillSuggestion.tsx ✅
**Arquivo:** `client/src/components/openclaw/SkillSuggestion.tsx`
**Objetivo:** Modal com preview da skill sugerida
```typescript
export function SkillSuggestion({ pattern, suggestion }) {
// Modal mostra:
// 1. "Detectamos um padrão na sua utilização:"
// 2. Preview do padrão (ações repetidas)
// 3. Preview da automação gerada
// 4. Campo de nome (auto-preenchido)
// 5. Descrição (auto-gerada)
// 6. Botão "Criar Skill" ou "Cancelar"
const handleCreate = async () => {
// POST /api/openclaw/confirm-skill
// Esperar resposta (skill DRAFT)
// Toast: "Skill criada! Vá para Dev Center para publicar"
};
}
```
**Integração:** Chama `POST /api/openclaw/confirm-skill`
**Feedback:** Toast notification
#### Task 2.3: Hook useAgentEmergence.ts ✅
**Arquivo:** `client/src/hooks/useAgentEmergence.ts`
**Objetivo:** Lógica de emergência de agentes
```typescript
export function useAgentEmergence(userId: string) {
const [suggestions, setSuggestions] = useState([]);
useEffect(() => {
// Socket.IO listener
socket.on("openclaw:pattern-detected", (pattern) => {
// Filtrar por tenant, user prefs
// Atualizar suggestions state
setSuggestions([...suggestions, pattern]);
});
}, []);
return { suggestions, dismiss, confirm };
}
```
#### Task 2.4: Integrar Widget em layout principal ✅
**Arquivo:** `client/src/App.tsx` ou `client/src/layouts/MainLayout.tsx`
**Ação:** Adicionar OpenClawWidget ao layout persistente
```typescript
import { OpenClawWidget } from "@/components/openclaw/OpenClawWidget";
export default function App() {
return (
<>
{/* Conteúdo existente */}
<OpenClawWidget /> {/* ← Adicionar aqui */}
</>
);
}
```
### SPRINT 3 (30-31/03): Configuração e Integração — ⏳ PRÓXIMO
#### Task 3.1: Criar arcadia.config.yaml
**Arquivo:** `server/modules/openclaw/config/arcadia.config.yaml`
```yaml
openclaw:
enabled: true
# Endpoints
arcadia_api_url: http://localhost:3000/api
arcadia_kg_url: http://localhost:7474
blackboard_url: http://localhost:3000/api/blackboard
# Tenant awareness
tenant_aware: true
# Detecção de padrões
pattern_detection:
enabled: true
min_occurrences: 3
time_window_days: 30
confidence_threshold: 0.8
scheduled_check_interval_minutes: 60
# Sugestões
suggestions:
enabled: true
auto_suggest: false # Sempre pedir confirmação do usuário
channels: ['widget', 'chat']
priority_threshold: 0.85
# Skills emergentes
emergence:
create_as_draft: true # Sempre requer aprovação
auto_publish: false
notify_admins: true
skill_prefix: "emergent_" # Prefixo para skills auto-geradas
# Logging
logging:
enabled: true
store_patterns: true
store_in_kg: true
audit_trail: true
```
#### Task 3.2: Integrar Socket.IO events
**Arquivo:** `server/socket-io.ts`
**Ação:** Adicionar listeners e emitters para OpenClaw
```typescript
socket.on("openclaw:confirm-skill", async (data) => {
// Receber confirmação do usuário
// Chamar SkillEmergence.generateSkillDraft()
// Emitir "openclaw:skill-created"
});
// Emitter no PatternDetector
io.emit("openclaw:pattern-detected", pattern);
```
#### Task 3.3: Database schema para OpenClaw
**Arquivo:** `shared/schema.ts` (Drizzle ORM)
**Adicionar tabelas:**
```typescript
// Padrões detectados
export const detectedPatterns = pgTable("detected_patterns", {
id: serial().primaryKey(),
tenant_id: uuid().notNull(),
user_id: uuid().notNull(),
action_type: varchar(255).notNull(),
occurrences: integer().notNull(),
confidence: numeric(3, 2).notNull(),
time_window_days: integer().notNull(),
created_at: timestamp().defaultNow(),
metadata: jsonb(),
});
// Sugestões geradas
export const skillSuggestions = pgTable("skill_suggestions", {
id: serial().primaryKey(),
pattern_id: integer().references(() => detectedPatterns.id),
skill_draft_id: uuid().references(() => skills.id),
status: varchar(50).notNull(), // 'suggested', 'accepted', 'rejected'
created_at: timestamp().defaultNow(),
});
```
#### Task 3.4: Migration SQL
**Arquivo:** `migrations/0004_openclaw_tables.sql`
```sql
CREATE TABLE detected_patterns (
id SERIAL PRIMARY KEY,
tenant_id UUID NOT NULL,
user_id UUID NOT NULL,
action_type VARCHAR(255) NOT NULL,
occurrences INTEGER NOT NULL,
confidence NUMERIC(3,2) NOT NULL,
time_window_days INTEGER NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
metadata JSONB
);
CREATE TABLE skill_suggestions (
id SERIAL PRIMARY KEY,
pattern_id INTEGER REFERENCES detected_patterns(id),
skill_draft_id UUID REFERENCES skills(id),
status VARCHAR(50) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_detected_patterns_user_tenant
ON detected_patterns(user_id, tenant_id);
```
#### Task 3.5: Testes unitários
**Arquivo:** `tests/openclaw.test.ts`
- PatternDetector detecta padrão corretamente
- OpenClawEngine orquestra fluxo correto
- SkillEmergence integra com Blackboard
- Socket.IO emite eventos corretamente
#### Task 3.6: Documentação
**Arquivo:** `DOCUMENTATION.md` (adicionar seção)
- Como OpenClaw funciona
- Configuração por tenant
- Troubleshooting
---
## 5. TIMELINE
| Data | Sprint | Tasks | Status |
|------|--------|-------|--------|
| 26-27/03 | 1 | PatternDetector backend | ✅ Completo |
| 26/03 | 2 | OpenClawWidget frontend | ✅ Completo (commit 5231166) |
| 26/03 | 3 | Migration DB + testes | ⏳ Próximo |
| 01-08/04 | Buffer | Refinamento, testes E2E | ⏳ Próximo |
**Nota:** Trabalho em `/opt/arcadia_merged/` (diretório canônico do projeto).
---
## 6. CRITÉRIOS DE SUCESSO
✅ **PatternDetector funciona:**
- Detecta padrão após 3 ocorrências em 30 dias
- Confiança calculada corretamente
- Eventos emitidos para Socket.IO
✅ **OpenClawWidget funciona:**
- Widget aparece quando padrão é detectado
- Modal mostra sugestão corretamente
- Usuário pode confirmar ou rejeitar
✅ **SkillEmergence funciona:**
- Integra com Blackboard
- Gera skill como DRAFT
- Aparece em Dev Center para aprovação
✅ **E2E funciona:**
- User faz ação 3x
- Widget sugere
- Approva
- Skill publicada
- Skill disponível para reutilização
---
## 7. RISCOS E MITIGAÇÕES
| Risco | Probabilidade | Mitigação |
|-------|--------------|-----------|
| Blackboard não retorna código correto | Média | Testar integração antes, ter fallback |
| Padrões detectados incorretamente | Média | Validar threshold, testes no KG |
| Socket.IO não emite em tempo real | Baixa | Já funciona em outras partes |
| Performance com muitos eventos | Média | Usar índices em PG, cache em Redis |
---
## 8. BRANCH E COMMITS
**Branch:** `Servidor` (deploy branch, conforme feedback anterior)
**Commit pattern:**
```
feat(openclaw): PatternDetector backend
feat(openclaw): OpenClawWidget frontend
feat(openclaw): Integração Blackboard + esquema DB
docs(openclaw): Documentação e testes
```
---
## 9. PRÓXIMAS AÇÕES
1. ✅ **26/03:** Sprint 1 concluído (PatternDetector, OpenClawEngine, SkillEmergence, routes, config)
2. ✅ **26/03:** Sprint 2 concluído (OpenClawWidget, SkillSuggestion, useAgentEmergence, App.tsx — commit 5231166)
3. ⏳ **Agora:** Sprint 3 — migration SQL + vitest + testes unitários em `/opt/arcadia_merged/`
4. ⏳ **01-08/04:** Refinamento e testes E2E
---
*Fase 4: OpenClaw Embutido — Plano Executável*
*Data: 2026-03-26*
*Pronto para implementação ✅*

View File

@ -0,0 +1,71 @@
# Quick Task 260326-kmz: Implementar Skill Fabric
**Description:** Implementar Skill Fabric — gerador e gestor de skills da Arcádia Suite
**Date:** 2026-03-26
**Mode:** quick
---
## Task 1: Backend — Skill Fabric Module
**Files:**
- `server/modules/skill-fabric/src/types/index.ts`
- `server/modules/skill-fabric/src/core/skill/Skill.entity.ts`
- `server/modules/skill-fabric/src/core/compiler/VisualCompiler.ts`
- `server/modules/skill-fabric/src/core/compiler/CodeCompiler.ts`
- `server/modules/skill-fabric/src/core/compiler/MarkdownCompiler.ts`
- `server/modules/skill-fabric/src/core/validator/ValidationPipeline.ts`
- `server/modules/skill-fabric/src/core/executor/SkillExecutor.ts`
- `server/modules/skill-fabric/src/core/executor/Sandbox.ts`
- `server/modules/skill-fabric/src/core/lifecycle/LifecycleManager.ts`
- `server/modules/skill-fabric/src/api/routes/skills.routes.ts`
- `server/modules/skill-fabric/src/api/controllers/skills.controller.ts`
- `server/modules/skill-fabric/index.ts`
**Action:** Create the full backend Skill Fabric module with types, entity, compilers, validator pipeline, executor/sandbox, lifecycle manager, and API routes. The project uses TypeScript + Express + Drizzle ORM. Reuse existing `db` and `skills` schema from `../../shared/schema`. No new DB migration needed — extend existing skills table logic.
**Context:**
- Existing `server/skills/routes.ts` has basic CRUD. The new module adds compilation, validation, execution, versioning, lifecycle.
- DB import: `import { db } from "@/db"` or `import { db } from "../../../db"` — check what works
- Existing schema: `skills`, `skillExecutions` from `../../shared/schema`
- Do NOT use vm2 (needs install) — use Node.js built-in `vm` module for sandbox
- Do NOT use ReactFlow/Monaco yet (frontend task) — backend only here
**Done:** Files created, module compiles without errors
---
## Task 2: Frontend — Skill Fabric UI
**Files:**
- `client/src/modules/skill-fabric/index.ts`
- `client/src/modules/skill-fabric/types.ts`
- `client/src/modules/skill-fabric/api.ts`
- `client/src/modules/skill-fabric/code-ide/components/SkillCodeEditor.tsx`
- `client/src/modules/skill-fabric/markdown-studio/components/MarkdownEditor.tsx`
- `client/src/modules/skill-fabric/shared/components/SkillToolbar.tsx`
- `client/src/modules/skill-fabric/shared/components/VersionSelector.tsx`
- `client/src/modules/skill-fabric/shared/components/ValidationPanel.tsx`
- `client/src/pages/SkillFabricPage.tsx`
**Action:** Create frontend Skill Fabric module. Use textarea-based code editor (Monaco not installed — don't add it). Use Tailwind CSS + shadcn/ui components matching the existing project style. Create a SkillFabricPage that lists skills, allows creating/editing (code + markdown modes), compiling, validating, and executing. Wire to the backend API routes from Task 1.
**Context:**
- Check `client/src/components/` for existing UI patterns (shadcn components available)
- Check `client/src/App.tsx` to see how routes are registered — add the new page there
- The visual canvas (ReactFlow) is a stretch goal — skip it, implement code + markdown modes only
- API base: `/api/skills` (existing) + `/api/skill-fabric` (new from Task 1)
**Done:** SkillFabricPage renders, skills can be listed and created
---
## Task 3: Register Routes & Integration
**Files:**
- `server/modules/loader.ts`
- `client/src/App.tsx` (or router file)
**Action:** Register the new skill-fabric backend routes in `server/modules/loader.ts`. Add the SkillFabricPage route in the frontend router. Verify existing skills route still works.
**Done:** `/api/skill-fabric/*` routes registered, frontend page accessible

View File

@ -4,16 +4,12 @@ hidden = [".config", ".git", "generated-icon.png", "node_modules", "dist"]
[nix] [nix]
channel = "stable-24_05" channel = "stable-24_05"
packages = ["glibcLocales", "libiconv", "libxcrypt", "openssl", "pkg-config", "unzip", "zlib", "zip"] packages = ["cairo", "cargo", "ffmpeg-full", "freetype", "ghostscript", "gitFull", "glibcLocales", "gobject-introspection", "gtk3", "libiconv", "libxcrypt", "mysql80", "openssl", "php82", "php82Packages.composer", "pkg-config", "playwright-driver", "qhull", "rustc", "tcl", "tk", "unzip", "xcodebuild", "zlib", "jdk21"]
[[ports]] [[ports]]
localPort = 5000 localPort = 5000
externalPort = 80 externalPort = 80
[[ports]]
localPort = 5904
externalPort = 6800
[[ports]] [[ports]]
localPort = 8001 localPort = 8001
externalPort = 3001 externalPort = 3001
@ -53,7 +49,7 @@ PORT = "5000"
deploymentTarget = "autoscale" deploymentTarget = "autoscale"
build = ["npm", "run", "build"] build = ["npm", "run", "build"]
publicDir = "dist/public" publicDir = "dist/public"
run = ["npm", "run", "start"] run = ["node", "./dist/index.cjs"]
[workflows] [workflows]
runButton = "Project" runButton = "Project"

553
DOCUMENTATION.md Normal file
View File

@ -0,0 +1,553 @@
# Arcádia Suite - Documentação Técnica Completa
**Versão:** 1.0
**Data:** Janeiro 2026
**Desenvolvido por:** Arcádia Technology
---
## Sumário
1. [Visão Geral](#visão-geral)
2. [Arquitetura do Sistema](#arquitetura-do-sistema)
3. [Módulos do Sistema](#módulos-do-sistema)
4. [Modelo de Dados](#modelo-de-dados)
5. [APIs e Endpoints](#apis-e-endpoints)
6. [Integrações Externas](#integrações-externas)
7. [Segurança e Autenticação](#segurança-e-autenticação)
8. [Guia de Implantação](#guia-de-implantação)
---
## Visão Geral
O **Arcádia Suite** é um Sistema Operacional Empresarial (Business Operating System) alimentado por Inteligência Artificial, projetado para revolucionar operações empresariais. O sistema integra cinco pilares fundamentais:
### Os 5 Pilares
1. **Knowledge Graph** - Grafo de conhecimento para dados empresariais interconectados
2. **Central Intelligence (Scientist)** - Módulo de IA para geração automática de soluções
3. **Manus (Agente Autônomo)** - Execução de tarefas e automação
4. **Centro de Comunicação Unificado** - Interação com clientes via múltiplas plataformas
5. **IDE Completa** - Ambiente de desenvolvimento multi-modal
### Segmentação de Produtos
| Produto | Público-Alvo | Stack Tecnológica |
|---------|-------------|-------------------|
| **Arcádia Plus** | Pequenas empresas | Node.js + Python + PostgreSQL |
| **Arcádia Next** | Médias/Grandes empresas | Frappe Framework + PostgreSQL |
Ambos compartilham o **Arcádia Fisco** como motor fiscal centralizado.
---
## Arquitetura do Sistema
### Arquitetura em 4 Camadas
```
┌─────────────────────────────────────────────────────────────────┐
│ CAMADA DE APRESENTAÇÃO │
│ React 18 + TypeScript + Tailwind CSS + shadcn/ui │
│ Interface estilo navegador com abas e omnibox │
│ Porta: 5000 │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ CAMADA DE ORQUESTRAÇÃO │
│ Express.js + Socket.IO + Manus Agent │
│ API REST + WebSocket em tempo real │
│ Porta: 5000 │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ CAMADA DE INTELIGÊNCIA │
│ FastAPI (Python) + OpenAI API │
│ Scientist, Embeddings, RPA, Workflows │
│ Porta: 8001 (IA) / 8002 (Fisco) │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ CAMADA DE DADOS │
│ PostgreSQL + Knowledge Graph + ChromaDB │
│ Drizzle ORM + Session Store │
└─────────────────────────────────────────────────────────────────┘
```
### Estrutura de Diretórios
```
arcadia-suite/
├── client/ # Frontend React
│ └── src/
│ ├── components/ # Componentes reutilizáveis
│ ├── hooks/ # React hooks customizados
│ ├── lib/ # Utilitários e configurações
│ └── pages/ # Páginas da aplicação
├── server/ # Backend Node.js
│ ├── admin/ # Rotas administrativas
│ ├── api-central/ # Central de APIs
│ ├── automations/ # Motor de automações
│ ├── bi/ # Business Intelligence
│ ├── chat/ # Chat interno
│ ├── compass/ # Process Compass (clientes/projetos)
│ ├── crm/ # Gestão de relacionamento
│ ├── email/ # Serviço de e-mail
│ ├── erp/ # Integração ERP
│ ├── fisco/ # Motor fiscal (NF-e)
│ ├── ide/ # IDE integrada
│ ├── learning/ # Sistema de aprendizado
│ ├── login-bridge/ # Bridge de autenticação
│ ├── manus/ # Agente autônomo
│ ├── production/ # Gestão de produção
│ ├── productivity/ # Hub de produtividade
│ ├── proxy/ # Proxy reverso
│ ├── python/ # Serviços Python (FastAPI)
│ ├── support/ # Central de suporte
│ ├── valuation/ # Precificação e valuation
│ └── whatsapp/ # Integração WhatsApp
└── shared/ # Código compartilhado
└── schema.ts # Schemas do banco de dados
```
---
## Módulos do Sistema
### 1. Cockpit (Dashboard Principal)
**Arquivo:** `client/src/pages/Cockpit.tsx`
Painel central com visão geral do sistema:
- Widgets configuráveis
- Métricas em tempo real
- Atividades recentes
- Atalhos para módulos
### 2. Process Compass
**Arquivo:** `client/src/pages/ProcessCompass.tsx`
**API:** `/api/compass/*`
Gestão completa de processos empresariais:
- **Clientes:** Cadastro, histórico, segmentação
- **Projetos:** Cronograma, tarefas, milestones
- **Contratos:** Gestão de contratos e renovações
- **Timesheet:** Controle de horas trabalhadas
### 3. Comunicação Unificada
**Arquivo:** `client/src/pages/Comunicacao.tsx`
**API:** `/api/whatsapp/*`
Centro de comunicação multi-canal:
- **WhatsApp Business:** Atendimento via Baileys
- **Chat Interno:** Comunicação da equipe
- **E-mail:** Integração IMAP/SMTP
- **Tickets:** Sistema de filas de atendimento
### 4. CRM (Customer Relationship Management)
**Arquivo:** `client/src/pages/Crm.tsx`
**API:** `/api/crm/*`
Gestão de relacionamento com clientes:
- Pipeline de vendas
- Funil de conversão
- Gestão de oportunidades
- Comissionamento automático
- Integração com Google Calendar
### 5. Business Intelligence (Arcádia Insights)
**Arquivo:** `client/src/pages/BiWorkspace.tsx`
**API:** `/api/bi/*`
Análise e visualização de dados:
- Upload de arquivos (CSV, Excel)
- Gráficos interativos (Recharts)
- Dashboards personalizáveis
- Conexão com múltiplas fontes
### 6. Scientist (Central de Inteligência)
**Arquivo:** `client/src/pages/Scientist.tsx`
**API:** `/api/scientist/*`
Módulo de auto-programação com IA:
- Análise de dados automatizada
- Geração de código (Python/SQL)
- Execução em sandbox
- Armazenamento de soluções reutilizáveis
### 7. Manus (Agente Autônomo)
**Arquivo:** `client/src/pages/Agent.tsx`
**API:** `/api/manus/*`
Executor de tarefas autônomo:
- Loop pensamento-ação-observação
- Ferramentas disponíveis:
- Busca web
- Consulta ao Knowledge Graph
- Consulta ERP
- Cálculos
- Envio de mensagens
- Geração de relatórios
- Agendamentos
### 8. Arcádia Fisco (Motor Fiscal)
**Arquivo:** `client/src/pages/Fisco.tsx`
**API:** `/api/fisco/*`
Motor fiscal centralizado para compliance brasileiro:
- **NCM:** Nomenclatura Comum do Mercosul
- **CFOP:** Código Fiscal de Operações
- **CEST:** Código Especificador da Substituição Tributária
- **Grupos de Tributação:** Configuração de impostos
- **Certificados Digitais:** Gestão de A1/A3
- **NF-e/NFC-e:** Emissão de notas fiscais eletrônicas
- **IBS/CBS:** Campos para Reforma Tributária
#### Integração nfelib (Python)
**Arquivo:** `server/python/fisco_service.py`
Serviço FastAPI para processamento de NF-e:
- Geração de XML (layout 4.00)
- Assinatura digital com certificado A1
- Comunicação com SEFAZ (homologação/produção)
- Consulta, cancelamento e inutilização
### 9. Produção
**Arquivo:** `client/src/pages/Production.tsx`
**API:** `/api/production/*`
Gestão de produção e manufatura:
- Ordens de produção
- Controle de estoque
- Rastreabilidade
- Custos de produção
### 10. Valuation (Precificação)
**Arquivo:** `client/src/pages/Valuation.tsx`
**API:** `/api/valuation/*`
Sistema de precificação inteligente:
- Cálculo de custos
- Margem de contribuição
- Markup
- Simulações de preço
### 11. Suporte
**Arquivo:** `client/src/pages/Support.tsx`
**API:** `/api/support/*`
Central de atendimento:
- Tickets de suporte
- Base de conhecimento
- SLA e prioridades
- Histórico de atendimentos
### 12. Automações
**Arquivo:** `client/src/pages/Automations.tsx`
**API:** `/api/automations/*`
Motor de automações:
- Triggers e ações
- Workflows visuais
- Integrações via webhooks
- Agendamentos (cron)
### 13. Knowledge Base
**Arquivo:** `client/src/pages/Knowledge.tsx`
**API:** `/api/knowledge/*`
Base de conhecimento:
- Artigos e documentação
- Categorização
- Busca semântica
- Integração com IA
### 14. IDE
**Arquivo:** `client/src/pages/IDE.tsx`
**API:** `/api/ide/*`
Ambiente de desenvolvimento integrado:
- Monaco Editor
- Terminal (Xterm.js)
- Execução de código
- Gerenciamento de arquivos
### 15. Administração
**Arquivo:** `client/src/pages/Admin.tsx`
**API:** `/api/admin/*`
Painel administrativo:
- **Usuários:** Gestão de contas
- **Perfis:** Controle de acesso
- **Parceiros:** Hierarquia multi-tenant
- **Módulos:** Configuração de funcionalidades
- **Configurações:** Parâmetros do sistema
### 16. API Hub
**Arquivo:** `client/src/pages/ApiHub.tsx`
Documentação interativa de APIs:
- Listagem de endpoints
- Testes em tempo real
- Exemplos de uso
- Geração de código
---
## Modelo de Dados
### Entidades Principais
#### Usuários e Autenticação
```sql
users -- Usuários do sistema
profiles -- Perfis de acesso
roles -- Papéis (RBAC)
permissions -- Permissões granulares
role_permissions -- Associação papel-permissão
user_roles -- Associação usuário-papel
module_access -- Controle de acesso a módulos
```
#### Produtividade
```sql
workspace_pages -- Páginas estilo Notion
page_blocks -- Blocos de conteúdo
page_links -- Links bidirecionais
dashboard_widgets -- Widgets do dashboard
quick_notes -- Notas rápidas
activity_feed -- Feed de atividades
user_favorites -- Favoritos
command_history -- Histórico de comandos
```
#### Conversação e IA
```sql
conversations -- Conversas com agente
messages -- Mensagens
chat_attachments -- Anexos
knowledge_base -- Base de conhecimento
```
#### ERP e Integrações
```sql
erp_connections -- Conexões com ERPs
agent_tasks -- Tarefas do agente
task_executions -- Execuções de tarefas
```
#### Comunicação
```sql
chat_threads -- Threads de chat
chat_participants -- Participantes
chat_messages -- Mensagens de chat
whatsapp_sessions -- Sessões WhatsApp
whatsapp_contacts -- Contatos WhatsApp
whatsapp_messages -- Mensagens WhatsApp
whatsapp_queues -- Filas de atendimento
whatsapp_tickets -- Tickets de atendimento
```
#### Process Compass
```sql
compass_clients -- Clientes
compass_projects -- Projetos
compass_project_members -- Membros de projeto
compass_project_phases -- Fases de projeto
compass_contracts -- Contratos
compass_timesheet -- Timesheet
compass_invoices -- Faturas
compass_payments -- Pagamentos
```
#### CRM
```sql
crm_leads -- Leads
crm_opportunities -- Oportunidades
crm_activities -- Atividades
crm_pipelines -- Pipelines
crm_stages -- Estágios
crm_commissions -- Comissões
```
#### Fisco
```sql
fisco_ncm -- NCMs
fisco_cest -- CESTs
fisco_cfop -- CFOPs
fisco_grupos_tributacao -- Grupos de tributação
fisco_natureza_operacao -- Naturezas de operação
fisco_ibpt -- Tabela IBPT
fisco_certificados -- Certificados digitais
fisco_configuracoes -- Configurações fiscais
fisco_notas -- Notas fiscais
fisco_nota_itens -- Itens das notas
fisco_nota_eventos -- Eventos fiscais
```
#### Multi-Tenant
```sql
partners -- Parceiros
partner_invites -- Convites de parceiros
tenant_clients -- Clientes dos tenants
```
---
## APIs e Endpoints
### Estrutura Base
| Módulo | Base URL | Descrição |
|--------|----------|-----------|
| Admin | `/api/admin` | Administração do sistema |
| Compass | `/api/compass` | Process Compass |
| CRM | `/api/crm` | Gestão de relacionamento |
| WhatsApp | `/api/whatsapp` | Comunicação WhatsApp |
| Fisco | `/api/fisco` | Motor fiscal |
| BI | `/api/bi` | Business Intelligence |
| Production | `/api/production` | Gestão de produção |
| Valuation | `/api/valuation` | Precificação |
| Support | `/api/support` | Central de suporte |
| Automations | `/api/automations` | Automações |
| IDE | `/api/ide` | Ambiente de desenvolvimento |
| Learning | `/api/learning` | Sistema de aprendizado |
### Exemplos de Endpoints
#### Fisco - NF-e
```
GET /api/fisco/nfe/service-status # Status do serviço
POST /api/fisco/nfe/validar-certificado # Validar certificado A1
POST /api/fisco/nfe/gerar-xml # Gerar XML preview
POST /api/fisco/nfe/emitir # Emitir NF-e
POST /api/fisco/nfe/consultar # Consultar na SEFAZ
POST /api/fisco/nfe/cancelar # Cancelar NF-e
POST /api/fisco/nfe/inutilizar # Inutilizar numeração
```
#### Compass - Clientes
```
GET /api/compass/clients # Listar clientes
GET /api/compass/clients/:id # Detalhes do cliente
POST /api/compass/clients # Criar cliente
PUT /api/compass/clients/:id # Atualizar cliente
DELETE /api/compass/clients/:id # Excluir cliente
```
#### Admin - Usuários
```
GET /api/admin/users # Listar usuários
GET /api/admin/users/:id # Detalhes do usuário
POST /api/admin/users # Criar usuário
PUT /api/admin/users/:id # Atualizar usuário
DELETE /api/admin/users/:id # Excluir usuário
```
---
## Integrações Externas
### OpenAI API
- **Uso:** Agente de IA, Scientist, auto-replies
- **Modelo:** gpt-4o-mini
- **Configuração:** Via Replit Secrets
### Baileys (WhatsApp)
- **Uso:** Conexão multi-sessão WhatsApp
- **Recursos:** QR Code, mensagens em tempo real
- **Armazenamento:** Sessões no banco de dados
### nfelib (Python)
- **Uso:** Emissão de NF-e/NFC-e
- **Recursos:** XML, assinatura digital, SEFAZ
- **Certificados:** A1 (PFX)
### Frappe Framework
- **Uso:** Arcádia Next (futuro)
- **Recursos:** ERPNext integration
### Google Calendar
- **Uso:** Sincronização de eventos CRM
- **OAuth:** Configurável por usuário
---
## Segurança e Autenticação
### Autenticação
- **Método:** Session-based com Passport.js
- **Hash:** bcrypt para senhas
- **Sessões:** PostgreSQL session store
### Controle de Acesso (RBAC)
```
Hierarquia:
├── Master (Arcádia)
│ └── Parceiros
│ └── Clientes
```
### Permissões
- Baseadas em módulos e ações
- Código formato: `modulo.recurso.acao`
- Exemplo: `compass.clients.write`
### Certificados Digitais
- Tipo A1 (arquivo PFX)
- Armazenamento seguro com senha
- Validação de expiração
---
## Guia de Implantação
### Requisitos
- Node.js 20+
- Python 3.11+
- PostgreSQL 15+
- Certificado SSL (produção)
### Variáveis de Ambiente
```env
DATABASE_URL=postgresql://...
SESSION_SECRET=...
OPENAI_API_KEY=...
FISCO_PYTHON_URL=http://localhost:8002
FISCO_PORT=8002
```
### Comandos de Inicialização
```bash
# Instalar dependências
npm install
# Iniciar em desenvolvimento
npm run dev
# Serviço Python Fisco (separado)
cd server/python && python fisco_service.py
```
### Portas
| Serviço | Porta |
|---------|-------|
| Frontend + API | 5000 |
| Python Fisco | 8002 |
| Python IA | 8001 |
---
## Changelog
### Janeiro 2026
- Integração nfelib para NF-e
- Módulo Fisco completo
- Sistema de aprendizado automático
- Validação Zod em todas as rotas fiscais
---
**Arcádia Suite** - Transformando a gestão empresarial com Inteligência Artificial
*Documentação gerada automaticamente pelo sistema.*

705
MAPA_GERAL_RETAIL.md Normal file
View File

@ -0,0 +1,705 @@
# MAPA GERAL DO SISTEMA RETAIL - Arcádia Suite
---
## 1. VISÃO MACRO DO MÓDULO
```
╔══════════════════════════════════════════════════════════════════════════════════╗
║ ARCÁDIA RETAIL - MAPA GERAL ║
║ Loja de Celulares e Assistência Técnica ║
╠══════════════════════════════════════════════════════════════════════════════════╣
║ ║
║ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ║
║ │DASHBOARD│ │ PDV │ │ PESSOAS │ │ ESTOQUE │ │SERVIÇOS │ ║
║ │ (KPIs) │ │ (Caixa) │ │(Cadastro│ │(Depósito│ │ (O.S.) │ ║
║ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ ║
║ │ │ │ │ │ ║
║ ┌────┴────┐ ┌────┴────┐ ┌────┴────┐ ┌────┴────┐ ┌────┴────┐ ║
║ │TRADE-IN │ │ COMPRAS │ │CADASTROS│ │RELATÓRIO│ │COMISSÕES│ ║
║ │(Aval.) │ │(Aquis.) │ │ (Config)│ │(Reports)│ │ (Metas) │ ║
║ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ ║
║ │ │ │ │ │ ║
║ └────────────┴────────────┴─────┬──────┴────────────┘ ║
║ │ ║
║ ┌─────────┴─────────┐ ║
║ │ CONFIGURAÇÃO │ ║
║ │ Plus / Empresas │ ║
║ └───────────────────┘ ║
╚══════════════════════════════════════════════════════════════════════════════════╝
```
---
## 2. FLUXO PRINCIPAL DE OPERAÇÕES
```
┌──────────────────────────────────────────────┐
│ ENTRADA DE MERCADORIA │
└──────────────────────┬───────────────────────┘
┌──────────────────────┬┴──────────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ COMPRA │ │ TRADE-IN │ │ CONSIGNAÇÃO │
│ Fornecedor │ │ Cliente │ │ Parceiro │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
│ ┌──────┴───────┐ │
│ │ Avaliação │ │
│ │ 19 itens │ │
│ │ checklist │ │
│ └──────┬───────┘ │
│ │ │
│ ┌──────┴───────┐ │
│ │ Aprovação │ │
│ │ Gera Crédito│ │
│ │ Cria O.S. │ │
│ └──────┬───────┘ │
│ │ │
│ ┌──────┴───────┐ │
│ │ Revisão │ │
│ │ O.S. Interna│ │
│ │ Manutenção │ │
│ └──────┬───────┘ │
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────┐
│ ESTOQUE │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │Depósito │ │Depósito │ │Depósito │ │
│ │ Loja │ │ Central │ │Trânsito │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ │
│ └────────────┼────────────┘ │
│ │ │
│ ┌────────────────┬┴──────────────────┐ │
│ │ Dispositivos │ Produtos │ │
│ │ (por IMEI) │ (por qtd/série) │ │
│ └────────────────┴───────────────────┘ │
└────────────────────┬────────────────────────────────┘
┌─────────────────────────────────────────────────────┐
│ PDV │
│ │
│ ┌────────────┐ ┌────────────┐ ┌──────────────┐ │
│ │Dispositivos│ │ Produtos │ │ Faturar O.S. │ │
│ │ (IMEI) │ │(Acessórios)│ │ (Serviços) │ │
│ └─────┬──────┘ └─────┬──────┘ └──────┬───────┘ │
│ └───────────────┼────────────────┘ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ CARRINHO │ │
│ │ │ │
│ │ Subtotal │ │
│ │ - Desconto │ │
│ │ - Trade-In │ │
│ │ - Crédito │ │
│ │ = TOTAL │ │
│ └────────┬─────────┘ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ PAGAMENTO │ │
│ │ 💵 Dinheiro │ │
│ │ 💳 Débito │ │
│ │ 💳 Crédito (Nx) │ │
│ │ 📱 PIX │ │
│ │ 🔀 Combinado │ │
│ └────────┬─────────┘ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ VENDA │ │
│ │ Impressão A4 │ │
│ │ Sync Plus │ │
│ │ NF-e/NFC-e │ │
│ └──────────────────┘ │
└─────────────────────────────────────────────────────┘
┌────────────┼────────────────┐
▼ ▼ ▼
┌────────────┐ ┌─────────┐ ┌────────────┐
│ COMISSÃO │ │RELATÓRIO│ │ DEVOLUÇÃO │
│ Vendedor │ │Caixa │ │ Troca │
│ Metas │ │Diário │ │ Crédito │
└────────────┘ └─────────┘ └────────────┘
```
---
## 3. CICLO DE VIDA DO DISPOSITIVO (IMEI)
```
┌─────────────────────────────────────────────────────────────────────────┐
│ JORNADA DO DISPOSITIVO POR IMEI │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ENTRADA │
│ ═══════ │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ COMPRA │ │ TRADE-IN │ │ CONSIGNAÇÃO │ │
│ │ Fornecedor │ │ Cliente │ │ Parceiro │ │
│ │ condition: │ │ condition: │ │ condition: │ │
│ │ new │ │ used/refurb │ │ varies │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ │ ┌──────┴───────┐ │ │
│ │ │ AVALIAÇÃO │ │ │
│ │ │ Checklist │ │ │
│ │ │ 19 itens │ │ │
│ │ └──────┬───────┘ │ │
│ │ │ │ │
│ │ ┌──────┴───────┐ │ │
│ │ │ O.S. INT. │ │ │
│ │ │ Revisão │ │ │
│ │ │ Manutenção │ │ │
│ │ └──────┬───────┘ │ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ESTOQUE (status: in_stock) │ │
│ │ │ │
│ │ IMEI: 35XXXXXXXXXXXXX │ │
│ │ Marca: Samsung | Modelo: S24 Ultra │ │
│ │ Cor: Preto | Storage: 256GB | RAM: 12GB │ │
│ │ Condição: refurbished │ │
│ │ Preço Compra: R$ 2.500 | Preço Venda: R$ 3.500 │ │
│ │ Depósito: Loja Centro │ │
│ └──────────────────────┬──────────────────────────────┘ │
│ │ │
│ MOVIMENTAÇÕES │ │
│ ═══════════════ │ │
│ │ │
│ ┌──────────┐ ┌──────┴──────┐ ┌──────────────┐ │
│ │TRANSFER. │◄───│ DISPONÍVEL │───►│ ASSISTÊNCIA │ │
│ │Entre lojas│ │ para venda │ │ O.S. cliente│ │
│ │in_transit │ │ in_stock │ │ in_service │ │
│ └──────────┘ └──────┬──────┘ └──────┬───────┘ │
│ │ │ │
│ │ │ (retorna após reparo) │
│ │ └──────────┐ │
│ │ │ │
│ SAÍDA ▼ ▼ │
│ ═════ ┌──────────────┐ ┌──────────────┐ │
│ │ VENDA │ │ DE VOLTA AO │ │
│ │ PDV + NF-e │ │ ESTOQUE │ │
│ │ status:sold │ │ in_stock │ │
│ └──────┬───────┘ └──────────────┘ │
│ │ │
│ ┌──────┴───────┐ │
│ │ DEVOLUÇÃO? │ │
│ │ returned │──────────► Volta ao estoque │
│ └──────────────┘ │
│ │
│ RASTREAMENTO: device_history + imei_history (kardex completo) │
│ │
└─────────────────────────────────────────────────────────────────────────┘
```
---
## 4. FLUXO DO TRADE-IN (4 ETAPAS DETALHADAS)
```
╔═══════════════════════════════════════════════════════════════════════════╗
║ TRADE-IN - FLUXO COMPLETO ║
╠═══════════════════════════════════════════════════════════════════════════╣
║ ║
║ ETAPA 1: AVALIAÇÃO ║
║ ══════════════════ ║
║ ║
║ ┌────────────────────────────────────────────────────────────┐ ║
║ │ Cliente chega com dispositivo usado │ ║
║ │ │ ║
║ │ 1. Identificar cliente (busca por nome/CPF) │ ║
║ │ 2. Registrar IMEI, Marca, Modelo │ ║
║ │ 3. Preencher Checklist (19 itens): │ ║
║ │ │ ║
║ │ ┌──────────────────────────────────────────────────────┐ │ ║
║ │ │ ☐ Liga corretamente ☐ Sensores/NFC │ │ ║
║ │ │ ☐ Sem avarias/fantasma ☐ Face ID/Touch ID │ │ ║
║ │ │ ☐ Sem manchas na tela ☐ Microfones │ │ ║
║ │ │ ☐ Botões funcionando ☐ Áudio auricular │ │ ║
║ │ │ ☐ Marcas de uso ☐ Alto-falante │ │ ║
║ │ │ ☐ Wi-Fi ☐ Carregamento │ │ ║
║ │ │ ☐ Chip ☐ Câmeras │ │ ║
║ │ │ ☐ 4G/5G ☐ Flash │ │ ║
║ │ │ ☐ Possui carregador ☐ 3uTools OK │ │ ║
║ │ │ 🔋 Bateria: ____% │ │ ║
║ │ └──────────────────────────────────────────────────────┘ │ ║
║ │ │ ║
║ │ 4. Listar peças necessárias (se houver) │ ║
║ │ 5. Definir valor estimado │ ║
║ │ 6. Assinatura digital do cliente │ ║
║ │ 7. Imprimir comprovante │ ║
║ │ │ ║
║ │ Status: PENDING │ ║
║ └────────────────────────────────────────────┬───────────────┘ ║
║ │ ║
║ ▼ ║
║ ETAPA 2: APROVAÇÃO ║
║ ══════════════════ ║
║ ║
║ ┌────────────────────────────────────────────────────────────┐ ║
║ │ Gerente revisa avaliação e decide: │ ║
║ │ │ ║
║ │ [APROVAR] [REJEITAR] │ ║
║ │ │ │ │ ║
║ │ ▼ ▼ │ ║
║ │ Automaticamente: Avaliação │ ║
║ │ ✅ Gera CRÉDITO para cliente rejeitada. │ ║
║ │ (R$ do valor estimado) Sem ações. │ ║
║ │ ✅ Cria O.S. INTERNA │ ║
║ │ (INT2602XXXXXX) │ ║
║ │ ✅ Registra no IMEI History │ ║
║ │ │ ║
║ │ Status: APPROVED │ ║
║ └────────────────────────────────────────────┬───────────────┘ ║
║ │ ║
║ ▼ ║
║ ETAPA 3: REVISÃO / MANUTENÇÃO ║
║ ═════════════════════════════ ║
║ ║
║ ┌────────────────────────────────────────────────────────────┐ ║
║ │ Técnico trabalha na O.S. Interna: │ ║
║ │ │ ║
║ │ 1. Diagnóstico técnico │ ║
║ │ 2. Troca de peças (registra peças + custos) │ ║
║ │ 3. Limpeza e preparação │ ║
║ │ 4. Teste de qualidade │ ║
║ │ 5. Preenche checklist de conclusão │ ║
║ │ 6. Finaliza O.S. Interna │ ║
║ │ │ ║
║ │ Custos: │ ║
║ │ ├── Peças utilizadas: R$ XXX │ ║
║ │ ├── Mão de obra: R$ XXX │ ║
║ │ └── Total reparo: R$ XXX │ ║
║ │ │ ║
║ │ Status O.S.: COMPLETED │ ║
║ └────────────────────────────────────────────┬───────────────┘ ║
║ │ ║
║ ▼ ║
║ ETAPA 4: ENTRADA NO ESTOQUE ║
║ ═══════════════════════════ ║
║ ║
║ ┌────────────────────────────────────────────────────────────┐ ║
║ │ Ao finalizar O.S. Interna: │ ║
║ │ │ ║
║ │ ✅ Cria dispositivo no estoque (mobile_devices) │ ║
║ │ condition: "refurbished" │ ║
║ │ acquisitionType: "trade_in" │ ║
║ │ │ ║
║ │ 💰 Cálculo do preço sugerido: │ ║
║ │ Custo aquisição = Valor Trade-In + Custo Reparo │ ║
║ │ Preço sugerido = Custo × (1 + Margem%) │ ║
║ │ Ex: R$1500 + R$300 = R$1800 × 1.30 = R$ 2.340 │ ║
║ │ │ ║
║ │ ✅ Registra no IMEI History │ ║
║ │ ✅ Registra no Activity Feed │ ║
║ │ ✅ Disponível no PDV para venda │ ║
║ │ │ ║
║ │ Status: IN_STOCK (pronto para venda) │ ║
║ └────────────────────────────────────────────────────────────┘ ║
╚═══════════════════════════════════════════════════════════════════════════╝
```
---
## 5. MAPA DO PDV (PONTO DE VENDA)
```
╔═══════════════════════════════════════════════════════════════════════╗
║ PDV - PONTO DE VENDA ║
╠═══════════════════════════════════════════════════════════════════════╣
║ ║
║ BARRA SUPERIOR ║
║ ┌──────────────────────────────────────────────────────────────────┐ ║
║ │ 🏪 Empresa: Loja Centro 👤 Vendedor: João Silva │ ║
║ │ │ ║
║ │ [Sangria] [Reforço] [Devolução] [Selecionar Cliente] [Limpar] │ ║
║ └──────────────────────────────────────────────────────────────────┘ ║
║ ║
║ ┌───────────────────────────────────┬──────────────────────────────┐ ║
║ │ CATÁLOGO (3 colunas) │ CARRINHO (2 colunas) │ ║
║ │ │ │ ║
║ │ ┌──────────┬──────────┬────────┐ │ ┌──────────────────────────┐│ ║
║ │ │📱Disposit│📦Produtos│🔧Fat.OS│ │ │ Samsung S24 Ultra ││ ║
║ │ └──────────┴──────────┴────────┘ │ │ IMEI: 35XXX 📱Celular ││ ║
║ │ │ │ R$ 3.500,00 ││ ║
║ │ [🔍 Buscar... ] │ ├──────────────────────────┤│ ║
║ │ │ │ Capinha Silicone ││ ║
║ │ ┌──────────────────────────────┐ │ │ 2x R$ 45,00 ││ ║
║ │ │ Samsung S24 Ultra │ │ │ R$ 90,00 ││ ║
║ │ │ 256GB | Preto | IMEI: 35XX │ │ ├──────────────────────────┤│ ║
║ │ │ 🟢 Novo 📦 Loja Centro │ │ │ O.S. #OS2602ABC ││ ║
║ │ │ R$ 3.500,00 │ │ │ Troca de tela 🔧Serviço ││ ║
║ │ │ [+ Adicionar] │ │ │ R$ 450,00 ││ ║
║ │ ├──────────────────────────────┤ │ ╞══════════════════════════╡│ ║
║ │ │ iPhone 15 Pro │ │ │ Subtotal R$ 4.040,00 ││ ║
║ │ │ 128GB | Branco | IMEI: 86XX │ │ │ Desconto -R$ 40,00 ││ ║
║ │ │ 🔵 Recond 📦 Loja Centro │ │ │ Trade-In -R$ 1.500,00 ││ ║
║ │ │ R$ 4.200,00 │ │ │ Crédito -R$ 200,00 ││ ║
║ │ │ [+ Adicionar] │ │ │─────────────────────────││ ║
║ │ ├──────────────────────────────┤ │ │ TOTAL R$ 2.300,00 ││ ║
║ │ │ Xiaomi 14 │ │ │ ││ ║
║ │ │ 512GB | Azul | IMEI: 86XX │ │ │ 💙 Crédito: R$ 200,00 ││ ║
║ │ │ 🟡 Usado 📦 Loja Centro │ │ │ ││ ║
║ │ │ R$ 2.800,00 │ │ │ [↕️ Trade-In] ││ ║
║ │ │ [+ Adicionar] │ │ │ [💰 FINALIZAR VENDA] ││ ║
║ │ └──────────────────────────────┘ │ └──────────────────────────┘│ ║
║ └───────────────────────────────────┴──────────────────────────────┘ ║
║ ║
║ MODAL DE PAGAMENTO ║
║ ┌──────────────────────────────────────────────────────────────────┐ ║
║ │ Total: R$ 2.300,00 │ ║
║ │ │ ║
║ │ [Dinheiro ✓] [Débito] [Crédito] [PIX] [Combinado] │ ║
║ │ │ ║
║ │ Valor: [R$ 2.300,00] Parcelas: [1x] │ ║
║ │ │ ║
║ │ Desconto: [___]% ou [R$ ___] │ ║
║ │ │ ║
║ │ [CONFIRMAR PAGAMENTO] │ ║
║ └──────────────────────────────────────────────────────────────────┘ ║
╚═══════════════════════════════════════════════════════════════════════╝
```
---
## 6. FLUXO DA ORDEM DE SERVIÇO (12 ESTADOS)
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ ORDEM DE SERVIÇO - FLUXO DE STATUS │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ │
│ │ ABERTA │ ◄── Cliente entrega dispositivo │
│ │ open │ │
│ └────┬─────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ DIAGNÓSTICO │ ◄── Técnico analisa problema │
│ │ diagnosis │ │
│ └──────┬──────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ ORÇAMENTO │ ◄── Técnico elabora orçamento │
│ │ quote │ │
│ └──────┬──────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────┐ │
│ │ AGUARD. APROVAÇÃO │ ◄── Enviado para cliente │
│ │ pending_approval │ │
│ └─────┬────────┬─────┘ │
│ │ │ │
│ APROVOU │ │ REJEITOU │
│ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ │
│ │ APROVADA │ │ REJEITADA│ ──► FIM │
│ │ approved │ │ rejected │ │
│ └────┬─────┘ └──────────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │ EM REPARO│ ◄── Técnico inicia trabalho │
│ │in_repair │ │
│ └────┬─────┘ │
│ │ │
│ ┌──────────┼──────────┐ │
│ │ │ │
│ ▼ │ │
│ ┌──────────────┐ │ │
│ │ AGUARD. PEÇAS│ │ ◄── Se precisar de peça │
│ │waiting_parts │───────────────┘ │
│ └──────────────┘ (peça chegou, volta para reparo) │
│ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ QUALIDADE │ ◄── Verificação pós-reparo │
│ │quality_check │ │
│ └──────┬───────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ PRONTO PARA │ ◄── Disponível para retirada │
│ │ RETIRADA │ (aparece no PDV p/ faturar) │
│ │ ready_pickup │ │
│ └──────┬───────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ CONCLUÍDA │ ◄── Cliente retirou + pagou │
│ │ completed │ (faturada no PDV) │
│ └──────────────┘ │
│ │
│ Em qualquer momento: ──────────► ┌──────────────┐ │
│ │ CANCELADA │ │
│ │ cancelled │ │
│ └──────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
```
---
## 7. MAPA DE RELATÓRIOS
```
┌─────────────────────────────────────────────────────────────────────┐
│ RELATÓRIOS DO RETAIL │
├──────────────┬──────────────────────────────────────────────────────┤
│ │ │
│ OS POR │ Status │ Qtd │ Valor Total │
│ STATUS │ open │ 12 │ R$ 5.400 │
│ │ in_repair │ 8 │ R$ 3.200 │
│ │ completed │ 45 │ R$ 22.500 │
│ │ │
├──────────────┼──────────────────────────────────────────────────────┤
│ │ │
│ OS POR │ Técnico │ Total │ Concl. │ Andamento │ Receita │
│ TÉCNICO │ Carlos │ 15 │ 12 │ 3 │ R$8.500 │
│ │ Pedro │ 10 │ 8 │ 2 │ R$5.200 │
│ │ │
├──────────────┼──────────────────────────────────────────────────────┤
│ │ │
│ VENDAS POR │ Vendedor │ Vendas │ Receita │ Ticket │ Dias │
│ VENDEDOR │ João │ 28 │ R$45.000 │ R$1607 │ 22 │
│ │ Maria │ 22 │ R$38.000 │ R$1727 │ 20 │
│ │ │
├──────────────┼──────────────────────────────────────────────────────┤
│ │ │
│ MARGEM │ Dispositivo │ IMEI │ Custo │ Venda │ Margem │
│ POR IMEI │ S24 Ultra │ 35XXX │ R$2500 │ R$3500 │ 40.0% │
│ │ iPhone 15 │ 86XXX │ R$3800 │ R$4200 │ 10.5% │
│ │ │
├──────────────┼──────────────────────────────────────────────────────┤
│ │ │
│ CAIXA │ ┌────────────────────────────────────────────┐ │
│ DIÁRIO │ │ Total Vendas: R$ 12.500 (8 vendas) │ │
│ │ │ Dinheiro: R$ 4.200 │ Cartão: R$ 5.800 │ │
│ │ │ PIX: R$ 2.500 │ Combinado: R$ 0 │ │
│ (4 queries) │ │ Sangrias: -R$ 500 │ Reforços: +R$ 200 │ │
│ │ │ Saldo Caixa: R$ 3.900 │ │
│ │ └────────────────────────────────────────────┘ │
│ │ │
│ │ POR VENDEDOR: │
│ │ ┌──────┬──────┬──────┬──────┬──────┬──────┬──────┐ │
│ │ │Vendedor│Vendas│Dinh. │Déb. │Créd.│ PIX │TOTAL │ │
│ │ │João │ 5 │R$2100│R$1500│R$800│R$1200│R$5600│ │
│ │ │Maria │ 3 │R$2100│R$1300│R$600│R$1300│R$6900│ │
│ │ │TOTAL │ 8 │R$4200│R$2800│R$1400│R$2500│R$12.5│ │
│ │ └──────┴──────┴──────┴──────┴──────┴──────┴──────┘ │
│ │ │
├──────────────┼──────────────────────────────────────────────────────┤
│ │ │
│ GIRO DE │ Produto │ Estoque │ Vendas 30d │ Turnover │
│ ESTOQUE │ Capinha │ 50 │ 35 │ 0.70 │
│ │ Película │ 80 │ 42 │ 0.53 │
│ │ │
└──────────────┴──────────────────────────────────────────────────────┘
```
---
## 8. ARQUITETURA TÉCNICA
```
╔═══════════════════════════════════════════════════════════════════════╗
║ ARQUITETURA DO MÓDULO RETAIL ║
╠═══════════════════════════════════════════════════════════════════════╣
║ ║
║ FRONTEND (React + TypeScript + Tailwind + shadcn/ui) ║
║ ═══════════════════════════════════════════════════ ║
║ ║
║ client/src/pages/ArcadiaRetail.tsx ──── 10.067 linhas ║
║ │ ║
║ ├── Dashboard (KPIs, Feed, Alertas) ║
║ ├── PDV (Carrinho, Catálogo, Pagamento) ║
║ ├── Pessoas (CRUD, Papéis, Histórico) ║
║ ├── Estoque (Depósitos, Séries, Inventário) ║
║ ├── Serviços (O.S. CRUD, Checklist) ║
║ ├── Trade-In (Avaliações, Fluxo 4 etapas) ║
║ ├── Compras (Pedidos, Recebimento) ║
║ ├── Cadastros (Pagamento, Vendedores, Promoções) ║
║ ├── Relatórios (6 sub-abas) ║
║ ├── Comissões (Dashboard, Metas, Fechamento) ║
║ └── Configuração (Plus, Empresas, Sync) ║
║ ║
║ client/src/components/TradeInForm.tsx ─── 988 linhas ║
║ └── Formulário completo c/ checklist + assinatura ║
║ ║
║ ───────────────────────────────────────────────────────────── ║
║ API (fetch → /api/retail/*) ║
║ ───────────────────────────────────────────────────────────── ║
║ ║
║ BACKEND (Express.js + Drizzle ORM) ║
║ ══════════════════════════════════ ║
║ ║
║ server/retail/routes.ts ──── 5.218 linhas (~130 endpoints) ║
║ │ ║
║ ├── CRUD: Devices, Evaluations, ServiceOrders, ║
║ │ Persons, Products, Warehouses, etc. ║
║ ├── PDV: Sessions, Sales, Payments, CashMovements ║
║ ├── Trade-In: Approve, Process, FullFlow, Stock ║
║ ├── Reports: OsStatus, OsTech, SalesSeller, ║
║ │ MarginIMEI, DailyCash, StockTurnover ║
║ ├── Returns: Search, Process, Credits ║
║ └── Commissions: Plans, Goals, Calculate, Close ║
║ ║
║ server/retail/plus-sync.ts ──── 542 linhas ║
║ └── Sync: Customers, Sales+Items, NF-e ║
║ ║
║ ───────────────────────────────────────────────────────────── ║
║ Drizzle ORM → PostgreSQL ║
║ ───────────────────────────────────────────────────────────── ║
║ ║
║ BANCO DE DADOS (PostgreSQL + 40 tabelas) ║
║ ════════════════════════════════════════ ║
║ ║
║ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ║
║ │ Dispositivos │ │ Vendas │ │ Estoque │ ║
║ │ mobile_devices│ │ pos_sales │ │ warehouses │ ║
║ │ device_history│ │pos_sale_items│ │warehouse_stk │ ║
║ │ imei_history │ │pos_sessions │ │stock_movem. │ ║
║ └──────────────┘ │cash_movements│ │transfers │ ║
║ └──────────────┘ │serials │ ║
║ ┌──────────────┐ │inventories │ ║
║ │ Trade-In │ ┌──────────────┐ └──────────────┘ ║
║ │ evaluations │ │ Serviços │ ║
║ │ checklist_* │ │service_orders│ ┌──────────────┐ ║
║ │ acquisitions │ │ so_items │ │ Cadastros │ ║
║ │ transfer_doc │ │ warranties │ │payment_meth. │ ║
║ └──────────────┘ └──────────────┘ │ sellers │ ║
║ │ comm_plans │ ║
║ ┌──────────────┐ ┌──────────────┐ │ promotions │ ║
║ │ Créditos │ │ Devoluções │ │ price_tables │ ║
║ │customer_cred.│ │return_exch. │ │product_types │ ║
║ └──────────────┘ │return_items │ └──────────────┘ ║
║ └──────────────┘ ║
║ ║
║ ───────────────────────────────────────────────────────────── ║
║ ║
║ INTEGRAÇÃO PLUS (Laravel ERP - Porta 8080) ║
║ ══════════════════════════════════════════ ║
║ ║
║ ┌───────────┐ ┌───────────┐ ┌───────────┐ ║
║ │Proxy Rev. │ │ SSO │ │Auto-Start │ ║
║ │/plus/* → │ │HMAC-SHA256│ │php artisan│ ║
║ │:8080 │ │Token │ │serve │ ║
║ └───────────┘ └───────────┘ └───────────┘ ║
║ │ ║
║ ▼ ║
║ ┌─────────────────────────────────────────────────┐ ║
║ │ Plus (Laravel) │ ║
║ │ ├── NF-e / NFC-e (SEFAZ) │ ║
║ │ ├── Clientes / Fornecedores │ ║
║ │ ├── Vendas / Faturamento │ ║
║ │ └── Fiscal completo │ ║
║ └─────────────────────────────────────────────────┘ ║
║ ║
╚═══════════════════════════════════════════════════════════════════════╝
```
---
## 9. SEGURANÇA MULTI-TENANT
```
┌─────────────────────────────────────────────────────────────────────┐
│ CAMADAS DE SEGURANÇA │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ CAMADA 1: AUTENTICAÇÃO │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ Passport.js Session → req.isAuthenticated() │ │
│ │ Todas as rotas /api/retail/* exigem login │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ CAMADA 2: TENANT SCOPING │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ req.user.tenantId → obrigatório │ │
│ │ Se ausente → 403 "Tenant not identified" │ │
│ │ WHERE tenant_id = $tenantId em TODAS as queries │ │
│ │ Dados de um tenant NUNCA vazam para outro │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ CAMADA 3: MODULE GATING │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ requireModule('retail') → verifica tenants.features.retail │ │
│ │ Se módulo inativo → 403 "Módulo não habilitado" │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ CAMADA 4: OPERAÇÕES SENSÍVEIS │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ Devoluções → Exige senha do GERENTE │ │
│ │ Sangrias → Registra responsável + autorizador │ │
│ │ Exclusões → Soft delete quando possível │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ CAMADA 5: AUDITORIA │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ retail_activity_feed → Todas as operações relevantes │ │
│ │ device_history → Movimentação de IMEI │ │
│ │ imei_history → Kardex completo por IMEI │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
---
## 10. MAPA DE INTEGRAÇÕES
```
┌─────────────────────────────────────────────────────────────────────┐
│ INTEGRAÇÕES DO RETAIL │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────┐ │
│ │ ARCÁDIA RETAIL │ │
│ │ (Express) │ │
│ └────────┬─────────┘ │
│ │ │
│ ┌───────────────────┼───────────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ PLUS (PHP) │ │ ERPNEXT │ │ POSTGRESQL │ │
│ │ :8080 │ │ (API ext.) │ │ (Drizzle) │ │
│ │ │ │ │ │ │ │
│ │ ▸ NF-e/NFC-e│ │ ▸ Clientes │ │ ▸ 40+ tabelas│ │
│ │ ▸ Clientes │ │ ▸ Fornecedor │ │ ▸ Retail │ │
│ │ ▸ Vendas │ │ ▸ Estoque │ │ ▸ Multi-ten. │ │
│ │ ▸ Fiscal │ │ ▸ Financeiro │ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ SEFAZ │ │
│ │ NF-e / NFC-e (via Plus Laravel + nfelib) │ │
│ └──────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
---
**Métricas Finais:**
- **16.800+ linhas** de código
- **40+ tabelas** no banco
- **~130 endpoints** REST
- **11 abas** na interface
- **19 itens** no checklist de avaliação
- **12 estados** de O.S.
- **4 etapas** no fluxo de Trade-In
- **5 formas** de pagamento
- **4 queries** no caixa diário

676
MAPA_PLUS_ARCADIA.md Normal file
View File

@ -0,0 +1,676 @@
# Arcádia Plus - Mapa Completo dentro do Arcádia Suite
## O que é o Plus?
O **Arcádia Plus** é o **Motor de Execução Fiscal e Operacional** da plataforma Arcádia Suite. Enquanto o Arcádia Suite pensa, governa e orienta, o Plus executa, registra e obedece. É um ERP completo construído em Laravel/PHP que serve como backend fiscal para emissão de documentos eletrônicos (NF-e, NFC-e, CT-e, MDF-e) e operações comerciais.
---
## Posição na Arquitetura
```
┌──────────────────────────────────────────────────────────────────────┐
│ ARCÁDIA SUITE (Cérebro) │
│ Express.js - Porta 5000 │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ SOE - Sistema Operacional Empresarial │ │
│ │ Domínios: Pessoas, Produtos, Vendas, Financeiro, Fiscal │ │
│ │ Decisão + Governança + IA │ │
│ └────────────────────┬───────────────────────────────────────┘ │
│ │ │
│ │ SSO + Proxy (/plus) │
│ ▼ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ ARCÁDIA PLUS (Motor de Execução) │ │
│ │ Laravel/PHP - Porta 8080 │ │
│ │ Execução: NFe, NFCe, CTe, MDFe, PDV, Estoque, OS │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ Outros Motores: │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐│
│ │Contábil │ │ Fiscal │ │ BI │ │Automação │ │ Comm ││
│ │ 8003 │ │ 8002 │ │ 8004 │ │ 8005 │ │ 8006 ││
│ │ Python │ │ Python │ │ Python │ │ Python │ │ Node.js ││
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘│
└──────────────────────────────────────────────────────────────────────┘
```
---
## Integração Suite ↔ Plus
### 1. Proxy Reverso (`server/plus/proxy.ts`)
O Arcádia Suite atua como gateway. Todas as requisições para `/plus/*` são redirecionadas para o Laravel na porta 8080:
```
Navegador → /plus/vendas → Suite (5000) → Proxy → Laravel (8080/vendas)
Navegador → /plus/nfe → Suite (5000) → Proxy → Laravel (8080/nfe)
```
**Comportamentos do proxy:**
- `/plus` ou `/plus/` → redireciona para `/empresas` (home do Laravel)
- `/plus/home` → redireciona para `/empresas`
- Headers `X-Forwarded-Host`, `X-Forwarded-Proto`, `X-Forwarded-Prefix` repassados
- Respostas Location reescritas para manter o prefixo `/plus`
- Timeout: 60 segundos
- Status endpoint: `GET /api/plus/status`
### 2. SSO - Single Sign-On (`plus/app/Http/Controllers/Auth/SsoController.php`)
Login unificado: o usuário entra pelo Arcádia Suite e é autenticado automaticamente no Plus.
```
┌─────────────────┐ Token JWT (Base64) ┌──────────────────┐
│ Arcádia Suite │ ──────────────────────────▶ │ Arcádia Plus │
│ (Node.js) │ sig = HMAC-SHA256 │ (Laravel) │
│ │ ts = timestamp │ │
│ Gera token com: │ │ Valida: │
│ - email │ │ - iss = arcadia │
│ - name │ │ - aud = plus │
│ - role │ │ - timestamp ±5m │
│ - username │ │ - HMAC signature │
└─────────────────┘ └──────────────────┘
Auto-cria usuário
se não existir
```
**Segurança SSO:**
- Segredo compartilhado: `SSO_SECRET` (variável de ambiente)
- Expiração do token: 5 minutos (300 segundos)
- Assinatura: HMAC-SHA256
- Validação de issuer (`arcadia-suite`) e audience (`arcadia-plus`)
- Provisionamento automático: se o email não existe no Plus, cria o usuário
### 3. Launcher (`server/plus/launcher.ts`)
O Suite inicia o Laravel automaticamente na inicialização:
```
Suite inicia → Verifica PHP disponível → spawn php artisan serve --port=8080
┌───────┴───────┐
│ PHP existe? │
├── Sim → Inicia│
└── Não → Pula │
```
**Verificações:**
- `which php` - PHP instalado?
- `plus/artisan` existe?
- Timeout: 15 segundos para iniciar
---
## Números do Plus
| Métrica | Quantidade |
|---------|-----------|
| Controllers | 376 |
| Models | 320 |
| Services | 12 |
| Views (Blade) | 1.194 |
| Migrations | 260 |
| Rotas Web | 912 |
| Rotas API | 634 |
| **Total de Rotas** | **1.546** |
---
## Módulos do Plus - Detalhamento Completo
### 1. FISCAL - Documentos Eletrônicos
O coração do Plus. Emissão e gestão de todos os documentos fiscais brasileiros.
#### NF-e (Nota Fiscal Eletrônica)
| Componente | Arquivo |
|-----------|---------|
| Controller Web | `NfeController.php` |
| Controller API | `API/NFeController.php` |
| Painel API | `API/NFePainelController.php` |
| Service | `NFeService.php` |
| Service API | `NFeServiceApi.php` |
| Model | `Nfe.php`, `ItemNfe.php`, `FaturaNfe.php` |
| Views | `nfe/*.blade.php` |
| XML | `NfeXmlController.php` |
| Entrada XML | `NfeEntradaXmlController.php`, `NfeImportaXmlController.php` |
**Funcionalidades NF-e:**
- Emissão, cancelamento, carta de correção
- Importação de XML de entrada
- Download de XML/DANFE
- Manifesto do destinatário (DFe)
- Inutilização de numeração
- Contingência offline
#### NFC-e (Nota Fiscal do Consumidor)
| Componente | Arquivo |
|-----------|---------|
| Controller Web | `NfceController.php` |
| Controller API | `API/NFCeController.php` |
| Painel API | `API/NFCePainelController.php` |
| Service | `NFCeService.php` |
| Service API | `NFCeServiceApi.php` |
| Model | `Nfce.php`, `ItemNfce.php`, `FaturaNfce.php` |
| Views | `nfce/*.blade.php` |
| Contingência | `NfceContigenciaController.php` |
| Impressão | `ImprimirNfceController.php` |
#### CT-e (Conhecimento de Transporte)
| Componente | Arquivo |
|-----------|---------|
| Controller Web | `CteController.php` |
| Controller API | `API/CTeController.php` |
| CT-e OS | `CteOsController.php`, `API/CTeOsPainelController.php` |
| Service | `CTeService.php`, `CTeOsService.php` |
| Model | `Cte.php`, `MedidaCte.php`, `ComponenteCte.php` |
| Views | `cte/*.blade.php` |
#### MDF-e (Manifesto de Documentos Fiscais)
| Componente | Arquivo |
|-----------|---------|
| Controller Web | `MdfeController.php` |
| Controller API | `API/MdfeController.php` |
| Service | `MDFeService.php` |
| Model | `Mdfe.php`, `Percurso.php`, `UnidadeCarga.php` |
| Views | `mdfe/*.blade.php` |
#### Nota de Serviço (NFS-e)
| Componente | Arquivo |
|-----------|---------|
| Controller Web | `NotaServicoController.php` |
| Controller API | `API/NotaServicoController.php` |
| Config | `NotaServicoConfigController.php` |
| Webhook | `API/NfseWebHookController.php` |
#### Configurações Fiscais
| Componente | Descrição |
|-----------|-----------|
| `NaturezaOperacaoController.php` | Naturezas de operação (CFOP) |
| `NcmController.php` | NCM - Nomenclatura Comum do Mercosul |
| `IbptController.php` / `IbptService.php` | IBPT - Carga tributária |
| `DifalController.php` | DIFAL - Diferencial de alíquota |
| `SpedController.php` / `SpedConfigController.php` | SPED Fiscal |
| `SintegraController.php` | SINTEGRA |
| `ContigenciaController.php` | Contingência NF-e/NFC-e |
| `InutilizacaoController.php` | Inutilização de numeração |
---
### 2. VENDAS E PDV
| Controller | Descrição |
|-----------|-----------|
| `VendaController.php` | Vendas completas |
| `API/VendaController.php` | API de vendas |
| `API/PDV/*.php` | Ponto de Venda (LoginController, ClienteController) |
| `API/FrontBoxController.php` | Frontend do caixa |
| `FrontBoxController.php` | Interface do caixa Web |
| `OrcamentoController.php` | Orçamentos |
| `API/PreVendaController.php` | Pré-vendas |
| `TrocaController.php` | Devoluções/trocas |
| `DevolucaoController.php` | Devoluções formais |
| `CupomDescontoController.php` | Cupons de desconto |
| `CashBackConfigController.php` | Configuração de cashback |
---
### 3. ESTOQUE E PRODUTOS
| Controller | Descrição |
|-----------|-----------|
| `ProdutoController.php` | Cadastro de produtos |
| `EstoqueController.php` | Controle de estoque |
| `EstoqueLocalizacaoController.php` | Localização no estoque |
| `StockEntryController.php` | Entrada de estoque |
| `StockEntryService.php` | Serviço de entrada |
| `StockSaleService.php` | Serviço de saída por venda |
| `TransferenciaEstoqueController.php` | Transferências |
| `InventarioController.php` | Inventários |
| `VariacaoController.php` | Variações de produto |
| `ListaPrecoController.php` | Listas de preço |
| `ComboController.php` | Combos/kits |
| `UnidadeMedidaController.php` | Unidades de medida |
| `MarcaController.php` | Marcas |
| `ImportadorController.php` | Importação de produtos |
| `ImeiTrackingController.php` | Rastreamento de IMEI |
| `API/ImeiUnitController.php` | Unidades IMEI via API |
---
### 4. FINANCEIRO
| Controller | Descrição |
|-----------|-----------|
| `ContaPagarController.php` | Contas a pagar |
| `ContaReceberController.php` | Contas a receber |
| `CaixaController.php` | Controle de caixa |
| `SangriaController.php` | Sangrias de caixa |
| `SuprimentoController.php` | Suprimentos de caixa |
| `BoletoController.php` | Boletos |
| `ContaBoletoController.php` | Contas com boleto |
| `FinanceiroBoletoController.php` | Financeiro boletos |
| `FinanceiroDashboardController.php` | Dashboard financeiro |
| `FinanceiroPlanoController.php` | Planos financeiros |
| `TaxaCartaoController.php` | Taxas de cartão |
| `ApuracaoMensalController.php` | Apuração mensal |
| `FechamentoMensalController.php` | Fechamento mensal |
| `CategoriaContaController.php` | Categorias contábeis |
| `ContaEmpresaController.php` | Contas bancárias |
| `AsaasController.php` | Integração Asaas (pagamentos) |
| `API/PaymentController.php` | API de pagamentos |
---
### 5. CLIENTES E CRM
| Controller | Descrição |
|-----------|-----------|
| `ClienteController.php` | Cadastro de clientes |
| `ClienteDeliveryController.php` | Clientes de delivery |
| `ClienteScoreController.php` | Score/pontuação de clientes |
| `CrmController.php` | CRM completo |
| `MensagemPadraoCrmController.php` | Mensagens padrão CRM |
| `MensagemCrmLogController.php` | Log de mensagens CRM |
| `ConvenioController.php` | Convênios |
| `ContratoConfigController.php` | Contratos |
| `AssinarContratoController.php` | Assinatura de contratos |
| `API/CrmController.php` | API CRM |
| `API/EnvioFaturaWppController.php` | Envio de fatura via WhatsApp |
---
### 6. FOOD SERVICE (Restaurantes, Pizzarias, Bares)
| Controller | Descrição |
|-----------|-----------|
| **Cardápio Digital** | |
| `ConfigCardapioController.php` | Configuração do cardápio |
| `CarrosselCardapioController.php` | Carrossel do cardápio |
| `AvaliacaoCardapioController.php` | Avaliações |
| `API/Cardapio/*.php` | API do cardápio (carrinho, produtos) |
| **Delivery** | |
| `API/Delivery/*.php` | API delivery (carrinho, clientes, pedidos, produtos) |
| `FuncionamentoDeliveryController.php` | Horários de delivery |
| **Comanda** | |
| `API/Comanda/*.php` | Sistema de comandas (7 controllers) |
| **Atendimento** | |
| `AtendimentoController.php` | Atendimentos |
| `AtendimentoGarcomController.php` | Atendimento garçom |
| `MesaController.php` | Gestão de mesas |
| **Pizza** | |
| `TamanhoPizzaController.php` | Tamanhos de pizza |
| `AdicionalController.php` | Adicionais |
| `CategoriaAdicionalController.php` | Categorias de adicionais |
| **iFood** | |
| `IfoodConfigController.php` | Configuração iFood |
| `IfoodCatalogoController.php` | Catálogo iFood |
| `IfoodPedidoController.php` | Pedidos iFood |
| `IfoodProdutoController.php` | Produtos iFood |
---
### 7. SERVIÇOS E ORDENS DE SERVIÇO
| Controller | Descrição |
|-----------|-----------|
| `ServicoController.php` | Cadastro de serviços |
| `CategoriaServicoController.php` | Categorias |
| `OrdemServicoController.php` | Ordens de serviço |
| `API/OrdemServicoController.php` | API de OS |
| `AgendamentoController.php` | Agendamentos |
| `ConfiguracaoAgendamentoController.php` | Config agendamento |
| `FuncionamentoController.php` | Horários |
| `InterrupcoesController.php` | Interrupções |
| `GarantiaController.php` | Garantias |
---
### 8. PRODUÇÃO
| Controller | Descrição |
|-----------|-----------|
| `GestaoProducaoController.php` | Gestão de produção |
| `API/GestaoProducaoController.php` | API produção |
| `API/OrdemProducaoController.php` | Ordens de produção |
| `ApontamentoController.php` | Apontamentos |
| `CustoConfiguracaoController.php` | Configuração de custos |
| `API/PlanejamentoCustoController.php` | Planejamento de custos |
---
### 9. TRANSPORTE E LOGÍSTICA
| Controller | Descrição |
|-----------|-----------|
| `TransportadoraController.php` | Transportadoras |
| `FreteController.php` | Fretes |
| `TipoDespesaFreteController.php` | Tipos de despesa |
| `VeiculoController.php` | Veículos |
| `ManutencaoVeiculoController.php` | Manutenção |
| `MotoboyController.php` | Motoboys |
| `LocalizacaoController.php` | Localização/rastreamento |
| `ManifestoController.php` | Manifestos de carga |
---
### 10. E-COMMERCE E MARKETPLACE
| Controller | Descrição |
|-----------|-----------|
| **WooCommerce** | |
| `WoocommerceConfigController.php` | Configuração |
| `WoocommerceCategoriaController.php` | Categorias |
| `WoocommerceProdutoController.php` | Produtos |
| `WoocommercePedidoController.php` | Pedidos |
| **Mercado Livre** | |
| `MercadoLivreAuthController.php` | Autenticação OAuth |
| `MercadoLivreConfigController.php` | Configuração |
| `MercadoLivreProdutoController.php` | Produtos |
| `MercadoLivrePerguntaController.php` | Perguntas |
| **NuvemShop** | |
| `NuvemShopController.php` | Integração NuvemShop |
| **VendiZap** | |
| `VendiZapConfigController.php` | Configuração |
| `VendiZapProdutoController.php` | Produtos |
| `VendiZapPedidoController.php` | Pedidos |
| `VendiZapCategoriaController.php` | Categorias |
| **Marketplace Próprio** | |
| `MarketPlaceConfigController.php` | Configuração |
| `DestaqueMarketPlaceController.php` | Destaques |
| `ServicoMarketPlaceController.php` | Serviços |
| `SegmentoController.php` | Segmentos |
| **E-commerce Próprio** | |
| `EcommerceConfigController.php` | Configuração |
| `API/EcommerceController.php` | API e-commerce |
---
### 11. RH E COLABORADORES
| Controller | Descrição |
|-----------|-----------|
| `FuncionarioController.php` | Funcionários |
| `FuncionarioEventoController.php` | Eventos de funcionário |
| `EventoFuncionarioController.php` | Registro de eventos |
| `ComissaoController.php` | Comissões |
| `MargemComissaoController.php` | Margens de comissão |
| `MetaResultadoController.php` | Metas e resultados |
| `ControleAcessoController.php` | Controle de acesso |
| `RoleController.php` | Permissões/roles |
---
### 12. SEGMENTOS ESPECIALIZADOS
#### Ótica
| Controller | Descrição |
|-----------|-----------|
| `FormatoArmacaoOticaController.php` | Formatos de armação |
| `TipoArmacaoController.php` | Tipos de armação |
| `LaboratorioController.php` | Laboratórios óticos |
| `TratamentoOticaController.php` | Tratamentos de lente |
| `MedicoController.php` | Médicos/oftalmologistas |
#### Hotelaria
| Controller | Descrição |
|-----------|-----------|
| `AcomodacaoController.php` | Acomodações |
| `CategoriaAcomodacaoController.php` | Categorias |
| `ReservaController.php` | Reservas |
| `ConfigReservaController.php` | Configuração |
| `FrigobarController.php` | Frigobar |
| `EstacionamentoController.php` | Estacionamento |
---
### 13. COMPRAS E FORNECEDORES
| Controller | Descrição |
|-----------|-----------|
| `CompraController.php` | Compras |
| `ComprasIAController.php` | Compras com IA |
| `FornecedorController.php` | Fornecedores |
| `CotacaoController.php` | Cotações |
| `CotacaoRespostaController.php` | Respostas de cotação |
---
### 14. ADMINISTRAÇÃO MULTI-TENANT
| Controller | Descrição |
|-----------|-----------|
| `EmpresaController.php` | Gestão de empresas |
| `UsuarioController.php` | Usuários |
| `UsuarioEmissaoController.php` | Usuários de emissão |
| `ContadorController.php` | Contador |
| `ContadorAdminController.php` | Admin do contador |
| `ContadorEmpresaController.php` | Empresas do contador |
| `ImpersonateController.php` | Impersonar usuário |
| `GerenciarPlanoController.php` | Gestão de planos |
| `UpgradePlanoController.php` | Upgrade de planos |
| `ConfiguracaoSuperController.php` | Config super admin |
| `BackupController.php` | Backups |
| `SistemaController.php` | Sistema |
| `LogController.php` | Logs |
---
### 15. TEF (Transferência Eletrônica de Fundos)
| Controller | Descrição |
|-----------|-----------|
| `TefConfigController.php` | Configuração TEF |
| `TefController.php` | Operações TEF |
| `TefRegistroController.php` | Registros TEF |
| Model: `TefMultiPlusCard.php` | Multi Plus Card |
---
### 16. INTELIGÊNCIA ARTIFICIAL
| Componente | Descrição |
|-----------|-----------|
| `ComprasIAController.php` | Sugestões de compra via IA |
| `FinancePrevisaoService.php` | Previsão financeira com IA |
---
## Models (320 entidades)
### Categorias Principais
| Grupo | Models Exemplo |
|-------|---------------|
| **Core** | `User`, `Empresa`, `Cidade`, `Config` |
| **Produtos** | `Produto`, `ProdutoVariacao`, `ProdutoComposicao`, `ProdutoAdicional`, `Marca`, `CategoriaProduto`, `UnidadeMedida` |
| **Vendas** | `Venda`, `ItemVenda`, `Orcamento`, `PreVenda`, `VendaSuspensa`, `Troca`, `Devolucao` |
| **Fiscal NF-e** | `Nfe`, `ItemNfe`, `FaturaNfe`, `NaturezaOperacao`, `PadraoTributacaoProduto` |
| **Fiscal NFC-e** | `Nfce`, `ItemNfce`, `FaturaNfce`, `Inutilizacao` |
| **CT-e** | `Cte`, `MedidaCte`, `ComponenteCte`, `ChaveNfeCte` |
| **MDF-e** | `Mdfe`, `Percurso`, `UnidadeCarga`, `ValePedagio`, `Ciot` |
| **Financeiro** | `ContaPagar`, `ContaReceber`, `Caixa`, `SangriaCaixa`, `SuprimentoCaixa`, `Pagamento` |
| **Estoque** | `Estoque`, `MovimentacaoProduto`, `StockMove`, `TransferenciaEstoque`, `ItemInventario` |
| **Pessoas** | `Cliente`, `Fornecedor`, `Funcionario`, `Transportadora`, `Motoboy` |
| **Food Service** | `Pedido`, `ItemPedido`, `Adicional`, `TamanhoPizza`, `ConfiguracaoCardapio`, `CarrosselCardapio` |
| **CRM** | `CrmAnotacao`, `MensagemPadraoCrm`, `CupomDesconto`, `CashBackConfig` |
| **E-commerce** | `WoocommerceConfig`, `WoocommercePedido`, `WoocommerceItemPedido`, `VendiZapConfig` |
| **Agendamento** | `Agendamento`, `ItemAgendamento`, `Servico`, `Funcionamento`, `Interrupcao` |
| **OS** | `OrdemServico`, `ServicoOs`, `RelatorioOs`, `FuncionarioOs`, `ProdutoOs` |
| **Produção** | `OrdemProducao`, `Apontamento`, `CustoConfiguracao` |
| **Hotelaria** | `Acomodacao`, `Reserva`, `Frigobar` |
| **Ótica** | `OticaOs`, `Laboratorio`, `TratamentoOtica`, `FormatoArmacao`, `TipoArmacao` |
---
## Services (Motor de Negócios)
| Service | Responsabilidade |
|---------|-----------------|
| `NFeService.php` | Emissão/cancelamento/CC de NF-e via Cloud-DFE SDK |
| `NFeServiceApi.php` | API externa de NF-e |
| `NFCeService.php` | Emissão de NFC-e (consumidor final) |
| `NFCeServiceApi.php` | API externa de NFC-e |
| `CTeService.php` | CT-e para transportadoras |
| `CTeOsService.php` | CT-e OS (ordem de serviço de transporte) |
| `MDFeService.php` | MDF-e para manifestos de carga |
| `DFeService.php` | Manifesto do Destinatário |
| `StockEntryService.php` | Entrada automática de estoque por NF-e |
| `StockSaleService.php` | Baixa automática de estoque por venda |
| `IbptService.php` | Consulta IBPT para carga tributária |
| `SpeedService.php` | SPED fiscal |
| `FinancePrevisaoService.php` | Previsão financeira com IA |
---
## Migrations (260 tabelas)
### Evolução Cronológica
| Período | Tabelas Criadas | Destaque |
|---------|----------------|----------|
| 2022 | `users`, `empresas`, `produtos`, `clientes`, `caixas` | Core do sistema |
| 2023 Q2 | `nves` (NF-e), `nfces`, `item_nves` | Módulo fiscal |
| 2023 Q3 | `pagamentos`, `inutilizacaos` | Financeiro |
| 2023 Q4 | `ctes`, `conta_pagars`, `conta_recebers`, `mdves`, `ordem_servicos`, `pedidos` | Transporte, OS, Food |
| 2024 Q1 | `cte_os`, `ordem_producaos`, `produto_composicaos` | Produção |
| 2024 Q2 | `cotacaos`, `ifood_*`, `woocommerce_*`, `mercado_livre_*` | E-commerce |
| 2024 Q3 | `stock_moves`, `imei_units`, `crm_anotacaos` | IMEI, CRM |
| 2024 Q4 | `otica_os`, `laboratorios`, `acomodacaos`, `reservas` | Ótica, Hotelaria |
| 2025 | `item_inventarios`, `tef_*`, `nuvem_shop_*` | Inventário, TEF, NuvemShop |
---
## Docker Infrastructure
```yaml
# plus/docker-compose.yml
services:
app: # PHP 8.2 FPM
build: .
volumes: .:/var/www
networks: [controlplus]
web: # Nginx 1.25
image: nginx:1.25-alpine
ports: "8080:80"
depends_on: [app]
db: # MySQL 8.0
image: mysql:8.0
volumes: db_data:/var/lib/mysql
environment:
MYSQL_DATABASE: controlplus
```
**Dockerfile:**
- Base: `php:8.2-fpm`
- Extensions: GD, BCMath, PDO, Zip, Intl, Soap
- Node.js 18 (para assets frontend)
- Composer para dependências PHP
---
## Fluxo de Dados Suite ↔ Plus
```
┌────────────────────────────────────────────────────────────────┐
│ FLUXO COMPLETO │
│ │
│ 1. Usuário acessa Arcádia Suite (porta 5000) │
│ 2. Navega para módulo Plus (/plus) │
│ 3. Suite gera token SSO com dados do usuário │
│ 4. Proxy envia requisição para Laravel (porta 8080) │
│ 5. Laravel valida SSO, auto-login, exibe interface │
│ 6. Operações fiscais (NF-e/NFC-e) executadas no Plus │
│ 7. Dados sincronizados via SOE Motor Adapter │
│ 8. Suite consome dados para BI, dashboards, governança │
│ │
│ SUITE (Inteligência) PLUS (Execução) │
│ ├── Decide o que vender ├── Emite NF-e/NFC-e │
│ ├── Analisa tendências ├── Controla estoque │
│ ├── Gera relatórios BI ├── Processa pagamentos │
│ ├── Automatiza processos ├── Gerencia caixa │
│ └── Governa políticas └── Registra movimentações │
└────────────────────────────────────────────────────────────────┘
```
---
## APIs do Plus
### Rotas Web (912 rotas) - Agrupadas
| Grupo | Rotas | Exemplos |
|-------|-------|---------|
| Dashboard | ~10 | `/home`, `/empresas` |
| Produtos | ~60 | `/produtos/*`, `/estoque/*`, `/marcas/*` |
| Vendas/PDV | ~50 | `/vendas/*`, `/pdv/*`, `/orcamento/*` |
| Fiscal | ~80 | `/nfe/*`, `/nfce/*`, `/cte/*`, `/mdfe/*` |
| Financeiro | ~70 | `/conta-pagar/*`, `/conta-receber/*`, `/caixa/*` |
| Clientes/CRM | ~40 | `/clientes/*`, `/crm/*` |
| Food Service | ~80 | `/cardapio/*`, `/delivery/*`, `/comanda/*`, `/pedidos/*` |
| E-commerce | ~60 | `/woocommerce/*`, `/mercado-livre/*`, `/nuvem-shop/*` |
| OS/Serviços | ~40 | `/ordem-servico/*`, `/agendamento/*`, `/servicos/*` |
| Transporte | ~50 | `/cte/*`, `/mdfe/*`, `/veiculos/*`, `/motoboy/*` |
| Admin | ~100 | `/usuarios/*`, `/empresa/*`, `/configuracao/*` |
| Contabilidade | ~60 | `/sped/*`, `/apuracao/*`, `/fechamento/*` |
| Outros | ~212 | Reservas, ótica, laboratórios, tickets, etc. |
### Rotas API (634 rotas) - Agrupadas
| Grupo | Prefixo | Descrição |
|-------|---------|-----------|
| PDV | `/api/pdv/*` | Ponto de venda mobile |
| Cardápio | `/api/cardapio/*` | Cardápio digital |
| Delivery | `/api/delivery/*` | Delivery app |
| Comanda | `/api/comanda/*` | Sistema de comandas |
| Kotlin App | `/api/kotlin/*` | App mobile nativo |
| Fiscal | `/api/nfe/*`, `/api/nfce/*` | Emissão fiscal |
| CRM | `/api/crm/*` | CRM mobile |
| Estoque | `/api/stock-entry/*` | Entrada de estoque |
| E-commerce | `/api/woocommerce/*`, `/api/mercadolivre/*` | Integrações |
| iFood | `/api/ifood/*` | Integração iFood |
---
## Resumo Visual
```
┌──────────────────────────────────────────────────────────────────┐
│ ARCÁDIA PLUS - ERP COMPLETO │
│ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐│
│ │ FISCAL │ │ VENDAS │ │ ESTOQUE │ │ FINANCEIRO ││
│ │ NF-e/NFC-e │ │ PDV/Caixa │ │ IMEI Track │ │ Contas P/R ││
│ │ CT-e/MDF-e │ │ Orçamentos │ │ Inventário │ │ Boletos ││
│ │ NFS-e/SPED │ │ Trocas │ │ Transf. │ │ TEF ││
│ └────────────┘ └────────────┘ └────────────┘ └────────────┘│
│ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐│
│ │ FOOD │ │ E-COMMERCE │ │ SERVIÇOS │ │ TRANSPORTE ││
│ │ Cardápio │ │ WooCommerce│ │ O.S. │ │ CT-e ││
│ │ Delivery │ │ Merc.Livre │ │ Agendament │ │ MDF-e ││
│ │ iFood │ │ NuvemShop │ │ Garantias │ │ Logística ││
│ │ Comandas │ │ VendiZap │ │ │ │ ││
│ └────────────┘ └────────────┘ └────────────┘ └────────────┘│
│ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐│
│ │ CRM │ │ RH │ │ PRODUÇÃO │ │ SEGMENTOS ││
│ │ Clientes │ │ Comissões │ │ Ordens │ │ Ótica ││
│ │ Score │ │ Metas │ │ Apontament │ │ Hotelaria ││
│ │ Contratos │ │ Roles │ │ Custos │ │ Marketplace││
│ └────────────┘ └────────────┘ └────────────┘ └────────────┘│
│ │
│ 376 Controllers │ 320 Models │ 12 Services │ 1.194 Views │
│ 260 Migrations │ 912 Web Routes │ 634 API Routes │
└──────────────────────────────────────────────────────────────────┘
```
---
*Arcádia Plus v2.0 - O Motor de Execução do Arcádia Suite*

469
MAPA_SISTEMA_ARCADIA.md Normal file
View File

@ -0,0 +1,469 @@
# Arcádia Suite - Mapa Geral do Sistema
## Visão Geral
**Arcádia Suite** é o Escritório Estratégico para a Empresa Moderna. Uma plataforma que centraliza produtividade, inteligência, tomada de decisão e governança, orquestrando ERPs, pessoas e dados.
**Princípio Central:** Separação absoluta entre decisão e execução.
- Arcádia **pensa, governa e orienta**
- ERPs **executam, registram e obedecem**
---
## Arquitetura de 4 Camadas
```
┌─────────────────────────────────────────────────────────────────────┐
│ CAMADA DE APRESENTAÇÃO │
│ React 18 + TypeScript + Tailwind CSS + shadcn/ui │
│ Interface tipo browser com abas + omnibox │
│ 66 páginas/módulos │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ CAMADA DE ORQUESTRAÇÃO │
│ Express.js + Socket.IO + Manus Agent │
│ Porta 5000 (API + WebSocket) │
│ 38 arquivos de rotas / 23 ferramentas registradas │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ CAMADA DE INTELIGÊNCIA │
│ FastAPI (Contábil 8003, BI 8004, Automação 8005) │
│ Communication Engine (Node 8006) │
│ OpenAI GPT-4o (Manus/Dev Center) + GPT-4o-mini (WhatsApp) │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ CAMADA DE DADOS │
│ PostgreSQL + Drizzle ORM │
│ Knowledge Graph + ChromaDB (embeddings) │
│ Session Store + Multi-tenant │
└─────────────────────────────────────────────────────────────────────┘
```
---
## Mapa de Portas
| Porta | Serviço | Tecnologia |
|-------|---------|-----------|
| 5000 | API Principal + Frontend | Express.js + React |
| 8002 | Motor Fiscal (Fisco) | FastAPI (Python) |
| 8003 | Motor Contábil | FastAPI (Python) |
| 8004 | Motor BI (Insights) | FastAPI (Python) |
| 8005 | Motor Automação | FastAPI (Python) |
| 8006 | Motor Comunicação | Node.js/Express |
| 8080 | Arcádia Plus (Laravel) | PHP/Laravel |
---
## Módulos do Frontend (66 páginas)
### Núcleo & Administração
| Página | Rota | Descrição |
|--------|------|-----------|
| Home | `/` | Dashboard principal |
| SOE | `/soe` | Sistema Operacional Empresarial |
| ERP | `/erp` | Módulo ERP legado |
| Admin | `/admin` | Administração do sistema |
| SuperAdmin | `/super-admin` | Gestão multi-tenant |
### Módulos de Negócio
| Página | Rota | Descrição |
|--------|------|-----------|
| Financeiro | `/financeiro` | Contas a pagar/receber, fluxo de caixa |
| Contábil | `/contabil` | Contabilidade, DRE, balancetes |
| Fiscal | `/fisco` | NF-e, NFC-e, CFOP, NCM, CEST |
| CRM | `/crm` | Gestão de relacionamento com cliente |
| People | `/people` | RH, colaboradores, folha |
| Production | `/production` | Ordens de produção |
| Quality | `/quality` | Controle de qualidade |
### Varejo & Comércio
| Página | Rota | Descrição |
|--------|------|-----------|
| Retail | `/retail` | Varejo (celulares, assistência técnica) |
| RetailReports | `/retail-reports` | Relatórios do varejo |
| Marketplace | `/marketplace` | Marketplace integrado |
| Valuation | `/valuation` | Avaliação de trade-in |
### Comunicação
| Página | Rota | Descrição |
|--------|------|-----------|
| WhatsApp | `/whatsapp` | Multi-sessão WhatsApp |
| Chat | `/chat` | Chat interno |
| XOS Inbox | `/xos/inbox` | Caixa de entrada unificada |
| XOS CRM | `/xos/crm` | CRM unificado |
| XOS Campaigns | `/xos/campaigns` | Campanhas de marketing |
| XOS Tickets | `/xos/tickets` | Sistema de tickets |
### Inteligência & IA
| Página | Rota | Descrição |
|--------|------|-----------|
| Scientist | `/scientist` | Auto-programação com IA |
| Knowledge | `/knowledge` | Base de conhecimento/grafo |
| BI Workspace | `/bi` | Business Intelligence |
| Manus | `/agent` | Agente autônomo central |
### Desenvolvimento & DevOps
| Página | Rota | Descrição |
|--------|------|-----------|
| IDE | `/ide` | Editor Monaco + Terminal |
| Dev Center | `/dev-center` | Centro de desenvolvimento XOS |
| XOS Pipeline | `/xos/pipeline` | Pipeline autônomo de código |
| XOS Governance | `/xos/governance` | Governança e políticas |
| API Hub | `/api-hub` | Central de APIs |
| API Tester | `/api-tester` | Testador de APIs |
| DocType Builder | `/doctype-builder` | Construtor de tipos |
| Page Builder | `/page-builder` | Construtor de páginas |
| Workflow Builder | `/workflow-builder` | Construtor de workflows |
### Operações & Engenharia
| Página | Rota | Descrição |
|--------|------|-----------|
| Engineering Hub | `/engineering` | Hub de engenharia |
| Field Operations | `/field-ops` | Operações de campo |
| Process Compass | `/compass` | Bússola de processos |
| Suppliers Portal | `/suppliers` | Portal de fornecedores |
### Plataforma
| Página | Rota | Descrição |
|--------|------|-----------|
| Engine Room | `/engine-room` | Casa de Máquinas (status dos motores) |
| Automations | `/automations` | Motor de automações |
| Plus | `/plus` | ERP Laravel (proxy) |
| LMS | `/lms` | Sistema de aprendizagem |
| Communities | `/communities` | Comunidades |
| Support | `/support` | Central de suporte |
| Migration | `/migration` | Migração de dados |
| Central APIs | `/central-apis` | APIs centrais |
---
## APIs do Backend (38 grupos de rotas)
### Core
| Rota Base | Arquivo | Descrição |
|-----------|---------|-----------|
| `/api/login`, `/api/register` | `server/auth.ts` | Autenticação |
| `/api/admin/*` | `server/admin/routes.ts` | Administração |
| `/api/erp/*` | `server/erp/routes.ts` | ERP principal |
| `/api/soe/*` | `server/erp/routes.ts` | SOE (alias) |
| `/api/users/*` | `server/routes.ts` | Gestão de usuários |
### Negócio
| Rota Base | Arquivo | Descrição |
|-----------|---------|-----------|
| `/api/financeiro/*` | `server/financeiro/routes.ts` | Financeiro |
| `/api/contabil/*` | `server/contabil/routes.ts` | Contabilidade |
| `/api/fisco/*` | `server/fisco/routes.ts` | Fiscal |
| `/api/crm/*` | `server/crm/routes.ts` | CRM |
| `/api/people/*` | `server/people/routes.ts` | RH/Pessoas |
| `/api/production/*` | `server/production/routes.ts` | Produção |
| `/api/quality/*` | `server/quality/routes.ts` | Qualidade |
| `/api/retail/*` | `server/retail/routes.ts` | Varejo |
| `/api/valuation/*` | `server/valuation/routes.ts` | Avaliação trade-in |
| `/api/marketplace/*` | `server/marketplace/routes.ts` | Marketplace |
### Comunicação
| Rota Base | Arquivo | Descrição |
|-----------|---------|-----------|
| `/api/whatsapp/*` | `server/whatsapp/routes.ts` | WhatsApp multi-sessão |
| `/api/chat/*` | `server/chat/routes.ts` | Chat interno |
| `/api/email/*` | `server/email/routes.ts` | E-mail |
| `/api/comm/*` | proxy | Motor de Comunicação |
| `/api/xos/*` | `server/xos/routes.ts` | XOS CRM unificado |
### Inteligência
| Rota Base | Arquivo | Descrição |
|-----------|---------|-----------|
| `/api/manus/*` | `server/manus/routes.ts` | Agente Manus IA |
| `/api/knowledge/*` | `server/learning/routes.ts` | Knowledge Graph |
| `/api/bi/*` | `server/bi/routes.ts` | Business Intelligence |
| `/api/bi/metaset/*` | `server/metaset/routes.ts` | Motor BI MetaSet |
| `/api/scientist/*` | `server/routes.ts` | Cientista de dados |
### Desenvolvimento
| Rota Base | Arquivo | Descrição |
|-----------|---------|-----------|
| `/api/ide/*` | `server/ide/routes.ts` | IDE integrada |
| `/api/dev-center/*` | `server/blackboard/routes.ts` | Dev Center/Blackboard |
| `/api/xos/pipeline` | `server/blackboard/routes.ts` | Pipeline autônomo |
| `/api/governance/*` | `server/governance/routes.ts` | Governança |
| `/api/lowcode/*` | `server/lowcode/routes.ts` | Low-code engine |
### Protocolos de Interoperabilidade
| Rota Base | Arquivo | Descrição |
|-----------|---------|-----------|
| `/api/mcp/v1/*` | `server/mcp/routes.ts` | Model Context Protocol |
| `/api/a2a/v1/*` | `server/routes.ts` | Agent to Agent Protocol |
| `/api/api-central/*` | `server/api-central/routes.ts` | Central de APIs |
### Infraestrutura
| Rota Base | Arquivo | Descrição |
|-----------|---------|-----------|
| `/api/engine-room/*` | `server/engine-room/routes.ts` | Casa de Máquinas |
| `/api/automations/*` | `server/automations/routes.ts` | Motor de Automação |
| `/api/modules/*` | `server/modules/loader.ts` | Módulos dinâmicos |
| `/api/login-bridge/*` | `server/login-bridge/routes.ts` | SSO Bridge |
| `/api/migration/*` | `server/migration/routes.ts` | Migração |
---
## Motores (Engines)
### Motor IA - Manus (Node.js, porta 5000)
- **Modelo:** GPT-4o (Dev Center), GPT-4o-mini (WhatsApp)
- **Agentes:** 6 agentes autônomos (Architect, Generator, Validator, Executor, Researcher, Evolution)
- **Ferramentas:** 23 ferramentas registradas (GitHub, filesystem, BI, git)
- **Pipeline:** Design → Codegen → Validation → Staging → Evolution
### Motor Fiscal - Fisco (Python, porta 8002)
- NF-e / NFC-e via nfelib
- NCMs, CFOPs, CESTs, grupos tributários
- Certificados digitais
- Comunicação com SEFAZ
### Motor Contábil (Python, porta 8003)
- Plano de contas
- Lançamentos contábeis
- DRE, Balanço Patrimonial
### Motor BI - Insights (Python, porta 8004)
- Execução SQL
- Geração de gráficos
- Análise com Pandas
- Cache inteligente
### Motor Automação (Python, porta 8005)
- Cron scheduler
- Event bus
- Executor de workflows
### Motor Comunicação (Node.js, porta 8006)
- Unifica XOS CRM + WhatsApp + Email
- Contatos, threads, mensagens unificados
- Filas de atendimento
- Eventos para Knowledge Graph
### Arcádia Plus - ERP Laravel (PHP, porta 8080)
- NF-e/NFC-e/CT-e/MDF-e
- PDV (ponto de venda)
- Cardápio digital
- Ordens de serviço
- Estoque com rastreabilidade
- Integrações e-commerce (WooCommerce, Mercado Livre, NuvemShop)
- Integrações delivery (iFood)
---
## Dev Center XOS - 6 Agentes Autônomos
```
Prompt em Português
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Architect │ ──▶ │ Generator │ ──▶ │ Validator │
│ (Design) │ │ (Codegen) │ │ (Typecheck) │
└──────────────┘ └──────────────┘ └──────────────┘
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Evolution │ ◀── │ Researcher │ ◀── │ Executor │
│ (Aprende) │ │ (Pesquisa) │ │ (Staging) │
└──────────────┘ └──────────────┘ └──────────────┘
```
---
## Módulo Retail (Varejo de Celulares)
### Funcionalidades Core
- **Vendas com IMEI:** Rastreamento individual de aparelhos
- **Trade-in:** Avaliação com checklist de 19 itens
- **Ordens de Serviço:** Gestão completa de assistência técnica
- **Garantia:** Controle de garantias por IMEI
- **Caixa Diário:** Reconciliação de caixa com fechamento
- **Comissões:** Cálculo automático por vendedor
### Checklist Trade-in (19 itens)
1. Liga normalmente
2. Problemas na tela
3. WiFi funcionando
4. Bluetooth funcionando
5. Câmera frontal
6. Câmera traseira
7. Microfone
8. Alto-falante
9. Botões físicos
10. Sensor biométrico
11. Carregamento
12. Bateria saudável
13. GPS funcionando
14. Giroscópio
15. Acelerômetro
16. NFC
17. Resistência à água
18. Face ID / reconhecimento facial
19. Vibração
---
## Banco de Dados (PostgreSQL + Drizzle ORM)
### Tabelas Principais
| Grupo | Tabelas |
|-------|---------|
| **Identidade** | `users`, `profiles`, `tenants` |
| **Produtividade** | `workspace_pages`, `page_blocks`, `dashboard_widgets`, `quick_notes` |
| **Comunicação** | `conversations`, `messages`, `chat_threads`, `chat_messages` |
| **WhatsApp** | `whatsapp_sessions`, `whatsapp_contacts`, `whatsapp_messages`, `whatsapp_tickets` |
| **ERP Core** | `applications`, `erp_connections`, `agent_tasks`, `task_executions` |
| **Conhecimento** | `knowledge_base`, `knowledge_graph_nodes`, `knowledge_graph_edges` |
| **Governança** | `xos_governance_*`, `xos_job_queue`, `xos_agent_metrics` |
| **Pipeline** | `xos_staging_changes`, `xos_dev_pipelines` |
| **Comunicação Unificada** | `comm_contacts`, `comm_threads`, `comm_messages`, `comm_channels` |
| **Varejo** | Via módulos dinâmicos (`/api/modules/retail-reports`) |
| **Financeiro** | Contas, lançamentos, conciliação |
| **Fiscal** | NCMs, CFOPs, notas fiscais |
---
## Integrações Externas
| Serviço | Uso |
|---------|-----|
| **OpenAI** | GPT-4o (Manus, Dev Center), GPT-4o-mini (WhatsApp) |
| **GitHub** | Commits automáticos, análise de repositórios |
| **ERPNext** | Integração com ERP externo (clientes, produtos, vendas) |
| **WhatsApp/Baileys** | Multi-sessão de atendimento |
| **SEFAZ** | NF-e/NFC-e via nfelib |
| **Cloud-DFE** | SDK fiscal (NF-e, NFC-e, CT-e, MDF-e) |
| **WooCommerce** | E-commerce integration |
| **Mercado Livre** | Marketplace |
| **NuvemShop** | E-commerce |
| **iFood** | Delivery (pedidos, cardápio) |
| **Asaas** | Pagamentos, boletos |
---
## Protocolos de Interoperabilidade
| Protocolo | Rota | Descrição |
|-----------|------|-----------|
| **MCP** | `/api/mcp/v1/` | Model Context Protocol - exposição de ferramentas |
| **A2A** | `/api/a2a/v1/` | Agent to Agent - comunicação bidirecional |
| **AP2** | Planejado | Agent Payment Protocol |
| **UCP** | Planejado | Unified Commerce Protocol |
---
## Como Rodar Localmente
### Pré-requisitos
- Node.js 20+
- Python 3.11+
- PostgreSQL 16+
- PHP 8.2+ (opcional, para Arcádia Plus)
### Instalação
```bash
# 1. Extrair o backup
tar xzf arcadia-suite-backup.tar.gz
# 2. Instalar dependências Node
npm install
# 3. Instalar dependências Python
pip install fastapi uvicorn pandas numpy psycopg2-binary nfelib lxml cryptography
# 4. Configurar variáveis de ambiente
cp .env.example .env
# Editar .env com suas credenciais:
# DATABASE_URL=postgresql://user:pass@localhost:5432/arcadia
# OPENAI_API_KEY=sk-...
# GITHUB_TOKEN=ghp_...
# 5. Criar banco de dados
createdb arcadia
# 6. Executar migrations
npx drizzle-kit push
# 7. Iniciar em desenvolvimento
npm run dev
```
### Variáveis de Ambiente Necessárias
| Variável | Descrição |
|----------|-----------|
| `DATABASE_URL` | URL de conexão PostgreSQL |
| `OPENAI_API_KEY` | Chave da API OpenAI |
| `GITHUB_TOKEN` | Token GitHub para integrações |
| `ERPNEXT_URL` | URL do ERPNext (opcional) |
| `ERPNEXT_API_KEY` | Chave API ERPNext (opcional) |
| `ERPNEXT_API_SECRET` | Segredo API ERPNext (opcional) |
| `SESSION_SECRET` | Segredo para sessões Express |
### Credenciais Padrão
- **Usuário:** admin
- **Senha:** admin
- **Role:** master
---
## Estrutura de Diretórios
```
arcadia-suite/
├── client/ # Frontend React
│ ├── src/
│ │ ├── pages/ # 66 páginas
│ │ ├── components/ # Componentes reutilizáveis
│ │ ├── hooks/ # Custom hooks
│ │ └── lib/ # Utilitários
│ └── public/ # Assets estáticos
├── server/ # Backend Express
│ ├── admin/ # Administração
│ ├── autonomous/ # Ferramentas autônomas
│ ├── bi/ # Business Intelligence
│ ├── blackboard/ # Dev Center (6 agentes)
│ ├── chat/ # Chat interno
│ ├── communication/ # Motor de comunicação
│ ├── contabil/ # Motor contábil
│ ├── crm/ # CRM
│ ├── engine-room/ # Casa de Máquinas
│ ├── erp/ # ERP/SOE
│ ├── financeiro/ # Financeiro
│ ├── fisco/ # Fiscal
│ ├── governance/ # Governança XOS
│ ├── ide/ # IDE integrada
│ ├── integrations/ # Integrações externas
│ ├── learning/ # Knowledge Graph
│ ├── manus/ # Agente Manus
│ ├── mcp/ # Model Context Protocol
│ ├── modules/ # Módulos dinâmicos
│ ├── people/ # RH
│ ├── plus/ # Proxy Laravel
│ ├── production/ # Produção
│ ├── python/ # Scripts Python
│ ├── quality/ # Qualidade
│ ├── retail/ # Varejo
│ ├── whatsapp/ # WhatsApp
│ └── xos/ # XOS unificado
├── shared/ # Código compartilhado
│ ├── schema.ts # Schema principal (Drizzle)
│ └── schemas/ # Schemas modulares
├── plus/ # ERP Laravel (PHP)
├── python-service/ # Serviço Python
├── db/ # Configuração do banco
├── migrations/ # Migrations Drizzle
└── docs/ # Documentação
```
---
*Arcádia Suite v2.0 - O Escritório Estratégico para a Empresa Moderna*

787
PLANO_BI_SUPERSET.md Normal file
View File

@ -0,0 +1,787 @@
# PLANO BI — Substituição Metabase → Apache Superset
## Análise Real + Plano de Migração
### Versão 1.0 — Março 2026
---
## 1. DIAGNÓSTICO — ESTADO ATUAL
### 1.1 O que foi descoberto no código
O documento do Replit referenciava **Metabase** (Java, :8088) como o pilar visual do BI. Ao analisar o código real, a situação é diferente:
| Componente | Nome no doc | Nome real no código | Status |
|---|---|---|---|
| Plataforma BI Visual | "Metabase" | **"MetaSet"** (alias) | Metabase por baixo |
| Proxy | `/metabase/*` | `server/metabase/proxy.ts` | Funcional |
| Auth | Metabase session | `server/metaset/routes.ts` (autologin) | Funcional |
| Client | MetabaseClient | `server/metaset/client.ts` | Funcional |
| UI | Advanced tab iframe | `/api/bi/metaset/autologin``/metabase/` | Funcional |
| **Superset** | Não mencionado | **`docker-compose.yml` linha 165** | **JÁ CONFIGURADO** |
**Descoberta crítica:** O Apache Superset **já está no docker-compose.yml**:
```yaml
superset:
image: apache/superset:4.1.0
restart: unless-stopped
environment:
SUPERSET_SECRET_KEY: ${SUPERSET_SECRET_KEY:-superset-secret-change-in-prod}
DATABASE_URL: postgresql://arcadia:arcadia123@db:5432/arcadia_superset
ports:
- "8088:8088"
depends_on:
db:
condition: service_healthy
profiles: [bi]
```
O Superset está configurado, mas **não integrado ao gateway Node.js**. O que falta é a ponte.
### 1.2 Arquivos que precisam mudar vs o que fica igual
**Mudam:**
```
server/metabase/proxy.ts → substituir por server/superset/proxy.ts
server/metaset/routes.ts → substituir por server/superset/routes.ts
server/metaset/client.ts → substituir por server/superset/client.ts
client/src/pages/BiWorkspace.tsx (Advanced tab apenas)
docker-compose.yml (Superset config + init)
docker-compose.prod.yml (adicionar Superset)
.env.example (novas variáveis)
```
**NÃO mudam (ficam exatamente iguais):**
```
server/python/bi_engine.py ← Motor Python :8004 continua intacto
server/bi/routes.ts ← CRUD de datasets/charts/dashboards
server/bi/upload.ts ← ETL pipeline
server/bi/staging.ts ← Staging
server/bi/engine-proxy.ts ← Proxy para BI Engine Python
client/src/pages/BiWorkspace.tsx (7 de 8 tabs ficam iguais)
Todas as tabelas do banco (bi_datasets, bi_charts, etc.)
```
---
## 2. POR QUE SUPERSET É MELHOR PARA O CASO
### 2.1 Metabase vs Superset
| Critério | Metabase | Apache Superset |
|---|---|---|
| Runtime | Java (JVM, 500MB+ RAM) | Python/Flask (mais leve) |
| Embedding | Iframe com cookie de sessão | **Guest Token JWT** (seguro, sem credenciais) |
| "Qualquer tela" | Iframe estático, limitado | **`@superset-ui/embedded-sdk`** — componente React nativo |
| REST API | Limitada e não documentada | **REST API v1 completa e documentada** |
| Consistência de stack | Java (fora do padrão) | Python (mesmo stack dos motores existentes) |
| Docker image | `metabase/metabase` ~700MB | `apache/superset` ~400MB |
| SQL Lab | Básico | **Poderoso** (autocomplete, histórico, salvar queries) |
| Alertas | Básico | **Avançado** (webhooks, Slack, email) |
| Tipos de chart | ~20 | **50+** (incluindo mapas, sunburst, funnel) |
| Banco de metadados | H2/Postgres separado | **Mesmo PostgreSQL** da Arcádia |
### 2.2 A feature que o usuário pediu: "funcionar em qualquer tela"
O Superset resolve isso com **Guest Token + Embedded SDK**:
```
Fluxo atual (Metabase):
BiWorkspace → iframe → /metabase/ → autologin com cookie
❌ Só funciona na tab Advanced do BiWorkspace
❌ Cookie expira, precisa re-autenticar
❌ Não embeda em outras páginas facilmente
Fluxo novo (Superset):
QUALQUER página React → <EmbeddedDashboard dashboardId="xxx" />
✅ Funciona no Dashboard principal (/)
✅ Funciona no SOE (/soe tab Financeiro)
✅ Funciona no /contabil
✅ Funciona no /financeiro
✅ Funciona em qualquer tela do Arcádia
✅ Guest Token renovado automaticamente (sem re-login)
```
---
## 3. ARQUITETURA COM SUPERSET
### 3.1 Diagrama atualizado
```
┌─────────────────────────────────────────────────────────────────────┐
│ USUÁRIO / FRONTEND │
│ │
│ BiWorkspace.tsx (8 tabs — 7 iguais, Advanced → Superset) │
│ + QUALQUER outra página pode embedar um dashboard Superset │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ <SupersetDashboard dashboardId="finance-overview" /> │ │
│ │ Componente React reutilizável em QUALQUER rota │ │
│ └────────────────────────────────────────────────────────────┘ │
└─────────────────────────┬────────────────────────────────────────────┘
┌───────────────┼──────────────────────┐
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌────────────────┐ ┌──────────────────────┐
│ API BI Node │ │ BI Engine Py │ │ Superset (Python) │
│ /api/bi/* │ │ /api/bi-engine │ │ /superset/* │
│ (sem mudança)│ │ :8004 (igual) │ │ :8088 │
│ │ │ │ │ │
│ + novo: │ │ │ │ REST API v1 │
│ /api/superset│ │ │ │ Guest Token JWT │
│ /guest-token │ │ │ │ SQL Lab │
│ │ │ │ │ 50+ chart types │
└──────────────┘ └────────────────┘ └──────────┬───────────┘
┌──────────────────────┐
│ PostgreSQL │
│ arcadia (dados SOE) │
│ arcadia_superset │
│ (metadados Superset) │
└──────────────────────┘
```
### 3.2 Dois bancos PostgreSQL
```
Banco: arcadia → Dados do SOE (persons, products, sales_orders, etc.)
Superset conecta aqui em READ-ONLY para criar charts
Banco: arcadia_superset → Metadados do Superset (dashboards, queries, users)
Superset usa internamente
```
Ambos no mesmo servidor PostgreSQL, bancos separados. O docker-compose.yml já referencia `arcadia_superset`.
---
## 4. O QUE CONSTRUIR — Passo a Passo
### 4.1 Configuração do Superset (docker-compose.yml)
O container já existe. Faltam: init script, volumes, configuração de embedding.
```yaml
# docker-compose.yml — seção superset (expandida)
superset:
image: apache/superset:4.1.0
restart: unless-stopped
environment:
SUPERSET_SECRET_KEY: ${SUPERSET_SECRET_KEY:-superset-secret-change-in-prod}
DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD:-arcadia123}@db:5432/arcadia_superset
SUPERSET_WEBSERVER_PORT: 8088
PYTHONPATH: /app/pythonpath
# Para embedding funcionar:
FEATURE_FLAGS: '{"EMBEDDED_SUPERSET": true, "ENABLE_TEMPLATE_PROCESSING": true}'
ports:
- "8088:8088"
volumes:
- ./docker/superset/superset_config.py:/app/pythonpath/superset_config.py
- ./docker/superset/init.sh:/app/docker/init.sh
- superset_home:/app/superset_home
depends_on:
db:
condition: service_healthy
command: ["/bin/bash", "/app/docker/init.sh"]
profiles: [bi]
networks:
- arcadia
```
### 4.2 Arquivo de Configuração Superset
```python
# docker/superset/superset_config.py
import os
# Chave secreta (mesma do .env)
SECRET_KEY = os.environ.get("SUPERSET_SECRET_KEY", "change-in-production")
# Banco de metadados do Superset
SQLALCHEMY_DATABASE_URI = os.environ.get(
"DATABASE_URL",
"postgresql://arcadia:arcadia123@db:5432/arcadia_superset"
)
# CORS — permite o gateway Arcádia (:5000) chamar a API
ENABLE_CORS = True
CORS_OPTIONS = {
"supports_credentials": True,
"allow_headers": ["*"],
"resources": {r"/api/*": {"origins": "*"}},
}
# Embedding habilitado
FEATURE_FLAGS = {
"EMBEDDED_SUPERSET": True,
"ENABLE_TEMPLATE_PROCESSING": True,
"ALERT_REPORTS": True, # Alertas automáticos
"DRILL_TO_DETAIL": True, # Drill-down em charts
}
# Timeout de queries (30s por default, aumentar para análises pesadas)
SUPERSET_WEBSERVER_TIMEOUT = 300
# Cache (Redis se disponível, senão in-memory)
CACHE_CONFIG = {
"CACHE_TYPE": "SimpleCache",
"CACHE_DEFAULT_TIMEOUT": 300, # 5 min
}
# Tema/branding Arcádia (logo, cores)
APP_NAME = "Arcádia Insights"
APP_ICON = "/static/arcadia-logo.png"
```
### 4.3 Init Script do Superset
```bash
#!/bin/bash
# docker/superset/init.sh — roda ao iniciar o container
set -e
echo "[Superset Init] Iniciando configuração..."
# Aguarda PostgreSQL
until psql "${DATABASE_URL}" -c "SELECT 1" > /dev/null 2>&1; do
echo "[Superset Init] Aguardando PostgreSQL..."
sleep 2
done
# Cria banco arcadia_superset se não existir
psql "postgresql://${PGUSER:-arcadia}:${PGPASSWORD:-arcadia123}@db:5432/postgres" \
-c "CREATE DATABASE arcadia_superset OWNER arcadia;" 2>/dev/null || true
# Inicializa banco do Superset
superset db upgrade
# Cria usuário admin (só se não existir)
superset fab create-admin \
--username "${SUPERSET_ADMIN_USER:-admin}" \
--firstname "Arcádia" \
--lastname "Admin" \
--email "${SUPERSET_ADMIN_EMAIL:-admin@arcadia.app}" \
--password "${SUPERSET_ADMIN_PASSWORD:-arcadia2026}" 2>/dev/null || true
# Carrega exemplos (opcional, comentar em prod)
# superset load_examples
# Inicializa roles e permissões
superset init
# Registra conexão com o banco Arcádia (dados do SOE)
python - <<'PYEOF'
from superset import create_app
from superset.extensions import db
from superset.models.core import Database
import os
app = create_app()
with app.app_context():
existing = db.session.query(Database).filter_by(
database_name="Arcádia Suite"
).first()
if not existing:
arcadia_db = Database(
database_name="Arcádia Suite",
sqlalchemy_uri=os.environ.get(
"ARCADIA_DATABASE_URL",
"postgresql://arcadia:arcadia123@db:5432/arcadia"
),
expose_in_sqllab=True,
allow_run_async=True,
allow_csv_upload=False,
)
db.session.add(arcadia_db)
db.session.commit()
print("[Superset Init] Banco 'Arcádia Suite' registrado!")
else:
print("[Superset Init] Banco 'Arcádia Suite' já existe.")
PYEOF
echo "[Superset Init] Iniciando servidor..."
gunicorn \
--bind "0.0.0.0:${SUPERSET_WEBSERVER_PORT:-8088}" \
--access-logfile "-" \
--error-logfile "-" \
--workers 4 \
--timeout 120 \
--limit-request-line 0 \
--limit-request-field_size 0 \
"superset.app:create_app()"
```
### 4.4 Proxy Superset (substitui server/metabase/proxy.ts)
```typescript
// server/superset/proxy.ts
import { Express, Response } from "express";
import { createProxyMiddleware } from "http-proxy-middleware";
const SUPERSET_HOST = process.env.SUPERSET_HOST || "localhost";
const SUPERSET_PORT = parseInt(process.env.SUPERSET_PORT || "8088", 10);
const SUPERSET_TIMEOUT = 60000;
export function setupSupersetProxy(app: Express): void {
const target = `http://${SUPERSET_HOST}:${SUPERSET_PORT}`;
const supersetProxy = createProxyMiddleware({
target,
changeOrigin: true,
timeout: SUPERSET_TIMEOUT,
proxyTimeout: SUPERSET_TIMEOUT,
pathRewrite: { "^/superset": "" },
on: {
error: (err, _req, res) => {
console.error("[Superset Proxy] Error:", err.message);
if (res && typeof (res as Response).status === "function") {
(res as Response).status(502).json({
error: "Superset indisponível",
message: "O Superset está iniciando. Tente novamente em alguns segundos.",
target,
});
}
},
proxyRes: (proxyRes) => {
const location = proxyRes.headers["location"];
if (location && typeof location === "string" && location.startsWith("/")) {
proxyRes.headers["location"] = `/superset${location}`;
}
},
},
});
app.use("/superset", supersetProxy);
console.log(`[Superset Proxy] Configurado -> /superset/* => ${target}`);
}
```
### 4.5 Rotas Superset — Guest Token para Embedding
Esta é a peça-chave para "funcionar em qualquer tela":
```typescript
// server/superset/routes.ts
import type { Express, Request, Response } from "express";
const SUPERSET_HOST = process.env.SUPERSET_HOST || "localhost";
const SUPERSET_PORT = parseInt(process.env.SUPERSET_PORT || "8088", 10);
const SUPERSET_URL = `http://${SUPERSET_HOST}:${SUPERSET_PORT}`;
const SUPERSET_ADMIN_USER = process.env.SUPERSET_ADMIN_USER || "admin";
const SUPERSET_ADMIN_PASS = process.env.SUPERSET_ADMIN_PASSWORD || "arcadia2026";
// Cache do token de serviço (expira em 50 min)
let serviceToken: string | null = null;
let serviceTokenExpiry = 0;
async function getServiceToken(): Promise<string> {
if (serviceToken && Date.now() < serviceTokenExpiry) return serviceToken;
const resp = await fetch(`${SUPERSET_URL}/api/v1/security/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
username: SUPERSET_ADMIN_USER,
password: SUPERSET_ADMIN_PASS,
provider: "db",
refresh: true,
}),
});
if (!resp.ok) throw new Error("Falha ao autenticar no Superset");
const data = await resp.json();
serviceToken = data.access_token;
serviceTokenExpiry = Date.now() + 50 * 60 * 1000; // 50 min
return serviceToken!;
}
export function registerSupersetRoutes(app: Express): void {
// Guest Token — para embedding em qualquer tela
// O frontend chama este endpoint para obter um token temporário
// e renderiza o dashboard via @superset-ui/embedded-sdk
app.post("/api/superset/guest-token", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) return res.status(401).json({ error: "Not authenticated" });
const { dashboardId } = req.body;
if (!dashboardId) return res.status(400).json({ error: "dashboardId obrigatório" });
const token = await getServiceToken();
// Solicita guest token ao Superset
const guestResp = await fetch(`${SUPERSET_URL}/api/v1/security/guest_token/`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`,
},
body: JSON.stringify({
user: {
username: `arcadia_${req.user?.id}`,
first_name: req.user?.name?.split(" ")[0] || "User",
last_name: req.user?.name?.split(" ")[1] || "",
},
resources: [{ type: "dashboard", id: dashboardId }],
rls: [], // Row Level Security (futuro: por tenantId)
}),
});
if (!guestResp.ok) {
const err = await guestResp.text();
return res.status(502).json({ error: "Falha ao gerar guest token", detail: err });
}
const { token: guestToken } = await guestResp.json();
res.json({ token: guestToken, supersetUrl: "/superset" });
} catch (err: any) {
res.status(502).json({ error: err.message });
}
});
// Lista dashboards disponíveis (para o seletor na UI)
app.get("/api/superset/dashboards", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) return res.status(401).json({ error: "Not authenticated" });
const token = await getServiceToken();
const resp = await fetch(`${SUPERSET_URL}/api/v1/dashboard/?q=(order_column:changed_on_delta_humanized,order_direction:desc)`, {
headers: { "Authorization": `Bearer ${token}` },
});
if (!resp.ok) return res.status(502).json({ error: "Superset indisponível" });
const data = await resp.json();
res.json(data.result || []);
} catch (err: any) {
res.status(502).json({ error: err.message });
}
});
// Health check
app.get("/api/superset/health", async (_req: Request, res: Response) => {
try {
const resp = await fetch(`${SUPERSET_URL}/health`);
res.json({ online: resp.ok, url: SUPERSET_URL });
} catch {
res.json({ online: false, url: SUPERSET_URL });
}
});
}
```
### 4.6 Componente React — SupersetDashboard (reutilizável em QUALQUER tela)
```typescript
// client/src/components/SupersetDashboard.tsx
import { useEffect, useRef, useState } from "react";
import { embedDashboard } from "@superset-ui/embedded-sdk";
interface SupersetDashboardProps {
dashboardId: string;
height?: string | number;
className?: string;
}
export function SupersetDashboard({
dashboardId,
height = "calc(100vh - 300px)",
className = "",
}: SupersetDashboardProps) {
const containerRef = useRef<HTMLDivElement>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (!containerRef.current) return;
let mounted = true;
setLoading(true);
setError(null);
embedDashboard({
id: dashboardId,
supersetDomain: window.location.origin + "/superset",
mountPoint: containerRef.current,
// Busca guest token do gateway Arcádia
fetchGuestToken: async () => {
const resp = await fetch("/api/superset/guest-token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ dashboardId }),
credentials: "include",
});
if (!resp.ok) throw new Error("Falha ao obter token");
const { token } = await resp.json();
return token;
},
dashboardUiConfig: {
hideTitle: true, // Título próprio no Arcádia
hideChartControls: false, // Manter controles de chart
filters: {
visible: true, // Mostrar filtros
expanded: false,
},
},
}).then(() => {
if (mounted) setLoading(false);
}).catch((err) => {
if (mounted) {
setError(err.message);
setLoading(false);
}
});
return () => { mounted = false; };
}, [dashboardId]);
if (error) {
return (
<div className="flex items-center justify-center h-48 text-red-500 text-sm">
Superset indisponível: {error}
</div>
);
}
return (
<div className={`relative ${className}`} style={{ height }}>
{loading && (
<div className="absolute inset-0 flex items-center justify-center bg-white/80 z-10">
<div className="text-center text-[#1f334d]">
<div className="w-8 h-8 border-4 border-[#c89b3c] border-t-transparent rounded-full animate-spin mx-auto mb-2" />
<p className="text-sm">Carregando Arcádia Insights...</p>
</div>
</div>
)}
<div ref={containerRef} className="w-full h-full" />
</div>
);
}
```
**Uso em QUALQUER página:**
```tsx
// Exemplo: na página /financeiro
import { SupersetDashboard } from "@/components/SupersetDashboard";
// Dashboard financeiro embeddado diretamente no módulo Financeiro
<SupersetDashboard dashboardId="financial-overview" height={500} />
// Exemplo: na página /contabil
<SupersetDashboard dashboardId="dre-mensal" />
// Exemplo: no Dashboard principal (/)
<SupersetDashboard dashboardId="executive-summary" height={400} />
```
### 4.7 BiWorkspace.tsx — Advanced tab (única mudança na UI)
```tsx
// Substituir a tab Advanced (linhas 2918-2945)
<TabsContent value="advanced" className="mt-0">
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<h2 className="text-lg font-semibold text-[#1f334d]">Arcádia Insights — Apache Superset</h2>
<p className="text-sm text-gray-500">
SQL Lab avançado, 50+ tipos de gráfico, dashboards interativos
</p>
</div>
<a
href="/superset"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-4 py-2 bg-[#1f334d] text-white rounded-lg hover:bg-[#2a4466] transition-colors text-sm"
>
<ArrowRight className="w-4 h-4" /> Abrir em Nova Aba
</a>
</div>
{/* Dashboard selecionável */}
<SupersetDashboardSelector />
</div>
</TabsContent>
```
---
## 5. TABELAS E VARIÁVEIS
### 5.1 Nenhuma tabela nova no schema Arcádia
O Superset usa seu próprio banco (`arcadia_superset`) para metadados internos. As tabelas do Arcádia não mudam.
### 5.2 Novas variáveis de ambiente
```bash
# .env — adicionar/substituir:
# Superset (substitui METABASE_*)
SUPERSET_HOST=superset # nome do container Docker
SUPERSET_PORT=8088
SUPERSET_SECRET_KEY= # gerar: openssl rand -hex 32
SUPERSET_ADMIN_USER=admin
SUPERSET_ADMIN_EMAIL=admin@arcadia.app
SUPERSET_ADMIN_PASSWORD= # senha forte em prod
# URL do banco Arcádia para o Superset acessar (read-only ideal)
ARCADIA_DATABASE_URL=postgresql://arcadia_ro:pass@db:5432/arcadia
# Remover (não são mais necessários):
# METABASE_HOST
# METABASE_PORT
# METASET_ADMIN_EMAIL
# METASET_ADMIN_PASSWORD
```
---
## 6. ROADMAP — Fases de Implementação
### Fase 1 — Infraestrutura (2-3 dias)
```
[ ] Criar docker/superset/superset_config.py
[ ] Criar docker/superset/init.sh
[ ] Atualizar docker-compose.yml (expandir seção superset existente)
[ ] Adicionar volumes: superset_home no docker-compose
[ ] Testar: docker compose --profile bi up superset
[ ] Verificar: banco arcadia_superset criado, admin funciona, :8088 acessível
```
### Fase 2 — Gateway (1-2 dias)
```
[ ] Criar server/superset/proxy.ts (substitui server/metabase/proxy.ts)
[ ] Criar server/superset/routes.ts (guest token, lista dashboards, health)
[ ] Registrar no server/routes.ts: setupSupersetProxy() + registerSupersetRoutes()
[ ] Remover/comentar setupMetabaseProxy() e registerMetaSetRoutes()
[ ] Testar: GET /api/superset/health → { online: true }
[ ] Testar: POST /api/superset/guest-token → token JWT
```
### Fase 3 — Frontend (2-3 dias)
```
[ ] npm install @superset-ui/embedded-sdk
[ ] Criar client/src/components/SupersetDashboard.tsx
[ ] Atualizar BiWorkspace.tsx Advanced tab (substituir iframe MetaSet)
[ ] Testar: dashboard embeddado na Advanced tab
[ ] Criar 3 dashboards base no Superset:
- executive-summary (KPIs gerais)
- financial-overview (Financeiro)
- dre-mensal (DRE)
```
### Fase 4 — Embedding em outras telas (1 semana)
```
[ ] Embedar SupersetDashboard no /financeiro (tab Análise)
[ ] Embedar SupersetDashboard no /contabil (tab DRE/Balanço)
[ ] Embedar SupersetDashboard no SOE (tab Dashboard)
[ ] Embedar SupersetDashboard no Home / Dashboard principal
[ ] RLS por tenantId (Row Level Security nos dashboards)
```
### Fase 5 — Migração de dashboards (conforme necessário)
```
[ ] Recriar no Superset os dashboards que existiam no MetaSet (se houver)
[ ] Configurar alertas automáticos (Superset Alerts & Reports)
[ ] Criar dashboards para cada domínio do SOE:
- Vendas, Compras, Fiscal, Pessoas, Estoque
```
---
## 7. DIAGRAMA BI ATUALIZADO (completo)
```
┌────────────────────────────────────────────────────────────────────┐
│ ARCÁDIA BI STACK v2 │
│ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ FRONTEND REACT — Pode usar em QUALQUER rota │ │
│ │ │ │
│ │ BiWorkspace.tsx → 8 tabs (7 iguais + Advanced=Superset) │ │
│ │ <SupersetDashboard> → componente reutilizável │ │
│ │ Guest Token API → /api/superset/guest-token │ │
│ └────────────────────────────┬─────────────────────────────────┘ │
│ │ │
│ ┌─────────────────────┼──────────────────────┐ │
│ │ │ │ │
│ ┌──────▼──────┐ ┌────────▼───────┐ ┌─────────▼──────────┐ │
│ │ API BI │ │ BI Engine │ │ Apache Superset │ │
│ │ (Node.js) │ │ Python:8004 │ │ Python:8088 │ │
│ │ sem mudança│ │ sem mudança │ │ │ │
│ │ │ │ │ │ SQL Lab │ │
│ │ CRUD │ │ SQL+Charts │ │ 50+ chart types │ │
│ │ Upload/ETL │ │ Micro-BI │ │ Guest Token (embed) │ │
│ │ Staging │ │ Análise Pandas │ │ Alerts & Reports │ │
│ │ Backups │ │ Cache 5min │ │ REST API v1 │ │
│ └─────┬───────┘ └────────┬───────┘ └─────────┬───────────┘ │
│ │ │ │ │
│ └──────────────────────┼─────────────────────┘ │
│ │ │
│ ┌──────────▼──────────┐ │
│ │ PostgreSQL │ │
│ │ │ │
│ │ arcadia │ ← Dados SOE (read-only) │
│ │ arcadia_superset │ ← Metadados Superset │
│ └────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ CIENTISTA (Python) — IA/ML: análise, padrões, insights │ │
│ │ ASSISTENTE BI (GPT) — chat sobre dados │ │
│ │ (ambos sem mudança) │ │
│ └──────────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────────┘
```
---
## 8. RESUMO — O que muda, o que fica
| Componente | Ação | Esforço |
|---|---|---|
| `server/metabase/proxy.ts` | Substituir por `server/superset/proxy.ts` | 30 min |
| `server/metaset/routes.ts` | Substituir por `server/superset/routes.ts` | 2h |
| `server/metaset/client.ts` | Remover (lógica vai para routes.ts) | 5 min |
| `docker-compose.yml` Superset | Expandir configuração existente | 1h |
| `docker/superset/superset_config.py` | Criar (novo arquivo) | 30 min |
| `docker/superset/init.sh` | Criar (novo arquivo) | 1h |
| `BiWorkspace.tsx` Advanced tab | Substituir iframe MetaSet | 1h |
| `SupersetDashboard.tsx` | Criar componente (novo) | 2h |
| `npm install @superset-ui/embedded-sdk` | Instalar dependência | 5 min |
| BI Engine Python (:8004) | **Não muda nada** | — |
| API BI Node /api/bi/* | **Não muda nada** | — |
| Upload/ETL/Staging | **Não muda nada** | — |
| 7 tabs do BiWorkspace | **Não muda nada** | — |
| Tabelas do banco | **Não muda nada** | — |
**Total estimado: 1-2 dias de implementação real** (a infra já está 80% pronta).
---
## 9. VANTAGEM ESTRATÉGICA
> O Apache Superset não é apenas "trocar Metabase".
>
> É a virada de chave que permite que o BI deixe de ser uma página separada
> e vire um **componente vivo** que aparece **dentro de cada módulo do Arcádia**:
>
> - O usuário está no módulo Financeiro → vê o dashboard financeiro ali mesmo
> - O usuário está no módulo Contábil → vê o DRE ali mesmo
> - O usuário está no SOE → vê os KPIs do negócio ali mesmo
>
> Sem abrir nova aba. Sem sair do contexto. Sem re-autenticar.
> O Superset renderiza no lugar exato onde o usuário está.
---
*PLANO_BI_SUPERSET.md — Arcádia Suite v3.0*
*Gerado em: 2026-03-16 — Baseado na análise do MAPA_BI_ARCADIA.md (Replit) + código real*

421
PLANO_EVOLUCAO_ARCADIA.md Normal file
View File

@ -0,0 +1,421 @@
# PLANO ESTRATÉGICO DE EVOLUÇÃO
## Arcádia Suite → Frappe Framework
### Versão 1.0 - Janeiro 2026
---
## 1. VISÃO GERAL
### 1.1 Objetivo
Evoluir o Arcádia Suite para um **Business Operating System** completo, inspirado em três referências:
| Referência | O que Inspira |
|------------|---------------|
| **Notion** | Blocos modulares, banco de dados relacional, personalização |
| **Replit** | IDE no navegador, colaboração em tempo real, deploy instantâneo |
| **Discord** | Comunidades, canais contextuais, comunicação em tempo real |
### 1.2 Estratégia de Migração: Strangler Fig
A estratégia Strangler Fig permite:
- Manter WhatsApp, Manus, CRM funcionando durante toda a migração
- Construir o novo sistema em paralelo
- Migrar módulo a módulo até o sistema legado "desaparecer"
```
Sistema Atual (Express/React) continua funcionando
Você vai adicionando "camadas Frappe" por cima
Cada módulo migrado substitui o antigo
No final, o "núcleo antigo" sumiu naturalmente
```
### 1.3 Stack Técnico
| Camada | Atual | Futuro |
|--------|-------|--------|
| Frontend | React 18 + TypeScript | Frappe Desk + React |
| Backend | Express.js | Frappe Framework |
| Database | PostgreSQL | PostgreSQL (mesmo) |
| Real-time | Socket.IO | Frappe Realtime + Socket.IO |
| WhatsApp | Baileys | Frappe App (Baileys) |
| IDE | Monaco + Terminal | IDE 3 Modos |
---
## 2. ESTRUTURA MULTI-TENANT
### 2.1 Hierarquia de 3 Níveis
```
NÍVEL 1: MASTER (Arcádia)
═════════════════════════
• Equipe de desenvolvimento
• Acesso total ao sistema
• IDE Pro-Code completa
• Central de Bibliotecas (publica apps)
• Suporte N3 (acessa tenants para debug)
• Gerencia parceiros e planos
├───────────────────────┬───────────────────────┐
▼ ▼ ▼
NÍVEL 2: PARCEIROS
══════════════════
• Consultorias, integradores, revendas
• IDE Low-Code
• Gerencia seus clientes
• Comissões sobre vendas
• Suporte N2 aos clientes
• Baixa apps da biblioteca
┌────┴────┐
▼ ▼
NÍVEL 3: CLIENTES
═════════════════
• Empresas usuárias finais
• Cockpit personalizado
• CRM/ERP operacional
• WhatsApp (N sessões conforme plano)
• BI próprio
• Manus com tools básicas
```
### 2.2 Matriz de Permissões por Tipo de Tenant
| Módulo | Master | Parceiro | Cliente |
|--------|--------|----------|---------|
| **IDE Pro-Code** | ✅ | ❌ | ❌ |
| **IDE Low-Code** | ✅ | ✅ | ❌ |
| **IDE No-Code** | ✅ | ✅ | ✅ (se habilitado) |
| **Central de Bibliotecas** | ✅ Publicar | ✅ Baixar | ❌ |
| **Central de APIs** | ✅ Gerenciar | ⚠️ Seus conectores | ⚠️ Leitura |
| **WhatsApp** | ✅ Ilimitado | ✅ N sessões | ✅ N sessões |
| **CRM/ERP** | ✅ Global | ✅ Próprio | ✅ Próprio |
| **Manus (IA)** | ✅ Todas tools | ✅ Tools permitidas | ✅ Básicas |
| **BI/Relatórios** | ✅ Global | ✅ Próprio | ✅ Próprio |
| **Suporte N3** | ✅ Acessa tenants | ❌ | ❌ |
| **Ver Parceiros** | ✅ | ✅ Seus clientes | ❌ |
| **Comissões** | ✅ Gerencia | ✅ Visualiza suas | ❌ |
### 2.3 Alterações no Schema
```sql
-- Alterações na tabela tenants
ALTER TABLE tenants ADD COLUMN tenant_type TEXT DEFAULT 'client';
-- master = Arcádia, partner = Parceiros, client = Clientes
ALTER TABLE tenants ADD COLUMN parent_tenant_id INTEGER REFERENCES tenants(id);
-- Referência ao tenant pai (hierarquia)
ALTER TABLE tenants ADD COLUMN partner_code TEXT;
-- Código do parceiro para rastreamento
ALTER TABLE tenants ADD COLUMN max_users INTEGER DEFAULT 5;
ALTER TABLE tenants ADD COLUMN max_storage_mb INTEGER DEFAULT 1000;
ALTER TABLE tenants ADD COLUMN features JSONB;
ALTER TABLE tenants ADD COLUMN commission_rate NUMERIC(5,2);
ALTER TABLE tenants ADD COLUMN trial_ends_at TIMESTAMP;
-- Nova tabela: Planos
CREATE TABLE tenant_plans (
id SERIAL PRIMARY KEY,
code TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
tenant_type TEXT NOT NULL,
max_users INTEGER DEFAULT 5,
max_storage_mb INTEGER DEFAULT 1000,
features JSONB,
monthly_price INTEGER DEFAULT 0,
yearly_price INTEGER DEFAULT 0,
is_active TEXT DEFAULT 'true',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Nova tabela: Relacionamento Parceiro-Cliente
CREATE TABLE partner_clients (
id SERIAL PRIMARY KEY,
partner_id INTEGER NOT NULL REFERENCES tenants(id),
client_id INTEGER NOT NULL REFERENCES tenants(id),
commission_rate NUMERIC(5,2),
status TEXT DEFAULT 'active',
started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
ended_at TIMESTAMP
);
-- Nova tabela: Comissões
CREATE TABLE partner_commissions (
id SERIAL PRIMARY KEY,
partner_id INTEGER NOT NULL REFERENCES tenants(id),
client_id INTEGER NOT NULL REFERENCES tenants(id),
reference_month TEXT NOT NULL,
client_plan_value INTEGER NOT NULL,
commission_rate NUMERIC(5,2) NOT NULL,
commission_value INTEGER NOT NULL,
status TEXT DEFAULT 'pending',
paid_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
```
---
## 3. CRONOGRAMA GERAL (6-9 Meses)
```
Mês 1 Mês 2 Mês 3 Mês 4 Mês 5 Mês 6+
├──────────┼──────────┼──────────┼──────────┼──────────┼────────►
FASE 0 ████████████████████
FUNDAÇÃO
Setup + Tenants + SSO
FASE 1 ████████████████████████████
INFRAESTRUTURA
CRM/ERP + Central APIs + Manus
FASE 2 ████████████████████████████████
EXPERIÊNCIA
Cockpit + Comunidades + IDE
FASE 3 ████████████████████►
AUTOMAÇÃO
WhatsApp + RPA + Decommission
```
---
## 4. FASE 0: FUNDAÇÃO (Semanas 1-8)
### 4.1 Objetivo
Preparar a base técnica sem quebrar nada do sistema atual.
### 4.2 Entregas
| # | Entrega | Descrição | Semana |
|---|---------|-----------|--------|
| 0.1 | **Setup Frappe Bench** | Instalar Frappe em servidor paralelo | 1-2 |
| 0.2 | **Hierarquia de Tenants** | Novos campos e tabelas no PostgreSQL | 2-3 |
| 0.3 | **SSO Bridge** | Login unificado (usuário loga uma vez) | 3-4 |
| 0.4 | **CDC Pipeline** | Sincronização de dados PostgreSQL ↔ Frappe | 4-6 |
| 0.5 | **Vault de Secrets** | Gerenciamento seguro de API keys | 5-6 |
| 0.6 | **Feature Flags** | Sistema de features por plano/tenant | 6-7 |
| 0.7 | **Planos e Preços** | Tabela de planos (free, starter, pro, enterprise) | 7-8 |
### 4.3 Resultado
- ✅ Frappe rodando em paralelo
- ✅ Hierarquia master/partner/client funcionando
- ✅ Login único nos dois sistemas
- ✅ Dados sincronizados em tempo real
- ✅ Planos e features configuráveis
---
## 5. FASE 1: INFRAESTRUTURA (Semanas 6-16)
### 5.1 Objetivo
Migrar dados mestres e criar a Central de APIs.
### 5.2 Entregas
| # | Entrega | Descrição | Semana |
|---|---------|-----------|--------|
| 1.1 | **DocTypes CRM** | Clientes, Leads, Oportunidades no Frappe | 6-8 |
| 1.2 | **DocTypes ERP** | Produtos, Pedidos, Faturas no Frappe | 8-10 |
| 1.3 | **Central de APIs (MVP)** | Dashboard visual de integrações | 9-12 |
| 1.4 | **Conectores Básicos** | Interface para SEFAZ, Bancos (dados demo) | 12-14 |
| 1.5 | **Manus Frappe** | Agente IA via background jobs | 13-15 |
| 1.6 | **Knowledge Graph** | Migração do grafo para DocTypes | 14-16 |
### 5.3 Central de APIs - Detalhamento
**IMPORTANTE:** A Central de APIs é uma interface visual de gerenciamento. Os dados de integrações (SEFAZ, Bancos, Mercado Livre) são ILUSTRATIVOS/DEMO. Não fazemos integração real com APIs externas nesta fase.
O que construímos:
- ✅ Interface visual (React)
- ✅ CRUD de conectores (cadastrar, editar, remover)
- ✅ Status visual (online, warning, error)
- ✅ Logs fictícios para demonstração
- ✅ Configurações por conector
- ✅ Permissões por tenant type
O que NÃO fazemos:
- ❌ Conectar à SEFAZ real
- ❌ Conectar a bancos reais
- ❌ Chamadas API externas
### 5.4 Resultado
- ✅ CRM/ERP acessível via Frappe Desk
- ✅ Central de APIs funcionando (dados demo)
- ✅ Manus consultando dados do Frappe
- ✅ Knowledge Graph migrado
---
## 6. FASE 2: EXPERIÊNCIA (Semanas 12-24)
### 6.1 Objetivo
Construir a nova interface (Cockpit, Comunidades, IDE).
### 6.2 Entregas
| # | Entrega | Descrição | Semana |
|---|---------|-----------|--------|
| 2.1 | **Cockpit PARA** | Navegação Projetos/Áreas/Recursos/Arquivo | 12-15 |
| 2.2 | **Dashboard Tríade** | Importante/Urgente/Circunstancial | 14-16 |
| 2.3 | **Widgets Sistema** | Tarefas, Calendário, Gráficos | 15-17 |
| 2.4 | **Comunidades MVP** | Canais por projeto (Socket.IO via Frappe) | 16-19 |
| 2.5 | **IDE No-Code** | DocType Builder visual | 18-20 |
| 2.6 | **IDE Low-Code** | Templates de scripts | 20-22 |
| 2.7 | **IDE Pro-Code** | Monaco + Terminal + Live Preview | 21-23 |
| 2.8 | **Central de Bibliotecas** | Repositório de apps Frappe | 22-24 |
### 6.3 Cockpit PARA + Tríade
O Cockpit é a interface principal do usuário, baseado em duas metodologias:
**Método PARA (Tiago Forte):**
- **P**rojetos: Todos os projetos ativos com metas e prazos
- **Á**reas: Áreas de responsabilidade contínua (Vendas, Financeiro, RH)
- **R**ecursos: Base de conhecimento, templates, manuais
- **A**rquivo: Tudo concluído ou inativo, para consulta futura
**Tríade do Tempo (Christian Barbosa):**
- 🟢 **Importante** (70% do tempo): Atividades que geram valor
- 🟡 **Urgente** (20% do tempo): Atividades com prazo apertado
- 🔴 **Circunstancial** (10% do tempo): Atividades que não agregam
### 6.4 IDE 3 Modos
| Modo | Quem Usa | O que Faz |
|------|----------|-----------|
| **No-Code** | Clientes | Criar formulários arrastando, workflows visuais, relatórios com filtros |
| **Low-Code** | Parceiros | Server Scripts com templates, validações, webhooks, fórmulas |
| **Pro-Code** | Arcádia | Monaco Editor completo, Terminal, Git, Deploy de apps |
### 6.5 Resultado
- ✅ Cockpit PARA + Tríade funcionando
- ✅ Comunidades com canais por projeto
- ✅ IDE com 3 modos operando
- ✅ Central de Bibliotecas publicando apps
---
## 7. FASE 3: AUTOMAÇÃO E DECOMMISSION (Semana 20+)
### 7.1 Objetivo
Migrar serviços restantes e desligar o legado.
### 7.2 Entregas
| # | Entrega | Descrição | Semana |
|---|---------|-----------|--------|
| 3.1 | **WhatsApp Frappe App** | Reconstruir Baileys como app nativo | 20-24 |
| 3.2 | **Motor de Workflows** | Automações visuais (RPA) | 22-26 |
| 3.3 | **Scientist Frappe** | Migrar para Frappe Workers | 24-28 |
| 3.4 | **Validação de Paridade** | Testes A/B, métricas | 26-30 |
| 3.5 | **Decommission Express** | Desligar endpoints legados | 30+ |
### 7.3 Resultado
- ✅ Sistema 100% unificado no Frappe
- ✅ Express/React desligado
- ✅ Uma única plataforma para manter
---
## 8. MAPEAMENTO DE MÓDULOS
| Módulo Atual | O que Acontece | Fase |
|--------------|----------------|------|
| **users, tenants** | Expande com hierarquia | 0 |
| **profiles, roles, permissions** | Migra para Frappe RBAC | 0 |
| **whatsapp_contacts, messages, tickets** | Mantém → Migra na Fase 3 | 3 |
| **pc_crm_leads, stages, opportunities** | Migra para Frappe DocTypes | 1 |
| **pc_clients, projects, tasks** | Migra para Frappe DocTypes | 1 |
| **graph_nodes, graph_edges** | Migra para Frappe Knowledge Graph | 1 |
| **internal_chat_*** | Evolui para Comunidades | 2 |
| **manus_*** | Integra via background jobs | 1 |
| **bi_*** | Mantém + novos widgets Cockpit | 2 |
| **ide_*** | Evolui para 3 modos | 2 |
---
## 9. RISCOS E MITIGAÇÕES
| Risco | Probabilidade | Impacto | Mitigação |
|-------|---------------|---------|-----------|
| **Drift de dados** | Média | Alto | CDC com validação contínua |
| **Performance chat** | Média | Médio | Load test antes de migrar |
| **Tokens WhatsApp** | Baixa | Alto | Vault de secrets |
| **Curva aprendizado Frappe** | Alta | Médio | Treinamento na Fase 0 |
| **Regressões funcionais** | Média | Alto | Testes A/B, telemetria |
| **Resistência usuários** | Média | Médio | Piloto gradual: Master → Partners → Clients |
---
## 10. QUICK WINS (Entregas Rápidas)
| Item | Tempo | Valor |
|------|-------|-------|
| **Hierarquia de Tenants** | 2 semanas | Estrutura para parceiros |
| **SSO unificado** | 2 semanas | Login único |
| **Central de APIs (UI)** | 3 semanas | Visibilidade integrações |
| **Dashboard Tríade** | 2 semanas | Consciência sobre tempo |
| **Planos e Features** | 2 semanas | Monetização estruturada |
---
## 11. OS 5 PILARES DO SISTEMA
### Pilar 1: Knowledge Graph
- Todos os dados do negócio conectados e pesquisáveis
- Navegação visual entre entidades relacionadas
- Base para IA contextual
### Pilar 2: Central Intelligence (Scientist)
- IA que aprende com interações do sistema
- Gera e executa código automaticamente
- Detecta padrões e sugere otimizações
### Pilar 3: Autonomous Agent (Manus)
- Executa tarefas multi-step de forma autônoma
- Acessa ferramentas e APIs
- Deep research com planejamento
### Pilar 4: Unified Communication
- WhatsApp integrado com CRM
- Chat interno com canais por projeto
- Email (futuro)
- Todos os canais em um lugar
### Pilar 5: Complete IDE
- 3 modos de desenvolvimento (No/Low/Pro Code)
- Central de Bibliotecas
- Deploy integrado
---
## 12. DOCUMENTOS DE REFERÊNCIA
Os documentos originais que basearam este plano estão em:
- `attached_assets/cocpti_docs/cocpti/` - Cockpit e DNA Notion
- `attached_assets/cocpti_docs/Ide Arcadia/` - Proposta IDE
- `attached_assets/cocpti_docs/Rota de desenvolviento/` - Roadmap original
- `attached_assets/cocpti_docs/Central de API/` - Central de APIs
---
## 13. PRÓXIMOS PASSOS
1. [ ] Implementar hierarquia de tenants no schema
2. [ ] Criar tabelas de planos e comissões
3. [ ] Documentar arquitetura CDC
4. [ ] Provisionar servidor Frappe
5. [ ] Implementar Central de APIs (UI com dados demo)
6. [ ] Construir Cockpit PARA + Tríade
---
*Documento criado em Janeiro 2026*
*Última atualização: Janeiro 2026*

733
PLANO_SOE_CENTRAL.md Normal file
View File

@ -0,0 +1,733 @@
# PLANO SOE CENTRAL — Arcádia Suite
## Orquestração Inteligente: Plus + ERPNext invisíveis, Central visível
### Versão 1.0 — Março 2026
---
## 1. ONDE ESTAMOS (Orientação Geral)
### O Projeto nasceu no Replit como…
- Uma plataforma de **escritório empresarial com IA central** (Manus Agent)
- Com proxy para o **Arcádia Plus** (Laravel/PHP, porta 8080) — ERP fiscal completo
- Com integração ao **ERPNext/Frappe** via API
- Stack principal: **React 18 + Express.js + PostgreSQL + FastAPI (Python)**
### O que JÁ existe e funciona:
| Componente | Status | Onde vive |
|---|---|---|
| Manus Agent (IA, 30+ tools) | ✅ Funcionando | `server/manus/` |
| Arcádia Plus (Laravel ERP) | ✅ Funcionando | `plus/` → porta 8080 |
| SSO Plus ↔ Suite | ✅ Funcionando | `server/plus/` |
| Motor Fiscal (nfelib) | ✅ Funcionando | `server/python/` → porta 8002 |
| Motor Contábil | ✅ Funcionando | `server/python/` → porta 8003 |
| Motor BI | ✅ Funcionando | `server/python/` → porta 8004 |
| Motor Automação | ✅ Funcionando | `server/python/` → porta 8005 |
| Motor Comunicação (Comm) | ✅ Funcionando | porta 8006 |
| Dev Center XOS (6 agentes) | ✅ Funcionando | `server/blackboard/` |
| LiteLLM Gateway | ✅ Configurado | porta 4000 |
| CRM, WhatsApp, Chat | ✅ Funcionando | `server/crm/`, `server/whatsapp/` |
| ERPNext integração | ⚠️ Parcial | `server/erp/routes.ts` |
### O que FALTA para o plano atual:
| Componente | Status |
|---|---|
| **SOE Central (Regras Unificadas)** | ❌ Não existe — este é o plano |
| Fiscal rules centralizadas (acima do Plus) | ❌ Dispersas no Plus |
| ERPNext rules centralizadas | ❌ Não existe camada de abstração |
| Contábil Avançado (lançamentos automáticos) | ❌ Motor existe, regras não |
| Financeiro Avançado (previsão, DRE automático) | ❌ Motor existe, regras não |
| Testes automatizados / CI-CD | ❌ |
| Monitoramento (APM, Sentry) | ❌ |
---
## 2. O PROBLEMA QUE VAMOS RESOLVER
### Situação Atual (problemática)
```
Usuário → Arcádia Suite
├── /plus → acessa Laravel diretamente (usuário VÊ o Plus)
├── /erp → acessa ERPNext diretamente (usuário VÊ o ERPNext)
└── Regras fiscais ficam dentro do Plus
Regras contábeis ficam no Motor 8003
Regras ERP ficam no ERPNext
→ Fragmentado, sem governança central
```
### Situação Desejada (SOE Central)
```
Usuário → Arcádia Suite (SOE)
▼ [Central de Regras SOE]
├── Fiscal: regras unificadas (CFOP, NCM, tributação, NF-e, CT-e...)
├── ERP: regras de negócio (pedidos, estoque, compras...)
├── Contábil: regras de lançamentos, plano de contas, DRE...
└── Financeiro: regras de fluxo, provisões, conciliação...
▼ [Execução invisível]
├── Plus → emite documentos, registra no banco
└── ERPNext → executa operações, registra no banco
O usuário NUNCA VÊ o Plus nem o ERPNext.
Só vê a Central SOE do Arcádia.
```
---
## 3. ARQUITETURA SOE CENTRAL
### 3.1 Diagrama Completo
```
┌─────────────────────────────────────────────────────────────────────────┐
│ ARCÁDIA SUITE (Frontend React) │
│ /financeiro /contabil /fisco /erp /people /production /bi │
│ Usuário nunca acessa /plus ou ERPNext diretamente │
└───────────────────────────────┬─────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ SOE — SISTEMA OPERACIONAL EMPRESARIAL (Nova Camada) │
│ Express.js / porta 5000 / server/soe/ │
│ │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ CENTRAL FISCAL │ │ CENTRAL ERP │ │
│ │ server/soe/fiscal/ │ │ server/soe/erp/ │ │
│ │ │ │ │ │
│ │ • Regras NCM/CFOP │ │ • Regras de pedidos │ │
│ │ • Políticas tribut. │ │ • Regras de estoque │ │
│ │ • Emissão NF-e/NFC-e │ │ • Regras de compras │ │
│ │ • Cancelamento │ │ • Regras de pessoas │ │
│ │ • SPED/SINTEGRA │ │ • Integrações ext. │ │
│ └──────────┬───────────┘ └──────────┬─────────────┘ │
│ │ │ │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ CENTRAL CONTÁBIL │ │ CENTRAL FINANCEIRO │ │
│ │ server/soe/contabil/│ │ server/soe/financ/ │ │
│ │ │ │ │ │
│ │ • Plano de contas │ │ • Fluxo de caixa │ │
│ │ • Regras de lanç. │ │ • Contas a P/R │ │
│ │ • DRE automático │ │ • Conciliação │ │
│ │ • Balancete │ │ • Previsão (IA) │ │
│ │ • Fechamento │ │ • Impostos │ │
│ └──────────┬───────────┘ └──────────┬─────────────┘ │
│ │ │ │
│ └────────────┬─────────────┘ │
│ ▼ │
│ ┌─────────────────────────────────┐ │
│ │ SOE RULE ENGINE (Núcleo) │ │
│ │ server/soe/rule-engine.ts │ │
│ │ │ │
│ │ • Resolução de conflitos │ │
│ │ • Aplicação de políticas XOS │ │
│ │ • Audit trail de todas regras │ │
│ │ • Manus consulta este engine │ │
│ └─────────────────┬───────────────┘ │
└────────────────────────────┼────────────────────────────────────────────┘
┌──────────────┼───────────────┐
▼ ▼ ▼
┌─────────────────┐ ┌──────────────┐ ┌──────────────────┐
│ ARCÁDIA PLUS │ │ ERPNEXT │ │ MOTORES PYTHON │
│ Laravel :8080 │ │ Frappe : │ │ 8002 Fiscal │
│ (invisível) │ │ (invisível) │ │ 8003 Contábil │
│ │ │ │ │ 8004 BI │
│ Emite docs │ │ Executa ERP │ │ 8005 Automação │
│ fiscais │ │ operacional │ │ │
└─────────────────┘ └──────────────┘ └──────────────────┘
```
---
## 4. CENTRAL FISCAL — Detalhamento
### 4.1 O que ela resolve
O Plus tem **376 controllers** e **1.546 rotas** — tudo fiscal/operacional. A Central Fiscal **não substitui** o Plus, ela **orquestra** o Plus com regras definidas no SOE.
### 4.2 Regras que vão para a Central (tiradas do Plus)
```typescript
// server/soe/fiscal/rules.ts — Exemplo de estrutura
interface FiscalRule {
id: string
nome: string
trigger: 'pre_emissao' | 'pos_emissao' | 'cancelamento' | 'validacao'
condicao: FiscalCondition
acao: FiscalAction
prioridade: number
ativo: boolean
}
// Exemplos de regras:
const REGRAS_FISCAIS: FiscalRule[] = [
{
id: 'NFE_CFOP_INTERESTADUAL',
nome: 'Ajusta CFOP para operação interestadual',
trigger: 'pre_emissao',
condicao: { uf_emitente: '!=', uf_destinatario: true },
acao: { ajustar_cfop: 'interestadual' },
prioridade: 1,
ativo: true
},
{
id: 'NFE_SIMPLES_NACIONAL',
nome: 'Aplica tributação Simples Nacional',
trigger: 'pre_emissao',
condicao: { regime_tributario: 'simples_nacional' },
acao: { aplicar_cst: 'tabela_simples', remover_pis_cofins: true },
prioridade: 2,
ativo: true
},
{
id: 'NFE_VALIDACAO_NCM',
nome: 'Valida NCM obrigatório para NF-e',
trigger: 'validacao',
condicao: { tipo_documento: 'nfe' },
acao: { validar_campo: 'ncm', obrigatorio: true },
prioridade: 1,
ativo: true
}
]
```
### 4.3 Fluxo de Emissão via SOE Central
```
Usuário clica "Emitir NF-e" no Arcádia
POST /api/soe/fiscal/emitir-nfe
SOE Rule Engine: aplica REGRAS_FISCAIS (pre_emissao)
• Verifica regime tributário
• Aplica CFOP correto
• Calcula impostos (ICMS, PIS, COFINS, IPI)
• Valida NCM obrigatório
• Adiciona dados da empresa emitente
SOE envia para Plus via API interna (invisível)
POST http://plus:8080/api/nfe/emitir
+ token SSO automático
Plus emite via Cloud-DFE → SEFAZ
SOE recebe retorno
• Registra no banco SOE (tabela soe_fiscal_eventos)
• Dispara lançamento contábil automático
• Notifica via WebSocket o frontend
Usuário vê resultado no Arcádia (nunca soube do Plus)
```
### 4.4 Tabelas SOE para Fiscal
```sql
-- Tabelas a criar no schema Arcádia (PostgreSQL)
CREATE TABLE soe_fiscal_regras (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
nome VARCHAR(200) NOT NULL,
trigger VARCHAR(50) NOT NULL,
condicao JSONB NOT NULL,
acao JSONB NOT NULL,
prioridade INTEGER DEFAULT 10,
ativo BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE soe_fiscal_eventos (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tipo VARCHAR(50) NOT NULL, -- 'emissao_nfe', 'cancelamento', etc.
empresa_id INTEGER NOT NULL,
documento_chave VARCHAR(100),
regras_aplicadas JSONB, -- quais regras foram aplicadas
payload_enviado JSONB, -- o que foi enviado ao Plus
payload_retorno JSONB, -- o que o Plus/SEFAZ respondeu
status VARCHAR(50) NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE soe_fiscal_configuracoes (
empresa_id INTEGER PRIMARY KEY,
regime_tributario VARCHAR(50), -- simples, lucro_presumido, lucro_real
uf VARCHAR(2),
cnpj VARCHAR(18),
certificado_tipo VARCHAR(20),
aliquotas JSONB, -- alíquotas padrão por operação
cfops_padrao JSONB, -- CFOPs padrão por tipo de operação
created_at TIMESTAMP DEFAULT NOW()
);
```
---
## 5. CENTRAL ERP (ERPNext/Frappe) — Detalhamento
### 5.1 O que ela resolve
O ERPNext tem **APIs complexas**. A Central ERP abstrai tudo isso em operações simples que o Arcádia entende.
### 5.2 Adapter Layer ERPNext
```typescript
// server/soe/erp/frappe-adapter.ts
interface ERPAdapter {
// Produtos
criarProduto(data: ProdutoSOE): Promise<ERPProduto>
buscarProduto(codigo: string): Promise<ProdutoSOE>
atualizarEstoque(movimentacao: MovimentacaoEstoque): Promise<void>
// Pedidos
criarPedidoVenda(pedido: PedidoSOE): Promise<ERPPedido>
aprovarPedido(id: string): Promise<void>
faturarPedido(id: string): Promise<ERPFatura>
// Financeiro
criarLancamento(lancamento: LancamentoSOE): Promise<ERPLancamento>
buscarPlanoContas(): Promise<ContaContabil[]>
// Compras
criarPedidoCompra(data: PedidoCompraSOE): Promise<ERPPedidoCompra>
receberMercadoria(data: RecebimentoSOE): Promise<void>
}
// As regras ficam no SOE, o adapter só traduz
class FrappeAdapter implements ERPAdapter {
private baseUrl: string
private apiKey: string
async criarPedidoVenda(pedido: PedidoSOE): Promise<ERPPedido> {
// Traduz do modelo SOE para o modelo Frappe
const frappePedido = this.traduzirParaFrappe(pedido)
// Envia para ERPNext (invisível para o usuário)
const response = await fetch(`${this.baseUrl}/api/resource/Sales Order`, {
method: 'POST',
headers: { 'Authorization': `token ${this.apiKey}` },
body: JSON.stringify(frappePedido)
})
// Traduz retorno de volta para modelo SOE
return this.traduzirDeVoltaSOE(await response.json())
}
}
```
### 5.3 Regras de Negócio ERP no SOE
```typescript
// server/soe/erp/rules.ts
const REGRAS_ERP = [
{
id: 'PEDIDO_APROVACAO_AUTOMATICA',
nome: 'Aprovação automática até R$ 5.000',
trigger: 'pre_aprovacao_pedido',
condicao: { valor_total: { menor_que: 5000 } },
acao: { aprovar_automaticamente: true }
},
{
id: 'ESTOQUE_RESERVA_VENDA',
nome: 'Reservar estoque ao confirmar venda',
trigger: 'pos_confirmacao_venda',
condicao: { status: 'confirmado' },
acao: { reservar_estoque: true, gerar_separacao: true }
},
{
id: 'NF_AUTOMATICA_FATURAMENTO',
nome: 'Emite NF-e automaticamente ao faturar',
trigger: 'pos_faturamento',
condicao: { tipo_cliente: ['pj', 'pf_contribuinte'] },
acao: { emitir_nfe: true, via_soe_fiscal: true }
}
]
```
---
## 6. CENTRAL CONTÁBIL AVANÇADA
### 6.1 Motor Atual (porta 8003) + Regras SOE
O Motor Contábil Python já faz DRE e Balancete. O que falta é **alimentação automática**.
### 6.2 Lançamentos Automáticos (grande salto)
```typescript
// server/soe/contabil/auto-lancamentos.ts
interface RegrasLancamentoAuto {
gatilho: string
debito: string // Conta contábil
credito: string // Conta contábil
historico: string
formula: string // Como calcular o valor
}
const LANCAMENTOS_AUTOMATICOS: RegrasLancamentoAuto[] = [
// Quando emite NF-e de venda
{
gatilho: 'emissao_nfe_venda',
debito: '1.1.3.01', // Clientes
credito: '3.1.1.01', // Receita Bruta de Vendas
historico: 'Venda conforme NF-e {numero}',
formula: 'valor_total_nota'
},
{
gatilho: 'emissao_nfe_venda',
debito: '3.1.2.01', // Dedução de Receita (ICMS)
credito: '2.1.2.01', // ICMS a Recolher
historico: 'ICMS s/ Venda NF-e {numero}',
formula: 'valor_icms'
},
// Quando paga conta
{
gatilho: 'pagamento_conta',
debito: '2.1.1.{categoria}', // Conta a Pagar específica
credito: '1.1.1.01', // Banco/Caixa
historico: 'Pagamento: {descricao}',
formula: 'valor_pago'
},
// Quando recebe pagamento
{
gatilho: 'recebimento',
debito: '1.1.1.01', // Banco/Caixa
credito: '1.1.3.01', // Clientes
historico: 'Recebimento: {descricao}',
formula: 'valor_recebido'
},
// Compra de mercadoria
{
gatilho: 'entrada_nfe_compra',
debito: '1.1.4.01', // Estoque
credito: '2.1.1.01', // Fornecedores
historico: 'Compra conforme NF-e {numero} / {fornecedor}',
formula: 'valor_mercadorias'
}
]
```
### 6.3 DRE Automático via SOE
```
Usuário pede DRE do mês
SOE Contábil consulta:
1. Todos lançamentos do período (tabela soe_lancamentos)
2. Agrupa por conta contábil
3. Aplica estrutura DRE configurada
4. Envia para Motor Python 8003 para cálculos
5. Retorna DRE formatado
Usuário vê DRE no /contabil do Arcádia
(Nunca soube que teve lançamentos automáticos de NF-e, pagamentos, etc.)
```
---
## 7. CENTRAL FINANCEIRO AVANÇADO
### 7.1 Regras Financeiras no SOE
```typescript
// server/soe/financeiro/rules.ts
const REGRAS_FINANCEIRO = [
{
id: 'BOLETO_AUTO_VENCIMENTO',
nome: 'Gerar boleto automático para vendas a prazo',
trigger: 'pos_confirmacao_venda_prazo',
acao: { gerar_boleto: true, via: 'asaas', vencimento: '+{prazo}_dias' }
},
{
id: 'ALERTA_VENCIMENTO_3DIAS',
nome: 'Alerta de vencimento 3 dias antes',
trigger: 'cron_diario',
condicao: { vencimento_em: 3 },
acao: { notificar: ['whatsapp', 'email'], via_manus: true }
},
{
id: 'PROVISAO_IMPOSTOS',
nome: 'Provisão mensal de impostos',
trigger: 'cron_mensal',
acao: { calcular_provisao: ['icms', 'pis', 'cofins', 'csll', 'irpj'], registrar_lancamento: true }
},
{
id: 'CONCILIACAO_AUTO',
nome: 'Conciliação bancária automática via OFX',
trigger: 'importacao_ofx',
acao: { tentar_conciliar: true, confianca_minima: 0.85, pendentes_para_revisao: true }
}
]
```
---
## 8. MANUS AGENT + SOE CENTRAL
### 8.1 O Manus consulta a Central
```
Pergunta: "Qual foi o resultado do mês passado?"
Manus → SOE Central
• GET /api/soe/contabil/dre?periodo=2026-02
• GET /api/soe/financeiro/fluxo?periodo=2026-02
• GET /api/soe/fiscal/resumo?periodo=2026-02
SOE agrega dados dos motores (Python) + Plus + ERPNext
Manus recebe contexto completo e responde:
"Faturamento: R$ 120.000
Deduções (impostos): R$ 18.000
Receita Líquida: R$ 102.000
Custo das mercadorias: R$ 65.000
Lucro Bruto: R$ 37.000
..."
```
### 8.2 Tools do Manus para o SOE
```typescript
// Novas tools do Manus que consultam o SOE Central
{
name: 'consultar_situacao_fiscal',
description: 'Consulta situação fiscal da empresa (NFs pendentes, impostos, SPED)',
endpoint: 'GET /api/soe/fiscal/situacao'
},
{
name: 'emitir_documento_fiscal',
description: 'Emite NF-e, NFC-e, CT-e ou MDF-e',
endpoint: 'POST /api/soe/fiscal/emitir'
},
{
name: 'consultar_dre',
description: 'Gera DRE de qualquer período',
endpoint: 'GET /api/soe/contabil/dre'
},
{
name: 'consultar_fluxo_caixa',
description: 'Consulta fluxo de caixa e previsões',
endpoint: 'GET /api/soe/financeiro/fluxo'
},
{
name: 'registrar_lancamento_contabil',
description: 'Registra lançamento contábil manualmente',
endpoint: 'POST /api/soe/contabil/lancamento'
}
```
---
## 9. ESTRUTURA DE PASTAS A CRIAR
```
server/
└── soe/ ← Nova pasta (Central SOE)
├── index.ts ← Router principal do SOE
├── rule-engine.ts ← Motor de regras central
├── fiscal/
│ ├── routes.ts ← GET/POST /api/soe/fiscal/*
│ ├── rules.ts ← Regras fiscais (CFOP, NCM, tributação)
│ ├── emissao.ts ← Orquestração de emissão (→ Plus)
│ ├── plus-adapter.ts ← Adaptador para o Plus (invisível)
│ └── sefaz-adapter.ts ← Comunicação direta SEFAZ (via Python 8002)
├── erp/
│ ├── routes.ts ← GET/POST /api/soe/erp/*
│ ├── rules.ts ← Regras de negócio ERP
│ ├── frappe-adapter.ts ← Adaptador ERPNext (invisível)
│ └── plus-erp-adapter.ts ← Adaptador Plus-ERP (para dados de venda)
├── contabil/
│ ├── routes.ts ← GET/POST /api/soe/contabil/*
│ ├── rules.ts ← Regras contábeis
│ ├── auto-lancamentos.ts ← Lançamentos automáticos
│ ├── plano-contas.ts ← Plano de contas SOE
│ └── python-adapter.ts ← Adaptador Motor Python 8003
├── financeiro/
│ ├── routes.ts ← GET/POST /api/soe/financeiro/*
│ ├── rules.ts ← Regras financeiras
│ ├── previsao.ts ← Previsão com IA
│ └── conciliacao.ts ← Conciliação bancária
└── shared/
├── types.ts ← Tipos SOE compartilhados
├── audit.ts ← Registro de todas as operações
└── event-bus.ts ← Eventos entre Centrais
```
---
## 10. TABELAS DO BANCO A CRIAR (Drizzle ORM)
```typescript
// shared/schema.ts — Adicionar:
// Configuração da empresa no SOE
export const soeEmpresaConfig = pgTable('soe_empresa_config', {
id: serial('id').primaryKey(),
empresaId: integer('empresa_id').notNull(),
regimeTributario: varchar('regime_tributario', { length: 50 }), // simples, presumido, real
uf: varchar('uf', { length: 2 }),
cnpj: varchar('cnpj', { length: 18 }),
planoContasId: integer('plano_contas_id'),
configFiscal: jsonb('config_fiscal'), // alíquotas, CFOPs padrão
configErp: jsonb('config_erp'), // qual ERP: plus, erpnext, ambos
createdAt: timestamp('created_at').defaultNow(),
})
// Regras configuráveis (editáveis pelo usuário)
export const soeRegras = pgTable('soe_regras', {
id: uuid('id').primaryKey().defaultRandom(),
empresaId: integer('empresa_id'), // null = regra global
dominio: varchar('dominio', { length: 50 }).notNull(), // 'fiscal', 'erp', 'contabil', 'financeiro'
nome: varchar('nome', { length: 200 }).notNull(),
trigger: varchar('trigger', { length: 100 }).notNull(),
condicao: jsonb('condicao').notNull(),
acao: jsonb('acao').notNull(),
prioridade: integer('prioridade').default(10),
ativo: boolean('ativo').default(true),
createdAt: timestamp('created_at').defaultNow(),
})
// Log de todas as operações SOE (audit trail)
export const soeEventos = pgTable('soe_eventos', {
id: uuid('id').primaryKey().defaultRandom(),
empresaId: integer('empresa_id').notNull(),
dominio: varchar('dominio', { length: 50 }).notNull(),
tipo: varchar('tipo', { length: 100 }).notNull(),
regraIds: jsonb('regra_ids'), // regras aplicadas
payloadEntrada: jsonb('payload_entrada'),
payloadSaida: jsonb('payload_saida'),
erp: varchar('erp', { length: 50 }), // 'plus', 'erpnext', 'python_8003', etc.
status: varchar('status', { length: 50 }).notNull(),
duracao: integer('duracao'), // ms
createdAt: timestamp('created_at').defaultNow(),
})
// Lançamentos contábeis centralizados
export const soeLancamentos = pgTable('soe_lancamentos', {
id: uuid('id').primaryKey().defaultRandom(),
empresaId: integer('empresa_id').notNull(),
data: date('data').notNull(),
contaDebito: varchar('conta_debito', { length: 30 }).notNull(),
contaCredito: varchar('conta_credito', { length: 30 }).notNull(),
valor: numeric('valor', { precision: 15, scale: 2 }).notNull(),
historico: text('historico').notNull(),
origem: varchar('origem', { length: 100 }), // 'emissao_nfe', 'pagamento', etc.
origemId: varchar('origem_id', { length: 100 }),
period: varchar('period', { length: 7 }), // '2026-03'
createdAt: timestamp('created_at').defaultNow(),
})
```
---
## 11. ROADMAP DE IMPLEMENTAÇÃO
### Fase 1 — Fundação (23 semanas)
```
[ ] 1. Criar server/soe/ com estrutura básica
[ ] 2. Criar tabelas: soe_empresa_config, soe_regras, soe_eventos, soe_lancamentos
[ ] 3. Implementar Plus Adapter (encapsula chamadas ao Laravel)
[ ] 4. Implementar Frappe Adapter (encapsula chamadas ao ERPNext)
[ ] 5. Endpoint GET /api/soe/status (health check de todos os motores)
[ ] 6. Migrar /api/fisco/* para passar pelo SOE
```
### Fase 2 — Central Fiscal (23 semanas)
```
[ ] 7. Implementar Rule Engine para fiscal
[ ] 8. Criar regras fiscais base (NCM, CFOP, tributação por regime)
[ ] 9. POST /api/soe/fiscal/emitir-nfe → Plus (invisível)
[ ] 10. POST /api/soe/fiscal/cancelar-nfe → Plus (invisível)
[ ] 11. GET /api/soe/fiscal/situacao → agrega Plus + Python 8002
[ ] 12. Atualizar frontend /fisco para usar /api/soe/fiscal/*
```
### Fase 3 — Central Contábil (2 semanas)
```
[ ] 13. Implementar auto-lancamentos para eventos fiscais
[ ] 14. Integrar Motor Python 8003 via SOE
[ ] 15. GET /api/soe/contabil/dre → resultado automático
[ ] 16. GET /api/soe/contabil/balancete → atualizado em tempo real
[ ] 17. Atualizar frontend /contabil para usar /api/soe/contabil/*
```
### Fase 4 — Central Financeiro (2 semanas)
```
[ ] 18. Regras de boleto automático (via Asaas)
[ ] 19. Alertas de vencimento (via Manus/WhatsApp)
[ ] 20. Conciliação bancária semi-automática
[ ] 21. Previsão financeira com IA (Motor Python 8003 + Manus)
[ ] 22. Atualizar frontend /financeiro para usar /api/soe/financeiro/*
```
### Fase 5 — Manus + SOE (1 semana)
```
[ ] 23. Registrar novas tools Manus apontando para SOE
[ ] 24. Manus usa SOE para responder perguntas de negócio
[ ] 25. Dashboard SOE: visão geral de todos os eventos
```
### Fase 6 — Central ERP (3 semanas)
```
[ ] 26. Frappe Adapter completo (produtos, pedidos, estoque, compras)
[ ] 27. Regras de negócio ERP no SOE
[ ] 28. Sincronização bidirecional SOE ↔ ERPNext (via eventos)
[ ] 29. Atualizar /erp para usar /api/soe/erp/*
```
---
## 12. RESULTADO FINAL — O QUE O USUÁRIO EXPERIMENTA
```
Antes (fragmentado):
Usuário → /plus → vê Laravel diretamente
Usuário → /erp → vê ERPNext diretamente
Regras fiscais só no Plus
Contabilidade manual
Financeiro sem automação
Depois (SOE Central):
Usuário → /fisco → emite NF-e, Plus executa invisível
Usuário → /contabil → vê DRE gerado automaticamente
Usuário → /financeiro → vê fluxo, previsões, alertas automáticos
Usuário → Manus → pergunta "qual meu resultado?" → resposta completa
Plus e ERPNext: INVISÍVEIS, executando em background
Regras: CENTRALIZADAS no SOE, editáveis
Lançamentos: AUTOMÁTICOS baseados em eventos (NF-e, pagamentos, etc.)
Auditoria: TOTAL de todas as operações
```
---
## 13. PRINCÍPIO ORIENTADOR
> **"O usuário não sabe o que está por trás. Ele só sabe que funciona."**
>
> O Arcádia Suite é o **Sistema Operacional** da empresa.
> O Plus e o ERPNext são **drivers de hardware** — poderosos, mas invisíveis.
> O SOE Central é o **kernel** que os orquestra com regras e inteligência.
---
*PLANO_SOE_CENTRAL.md — Arcádia Suite v3.0*
*Gerado em: 2026-03-16*

666
PLANO_SOE_CENTRAL_v2.md Normal file
View File

@ -0,0 +1,666 @@
# PLANO SOE CENTRAL v2 — Análise Real + Evolução
## Baseado no MAPA_SOE_ARCADIA.md (Replit) + Auditoria do Código
### Versão 2.0 — Março 2026
---
## 1. DIAGNÓSTICO — O QUE JÁ EXISTE (e o plano v1 não sabia)
O MAPA_SOE_ARCADIA.md do Replit revelou que **o SOE já é uma realidade arquitetural robusta**, não apenas um conceito. Antes de planejar qualquer coisa, é crítico entender o que já funciona.
### 1.1 O que já está construído e funcionando
| Componente | Status | Localização |
|---|---|---|
| `SOE.tsx` | ✅ 2.414 linhas — UI completa com 9 tabs | `client/src/pages/SOE.tsx` |
| `SoeMotorContext.tsx` | ✅ Seletor de motor (plus/erpnext) | `client/src/contexts/` |
| `ErpApiClient` (interface) | ✅ Interface unificada definida | `server/erp/index.ts` |
| `ArcadiaPlusClient` | ✅ Adaptador HTTP → Plus:8080 | `server/erp/index.ts` |
| `ArcadiaNextClient` | ✅ Adaptador HTTP → ERPNext API | `server/erp/index.ts` |
| `registerSoeRoutes()` | ✅ 60+ endpoints em /api/soe/* | `server/erp/routes.ts` |
| `server/plus/proxy.ts` | ✅ Proxy reverso para Plus | `server/plus/` |
| `server/plus/sso.ts` | ✅ SSO automático Plus ↔ Suite | `server/plus/` |
| `server/plus/launcher.ts` | ✅ Lança PHP artisan serve | `server/plus/` |
| Módulo de Pessoas unificado | ✅ `persons` + `person_roles` | Schema PostgreSQL |
| Módulo de Produtos | ✅ Com NCM, CST, CFOP, IMEI | Schema PostgreSQL |
| Módulo Financeiro | ✅ Contas a P/R + transações | Schema PostgreSQL |
| Motor Fisco (:8002) | ✅ NF-e/NFC-e via nfelib | `server/python/` |
| Motor Contábil (:8003) | ✅ DRE, Balanço, SPED | `server/python/` |
| Sistema de módulos por tenant | ✅ `tenants.features` (JSONB) | Schema PostgreSQL |
| Middleware de gating | ✅ `requireModule()` | `server/` |
### 1.2 Bancos de Dados por Motor (detalhe crítico)
```
Arcádia Suite → PostgreSQL 16 (dados do SOE, pessoas, produtos, vendas...)
Arcádia Plus → MySQL 8.0 (dados do Laravel — vendas, estoque, NF-e...)
ERPNext → MariaDB (dados do Frappe — CRM, HR, projetos...)
Motor Fisco → sem banco (só processa, retorna resultado)
Motor Contábil → sem banco (só processa, retorna resultado)
```
**Implicação:** Não há sincronização automática entre esses bancos. Um pedido criado no SOE (PostgreSQL) não aparece no Plus (MySQL) automaticamente — e vice-versa.
### 1.3 O padrão motor/adaptador atual
```
Request do usuário
/api/soe/sales-orders (POST)
Verifica motor ativo (localStorage: arcadia_soe_motor)
├── motor = "local" → INSERT direto no PostgreSQL
├── motor = "plus" → POST → Plus:8080 → MySQL
└── motor = "erpnext" → POST → ERPNext API → MariaDB
```
---
## 2. O PROBLEMA REAL — Onde o SOE atual para
O SOE atual é um **roteador inteligente**: recebe o request, decide para qual motor enviar, e despacha. Ele **não tem inteligência própria**.
### 2.1 O que falta (lacuna real)
```
Situação atual:
SOE.tsx → /api/soe/* ────────────────────────────► Plus/ERPNext
(sem regras, sem processamento)
Regras ficam DENTRO do Plus
Regras ficam DENTRO do ERPNext
Situação desejada:
SOE.tsx → /api/soe/* → [RULE ENGINE SOE] ──────► Plus/ERPNext
│ (só executa)
├── Regras Fiscais
├── Regras Contábeis
├── Regras Financeiras
└── Regras de Negócio
```
### 2.2 Problemas concretos hoje
| Problema | Impacto |
|---|---|
| Fiscal rules dentro do Plus | Se trocar de motor (Plus→ERPNext), perde todas as regras |
| Nenhum lançamento contábil automático | DRE fica vazio, contabilidade manual |
| Motor Contábil (:8003) existe mas nada alimenta ele | 0 lançamentos automáticos |
| Plus tem UI própria (/plus) | Usuário pode bypassar o SOE e ir direto ao Plus |
| Sem event bus entre motores | Uma NF-e emitida no Plus não dispara nada no SOE |
| 25 conectores ERP definidos, zero chamadas reais | Integração existe no papel, não funciona |
### 2.3 O que o usuário vê hoje vs o que deveria ver
```
Hoje:
/soe → UI do SOE (tabs: dashboard, pessoas, produtos, vendas...)
/plus → UI do Plus (Laravel) — direto, sem passar pelo SOE
/erp → UI do ERP legado
(usuário VÊ o Plus se quiser)
Desejado:
/soe → UI do SOE (mesma, mas com inteligência)
/plus → BLOQUEADO ou redirecionado para /soe
/erp → BLOQUEADO ou redirecionado para /soe
(Plus e ERPNext = invisíveis, executando em background)
```
---
## 3. ARQUITETURA EVOLUÍDA — SOE com Rule Engine
### 3.1 O que muda (e o que fica igual)
**NÃO MUDA:**
- `SOE.tsx` — a UI continua a mesma (só ganha funcionalidades)
- `SoeMotorContext.tsx` — seleção de motor continua existindo
- `ArcadiaPlusClient` / `ArcadiaNextClient` — adaptadores continuam
- Todas as rotas `/api/soe/*` — continuam funcionando
- Todos os bancos — PostgreSQL, MySQL, MariaDB — continuam separados
**MUDA / ADICIONA:**
- Uma **camada de Rule Engine** entre as rotas e os adaptadores
- **Event Bus SOE** que dispara lançamentos contábeis automaticamente
- **Ocultação do Plus**`/plus` passa a ser controlado pelo SOE
- **Regras fiscais** saem do Plus e entram no SOE
### 3.2 Diagrama completo com Rule Engine
```
┌─────────────────────────────────────────────────────────────────────┐
│ ARCÁDIA SUITE — CAMADA DE APRESENTAÇÃO │
│ │
│ SOE.tsx (2.414+ linhas) │
│ Tabs: Dashboard | Pessoas | Produtos | Vendas | Compras │
│ Financeiro | CRM | Sincronização | Config │
│ │
│ SoeMotorContext.tsx → motor = "plus" | "erpnext" | "local" │
└───────────────────────────────┬─────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ /api/soe/* — CAMADA DE API (já existe) │
│ registerSoeRoutes() em server/erp/routes.ts │
│ requireAuth + requireModule middleware │
└───────────────────────────────┬─────────────────────────────────────┘
▼ ◄── INSERIR AQUI (novo)
┌─────────────────────────────────────────────────────────────────────┐
│ SOE RULE ENGINE (NOVO) │
│ server/soe/rule-engine/ │
│ │
│ Antes de despachar para o motor, o Rule Engine: │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 1. FISCAL RULES (server/soe/rule-engine/fiscal.ts) │ │
│ │ • Resolve CFOP correto (intra/interestadual) │ │
│ │ • Aplica tributação por regime (Simples/Presumido/Real) │ │
│ │ • Valida NCM obrigatório │ │
│ │ • Define CST/CSOSN automático │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 2. BUSINESS RULES (server/soe/rule-engine/business.ts) │ │
│ │ • Aprovação automática de pedidos < R$ X
│ │ • Reserva de estoque ao confirmar venda │ │
│ │ • Trigger de NF-e ao faturar pedido │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 3. ACCOUNTING RULES (server/soe/rule-engine/accounting.ts) │ │
│ │ • Emitiu NF-e → lançamento automático D: Clientes │ │
│ │ C: Receita │ │
│ │ • Pagou conta → lançamento automático D: Fornecedor │ │
│ │ C: Banco │ │
│ └─────────────────────────────────────────────────────────────┘ │
└───────────────────────────────┬─────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ DESPACHADOR DE MOTOR (já existe, aprimorado) │
│ createErpClient(connection) em server/erp/index.ts │
│ │
│ motor = "local" → INSERT PostgreSQL │
│ motor = "plus" → ArcadiaPlusClient → Plus:8080 → MySQL │
│ motor = "erpnext" → ArcadiaNextClient → ERPNext API → MariaDB │
└───────────────────────────────┬─────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ SOE EVENT BUS (NOVO) │
│ server/soe/event-bus.ts │
│ │
│ Depois que o motor executa, o Event Bus dispara: │
│ │
│ evento: "nfe_emitida" → Motor Contábil (lançamento auto) │
│ evento: "venda_confirmada" → Reserva estoque + alerta WhatsApp │
│ evento: "pagamento_feito" → Baixa C/P + lançamento contábil │
│ evento: "recebimento" → Baixa C/R + lançamento contábil │
└─────────────────────────────────────────────────────────────────────┘
```
---
## 4. O QUE CONSTRUIR — Detalhamento por Módulo
### 4.1 SOE Rule Engine (PRIORIDADE MÁXIMA)
**Localização:** `server/soe/rule-engine/`
Este é o coração da evolução. É um middleware que intercepta as chamadas às rotas SOE **antes** de despachar para o motor.
#### Como funciona na prática
```typescript
// server/erp/routes.ts — modificação cirúrgica
// ANTES (atual):
app.post("/api/soe/sales-orders", requireAuth, async (req, res) => {
const client = getActiveClient(req) // pega Plus ou ERPNext
const result = await client.createSalesOrder(req.body) // despacha direto
res.json(result)
})
// DEPOIS (com Rule Engine):
app.post("/api/soe/sales-orders", requireAuth, async (req, res) => {
// 1. Aplica regras ANTES de despachar
const enriched = await ruleEngine.apply('pre_sales_order', req.body, req.user)
// 2. Despacha para o motor com dados enriquecidos
const client = getActiveClient(req)
const result = await client.createSalesOrder(enriched.payload)
// 3. Dispara eventos DEPOIS da execução
await eventBus.emit('sales_order_created', { result, rules: enriched.appliedRules })
res.json(result)
})
```
#### Estrutura do Rule Engine
```typescript
// server/soe/rule-engine/index.ts
interface SoeRule {
id: string
dominio: 'fiscal' | 'business' | 'accounting' | 'financial'
trigger: string // ex: 'pre_sales_order', 'post_nfe_emitida'
condicao: RuleCondition // quando esta regra se aplica
acao: RuleAction // o que ela faz ao payload
prioridade: number
ativo: boolean
empresaId?: number // null = regra global de todos os tenants
}
class SoeRuleEngine {
async apply(trigger: string, payload: any, context: RuleContext): Promise<EnrichedPayload> {
// 1. Busca regras ativas para este trigger (do banco + defaults hardcoded)
const rules = await this.getActiveRules(trigger, context.tenantId)
// 2. Filtra regras cuja condição se aplica ao payload
const applicable = rules.filter(r => this.avaliarCondicao(r.condicao, payload, context))
// 3. Aplica cada regra em ordem de prioridade
let enriched = { ...payload }
const applied: string[] = []
for (const rule of applicable.sort((a, b) => a.prioridade - b.prioridade)) {
enriched = await this.aplicarAcao(rule.acao, enriched, context)
applied.push(rule.id)
}
return { payload: enriched, appliedRules: applied }
}
}
```
### 4.2 Regras Fiscais no SOE
Estas regras **hoje vivem dentro do Plus** (Laravel controllers). A ideia é trazê-las para o SOE.
**Nota importante:** Não é mover o código PHP para TypeScript. É criar regras **declarativas** no SOE que enriquecem o payload **antes** de enviar ao Plus. O Plus ainda processa e emite — mas recebe o payload já correto.
```typescript
// server/soe/rule-engine/fiscal-rules.ts
export const FISCAL_RULES_PADRAO: SoeRule[] = [
// CFOP — resolve automaticamente por operação
{
id: 'CFOP_VENDA_DENTRO_ESTADO',
dominio: 'fiscal',
trigger: 'pre_sales_order',
condicao: { uf_emitente: '==', uf_destinatario: true, tipo_op: 'venda_produto' },
acao: { set: { 'items[*].cfop': '5.102' } },
prioridade: 10, ativo: true
},
{
id: 'CFOP_VENDA_OUTRO_ESTADO',
dominio: 'fiscal',
trigger: 'pre_sales_order',
condicao: { uf_emitente: '!=', uf_destinatario: true, tipo_op: 'venda_produto' },
acao: { set: { 'items[*].cfop': '6.102' } },
prioridade: 10, ativo: true
},
// Tributação por Regime
{
id: 'CST_SIMPLES_NACIONAL',
dominio: 'fiscal',
trigger: 'pre_nfe_emissao',
condicao: { regime_tributario: 'simples_nacional' },
acao: {
set: { 'items[*].cst_icms': '400', 'items[*].pis': null, 'items[*].cofins': null },
// No Simples Nacional: CSOSN 400 = tributado, sem destaque PIS/COFINS
},
prioridade: 5, ativo: true
},
{
id: 'CST_LUCRO_PRESUMIDO',
dominio: 'fiscal',
trigger: 'pre_nfe_emissao',
condicao: { regime_tributario: 'lucro_presumido' },
acao: { set: { 'items[*].cst_pis': '01', 'items[*].cst_cofins': '01' } },
prioridade: 5, ativo: true
},
// Validações obrigatórias
{
id: 'VALIDAR_NCM_NFE',
dominio: 'fiscal',
trigger: 'pre_nfe_emissao',
condicao: { tipo_documento: ['nfe', 'nfce'] },
acao: { validar: { campo: 'items[*].ncm', obrigatorio: true, formato: /^\d{8}$/ } },
prioridade: 1, ativo: true
},
]
```
### 4.3 SOE Event Bus — Lançamentos Contábeis Automáticos
Este é o **maior salto de valor**. Hoje o Motor Contábil (:8003) existe mas nada o alimenta automaticamente.
```typescript
// server/soe/event-bus.ts
type SoeEvent =
| 'nfe_emitida'
| 'nfe_cancelada'
| 'venda_confirmada'
| 'compra_recebida'
| 'pagamento_realizado'
| 'recebimento_realizado'
| 'boleto_gerado'
interface SoeEventPayload {
empresaId: number
tenantId: number
evento: SoeEvent
dados: Record<string, any>
motorOrigem: 'plus' | 'erpnext' | 'local'
}
class SoeEventBus {
async emit(evento: SoeEvent, payload: SoeEventPayload) {
// Registra no banco (audit trail)
await db.insert(soeEventos).values({ ...payload, createdAt: new Date() })
// Dispara handlers
await Promise.allSettled(
this.getHandlers(evento).map(h => h(payload))
)
}
// Handlers por evento
private handlers: Record<SoeEvent, EventHandler[]> = {
'nfe_emitida': [
contabilHandler.lancamentoVenda, // D: Clientes / C: Receita Bruta
contabilHandler.lancamentoImpostos, // D: Impostos / C: ICMS/PIS/COFINS a Recolher
financialHandler.gerarRecebivel, // Cria C/R automaticamente
],
'pagamento_realizado': [
contabilHandler.lancamentoPagamento, // D: Fornecedores / C: Banco
financialHandler.baixarContaPagar, // Baixa C/P
],
'recebimento_realizado': [
contabilHandler.lancamentoRecebimento, // D: Banco / C: Clientes
financialHandler.baixarContaReceber, // Baixa C/R
],
'compra_recebida': [
contabilHandler.lancamentoCompra, // D: Estoque / C: Fornecedores
],
}
}
```
#### Lançamentos automáticos para cada evento
```
NF-e emitida (venda):
D: 1.1.3.01 — Clientes (valor total da nota)
C: 3.1.1.01 — Receita Bruta de Vendas
D: 3.1.2.01 — Deduções (ICMS)
C: 2.1.2.01 — ICMS a Recolher
D: 3.1.2.02 — Deduções (PIS)
C: 2.1.2.02 — PIS a Recolher
D: 3.1.2.03 — Deduções (COFINS)
C: 2.1.2.03 — COFINS a Recolher
Pagamento de fornecedor:
D: 2.1.1.{categoria} — Fornecedores
C: 1.1.1.01 — Banco (conta configurada)
Recebimento de cliente:
D: 1.1.1.01 — Banco
C: 1.1.3.01 — Clientes
Compra recebida (NF-e entrada):
D: 1.1.4.01 — Estoque / Mercadorias
C: 2.1.1.01 — Fornecedores
```
### 4.4 Ocultação do Plus e ERPNext
O Plus tem página própria em `/plus` (client/src/pages/Plus.tsx). O usuário pode acessá-la diretamente. Precisamos de 3 mudanças:
#### a) Redirecionar `/plus` para `/soe`
```typescript
// client/src/App.tsx — modificação simples
// Trocar: <Route path="/plus" component={Plus} />
// Por: <Route path="/plus" render={() => <Redirect to="/soe" />} />
```
**Mas manter o proxy reverso Plus ativo** — o Plus continua funcionando em background, só não fica visível como página separada.
#### b) Ocultar o seletor de motor do usuário final
```typescript
// client/src/contexts/SoeMotorContext.tsx
// O seletor fica SOMENTE na tab Config do SOE, e somente para admins
// A tab Config na SOE.tsx — adicionar role check:
{user?.role === 'admin' && <TabsTrigger value="config">Config</TabsTrigger>}
```
#### c) Na tab Config (admin), o label muda
```
Antes: "Selecione o motor: Plus | ERPNext"
Depois: "Motor de execução: Plus | ERPNext | Local"
(invisível para usuário final, só admin de tenant vê)
```
---
## 5. TABELAS A CRIAR (complemento ao schema existente)
As tabelas canônicas do SOE já existem. Precisamos adicionar apenas as tabelas do Rule Engine.
```typescript
// shared/schema.ts — adicionar ao final
// Regras do SOE — configuráveis por tenant
export const soeRegras = pgTable('soe_regras', {
id: uuid('id').primaryKey().defaultRandom(),
tenantId: integer('tenant_id'), // null = regra global (todas as empresas)
empresaId: integer('empresa_id'), // null = todas as empresas do tenant
dominio: varchar('dominio', { length: 50 }).notNull(),
trigger: varchar('trigger', { length: 100 }).notNull(),
nome: varchar('nome', { length: 200 }).notNull(),
condicao: jsonb('condicao').notNull(),
acao: jsonb('acao').notNull(),
prioridade: integer('prioridade').default(10),
ativo: boolean('ativo').default(true),
origemPadrao: boolean('origem_padrao').default(false), // true = regra default do sistema
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow(),
})
// Log de todos os eventos SOE (audit trail completo)
export const soeEventos = pgTable('soe_eventos', {
id: uuid('id').primaryKey().defaultRandom(),
tenantId: integer('tenant_id').notNull(),
empresaId: integer('empresa_id'),
evento: varchar('evento', { length: 100 }).notNull(),
motorOrigem: varchar('motor_origem', { length: 50 }), // 'plus', 'erpnext', 'local'
regraIds: jsonb('regra_ids'), // regras aplicadas
payloadEntrada: jsonb('payload_entrada'),
payloadSaida: jsonb('payload_saida'),
status: varchar('status', { length: 50 }).notNull(),
duracaoMs: integer('duracao_ms'),
erro: text('erro'),
createdAt: timestamp('created_at').defaultNow(),
})
// Lançamentos contábeis automáticos do SOE
export const soeLancamentos = pgTable('soe_lancamentos', {
id: uuid('id').primaryKey().defaultRandom(),
tenantId: integer('tenant_id').notNull(),
empresaId: integer('empresa_id').notNull(),
data: date('data').notNull(),
contaDebito: varchar('conta_debito', { length: 30 }).notNull(),
contaCredito: varchar('conta_credito', { length: 30 }).notNull(),
valor: numeric('valor', { precision: 15, scale: 2 }).notNull(),
historico: text('historico').notNull(),
origemEvento: varchar('origem_evento', { length: 100 }), // 'nfe_emitida', etc.
origemEventoId: uuid('origem_evento_id'), // FK para soe_eventos
periodo: varchar('periodo', { length: 7 }), // '2026-03'
enviado8003: boolean('enviado_8003').default(false), // sincronizado com Motor Contábil
createdAt: timestamp('created_at').defaultNow(),
})
```
---
## 6. ESTRUTURA DE PASTAS (Novo código a criar)
```
server/
└── soe/ ← NOVA PASTA
├── rule-engine/
│ ├── index.ts ← SoeRuleEngine class
│ ├── fiscal-rules.ts ← Regras fiscais padrão
│ ├── business-rules.ts ← Regras de negócio
│ └── accounting-rules.ts ← Regras de lançamento contábil
├── event-bus.ts ← SoeEventBus class
├── handlers/
│ ├── contabil-handler.ts ← Lançamentos automáticos
│ └── financial-handler.ts ← Automação financeira
└── fiscal/
└── cfop-resolver.ts ← Resolução de CFOP (lógica complexa)
```
**Modificações em arquivos existentes:**
```
server/erp/routes.ts ← Inserir ruleEngine.apply() antes do dispatch
client/src/App.tsx ← Redirecionar /plus para /soe
client/src/pages/SOE.tsx ← Ocultar Config tab para não-admins
```
---
## 7. ROADMAP — Fases de Implementação
### Fase 1 — Rule Engine Básico (1 semana)
```
[ ] Criar server/soe/rule-engine/index.ts (SoeRuleEngine)
[ ] Criar tabelas soe_regras e soe_eventos (drizzle push)
[ ] Inserir ruleEngine.apply() em 3 rotas críticas:
- POST /api/soe/sales-orders
- POST /api/soe/sales-orders/:id/generate-nfe
- POST /api/soe/purchase-orders
[ ] Seed das regras fiscais padrão (CFOP, CST, validações NCM)
[ ] Testar: criar pedido → verificar que payload está sendo enriquecido
```
### Fase 2 — Event Bus + Contabilidade Automática (1 semana)
```
[ ] Criar server/soe/event-bus.ts
[ ] Criar tabela soe_lancamentos (drizzle push)
[ ] Criar handlers: contabil-handler.ts + financial-handler.ts
[ ] Conectar: POST generate-nfe → emit('nfe_emitida') → lançamentos
[ ] Conectar: pagamento/recebimento → emit() → lançamentos
[ ] Criar endpoint: GET /api/soe/contabil/lancamentos?periodo=YYYY-MM
[ ] DRE: agora tem dados reais (via soe_lancamentos → Motor :8003)
```
### Fase 3 — Ocultar Plus/ERPNext (3 dias)
```
[ ] client/src/App.tsx: /plus → redirect para /soe
[ ] client/src/pages/SOE.tsx: Config tab só para admins
[ ] Manter proxy Plus funcionando (invisível)
[ ] Testar: fluxo completo sem nenhum acesso direto ao Plus UI
```
### Fase 4 — Regras Configuráveis pelo Usuário (1 semana)
```
[ ] UI na tab Config do SOE: "Regras de Negócio"
[ ] CRUD de regras via /api/soe/rules
[ ] Admin pode criar regras custom sem código
[ ] Exemplo: "Pedidos acima de R$10.000 requerem aprovação manual"
```
### Fase 5 — Financeiro Avançado (2 semanas)
```
[ ] Auto-boleto ao confirmar venda (Asaas API)
[ ] Alertas de vencimento (Manus → WhatsApp)
[ ] Conciliação bancária OFX semi-automática
[ ] Previsão financeira (Motor BI :8004 + histórico soe_lancamentos)
```
### Fase 6 — Contábil Avançado (2 semanas)
```
[ ] DRE automático mensal (100% alimentado pelo Event Bus)
[ ] Balanço Patrimonial automático
[ ] Fechamento contábil mensal
[ ] SPED ECD/ECF alimentado por soe_lancamentos
[ ] Exportação para escritório contábil (OFX, Excel, PDF)
```
---
## 8. COMPARAÇÃO: PLANO v1 vs PLANO v2
| Aspecto | Plano v1 (antes do Replit .md) | Plano v2 (com conhecimento real) |
|---|---|---|
| Adaptadores Plus/ERPNext | "Precisam ser criados" | Já existem — não tocar |
| SOE Routes | "Precisam ser criadas" | 60+ endpoints já existem |
| SOE.tsx | "Precisa ser construída" | 2.414 linhas — já funciona |
| Onde adicionar código | server/soe/ do zero | Middleware nas rotas existentes |
| Esforço estimado | Alto (refatoração total) | Médio (adição cirúrgica) |
| Risco de quebrar | Alto | Baixo (adição, não substituição) |
| Banco Plus | Ignorado | MySQL — separado, nunca tocar direto |
---
## 9. PRINCÍPIO ORIENTADOR (REVISADO)
> O SOE já é o roteador certo. Agora vamos torná-lo o **árbitro de regras**.
>
> O padrão motor/adaptador está perfeito. O que falta é a **inteligência entre a rota e o adaptador**.
>
> Plus e ERPNext continuam existindo e funcionando — só que o usuário final não sabe.
> Ele só vê a Central SOE. O restante é infraestrutura.
---
## 10. RESUMO EXECUTIVO — O que fazer agora
**1 ação que entrega valor imediato:**
```
Criar o SoeRuleEngine e plugar nas rotas de venda/NF-e.
Resultado: Ao emitir uma NF-e, o CFOP correto é calculado
automaticamente pelo SOE — sem o usuário saber que o Plus está
sendo chamado por baixo.
```
**1 ação que entrega valor a médio prazo:**
```
Criar o Event Bus com o handler de contabilidade.
Resultado: Cada NF-e emitida gera lançamentos contábeis
automaticamente. O DRE do mês é gerado sem entrada manual de dados.
```
**1 ação que fecha o conceito:**
```
Redirecionar /plus → /soe.
Resultado: O usuário não tem mais acesso ao Plus diretamente.
Ele só vê o Arcádia. O Plus é infraestrutura invisível.
```
---
*PLANO_SOE_CENTRAL_v2.md — Arcádia Suite v3.0*
*Gerado em: 2026-03-16 — Baseado na análise real do MAPA_SOE_ARCADIA.md (Replit)*

1261
RELATORIO_TECNICO_RETAIL.md Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

@ -9,6 +9,7 @@ import { SoeMotorProvider } from "@/contexts/SoeMotorContext";
import { ProtectedRoute } from "@/lib/protected-route"; import { ProtectedRoute } from "@/lib/protected-route";
import { CommandPalette } from "@/components/CommandPalette"; import { CommandPalette } from "@/components/CommandPalette";
import { KnowledgeCollectorInit } from "@/components/KnowledgeCollectorInit"; import { KnowledgeCollectorInit } from "@/components/KnowledgeCollectorInit";
import { OpenClawWidget } from "@/components/openclaw/OpenClawWidget";
import NotFound from "@/pages/not-found"; import NotFound from "@/pages/not-found";
import AuthPage from "@/pages/auth-page"; import AuthPage from "@/pages/auth-page";
@ -70,8 +71,6 @@ const XosPipeline = lazy(() => import("@/pages/XosPipeline"));
const XosSupervisor = lazy(() => import("@/pages/XosSupervisor")); const XosSupervisor = lazy(() => import("@/pages/XosSupervisor"));
const XosReports = lazy(() => import("@/pages/XosReports")); const XosReports = lazy(() => import("@/pages/XosReports"));
const XosProtocols = lazy(() => import("@/pages/XosProtocols")); const XosProtocols = lazy(() => import("@/pages/XosProtocols"));
const Skills = lazy(() => import("@/pages/Skills"));
const AutomationCenter = lazy(() => import("@/pages/AutomationCenter"));
function LoadingFallback() { function LoadingFallback() {
@ -148,8 +147,6 @@ function Router() {
<ProtectedRoute path="/page-builder" component={PageBuilder} /> <ProtectedRoute path="/page-builder" component={PageBuilder} />
<ProtectedRoute path="/migration" component={Migration} /> <ProtectedRoute path="/migration" component={Migration} />
<ProtectedRoute path="/dev-center" component={DevCenter} /> <ProtectedRoute path="/dev-center" component={DevCenter} />
<ProtectedRoute path="/skills" component={Skills} />
<ProtectedRoute path="/automations-center" component={AutomationCenter} />
<ProtectedRoute path="/page/:id" component={WorkspacePage} /> <ProtectedRoute path="/page/:id" component={WorkspacePage} />
<ProtectedRoute path="/app/:id" component={AppViewer} /> <ProtectedRoute path="/app/:id" component={AppViewer} />
<Route path="/auth" component={AuthPage} /> <Route path="/auth" component={AuthPage} />
@ -168,6 +165,7 @@ function App() {
<KnowledgeCollectorInit /> <KnowledgeCollectorInit />
<Toaster /> <Toaster />
<CommandPalette /> <CommandPalette />
<OpenClawWidget />
<Router /> <Router />
</TooltipProvider> </TooltipProvider>
</SoeMotorProvider> </SoeMotorProvider>

View File

@ -1,7 +1,7 @@
import { useLocation } from "wouter"; import { useLocation } from "wouter";
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { Bot, Settings, MessageCircle, Zap, LayoutDashboard, Compass, Users, Ticket, LogOut, User, Shield, Receipt, Package, Rocket, Beaker, TrendingUp, MapPin, Droplets, Star, Database, Layout, Code, Code2, Store, Layers, Building2 } from "lucide-react"; import { Bot, Settings, MessageCircle, Zap, LayoutDashboard, Compass, Users, Ticket, LogOut, User, Shield, Receipt, Package, Rocket, Beaker, TrendingUp, MapPin, Droplets, Star, Database, Layout, Code, Code2, Store, Layers } from "lucide-react";
const browserIcon = "/arcadia_suite_icon.png"; import browserIcon from "@assets/arcadia_branding/arcadia_suite_icon.png";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
import { useNavigationTracking } from "@/hooks/use-navigation-tracking"; import { useNavigationTracking } from "@/hooks/use-navigation-tracking";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@ -31,7 +31,7 @@ function CompactNavigationBar() {
return ( return (
<div className="h-10 bg-background border-b border-border flex items-center px-3 gap-2 text-xs text-muted-foreground shadow-xs z-10"> <div className="h-10 bg-background border-b border-border flex items-center px-3 gap-2 text-xs text-muted-foreground shadow-xs z-10">
<div className="flex items-center gap-1 overflow-x-auto scrollbar-hide flex-1"> <div className="flex items-center gap-1 overflow-x-auto scrollbar-hide flex-1">
{(user?.role === "admin" || user?.role === "master") && ( {user?.role === "admin" && (
<div <div
className="flex items-center gap-1 hover:bg-muted px-2 py-1.5 rounded cursor-pointer transition-colors flex-shrink-0" className="flex items-center gap-1 hover:bg-muted px-2 py-1.5 rounded cursor-pointer transition-colors flex-shrink-0"
onClick={() => navigateTo("/admin", "Administração")} onClick={() => navigateTo("/admin", "Administração")}
@ -123,13 +123,13 @@ function CompactNavigationBar() {
</div> </div>
<div <div
className="flex items-center gap-1 hover:bg-muted px-2 py-1.5 rounded cursor-pointer transition-colors flex-shrink-0" className="flex items-center gap-1 hover:bg-muted px-2 py-1.5 rounded cursor-pointer transition-colors flex-shrink-0"
onClick={() => navigateTo("/soe", "SOE")} onClick={() => navigateTo("/erp", "ERP")}
data-testid="bookmark-soe" data-testid="bookmark-erp"
> >
<div className="w-4 h-4 bg-gradient-to-br from-blue-600 to-blue-800 rounded-sm flex items-center justify-center"> <div className="w-4 h-4 bg-gradient-to-br from-blue-600 to-blue-800 rounded-sm flex items-center justify-center">
<Building2 className="w-2.5 h-2.5 text-white" /> <Package className="w-2.5 h-2.5 text-white" />
</div> </div>
<span className="hidden md:inline">SOE</span> <span className="hidden md:inline">ERP</span>
</div> </div>
<div <div
className="flex items-center gap-1 hover:bg-muted px-2 py-1.5 rounded cursor-pointer transition-colors flex-shrink-0" className="flex items-center gap-1 hover:bg-muted px-2 py-1.5 rounded cursor-pointer transition-colors flex-shrink-0"
@ -141,7 +141,16 @@ function CompactNavigationBar() {
</div> </div>
<span className="hidden md:inline">Retail</span> <span className="hidden md:inline">Retail</span>
</div> </div>
{/* Plus hidden temporarily */} <div
className="flex items-center gap-1 hover:bg-muted px-2 py-1.5 rounded cursor-pointer transition-colors flex-shrink-0"
onClick={() => navigateTo("/plus", "Plus")}
data-testid="bookmark-plus"
>
<div className="w-4 h-4 bg-gradient-to-br from-purple-500 to-purple-700 rounded-sm flex items-center justify-center">
<Layers className="w-2.5 h-2.5 text-white" />
</div>
<span className="hidden md:inline">Plus</span>
</div>
<div <div
className="flex items-center gap-1 hover:bg-muted px-2 py-1.5 rounded cursor-pointer transition-colors flex-shrink-0" className="flex items-center gap-1 hover:bg-muted px-2 py-1.5 rounded cursor-pointer transition-colors flex-shrink-0"
onClick={() => navigateTo("/fisco", "Fisco")} onClick={() => navigateTo("/fisco", "Fisco")}
@ -179,10 +188,10 @@ function CompactNavigationBar() {
<div className="flex flex-col space-y-1"> <div className="flex flex-col space-y-1">
<p className="text-sm font-medium leading-none">{user?.name || user?.username}</p> <p className="text-sm font-medium leading-none">{user?.name || user?.username}</p>
<p className="text-xs leading-none text-muted-foreground">@{user?.username}</p> <p className="text-xs leading-none text-muted-foreground">@{user?.username}</p>
{(user?.role === "admin" || user?.role === "master") && ( {user?.role === "admin" && (
<p className="text-xs leading-none text-primary flex items-center gap-1 mt-1"> <p className="text-xs leading-none text-primary flex items-center gap-1 mt-1">
<Shield className="w-3 h-3" /> <Shield className="w-3 h-3" />
{user?.role === "master" ? "Administrador Master" : "Administrador"} Administrador
</p> </p>
)} )}
</div> </div>
@ -192,7 +201,7 @@ function CompactNavigationBar() {
<User className="mr-2 h-4 w-4" /> <User className="mr-2 h-4 w-4" />
<span>Meu Perfil</span> <span>Meu Perfil</span>
</DropdownMenuItem> </DropdownMenuItem>
{(user?.role === "admin" || user?.role === "master") && ( {user?.role === "admin" && (
<> <>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuLabel className="text-xs text-muted-foreground">Plataforma</DropdownMenuLabel> <DropdownMenuLabel className="text-xs text-muted-foreground">Plataforma</DropdownMenuLabel>

View File

@ -80,10 +80,10 @@ export function Omnibox({ url, onNavigate, isLoading }: OmniboxProps) {
<div className="flex flex-col space-y-1"> <div className="flex flex-col space-y-1">
<p className="text-sm font-medium leading-none">{user?.name || user?.username}</p> <p className="text-sm font-medium leading-none">{user?.name || user?.username}</p>
<p className="text-xs leading-none text-muted-foreground">@{user?.username}</p> <p className="text-xs leading-none text-muted-foreground">@{user?.username}</p>
{(user?.role === "admin" || user?.role === "master") && ( {user?.role === "admin" && (
<p className="text-xs leading-none text-primary flex items-center gap-1 mt-1"> <p className="text-xs leading-none text-primary flex items-center gap-1 mt-1">
<Shield className="w-3 h-3" /> <Shield className="w-3 h-3" />
{user?.role === "master" ? "Administrador Master" : "Administrador"} Administrador
</p> </p>
)} )}
</div> </div>

View File

@ -121,10 +121,10 @@ export function CommandPalette() {
}, },
{ {
id: "nav-crm", id: "nav-crm",
name: "Abrir Manager Partners", name: "Abrir Arcádia CRM",
icon: <Handshake className="h-4 w-4" />, icon: <Handshake className="h-4 w-4" />,
action: () => navigateTo("/crm"), action: () => navigateTo("/crm"),
keywords: ["crm", "parceiros", "contratos", "manager", "partners"], keywords: ["crm", "parceiros", "contratos", "vendas", "whatsapp"],
group: "Navegação", group: "Navegação",
}, },
{ {

View File

@ -27,8 +27,7 @@ export function SupersetDashboard({
const [status, setStatus] = useState<"loading" | "ready" | "error" | "unavailable">("loading"); const [status, setStatus] = useState<"loading" | "ready" | "error" | "unavailable">("loading");
const [errorMsg, setErrorMsg] = useState<string>(""); const [errorMsg, setErrorMsg] = useState<string>("");
// Busca token e embeddedId (UUID) do backend const fetchGuestToken = useCallback(async (): Promise<string> => {
const fetchTokenData = useCallback(async (): Promise<{ token: string; embeddedId: string }> => {
const resp = await fetch("/api/superset/guest-token", { const resp = await fetch("/api/superset/guest-token", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
@ -39,7 +38,8 @@ export function SupersetDashboard({
const err = await resp.json().catch(() => ({ error: "Erro desconhecido" })); const err = await resp.json().catch(() => ({ error: "Erro desconhecido" }));
throw new Error(err.error || `HTTP ${resp.status}`); throw new Error(err.error || `HTTP ${resp.status}`);
} }
return resp.json(); const { token } = await resp.json();
return token;
}, [dashboardId]); }, [dashboardId]);
const loadDashboard = useCallback(async () => { const loadDashboard = useCallback(async () => {
@ -56,9 +56,6 @@ export function SupersetDashboard({
return; return;
} }
// Resolve embeddedId (UUID) e obtém token inicial
const { embeddedId } = await fetchTokenData();
// Importa SDK dinamicamente (só quando necessário) // Importa SDK dinamicamente (só quando necessário)
const { embedDashboard } = await import("@superset-ui/embedded-sdk"); const { embedDashboard } = await import("@superset-ui/embedded-sdk");
@ -68,13 +65,10 @@ export function SupersetDashboard({
} }
sdkRef.current = await embedDashboard({ sdkRef.current = await embedDashboard({
id: embeddedId, // UUID do embedded config (não o slug) id: dashboardId,
supersetDomain: "https://bi.onboardbi.com.br", supersetDomain: window.location.origin + "/superset",
mountPoint: containerRef.current, mountPoint: containerRef.current,
fetchGuestToken: async () => { fetchGuestToken,
const { token } = await fetchTokenData();
return token;
},
dashboardUiConfig: { dashboardUiConfig: {
hideTitle: true, hideTitle: true,
hideChartControls: false, hideChartControls: false,
@ -82,21 +76,18 @@ export function SupersetDashboard({
}, },
}); });
// O SDK cria um <iframe> sem width/height explícitos — forçar 100%
const iframe = containerRef.current?.querySelector("iframe");
if (iframe) {
iframe.style.width = "100%";
iframe.style.height = "100%";
iframe.style.border = "none";
}
setStatus("ready"); setStatus("ready");
} catch (err: any) { } catch (err: any) {
console.error("[SupersetDashboard] Erro:", err.message, err); console.error("[SupersetDashboard] Erro:", err.message);
setErrorMsg(err.message || "Erro desconhecido"); // Se SDK não estiver instalado, usa iframe como fallback
setStatus("error"); if (err.message?.includes("Cannot find module") || err.message?.includes("embedDashboard")) {
setStatus("unavailable");
} else {
setErrorMsg(err.message);
setStatus("error");
}
} }
}, [dashboardId, fetchTokenData]); }, [dashboardId, fetchGuestToken]);
useEffect(() => { useEffect(() => {
loadDashboard(); loadDashboard();
@ -109,16 +100,13 @@ export function SupersetDashboard({
if (status === "unavailable") { if (status === "unavailable") {
return ( return (
<div className={`flex flex-col items-center justify-center gap-4 rounded-xl border border-yellow-100 bg-yellow-50 ${className}`} style={{ height: heightStyle }}> <div className={`rounded-xl overflow-hidden border border-[#c89b3c]/20 bg-white shadow-sm ${className}`} style={{ height: heightStyle }}>
<AlertTriangle className="w-8 h-8 text-yellow-500" /> <iframe
<div className="text-center"> src="/superset/login"
<p className="font-medium text-yellow-800 text-sm">Arcádia Insights indisponível</p> className="w-full h-full border-0"
<p className="text-yellow-600 text-xs mt-1">O serviço de BI está iniciando. Tente novamente em alguns segundos.</p> title="Arcádia Insights — Apache Superset"
</div> allow="fullscreen"
<button onClick={loadDashboard} className="flex items-center gap-2 px-4 py-2 bg-yellow-600 text-white rounded-lg text-sm hover:bg-yellow-700 transition-colors"> />
<RefreshCw className="w-4 h-4" /> Tentar novamente
</button>
<a href="https://bi.onboardbi.com.br" target="_blank" rel="noopener noreferrer" className="text-xs text-yellow-700 underline">Abrir em nova aba</a>
</div> </div>
); );
} }
@ -154,7 +142,7 @@ export function SupersetDashboard({
{showOpenLink && status === "ready" && ( {showOpenLink && status === "ready" && (
<a <a
href="https://bi.onboardbi.com.br" href="/superset"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="absolute top-3 right-3 z-10 flex items-center gap-1 px-2 py-1 bg-white/80 backdrop-blur text-[#1f334d] text-xs rounded-lg border border-gray-200 hover:bg-white transition-colors shadow-sm" className="absolute top-3 right-3 z-10 flex items-center gap-1 px-2 py-1 bg-white/80 backdrop-blur text-[#1f334d] text-xs rounded-lg border border-gray-200 hover:bg-white transition-colors shadow-sm"
@ -163,16 +151,9 @@ export function SupersetDashboard({
</a> </a>
)} )}
<style>{`
.superset-embed-container iframe {
width: 100% !important;
height: 100% !important;
border: none !important;
}
`}</style>
<div <div
ref={containerRef} ref={containerRef}
className="superset-embed-container w-full h-full rounded-xl overflow-hidden border border-[#c89b3c]/20" className="w-full h-full rounded-xl overflow-hidden border border-[#c89b3c]/20"
/> />
</div> </div>
); );

View File

@ -110,7 +110,7 @@ export default function TradeInForm({ onClose, onSave, initialEvaluation, custom
useEffect(() => { useEffect(() => {
const loadPersons = async () => { const loadPersons = async () => {
try { try {
const res = await fetch("/api/soe/persons", { credentials: "include" }); const res = await fetch("/api/erp/persons", { credentials: "include" });
if (res.ok) { if (res.ok) {
const data = await res.json(); const data = await res.json();
setPersonsList(data); setPersonsList(data);

View File

@ -0,0 +1,131 @@
/**
* OpenClawWidget.tsx
*
* Widget flutuante do OpenClaw aparece quando sugestões pendentes.
* Posicionado no canto inferior direito, fora das rotas.
* Abre o modal SkillSuggestion para confirmação.
*
* Fase 4: OpenClaw Sprint 2
*/
import { useState } from "react";
import { Sparkles, X, ChevronUp, ChevronDown } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { useAgentEmergence } from "@/hooks/useAgentEmergence";
import { SkillSuggestion } from "./SkillSuggestion";
import { useAuth } from "@/hooks/use-auth";
export function OpenClawWidget() {
const { user } = useAuth();
const {
suggestions,
activeSuggestion,
pendingCount,
confirmSkill,
rejectSkill,
openSuggestion,
closeSuggestion,
} = useAgentEmergence();
const [collapsed, setCollapsed] = useState(false);
const [dismissed, setDismissed] = useState(false);
// Não renderiza se não há usuário, não há sugestões, ou foi dispensado
if (!user || pendingCount === 0 || dismissed) return null;
return (
<>
{/* Widget flutuante */}
<div
className="fixed bottom-6 right-6 z-50 flex flex-col items-end gap-2"
role="complementary"
aria-label="OpenClaw - Sugestões de Skills"
>
{/* Painel expandido */}
{!collapsed && (
<div className="w-72 rounded-xl border bg-background shadow-lg overflow-hidden">
<div className="flex items-center justify-between px-3 py-2.5 border-b bg-violet-50 dark:bg-violet-900/20">
<div className="flex items-center gap-2">
<Sparkles className="h-4 w-4 text-violet-600 dark:text-violet-400" />
<span className="text-sm font-semibold text-violet-700 dark:text-violet-300">
OpenClaw
</span>
<Badge className="bg-violet-600 text-white text-xs px-1.5 py-0">
{pendingCount}
</Badge>
</div>
<div className="flex items-center gap-1">
<button
onClick={() => setCollapsed(true)}
className="p-0.5 rounded hover:bg-violet-100 dark:hover:bg-violet-900/40 text-muted-foreground"
aria-label="Minimizar"
>
<ChevronDown className="h-3.5 w-3.5" />
</button>
<button
onClick={() => setDismissed(true)}
className="p-0.5 rounded hover:bg-violet-100 dark:hover:bg-violet-900/40 text-muted-foreground"
aria-label="Fechar"
>
<X className="h-3.5 w-3.5" />
</button>
</div>
</div>
<div className="p-3 space-y-2 max-h-64 overflow-y-auto">
<p className="text-xs text-muted-foreground">
Detectei padrões de uso que podem virar skills automáticas:
</p>
{suggestions.map((s) => (
<button
key={s.id}
onClick={() => openSuggestion(s)}
className="w-full text-left rounded-lg border p-2.5 hover:bg-muted/50 transition-colors space-y-1"
>
<div className="flex items-start justify-between gap-2">
<span className="text-sm font-medium leading-tight line-clamp-1">
{s.suggested_skill_name}
</span>
<Badge variant="outline" className="text-xs shrink-0">
{Math.round(s.confidence * 100)}%
</Badge>
</div>
<p className="text-xs text-muted-foreground line-clamp-2">
{s.suggested_description}
</p>
</button>
))}
</div>
</div>
)}
{/* Botão minimizado */}
{collapsed && (
<button
onClick={() => setCollapsed(false)}
className="flex items-center gap-2 rounded-full bg-violet-600 hover:bg-violet-700 text-white px-4 py-2.5 shadow-lg transition-colors"
aria-label="Expandir sugestões de skills"
>
<Sparkles className="h-4 w-4" />
<span className="text-sm font-medium">Skills sugeridas</span>
<Badge className="bg-white text-violet-700 text-xs px-1.5 py-0 ml-0.5">
{pendingCount}
</Badge>
<ChevronUp className="h-3.5 w-3.5 ml-0.5" />
</button>
)}
</div>
{/* Modal de detalhes */}
{activeSuggestion && (
<SkillSuggestion
suggestion={activeSuggestion}
onConfirm={confirmSkill}
onReject={rejectSkill}
onClose={closeSuggestion}
/>
)}
</>
);
}

View File

@ -0,0 +1,101 @@
/**
* SkillSuggestion.tsx
*
* Modal com preview da skill sugerida pelo OpenClaw.
* Apresenta padrão detectado, nome, descrição e preview de automação.
* Usuário pode confirmar ou rejeitar.
*
* Fase 4: OpenClaw Sprint 2
*/
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Sparkles, X, Check, Zap, BarChart2 } from "lucide-react";
import type { SkillSuggestionData } from "@/hooks/useAgentEmergence";
interface SkillSuggestionProps {
suggestion: SkillSuggestionData;
onConfirm: (id: string) => void;
onReject: (id: string) => void;
onClose: () => void;
}
export function SkillSuggestion({ suggestion, onConfirm, onReject, onClose }: SkillSuggestionProps) {
const confidencePct = Math.round(suggestion.confidence * 100);
return (
<Dialog open onOpenChange={onClose}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<div className="flex items-center gap-2">
<div className="p-1.5 rounded-md bg-violet-100 dark:bg-violet-900/30">
<Sparkles className="h-4 w-4 text-violet-600 dark:text-violet-400" />
</div>
<DialogTitle className="text-base">Nova skill detectada</DialogTitle>
</div>
</DialogHeader>
{/* Padrão detectado */}
<div className="rounded-lg border bg-muted/40 p-3 text-sm space-y-1">
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wide">Padrão detectado</p>
<p className="font-medium">{suggestion.pattern.description || suggestion.pattern.action_type}</p>
<div className="flex items-center gap-3 text-muted-foreground text-xs mt-1">
<span className="flex items-center gap-1">
<BarChart2 className="h-3 w-3" />
{suggestion.pattern.frequency}x detectado
</span>
<span className="flex items-center gap-1">
<Zap className="h-3 w-3" />
Confiança: {confidencePct}%
</span>
</div>
</div>
{/* Skill sugerida */}
<div className="space-y-3">
<div>
<p className="text-xs font-medium text-muted-foreground mb-1">Nome da skill</p>
<div className="flex items-center gap-2">
<Badge variant="secondary" className="font-mono text-xs">
{suggestion.suggested_skill_name}
</Badge>
</div>
</div>
<div>
<p className="text-xs font-medium text-muted-foreground mb-1">Descrição</p>
<p className="text-sm">{suggestion.suggested_description}</p>
</div>
<div>
<p className="text-xs font-medium text-muted-foreground mb-1">O que será automatizado</p>
<div className="rounded-md border bg-muted/20 p-2.5 text-sm text-muted-foreground">
{suggestion.estimated_automation}
</div>
</div>
</div>
<DialogFooter className="gap-2 sm:gap-0">
<Button
variant="outline"
size="sm"
onClick={() => onReject(suggestion.id)}
className="gap-1.5"
>
<X className="h-3.5 w-3.5" />
Rejeitar
</Button>
<Button
size="sm"
onClick={() => onConfirm(suggestion.id)}
className="gap-1.5 bg-violet-600 hover:bg-violet-700"
>
<Check className="h-3.5 w-3.5" />
Criar skill
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@ -1,63 +1,25 @@
import { createContext, useContext, useState, useEffect, ReactNode } from "react"; import { createContext, useContext, useState, ReactNode } from "react";
export type SoeMotor = "plus" | "erpnext"; interface SoeMotorContextValue {
activeCompany: string | null;
interface SoeMotorContextType { setActiveCompany: (id: string | null) => void;
motor: SoeMotor;
setMotor: (motor: SoeMotor) => void;
usePlus: boolean;
useERPNext: boolean;
getApiUrl: (localPath: string, plusPath: string, erpnextPath?: string) => string;
profile: SoeMotor;
setProfile: (motor: SoeMotor) => void;
} }
const SoeMotorCtx = createContext<SoeMotorContextType | null>(null); const SoeMotorContext = createContext<SoeMotorContextValue>({
activeCompany: null,
setActiveCompany: () => {},
});
export function SoeMotorProvider({ children }: { children: ReactNode }) { export function SoeMotorProvider({ children }: { children: ReactNode }) {
const [motor, setMotorState] = useState<SoeMotor>(() => { const [activeCompany, setActiveCompany] = useState<string | null>(null);
const saved = localStorage.getItem("arcadia_soe_motor") || localStorage.getItem("arcadia_erp_profile");
return (saved as SoeMotor) || "plus";
});
const setMotor = (newMotor: SoeMotor) => {
setMotorState(newMotor);
localStorage.setItem("arcadia_soe_motor", newMotor);
localStorage.setItem("arcadia_erp_profile", newMotor);
};
const usePlus = motor === "plus";
const useERPNext = motor === "erpnext";
const getApiUrl = (localPath: string, plusPath: string, erpnextPath?: string): string => {
if (usePlus) {
return `/plus/api${plusPath}`;
}
if (useERPNext && erpnextPath) {
return erpnextPath;
}
return localPath;
};
return ( return (
<SoeMotorCtx.Provider value={{ motor, setMotor, usePlus, useERPNext, getApiUrl, profile: motor, setProfile: setMotor }}> <SoeMotorContext.Provider value={{ activeCompany, setActiveCompany }}>
{children} {children}
</SoeMotorCtx.Provider> </SoeMotorContext.Provider>
); );
} }
export function useSoeMotor() { export function useSoeMotor() {
const context = useContext(SoeMotorCtx); return useContext(SoeMotorContext);
if (!context) {
throw new Error("useSoeMotor must be used within SoeMotorProvider");
}
return context;
} }
export function useErpProfile() {
const ctx = useSoeMotor();
return ctx;
}
export type ErpProfile = SoeMotor;
export const ErpProfileProvider = SoeMotorProvider;

View File

@ -0,0 +1,143 @@
/**
* useAgentEmergence.ts
*
* Hook para gerenciar sugestões de skills do OpenClaw.
* Escuta eventos Socket.IO e coordena confirmações/rejeições.
*
* Fase 4: OpenClaw Sprint 2
*/
import { useState, useEffect, useCallback } from "react";
import { useAuth } from "./use-auth";
export interface SkillSuggestionData {
id: string;
pattern: {
id: string;
action_type: string;
frequency: number;
confidence: number;
description: string;
};
suggested_skill_name: string;
suggested_description: string;
estimated_automation: string;
confidence: number;
created_at: string;
status: "pending" | "accepted" | "rejected";
}
interface UseAgentEmergenceReturn {
suggestions: SkillSuggestionData[];
activeSuggestion: SkillSuggestionData | null;
isOpen: boolean;
pendingCount: number;
confirmSkill: (suggestionId: string) => Promise<void>;
rejectSkill: (suggestionId: string) => Promise<void>;
openSuggestion: (suggestion: SkillSuggestionData) => void;
closeSuggestion: () => void;
dismissWidget: () => void;
}
export function useAgentEmergence(): UseAgentEmergenceReturn {
const { user } = useAuth();
const [suggestions, setSuggestions] = useState<SkillSuggestionData[]>([]);
const [activeSuggestion, setActiveSuggestion] = useState<SkillSuggestionData | null>(null);
const [isOpen, setIsOpen] = useState(false);
// Carrega sugestões pendentes do servidor
const fetchPendingSuggestions = useCallback(async () => {
if (!user) return;
try {
const res = await fetch("/api/openclaw/suggestions");
if (res.ok) {
const data = await res.json();
setSuggestions(data.suggestions || []);
}
} catch {
// silencioso — não bloqueia o usuário
}
}, [user]);
// Escuta eventos Socket.IO via window (padrão da suite)
useEffect(() => {
if (!user) return;
fetchPendingSuggestions();
const handleOpenClawEvent = (event: CustomEvent) => {
const suggestion: SkillSuggestionData = event.detail;
setSuggestions((prev) => {
const exists = prev.find((s) => s.id === suggestion.id);
if (exists) return prev;
return [suggestion, ...prev];
});
};
window.addEventListener("openclaw:suggestion" as any, handleOpenClawEvent);
return () => {
window.removeEventListener("openclaw:suggestion" as any, handleOpenClawEvent);
};
}, [user, fetchPendingSuggestions]);
const confirmSkill = useCallback(async (suggestionId: string) => {
try {
const res = await fetch("/api/openclaw/confirm-skill", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ suggestion_id: suggestionId, action: "confirm" }),
});
if (res.ok) {
setSuggestions((prev) => prev.filter((s) => s.id !== suggestionId));
setActiveSuggestion(null);
setIsOpen(false);
}
} catch {
// erro tratado na UI via toast externo
}
}, []);
const rejectSkill = useCallback(async (suggestionId: string) => {
try {
const res = await fetch("/api/openclaw/confirm-skill", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ suggestion_id: suggestionId, action: "reject" }),
});
if (res.ok) {
setSuggestions((prev) => prev.filter((s) => s.id !== suggestionId));
setActiveSuggestion(null);
setIsOpen(false);
}
} catch {
// silencioso
}
}, []);
const openSuggestion = useCallback((suggestion: SkillSuggestionData) => {
setActiveSuggestion(suggestion);
setIsOpen(true);
}, []);
const closeSuggestion = useCallback(() => {
setActiveSuggestion(null);
setIsOpen(false);
}, []);
const dismissWidget = useCallback(() => {
setIsOpen(false);
setActiveSuggestion(null);
}, []);
return {
suggestions,
activeSuggestion,
isOpen,
pendingCount: suggestions.length,
confirmSkill,
rejectSkill,
openSuggestion,
closeSuggestion,
dismissWidget,
};
}

View File

@ -7,7 +7,7 @@ export function ProtectedRoute({
component: Component, component: Component,
}: { }: {
path: string; path: string;
component: () => React.JSX.Element; component: React.ComponentType<any>;
}) { }) {
const { user, isLoading } = useAuth(); const { user, isLoading } = useAuth();

File diff suppressed because it is too large Load Diff

View File

@ -361,18 +361,6 @@ async function fetchManusRun(id: number): Promise<ManusRun> {
return response.json(); return response.json();
} }
async function cancelManusRun(id: number): Promise<void> {
await fetch(`/api/manus/runs/${id}`, { method: "DELETE", credentials: "include" });
}
async function deleteManusRun(id: number): Promise<void> {
await fetch(`/api/manus/runs/${id}`, { method: "DELETE", credentials: "include" });
}
async function deleteAllManusRuns(): Promise<void> {
await fetch("/api/manus/runs", { method: "DELETE", credentials: "include" });
}
async function startManusRun(data: { prompt: string; attachedFiles?: AttachedFile[]; conversationHistory?: Array<{role: string; content: string}> }): Promise<{ runId: number }> { async function startManusRun(data: { prompt: string; attachedFiles?: AttachedFile[]; conversationHistory?: Array<{role: string; content: string}> }): Promise<{ runId: number }> {
const response = await fetch("/api/manus/run", { const response = await fetch("/api/manus/run", {
method: "POST", method: "POST",
@ -1254,13 +1242,7 @@ export default function Agent() {
if (line.startsWith("data: ")) { if (line.startsWith("data: ")) {
try { try {
const data = JSON.parse(line.slice(6)); const data = JSON.parse(line.slice(6));
if (data.tool_status) {
setCurrentToolName(data.tool_status);
setProcessingMode("using_tools");
}
if (data.content) { if (data.content) {
setCurrentToolName(null);
setProcessingMode("idle");
fullContent += data.content; fullContent += data.content;
setStreamingContent(fullContent); setStreamingContent(fullContent);
} }
@ -1499,21 +1481,8 @@ export default function Agent() {
Nova Tarefa Nova Tarefa
</Button> </Button>
</div> </div>
<div className="px-3 pt-3 pb-1 flex items-center justify-between"> <div className="px-3 pt-3 pb-1">
<p className="text-xs text-white/40 uppercase tracking-wider font-medium">Todas as Tarefas</p> <p className="text-xs text-white/40 uppercase tracking-wider font-medium">Todas as Tarefas</p>
{manusRuns.length > 0 && (
<button
onClick={async () => {
await deleteAllManusRuns();
setSelectedRun(null);
queryClient.invalidateQueries({ queryKey: ["manus-runs"] });
}}
className="text-[10px] text-white/30 hover:text-red-400 transition-colors"
title="Limpar histórico"
>
Limpar tudo
</button>
)}
</div> </div>
<ScrollArea className="flex-1 px-2"> <ScrollArea className="flex-1 px-2">
{loadingRuns ? ( {loadingRuns ? (
@ -1525,35 +1494,22 @@ export default function Agent() {
) : ( ) : (
<div className="space-y-1 pb-2"> <div className="space-y-1 pb-2">
{manusRuns.map((run) => ( {manusRuns.map((run) => (
<div key={run.id} className="group relative"> <button
<button key={run.id}
onClick={() => setSelectedRun(run.id)} onClick={() => setSelectedRun(run.id)}
className={`w-full text-left p-2.5 rounded-lg transition-all pr-8 ${ className={`w-full text-left p-2.5 rounded-lg transition-all ${
selectedRun === run.id selectedRun === run.id
? "bg-[#c89b3c]/20 border border-[#c89b3c]/50" ? "bg-[#c89b3c]/20 border border-[#c89b3c]/50"
: "hover:bg-[#162638] border border-transparent" : "hover:bg-[#162638] border border-transparent"
}`} }`}
data-testid={`run-${run.id}`} data-testid={`run-${run.id}`}
> >
<p className="text-xs text-white line-clamp-2 mb-1.5">{run.prompt}</p> <p className="text-xs text-white line-clamp-2 mb-1.5">{run.prompt}</p>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{getStatusBadge(run.status)} {getStatusBadge(run.status)}
<span className="text-[10px] text-white/40">{formatTime(run.createdAt)}</span> <span className="text-[10px] text-white/40">{formatTime(run.createdAt)}</span>
</div> </div>
</button> </button>
<button
onClick={async (e) => {
e.stopPropagation();
await deleteManusRun(run.id);
if (selectedRun === run.id) setSelectedRun(null);
queryClient.invalidateQueries({ queryKey: ["manus-runs"] });
}}
className="absolute right-2 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 text-white/30 hover:text-red-400 transition-all"
title="Excluir"
>
<X className="w-3.5 h-3.5" />
</button>
</div>
))} ))}
</div> </div>
)} )}
@ -1973,21 +1929,6 @@ export default function Agent() {
className="flex-1 border-[#e1e8f0] focus:border-[#c89b3c] focus:ring-[#c89b3c]/20" className="flex-1 border-[#e1e8f0] focus:border-[#c89b3c] focus:ring-[#c89b3c]/20"
data-testid="input-message" data-testid="input-message"
/> />
{isStreaming && (
<Button
onClick={async () => {
if (selectedRun) await cancelManusRun(selectedRun);
setIsStreaming(false);
setProcessingMode("idle");
}}
variant="destructive"
className="shrink-0"
data-testid="button-cancel-run"
title="Cancelar"
>
<X className="w-4 h-4" />
</Button>
)}
<Button <Button
onClick={handleSendMessage} onClick={handleSendMessage}
disabled={!chatInput.trim() || isStreaming} disabled={!chatInput.trim() || isStreaming}

View File

@ -1,11 +1,13 @@
import { BrowserFrame } from "@/components/Browser/BrowserFrame"; import { BrowserFrame } from "@/components/Browser/BrowserFrame";
import { useState } from "react"; import { useState } from "react";
import { useLocation } from "wouter"; import { useLocation } from "wouter";
import { useQuery } from "@tanstack/react-query";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { import {
Lock,
Search, Search,
ShoppingCart, ShoppingCart,
GraduationCap, GraduationCap,
@ -53,6 +55,7 @@ interface AppItem {
status: "active" | "coming_soon" | "beta"; status: "active" | "coming_soon" | "beta";
featured?: boolean; featured?: boolean;
color: string; color: string;
subscriptionCode?: string; // marketplace module code — if set, requires active subscription
} }
const apps: AppItem[] = [ const apps: AppItem[] = [
@ -80,11 +83,11 @@ const apps: AppItem[] = [
color: "from-orange-500 to-red-500", color: "from-orange-500 to-red-500",
}, },
{ {
id: "soe", id: "erp",
name: "Arcádia SOE", name: "Arcádia ERP",
description: "Sistema Operacional Empresarial - Kernel de negócios", description: "Gestão empresarial completa integrada ao ERPNext",
icon: <Building className="w-8 h-8" />, icon: <Building className="w-8 h-8" />,
route: "/soe", route: "/erp",
category: "negocios", category: "negocios",
status: "active", status: "active",
featured: true, featured: true,
@ -500,6 +503,24 @@ export default function AppCenter() {
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
const [activeCategory, setActiveCategory] = useState("todos"); const [activeCategory, setActiveCategory] = useState("todos");
const { data: myAppsData } = useQuery<{ subscribedCodes: string[] }>({
queryKey: ["/api/marketplace/my-apps"],
staleTime: 60_000,
});
const subscribedCodes = new Set(myAppsData?.subscribedCodes || []);
const isLocked = (app: AppItem) =>
!!app.subscriptionCode && subscribedCodes.size > 0 && !subscribedCodes.has(app.subscriptionCode);
const handleAppClick = (app: AppItem) => {
if (isLocked(app)) {
setLocation("/marketplace");
} else {
setLocation(app.route);
}
};
const filteredApps = apps.filter(app => { const filteredApps = apps.filter(app => {
const matchesSearch = app.name.toLowerCase().includes(searchTerm.toLowerCase()) || const matchesSearch = app.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
app.description.toLowerCase().includes(searchTerm.toLowerCase()); app.description.toLowerCase().includes(searchTerm.toLowerCase());
@ -546,7 +567,7 @@ export default function AppCenter() {
<Card <Card
key={app.id} key={app.id}
className="bg-white/5 border-white/10 hover:border-white/30 cursor-pointer transition-all hover:scale-105 group" className="bg-white/5 border-white/10 hover:border-white/30 cursor-pointer transition-all hover:scale-105 group"
onClick={() => setLocation(app.route)} onClick={() => handleAppClick(app)}
data-testid={`featured-app-${app.id}`} data-testid={`featured-app-${app.id}`}
> >
<CardContent className="p-4 text-center"> <CardContent className="p-4 text-center">
@ -579,27 +600,37 @@ export default function AppCenter() {
<TabsContent value={activeCategory} className="mt-0"> <TabsContent value={activeCategory} className="mt-0">
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"> <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{filteredApps.map(app => ( {filteredApps.map(app => {
const locked = isLocked(app);
return (
<Card <Card
key={app.id} key={app.id}
className="bg-white/5 border-white/10 hover:border-white/30 cursor-pointer transition-all group overflow-hidden" className={`border-white/10 cursor-pointer transition-all group overflow-hidden ${locked ? "bg-white/2 opacity-60 hover:opacity-80" : "bg-white/5 hover:border-white/30"}`}
onClick={() => setLocation(app.route)} onClick={() => handleAppClick(app)}
data-testid={`app-card-${app.id}`} data-testid={`app-card-${app.id}`}
> >
<CardContent className="p-5"> <CardContent className="p-5">
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
<div className={`w-14 h-14 rounded-2xl bg-gradient-to-br ${app.color} flex items-center justify-center text-white flex-shrink-0 group-hover:scale-110 transition-transform`}> <div className={`w-14 h-14 rounded-2xl bg-gradient-to-br ${app.color} flex items-center justify-center text-white flex-shrink-0 ${locked ? "" : "group-hover:scale-110"} transition-transform relative`}>
{app.icon} {app.icon}
{locked && (
<div className="absolute inset-0 rounded-2xl bg-black/50 flex items-center justify-center">
<Lock className="w-5 h-5 text-white" />
</div>
)}
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
<h3 className="font-semibold text-white group-hover:text-indigo-400 transition-colors truncate"> <h3 className={`font-semibold truncate ${locked ? "text-slate-400" : "text-white group-hover:text-indigo-400"} transition-colors`}>
{app.name} {app.name}
</h3> </h3>
{app.status === "beta" && ( {locked && (
<Badge className="bg-slate-600/50 text-slate-400 text-xs">Não contratado</Badge>
)}
{!locked && app.status === "beta" && (
<Badge className="bg-yellow-500/20 text-yellow-300 text-xs">Beta</Badge> <Badge className="bg-yellow-500/20 text-yellow-300 text-xs">Beta</Badge>
)} )}
{app.status === "coming_soon" && ( {!locked && app.status === "coming_soon" && (
<Badge className="bg-slate-500/20 text-slate-300 text-xs">Em breve</Badge> <Badge className="bg-slate-500/20 text-slate-300 text-xs">Em breve</Badge>
)} )}
</div> </div>
@ -607,13 +638,20 @@ export default function AppCenter() {
</div> </div>
</div> </div>
<div className="mt-4 flex justify-end"> <div className="mt-4 flex justify-end">
<span className="text-xs text-indigo-400 flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity"> {locked ? (
Abrir <ArrowRight className="w-3 h-3" /> <span className="text-xs text-slate-500 flex items-center gap-1">
</span> Ver no Marketplace <ArrowRight className="w-3 h-3" />
</span>
) : (
<span className="text-xs text-indigo-400 flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
Abrir <ArrowRight className="w-3 h-3" />
</span>
)}
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
))} );
})}
</div> </div>
{filteredApps.length === 0 && ( {filteredApps.length === 0 && (

View File

@ -150,18 +150,18 @@ const api = {
function DashboardModule({ tenantId }: { tenantId: number }) { function DashboardModule({ tenantId }: { tenantId: number }) {
const { data: stats = { customers: 0, suppliers: 0, products: 0, salesOrders: 0, purchaseOrders: 0 } } = useQuery({ const { data: stats = { customers: 0, suppliers: 0, products: 0, salesOrders: 0, purchaseOrders: 0 } } = useQuery({
queryKey: ["/api/soe/stats", tenantId], queryKey: ["/api/erp/stats", tenantId],
queryFn: () => api.get(`/api/soe/stats?tenantId=${tenantId}`) queryFn: () => api.get(`/api/erp/stats?tenantId=${tenantId}`)
}); });
const { data: customers = [] } = useQuery<Customer[]>({ const { data: customers = [] } = useQuery<Customer[]>({
queryKey: ["/api/soe/customers", tenantId], queryKey: ["/api/erp/customers", tenantId],
queryFn: () => api.get(`/api/soe/customers?tenantId=${tenantId}`) queryFn: () => api.get(`/api/erp/customers?tenantId=${tenantId}`)
}); });
const { data: salesOrders = [] } = useQuery<SalesOrder[]>({ const { data: salesOrders = [] } = useQuery<SalesOrder[]>({
queryKey: ["/api/soe/sales-orders", tenantId], queryKey: ["/api/erp/sales-orders", tenantId],
queryFn: () => api.get(`/api/soe/sales-orders?tenantId=${tenantId}`) queryFn: () => api.get(`/api/erp/sales-orders?tenantId=${tenantId}`)
}); });
const totalRevenue = salesOrders const totalRevenue = salesOrders
@ -296,14 +296,14 @@ function PersonsModule({ tenantId }: { tenantId: number }) {
const [formStatus, setFormStatus] = useState("active"); const [formStatus, setFormStatus] = useState("active");
const { data: persons = [], isLoading, refetch } = useQuery<Person[]>({ const { data: persons = [], isLoading, refetch } = useQuery<Person[]>({
queryKey: ["/api/soe/persons", tenantId, roleFilter], queryKey: ["/api/erp/persons", tenantId, roleFilter],
queryFn: () => api.get(`/api/soe/persons?tenantId=${tenantId}&role=${roleFilter}`) queryFn: () => api.get(`/api/erp/persons?tenantId=${tenantId}&role=${roleFilter}`)
}); });
const createMutation = useMutation({ const createMutation = useMutation({
mutationFn: (data: any) => api.post("/api/soe/persons", { ...data, tenantId }), mutationFn: (data: any) => api.post("/api/erp/persons", { ...data, tenantId }),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/soe/persons"] }); queryClient.invalidateQueries({ queryKey: ["/api/erp/persons"] });
setShowDialog(false); setShowDialog(false);
setEditingItem(null); setEditingItem(null);
toast({ title: "Pessoa criada com sucesso!" }); toast({ title: "Pessoa criada com sucesso!" });
@ -312,9 +312,9 @@ function PersonsModule({ tenantId }: { tenantId: number }) {
}); });
const updateMutation = useMutation({ const updateMutation = useMutation({
mutationFn: ({ id, data }: { id: number; data: any }) => api.put(`/api/soe/persons/${id}`, data), mutationFn: ({ id, data }: { id: number; data: any }) => api.put(`/api/erp/persons/${id}`, data),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/soe/persons"] }); queryClient.invalidateQueries({ queryKey: ["/api/erp/persons"] });
setShowDialog(false); setShowDialog(false);
setEditingItem(null); setEditingItem(null);
toast({ title: "Pessoa atualizada!" }); toast({ title: "Pessoa atualizada!" });
@ -322,9 +322,9 @@ function PersonsModule({ tenantId }: { tenantId: number }) {
}); });
const deleteMutation = useMutation({ const deleteMutation = useMutation({
mutationFn: (id: number) => api.delete(`/api/soe/persons/${id}`), mutationFn: (id: number) => api.delete(`/api/erp/persons/${id}`),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/soe/persons"] }); queryClient.invalidateQueries({ queryKey: ["/api/erp/persons"] });
toast({ title: "Pessoa excluída!" }); toast({ title: "Pessoa excluída!" });
} }
}); });
@ -601,15 +601,15 @@ function CustomersModule({ tenantId }: { tenantId: number }) {
const [formStatus, setFormStatus] = useState("active"); const [formStatus, setFormStatus] = useState("active");
const { data: customers = [], isLoading, refetch } = useQuery<Customer[]>({ const { data: customers = [], isLoading, refetch } = useQuery<Customer[]>({
queryKey: ["/api/soe/customers", tenantId], queryKey: ["/api/erp/customers", tenantId],
queryFn: () => api.get(`/api/soe/customers?tenantId=${tenantId}`) queryFn: () => api.get(`/api/erp/customers?tenantId=${tenantId}`)
}); });
const createMutation = useMutation({ const createMutation = useMutation({
mutationFn: (data: any) => api.post("/api/soe/customers", { ...data, tenantId }), mutationFn: (data: any) => api.post("/api/erp/customers", { ...data, tenantId }),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/soe/customers"] }); queryClient.invalidateQueries({ queryKey: ["/api/erp/customers"] });
queryClient.invalidateQueries({ queryKey: ["/api/soe/stats"] }); queryClient.invalidateQueries({ queryKey: ["/api/erp/stats"] });
setShowDialog(false); setShowDialog(false);
setEditingItem(null); setEditingItem(null);
toast({ title: "Cliente criado com sucesso!" }); toast({ title: "Cliente criado com sucesso!" });
@ -618,9 +618,9 @@ function CustomersModule({ tenantId }: { tenantId: number }) {
}); });
const updateMutation = useMutation({ const updateMutation = useMutation({
mutationFn: ({ id, data }: { id: number; data: any }) => api.put(`/api/soe/customers/${id}`, data), mutationFn: ({ id, data }: { id: number; data: any }) => api.put(`/api/erp/customers/${id}`, data),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/soe/customers"] }); queryClient.invalidateQueries({ queryKey: ["/api/erp/customers"] });
setShowDialog(false); setShowDialog(false);
setEditingItem(null); setEditingItem(null);
toast({ title: "Cliente atualizado!" }); toast({ title: "Cliente atualizado!" });
@ -628,10 +628,10 @@ function CustomersModule({ tenantId }: { tenantId: number }) {
}); });
const deleteMutation = useMutation({ const deleteMutation = useMutation({
mutationFn: (id: number) => api.delete(`/api/soe/customers/${id}`), mutationFn: (id: number) => api.delete(`/api/erp/customers/${id}`),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/soe/customers"] }); queryClient.invalidateQueries({ queryKey: ["/api/erp/customers"] });
queryClient.invalidateQueries({ queryKey: ["/api/soe/stats"] }); queryClient.invalidateQueries({ queryKey: ["/api/erp/stats"] });
toast({ title: "Cliente excluído!" }); toast({ title: "Cliente excluído!" });
} }
}); });
@ -837,15 +837,15 @@ function SuppliersModule({ tenantId }: { tenantId: number }) {
const [formStatus, setFormStatus] = useState("active"); const [formStatus, setFormStatus] = useState("active");
const { data: suppliers = [], isLoading, refetch } = useQuery<Supplier[]>({ const { data: suppliers = [], isLoading, refetch } = useQuery<Supplier[]>({
queryKey: ["/api/soe/suppliers", tenantId], queryKey: ["/api/erp/suppliers", tenantId],
queryFn: () => api.get(`/api/soe/suppliers?tenantId=${tenantId}`) queryFn: () => api.get(`/api/erp/suppliers?tenantId=${tenantId}`)
}); });
const createMutation = useMutation({ const createMutation = useMutation({
mutationFn: (data: any) => api.post("/api/soe/suppliers", { ...data, tenantId }), mutationFn: (data: any) => api.post("/api/erp/suppliers", { ...data, tenantId }),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/soe/suppliers"] }); queryClient.invalidateQueries({ queryKey: ["/api/erp/suppliers"] });
queryClient.invalidateQueries({ queryKey: ["/api/soe/stats"] }); queryClient.invalidateQueries({ queryKey: ["/api/erp/stats"] });
setShowDialog(false); setShowDialog(false);
setEditingItem(null); setEditingItem(null);
toast({ title: "Fornecedor criado com sucesso!" }); toast({ title: "Fornecedor criado com sucesso!" });
@ -853,9 +853,9 @@ function SuppliersModule({ tenantId }: { tenantId: number }) {
}); });
const updateMutation = useMutation({ const updateMutation = useMutation({
mutationFn: ({ id, data }: { id: number; data: any }) => api.put(`/api/soe/suppliers/${id}`, data), mutationFn: ({ id, data }: { id: number; data: any }) => api.put(`/api/erp/suppliers/${id}`, data),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/soe/suppliers"] }); queryClient.invalidateQueries({ queryKey: ["/api/erp/suppliers"] });
setShowDialog(false); setShowDialog(false);
setEditingItem(null); setEditingItem(null);
toast({ title: "Fornecedor atualizado!" }); toast({ title: "Fornecedor atualizado!" });
@ -863,10 +863,10 @@ function SuppliersModule({ tenantId }: { tenantId: number }) {
}); });
const deleteMutation = useMutation({ const deleteMutation = useMutation({
mutationFn: (id: number) => api.delete(`/api/soe/suppliers/${id}`), mutationFn: (id: number) => api.delete(`/api/erp/suppliers/${id}`),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/soe/suppliers"] }); queryClient.invalidateQueries({ queryKey: ["/api/erp/suppliers"] });
queryClient.invalidateQueries({ queryKey: ["/api/soe/stats"] }); queryClient.invalidateQueries({ queryKey: ["/api/erp/stats"] });
toast({ title: "Fornecedor excluído!" }); toast({ title: "Fornecedor excluído!" });
} }
}); });
@ -1052,8 +1052,8 @@ function ProductsModule({ tenantId }: { tenantId: number }) {
const [formTaxGroup, setFormTaxGroup] = useState(""); const [formTaxGroup, setFormTaxGroup] = useState("");
const { data: products = [], isLoading, refetch } = useQuery<Product[]>({ const { data: products = [], isLoading, refetch } = useQuery<Product[]>({
queryKey: ["/api/soe/products", tenantId], queryKey: ["/api/erp/products", tenantId],
queryFn: () => api.get(`/api/soe/products?tenantId=${tenantId}`) queryFn: () => api.get(`/api/erp/products?tenantId=${tenantId}`)
}); });
const { data: taxGroups = [] } = useQuery<TaxGroup[]>({ const { data: taxGroups = [] } = useQuery<TaxGroup[]>({
@ -1062,10 +1062,10 @@ function ProductsModule({ tenantId }: { tenantId: number }) {
}); });
const createMutation = useMutation({ const createMutation = useMutation({
mutationFn: (data: any) => api.post("/api/soe/products", { ...data, tenantId }), mutationFn: (data: any) => api.post("/api/erp/products", { ...data, tenantId }),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/soe/products"] }); queryClient.invalidateQueries({ queryKey: ["/api/erp/products"] });
queryClient.invalidateQueries({ queryKey: ["/api/soe/stats"] }); queryClient.invalidateQueries({ queryKey: ["/api/erp/stats"] });
setShowDialog(false); setShowDialog(false);
setEditingItem(null); setEditingItem(null);
toast({ title: "Produto criado com sucesso!" }); toast({ title: "Produto criado com sucesso!" });
@ -1073,9 +1073,9 @@ function ProductsModule({ tenantId }: { tenantId: number }) {
}); });
const updateMutation = useMutation({ const updateMutation = useMutation({
mutationFn: ({ id, data }: { id: number; data: any }) => api.put(`/api/soe/products/${id}`, data), mutationFn: ({ id, data }: { id: number; data: any }) => api.put(`/api/erp/products/${id}`, data),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/soe/products"] }); queryClient.invalidateQueries({ queryKey: ["/api/erp/products"] });
setShowDialog(false); setShowDialog(false);
setEditingItem(null); setEditingItem(null);
toast({ title: "Produto atualizado!" }); toast({ title: "Produto atualizado!" });
@ -1083,10 +1083,10 @@ function ProductsModule({ tenantId }: { tenantId: number }) {
}); });
const deleteMutation = useMutation({ const deleteMutation = useMutation({
mutationFn: (id: number) => api.delete(`/api/soe/products/${id}`), mutationFn: (id: number) => api.delete(`/api/erp/products/${id}`),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/soe/products"] }); queryClient.invalidateQueries({ queryKey: ["/api/erp/products"] });
queryClient.invalidateQueries({ queryKey: ["/api/soe/stats"] }); queryClient.invalidateQueries({ queryKey: ["/api/erp/stats"] });
toast({ title: "Produto excluído!" }); toast({ title: "Produto excluído!" });
} }
}); });
@ -1316,20 +1316,20 @@ function SalesModule({ tenantId }: { tenantId: number }) {
const [formCustomerId, setFormCustomerId] = useState(""); const [formCustomerId, setFormCustomerId] = useState("");
const { data: salesOrders = [], isLoading, refetch } = useQuery<SalesOrder[]>({ const { data: salesOrders = [], isLoading, refetch } = useQuery<SalesOrder[]>({
queryKey: ["/api/soe/sales-orders", tenantId], queryKey: ["/api/erp/sales-orders", tenantId],
queryFn: () => api.get(`/api/soe/sales-orders?tenantId=${tenantId}`) queryFn: () => api.get(`/api/erp/sales-orders?tenantId=${tenantId}`)
}); });
const { data: customers = [] } = useQuery<Customer[]>({ const { data: customers = [] } = useQuery<Customer[]>({
queryKey: ["/api/soe/customers", tenantId], queryKey: ["/api/erp/customers", tenantId],
queryFn: () => api.get(`/api/soe/customers?tenantId=${tenantId}`) queryFn: () => api.get(`/api/erp/customers?tenantId=${tenantId}`)
}); });
const createMutation = useMutation({ const createMutation = useMutation({
mutationFn: (data: any) => api.post("/api/soe/sales-orders", { ...data, tenantId }), mutationFn: (data: any) => api.post("/api/erp/sales-orders", { ...data, tenantId }),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/soe/sales-orders"] }); queryClient.invalidateQueries({ queryKey: ["/api/erp/sales-orders"] });
queryClient.invalidateQueries({ queryKey: ["/api/soe/stats"] }); queryClient.invalidateQueries({ queryKey: ["/api/erp/stats"] });
setShowDialog(false); setShowDialog(false);
setEditingItem(null); setEditingItem(null);
setFormCustomerId(""); setFormCustomerId("");
@ -1338,10 +1338,10 @@ function SalesModule({ tenantId }: { tenantId: number }) {
}); });
const updateMutation = useMutation({ const updateMutation = useMutation({
mutationFn: ({ id, data }: { id: number; data: any }) => api.put(`/api/soe/sales-orders/${id}`, data), mutationFn: ({ id, data }: { id: number; data: any }) => api.put(`/api/erp/sales-orders/${id}`, data),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/soe/sales-orders"] }); queryClient.invalidateQueries({ queryKey: ["/api/erp/sales-orders"] });
queryClient.invalidateQueries({ queryKey: ["/api/soe/stats"] }); queryClient.invalidateQueries({ queryKey: ["/api/erp/stats"] });
setShowDialog(false); setShowDialog(false);
setEditingItem(null); setEditingItem(null);
setFormCustomerId(""); setFormCustomerId("");
@ -1350,18 +1350,18 @@ function SalesModule({ tenantId }: { tenantId: number }) {
}); });
const deleteMutation = useMutation({ const deleteMutation = useMutation({
mutationFn: (id: number) => api.delete(`/api/soe/sales-orders/${id}`), mutationFn: (id: number) => api.delete(`/api/erp/sales-orders/${id}`),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/soe/sales-orders"] }); queryClient.invalidateQueries({ queryKey: ["/api/erp/sales-orders"] });
queryClient.invalidateQueries({ queryKey: ["/api/soe/stats"] }); queryClient.invalidateQueries({ queryKey: ["/api/erp/stats"] });
toast({ title: "Pedido excluído!" }); toast({ title: "Pedido excluído!" });
} }
}); });
const confirmMutation = useMutation({ const confirmMutation = useMutation({
mutationFn: (id: number) => api.post(`/api/soe/sales-orders/${id}/confirm`, {}), mutationFn: (id: number) => api.post(`/api/erp/sales-orders/${id}/confirm`, {}),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/soe/sales-orders"] }); queryClient.invalidateQueries({ queryKey: ["/api/erp/sales-orders"] });
toast({ title: "Pedido confirmado!" }); toast({ title: "Pedido confirmado!" });
} }
}); });
@ -1728,7 +1728,7 @@ export default function ArcadiaNext() {
<Building2 className="w-4 h-4 text-white" /> <Building2 className="w-4 h-4 text-white" />
</div> </div>
<div> <div>
<h1 className="font-bold text-lg">Arcádia SOE</h1> <h1 className="font-bold text-lg">Arcádia ERP</h1>
<p className="text-xs text-muted-foreground">Gestão Empresarial</p> <p className="text-xs text-muted-foreground">Gestão Empresarial</p>
</div> </div>
</div> </div>

View File

@ -386,7 +386,7 @@ function AutomationCard({ automation, onRefresh }: { automation: Automation; onR
size="icon" size="icon"
onClick={() => runMutation.mutate()} onClick={() => runMutation.mutate()}
disabled={runMutation.isPending} disabled={runMutation.isPending}
className="h-8 w-8 flex items-center justify-center text-white/50 hover:text-[#c89b3c] hover:bg-[#c89b3c]/10" className="h-8 w-8 text-white/50 hover:text-[#c89b3c] hover:bg-[#c89b3c]/10"
title="Executar agora" title="Executar agora"
data-testid={`run-automation-${automation.id}`} data-testid={`run-automation-${automation.id}`}
> >

View File

@ -1,5 +1,5 @@
import { BrowserFrame } from "@/components/Browser/BrowserFrame"; import { BrowserFrame } from "@/components/Browser/BrowserFrame";
import { useState } from "react"; import { useState, useEffect } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
@ -47,9 +47,9 @@ import {
X, X,
Sparkles, Sparkles,
MessageSquare, MessageSquare,
Brain,
} from "lucide-react"; } from "lucide-react";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { SupersetDashboard } from "@/components/SupersetDashboard";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@ -81,8 +81,73 @@ import {
ResponsiveContainer, ResponsiveContainer,
Cell, Cell,
} from "recharts"; } from "recharts";
import { SupersetDashboard } from "@/components/SupersetDashboard";
import { MiroFlowControl } from "@/components/MiroFlowControl"; // ── Componente da aba Insights (Apache Superset) ──────────────────────────────
function SupersetAdvancedTab() {
const [selectedDashboard, setSelectedDashboard] = useState<string>("");
const [dashboards, setDashboards] = useState<Array<{ id: string; title: string }>>([]);
useEffect(() => {
fetch("/api/superset/dashboards", { credentials: "include" })
.then(r => r.json())
.then(data => {
if (Array.isArray(data)) {
setDashboards(data.map((d: any) => ({ id: String(d.id), title: d.dashboard_title || d.title })));
if (data.length > 0) setSelectedDashboard(String(data[0].id));
}
})
.catch(() => setDashboards([]));
}, []);
return (
<div className="space-y-4">
<div className="flex items-center justify-between flex-wrap gap-3">
<div>
<h2 className="text-lg font-semibold text-[#1f334d]">Arcádia Insights Apache Superset</h2>
<p className="text-sm text-gray-500">SQL Lab avançado, 50+ tipos de gráfico, dashboards interativos</p>
</div>
<div className="flex items-center gap-2">
{dashboards.length > 0 && (
<select
value={selectedDashboard}
onChange={e => setSelectedDashboard(e.target.value)}
className="text-sm border border-gray-200 rounded-lg px-3 py-2 bg-white text-[#1f334d] focus:outline-none focus:ring-2 focus:ring-[#c89b3c]"
>
{dashboards.map(d => (
<option key={d.id} value={d.id}>{d.title}</option>
))}
</select>
)}
<a
href="/superset"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-4 py-2 bg-[#1f334d] text-white rounded-lg hover:bg-[#2a4466] transition-colors text-sm"
>
<ArrowRight className="w-4 h-4" /> Abrir completo
</a>
</div>
</div>
{selectedDashboard ? (
<SupersetDashboard
dashboardId={selectedDashboard}
height="calc(100vh - 320px)"
showOpenLink={false}
/>
) : (
<div className="rounded-xl overflow-hidden border border-[#c89b3c]/20 bg-white shadow-sm" style={{ height: "calc(100vh - 320px)" }}>
<iframe
src="/superset"
className="w-full h-full border-0"
title="Arcádia Insights — Apache Superset"
allow="fullscreen"
/>
</div>
)}
</div>
);
}
interface BiStats { interface BiStats {
dataSources: number; dataSources: number;
@ -2895,9 +2960,6 @@ export default function BiWorkspace() {
<TabsTrigger value="advanced" className="data-[state=active]:bg-[#c89b3c] data-[state=active]:text-[#1f334d] text-white/70"> <TabsTrigger value="advanced" className="data-[state=active]:bg-[#c89b3c] data-[state=active]:text-[#1f334d] text-white/70">
<Settings className="w-4 h-4 mr-2" /> Insights <Settings className="w-4 h-4 mr-2" /> Insights
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="cientifico" className="data-[state=active]:bg-[#c89b3c] data-[state=active]:text-[#1f334d] text-white/70">
<Brain className="w-4 h-4 mr-2" /> Científico
</TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="overview" className="mt-0"> <TabsContent value="overview" className="mt-0">
@ -2922,27 +2984,8 @@ export default function BiWorkspace() {
<StagingTab /> <StagingTab />
</TabsContent> </TabsContent>
<TabsContent value="advanced" className="mt-0"> <TabsContent value="advanced" className="mt-0">
<div className="space-y-4"> <SupersetAdvancedTab />
<div className="flex items-center justify-between">
<div>
<h2 className="text-lg font-semibold text-[#1f334d]">Arcádia Insights Apache Superset</h2>
<p className="text-sm text-gray-500">SQL Lab avançado, 50+ tipos de gráfico, dashboards interativos</p>
</div>
<a
href="https://bi.onboardbi.com.br"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-4 py-2 bg-[#1f334d] text-white rounded-lg hover:bg-[#2a4466] transition-colors text-sm"
data-testid="link-open-superset-external"
>
<ArrowRight className="w-4 h-4" /> Abrir em Nova Aba
</a>
</div>
<SupersetDashboard dashboardId="executive-summary" height="calc(100vh - 320px)" />
</div>
</TabsContent>
<TabsContent value="cientifico" className="mt-0">
<MiroFlowControl />
</TabsContent> </TabsContent>
</Tabs> </Tabs>
</div> </div>

View File

@ -42,7 +42,7 @@ import {
Link, Link,
File File
} from "lucide-react"; } from "lucide-react";
const browserIcon = "/arcadia_suite_icon.png"; import browserIcon from "@assets/arcadia_branding/arcadia_suite_icon.png";
import { DigitalClock } from "@/components/DigitalClock"; import { DigitalClock } from "@/components/DigitalClock";
type ParaTab = "projetos" | "areas" | "recursos" | "arquivo"; type ParaTab = "projetos" | "areas" | "recursos" | "arquivo";

View File

@ -306,7 +306,7 @@ export default function CommercialEnv() {
Comercial - Engenharia Ambiental Comercial - Engenharia Ambiental
</h1> </h1>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
Propostas, clientes e pipeline de vendas integrado ao Arcádia SOE Propostas, clientes e pipeline de vendas integrado ao Arcádia ERP
</p> </p>
</div> </div>
<Button onClick={() => refetchCustomers()} variant="outline" size="sm"> <Button onClick={() => refetchCustomers()} variant="outline" size="sm">
@ -660,7 +660,7 @@ export default function CommercialEnv() {
{salesOrders.length === 0 && ( {salesOrders.length === 0 && (
<TableRow> <TableRow>
<TableCell colSpan={7} className="text-center py-8 text-muted-foreground"> <TableCell colSpan={7} className="text-center py-8 text-muted-foreground">
Nenhum pedido encontrado. Sincronize com o Arcádia SOE. Nenhum pedido encontrado. Sincronize com o Arcádia ERP.
</TableCell> </TableCell>
</TableRow> </TableRow>
)} )}

View File

@ -1,5 +1,4 @@
import { useState } from "react"; import { useState } from "react";
import { SupersetDashboard } from "@/components/SupersetDashboard";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { import {
BookOpen, BookOpen,
@ -392,13 +391,12 @@ export default function Contabil() {
</div> </div>
<Tabs value={activeTab} onValueChange={setActiveTab}> <Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid w-full grid-cols-6"> <TabsList className="grid w-full grid-cols-5">
<TabsTrigger value="dashboard" data-testid="tab-dashboard">Dashboard</TabsTrigger> <TabsTrigger value="dashboard" data-testid="tab-dashboard">Dashboard</TabsTrigger>
<TabsTrigger value="plano" data-testid="tab-plano">Plano de Contas</TabsTrigger> <TabsTrigger value="plano" data-testid="tab-plano">Plano de Contas</TabsTrigger>
<TabsTrigger value="lancamentos" data-testid="tab-lancamentos">Lançamentos</TabsTrigger> <TabsTrigger value="lancamentos" data-testid="tab-lancamentos">Lançamentos</TabsTrigger>
<TabsTrigger value="centros" data-testid="tab-centros">Centros de Custo</TabsTrigger> <TabsTrigger value="centros" data-testid="tab-centros">Centros de Custo</TabsTrigger>
<TabsTrigger value="sped" data-testid="tab-sped">SPED ECD</TabsTrigger> <TabsTrigger value="sped" data-testid="tab-sped">SPED ECD</TabsTrigger>
<TabsTrigger value="analise" data-testid="tab-analise">Análise BI</TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="dashboard" className="space-y-4"> <TabsContent value="dashboard" className="space-y-4">
@ -787,9 +785,6 @@ export default function Contabil() {
</CardContent> </CardContent>
</Card> </Card>
</TabsContent> </TabsContent>
<TabsContent value="analise">
<SupersetDashboard dashboardId="dre-mensal" />
</TabsContent>
</Tabs> </Tabs>
<Dialog open={showContaDialog} onOpenChange={setShowContaDialog}> <Dialog open={showContaDialog} onOpenChange={setShowContaDialog}>

View File

@ -1,7 +1,6 @@
import { useState } from "react"; import { useState } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
import MultiTenantSection from "@/components/MultiTenantSection";
import { BrowserFrame } from "@/components/Browser/BrowserFrame"; import { BrowserFrame } from "@/components/Browser/BrowserFrame";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@ -43,8 +42,7 @@ import {
Zap, Zap,
AlertCircle, AlertCircle,
Clock, Clock,
Briefcase, Briefcase
Building2
} from "lucide-react"; } from "lucide-react";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
@ -884,8 +882,8 @@ export default function Crm() {
<div className="max-w-7xl mx-auto space-y-6"> <div className="max-w-7xl mx-auto space-y-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-3xl font-bold" data-testid="text-crm-title">Manager Partners</h1> <h1 className="text-3xl font-bold" data-testid="text-crm-title">Arcádia CRM</h1>
<p className="text-muted-foreground">Gestão de parceiros, tenants e contratos</p> <p className="text-muted-foreground">Gestão de parceiros, contratos e comunicação</p>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
{googleStatus?.connected ? ( {googleStatus?.connected ? (
@ -949,10 +947,6 @@ export default function Crm() {
<Settings className="w-4 h-4 mr-2" /> <Settings className="w-4 h-4 mr-2" />
Configurações Configurações
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="multitenant" data-testid="tab-multitenant">
<Building2 className="w-4 h-4 mr-2" />
Multi-Tenant
</TabsTrigger>
</TabsList> </TabsList>
</ScrollArea> </ScrollArea>
@ -2328,7 +2322,7 @@ export default function Crm() {
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="erpnext">Frappe ERPNext</SelectItem> <SelectItem value="erpnext">Frappe Arcádia ERP</SelectItem>
<SelectItem value="crm_next">Frappe Arcádia CRM</SelectItem> <SelectItem value="crm_next">Frappe Arcádia CRM</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
@ -2387,7 +2381,7 @@ export default function Crm() {
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="flex items-center justify-between text-sm"> <div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Sistema:</span> <span className="text-muted-foreground">Sistema:</span>
<Badge variant="outline" className="capitalize">{connector.targetSystem === "erpnext" ? "ERPNext" : "Arcádia CRM"}</Badge> <Badge variant="outline" className="capitalize">{connector.targetSystem === "erpnext" ? "Arcádia ERP" : "Arcádia CRM"}</Badge>
</div> </div>
<div className="flex items-center justify-between text-sm"> <div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Status:</span> <span className="text-muted-foreground">Status:</span>
@ -2688,10 +2682,6 @@ export default function Crm() {
</> </>
)} )}
</TabsContent> </TabsContent>
<TabsContent value="multitenant" className="space-y-4">
<MultiTenantSection />
</TabsContent>
</Tabs> </Tabs>
</div> </div>
</div> </div>

View File

@ -29,26 +29,8 @@ import {
HardDrive, HardDrive,
Workflow, Workflow,
Users, Users,
Brain,
Sparkles,
Search,
Code,
Terminal,
Globe,
Shield,
ArrowRight,
Layers,
Network,
X,
} from "lucide-react"; } from "lucide-react";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
} from "@/components/ui/dialog";
interface EngineStatus { interface EngineStatus {
name: string; name: string;
@ -85,7 +67,6 @@ interface EngineRoomData {
} }
const ENGINE_ICONS: Record<string, any> = { const ENGINE_ICONS: Record<string, any> = {
"manus-ia": Brain,
"plus": ShoppingCart, "plus": ShoppingCart,
"contabil": Calculator, "contabil": Calculator,
"fisco": FileText, "fisco": FileText,
@ -107,18 +88,13 @@ const STATUS_CONFIG = {
error: { icon: AlertCircle, color: "text-amber-400", bg: "bg-amber-500/10", label: "Erro" }, error: { icon: AlertCircle, color: "text-amber-400", bg: "bg-amber-500/10", label: "Erro" },
}; };
function EngineCard({ engine, onClick }: { engine: EngineStatus; onClick?: () => void }) { function EngineCard({ engine }: { engine: EngineStatus }) {
const statusConf = STATUS_CONFIG[engine.status]; const statusConf = STATUS_CONFIG[engine.status];
const StatusIcon = statusConf.icon; const StatusIcon = statusConf.icon;
const EngineIcon = ENGINE_ICONS[engine.name] || Server; const EngineIcon = ENGINE_ICONS[engine.name] || Server;
const isClickable = !!onClick;
return ( return (
<Card <Card data-testid={`engine-card-${engine.name}`} className="bg-[#1a1a2e] border-[#2a2a4a] hover:border-[#3a3a5a] transition-all">
data-testid={`engine-card-${engine.name}`}
className={`bg-[#1a1a2e] border-[#2a2a4a] hover:border-[#3a3a5a] transition-all ${isClickable ? "cursor-pointer hover:shadow-lg hover:shadow-violet-500/5 hover:border-violet-500/30" : ""}`}
onClick={onClick}
>
<CardContent className="p-4"> <CardContent className="p-4">
<div className="flex items-start justify-between mb-3"> <div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
@ -130,16 +106,9 @@ function EngineCard({ engine, onClick }: { engine: EngineStatus; onClick?: () =>
<p className="text-xs text-gray-400">{engine.description}</p> <p className="text-xs text-gray-400">{engine.description}</p>
</div> </div>
</div> </div>
<div className="flex items-center gap-2"> <div className={`flex items-center gap-1.5 px-2 py-1 rounded-full ${statusConf.bg}`}>
{isClickable && ( <StatusIcon className={`w-3.5 h-3.5 ${statusConf.color}`} />
<Badge variant="outline" className="border-violet-500/30 text-violet-400 bg-violet-500/5 text-[10px]"> <span className={`text-xs font-medium ${statusConf.color}`}>{statusConf.label}</span>
Clique para detalhes
</Badge>
)}
<div className={`flex items-center gap-1.5 px-2 py-1 rounded-full ${statusConf.bg}`}>
<StatusIcon className={`w-3.5 h-3.5 ${statusConf.color}`} />
<span className={`text-xs font-medium ${statusConf.color}`}>{statusConf.label}</span>
</div>
</div> </div>
</div> </div>
@ -191,208 +160,6 @@ function EngineCard({ engine, onClick }: { engine: EngineStatus; onClick?: () =>
); );
} }
function ManusStructurePanel({ open, onOpenChange, metrics }: { open: boolean; onOpenChange: (open: boolean) => void; metrics: any }) {
const AGENTS = [
{ name: "Architect", role: "Design & Arquitetura", icon: Layers, color: "text-blue-400", bg: "bg-blue-500/10" },
{ name: "Generator", role: "Geração de Código", icon: Code, color: "text-green-400", bg: "bg-green-500/10" },
{ name: "Validator", role: "Validação TypeScript", icon: Shield, color: "text-amber-400", bg: "bg-amber-500/10" },
{ name: "Executor", role: "Execução & Staging", icon: Terminal, color: "text-red-400", bg: "bg-red-500/10" },
{ name: "Researcher", role: "Pesquisa & Contexto", icon: Search, color: "text-cyan-400", bg: "bg-cyan-500/10" },
{ name: "Evolution", role: "Aprendizado Evolutivo", icon: Sparkles, color: "text-purple-400", bg: "bg-purple-500/10" },
];
const TOOL_CATEGORIES = [
{ name: "Busca Semântica", count: 8, icon: Search, color: "text-violet-400" },
{ name: "Leitura/Escrita de Arquivos", count: 12, icon: FileText, color: "text-blue-400" },
{ name: "Comandos Shell", count: 6, icon: Terminal, color: "text-green-400" },
{ name: "Web Research", count: 5, icon: Globe, color: "text-cyan-400" },
{ name: "Knowledge Graph", count: 8, icon: Network, color: "text-amber-400" },
{ name: "ERP & Database", count: 10, icon: Database, color: "text-emerald-400" },
{ name: "Análise de Código", count: 7, icon: Code, color: "text-pink-400" },
];
const uptime = metrics?.metrics?.uptime
? `${Math.floor(metrics.metrics.uptime / 3600)}h ${Math.floor((metrics.metrics.uptime % 3600) / 60)}m`
: "---";
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl max-h-[90vh] bg-[#0d0d1a] border-[#2a2a4a] text-white overflow-hidden p-0">
<div className="sticky top-0 z-10 bg-gradient-to-r from-violet-600/20 via-purple-600/20 to-indigo-600/20 border-b border-violet-500/20 p-6">
<DialogHeader>
<div className="flex items-center gap-3">
<div className="p-3 rounded-xl bg-gradient-to-br from-violet-500/30 to-purple-600/30 border border-violet-500/30">
<Brain className="w-7 h-7 text-violet-400" />
</div>
<div>
<DialogTitle className="text-xl font-bold text-white">Manus IA - Cérebro Central</DialogTitle>
<DialogDescription className="text-violet-300/80">
Arquitetura do motor de inteligência que alimenta todos os agentes
</DialogDescription>
</div>
</div>
</DialogHeader>
<div className="grid grid-cols-4 gap-3 mt-4">
<div className="text-center p-3 rounded-lg bg-[#1a1a2e]/80 border border-[#2a2a4a]">
<p className="text-2xl font-bold text-violet-400">{metrics?.model || "GPT-4o"}</p>
<p className="text-[10px] text-gray-400 uppercase mt-1">Modelo</p>
</div>
<div className="text-center p-3 rounded-lg bg-[#1a1a2e]/80 border border-[#2a2a4a]">
<p className="text-2xl font-bold text-white">{metrics?.metrics?.totalCalls || 0}</p>
<p className="text-[10px] text-gray-400 uppercase mt-1">Chamadas IA</p>
</div>
<div className="text-center p-3 rounded-lg bg-[#1a1a2e]/80 border border-[#2a2a4a]">
<p className="text-2xl font-bold text-emerald-400">{((metrics?.metrics?.totalTokens || 0) / 1000).toFixed(1)}k</p>
<p className="text-[10px] text-gray-400 uppercase mt-1">Tokens</p>
</div>
<div className="text-center p-3 rounded-lg bg-[#1a1a2e]/80 border border-[#2a2a4a]">
<p className="text-2xl font-bold text-cyan-400">{uptime}</p>
<p className="text-[10px] text-gray-400 uppercase mt-1">Uptime</p>
</div>
</div>
</div>
<ScrollArea className="max-h-[calc(90vh-220px)]">
<div className="p-6 space-y-6">
<div>
<h3 className="text-sm font-semibold text-gray-300 mb-3 flex items-center gap-2">
<Layers className="w-4 h-4 text-violet-400" />
Arquitetura: Fluxo de Inteligência
</h3>
<div className="relative p-4 rounded-xl bg-[#1a1a2e] border border-[#2a2a4a]">
<div className="flex flex-col items-center gap-2">
<div className="w-full p-3 rounded-lg bg-gradient-to-r from-violet-500/10 to-purple-500/10 border border-violet-500/20 text-center">
<div className="flex items-center justify-center gap-2 mb-1">
<Brain className="w-5 h-5 text-violet-400" />
<span className="text-sm font-bold text-violet-300">ManusIntelligence (Singleton)</span>
</div>
<p className="text-[10px] text-gray-400">GPT-4o + ToolManager + Context Enrichment</p>
</div>
<div className="flex items-center gap-1 text-gray-500">
<ArrowRight className="w-4 h-4 rotate-90" />
<span className="text-[10px]">generate() / think()</span>
<ArrowRight className="w-4 h-4 rotate-90" />
</div>
<div className="w-full p-3 rounded-lg bg-[#0d0d1a] border border-[#1a1a3a] text-center">
<span className="text-xs font-medium text-amber-400">enrichWithContext()</span>
<p className="text-[10px] text-gray-500 mt-1">ToolManager.search_code Contexto Semântico Automático</p>
</div>
<ArrowRight className="w-4 h-4 rotate-90 text-gray-500" />
<div className="grid grid-cols-3 gap-2 w-full">
{AGENTS.map((agent) => (
<div key={agent.name} className={`p-2.5 rounded-lg ${agent.bg} border border-[#2a2a4a] 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-gray-500 mt-0.5">{agent.role}</p>
</div>
))}
</div>
</div>
</div>
</div>
<div>
<h3 className="text-sm font-semibold text-gray-300 mb-3 flex items-center gap-2">
<Zap className="w-4 h-4 text-amber-400" />
Ferramentas Disponíveis ({metrics?.capabilities?.tools || 56})
</h3>
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
{TOOL_CATEGORIES.map((cat) => (
<div key={cat.name} className="p-3 rounded-lg bg-[#1a1a2e] border border-[#2a2a4a] hover:border-[#3a3a5a] 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-gray-400">{cat.name}</p>
</div>
))}
</div>
</div>
<div>
<h3 className="text-sm font-semibold text-gray-300 mb-3 flex items-center gap-2">
<CheckCircle className="w-4 h-4 text-emerald-400" />
Capacidades Ativas
</h3>
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
{[
{ name: "Cérebro Central (GPT-4o)", active: true },
{ name: "Enriquecimento de Contexto", active: true },
{ name: "Busca Semântica de Código", active: true },
{ name: "Knowledge Graph", active: true },
{ name: "Pipeline Autônomo de Dev", active: true },
{ name: "Orquestração de 6 Agentes", active: true },
{ name: "Leitura/Escrita de Arquivos", active: true },
{ name: "Execução de Comandos Shell", active: true },
{ name: "Web Research", active: true },
{ name: "Análise de Código", active: true },
{ name: "Validação TypeScript", active: true },
{ name: "Memória Evolutiva", active: true },
].map((cap) => (
<div key={cap.name} className="flex items-center gap-2 p-2 rounded-lg bg-[#1a1a2e] border border-[#2a2a4a]">
<CheckCircle className="w-3.5 h-3.5 text-emerald-400 shrink-0" />
<span className="text-xs text-gray-300">{cap.name}</span>
</div>
))}
</div>
</div>
{metrics?.metrics?.errorCount > 0 && (
<div className="p-3 rounded-lg bg-red-500/5 border border-red-500/20">
<div className="flex items-center gap-2 mb-1">
<AlertCircle className="w-4 h-4 text-red-400" />
<span className="text-xs font-medium text-red-400">Erros Registrados</span>
</div>
<p className="text-xs text-gray-400">
Total: <span className="text-red-400 font-medium">{metrics.metrics.errorCount}</span>
{metrics.metrics.lastCallAt && (
<> | Última chamada: <span className="text-gray-300">{new Date(metrics.metrics.lastCallAt).toLocaleString("pt-BR")}</span></>
)}
</p>
</div>
)}
<div className="p-4 rounded-xl bg-[#1a1a2e] border border-[#2a2a4a]">
<h3 className="text-sm font-semibold text-gray-300 mb-3 flex items-center gap-2">
<Network className="w-4 h-4 text-violet-400" />
Diagrama de Fluxo
</h3>
<div className="font-mono text-[11px] text-gray-400 space-y-0.5 bg-[#0a0a15] p-4 rounded-lg border border-[#1a1a3a]">
<p className="text-violet-400">{"┌──────────────────────────────────────────────────────────┐"}</p>
<p className="text-violet-400">{"│ MANUS INTELLIGENCE (Singleton GPT-4o) │"}</p>
<p className="text-violet-400">{"├──────────────────────────────────────────────────────────┤"}</p>
<p>{"│ .generate(prompt) → enrichWithContext() → OpenAI │"}</p>
<p>{"│ .think(prompt) → enrichWithContext() → OpenAI │"}</p>
<p>{"│ .getMetrics() → calls, tokens, errors, uptime │"}</p>
<p className="text-violet-400">{"├──────────────────────────────────────────────────────────┤"}</p>
<p className="text-amber-400">{"│ ToolManager.executeTool('search_code', query) │"}</p>
<p className="text-amber-400">{"│ → Contexto semântico injetado automaticamente │"}</p>
<p className="text-violet-400">{"├──────────────────────────────────────────────────────────┤"}</p>
<p className="text-blue-400">{"│ Architect ──────┐ │"}</p>
<p className="text-green-400">{"│ Generator ──────┤ │"}</p>
<p className="text-amber-400">{"│ Validator ──────┤── Todos via manusIntelligence ──► │"}</p>
<p className="text-red-400">{"│ Executor ──────┤ │"}</p>
<p className="text-cyan-400">{"│ Researcher ──────┤ │"}</p>
<p className="text-purple-400">{"│ Evolution ──────┘ │"}</p>
<p className="text-violet-400">{"├──────────────────────────────────────────────────────────┤"}</p>
<p className="text-emerald-400">{"│ /api/manus/health → Status, métricas, capacidades │"}</p>
<p className="text-violet-400">{"└──────────────────────────────────────────────────────────┘"}</p>
</div>
</div>
</div>
</ScrollArea>
</DialogContent>
</Dialog>
);
}
function AgentCard({ agent }: { agent: AgentStatus }) { function AgentCard({ agent }: { agent: AgentStatus }) {
return ( return (
<div data-testid={`agent-card-${agent.name}`} className="flex items-center justify-between p-3 rounded-lg bg-[#1a1a2e] border border-[#2a2a4a]"> <div data-testid={`agent-card-${agent.name}`} className="flex items-center justify-between p-3 rounded-lg bg-[#1a1a2e] border border-[#2a2a4a]">
@ -442,7 +209,6 @@ function SummaryCards({ summary }: { summary: EngineRoomData["summary"] }) {
export default function EngineRoom() { export default function EngineRoom() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [activeTab, setActiveTab] = useState("overview"); const [activeTab, setActiveTab] = useState("overview");
const [manusOpen, setManusOpen] = useState(false);
const { data, isLoading, isRefetching } = useQuery<EngineRoomData>({ const { data, isLoading, isRefetching } = useQuery<EngineRoomData>({
queryKey: ["/api/engine-room/status"], queryKey: ["/api/engine-room/status"],
@ -461,12 +227,6 @@ export default function EngineRoom() {
refetchInterval: 10000, refetchInterval: 10000,
}); });
const { data: manusMetrics } = useQuery<any>({
queryKey: ["/api/manus/health"],
enabled: activeTab === "manus" || manusOpen,
refetchInterval: 10000,
});
const handleRefresh = () => { const handleRefresh = () => {
queryClient.invalidateQueries({ queryKey: ["/api/engine-room/status"] }); queryClient.invalidateQueries({ queryKey: ["/api/engine-room/status"] });
}; };
@ -537,9 +297,6 @@ export default function EngineRoom() {
<TabsTrigger value="automation" data-testid="tab-automation" className="data-[state=active]:bg-purple-500/10 data-[state=active]:text-purple-400"> <TabsTrigger value="automation" data-testid="tab-automation" className="data-[state=active]:bg-purple-500/10 data-[state=active]:text-purple-400">
<Zap className="w-4 h-4 mr-1.5" /> Motor Automacao <Zap className="w-4 h-4 mr-1.5" /> Motor Automacao
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="manus" data-testid="tab-manus" className="data-[state=active]:bg-violet-500/10 data-[state=active]:text-violet-400">
<Brain className="w-4 h-4 mr-1.5" /> Manus IA
</TabsTrigger>
<TabsTrigger value="agents" data-testid="tab-agents" className="data-[state=active]:bg-cyan-500/10 data-[state=active]:text-cyan-400"> <TabsTrigger value="agents" data-testid="tab-agents" className="data-[state=active]:bg-cyan-500/10 data-[state=active]:text-cyan-400">
<Bot className="w-4 h-4 mr-1.5" /> Agentes XOS <Bot className="w-4 h-4 mr-1.5" /> Agentes XOS
</TabsTrigger> </TabsTrigger>
@ -548,11 +305,7 @@ export default function EngineRoom() {
<TabsContent value="overview"> <TabsContent value="overview">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{data.engines.map((engine) => ( {data.engines.map((engine) => (
<EngineCard <EngineCard key={engine.name} engine={engine} />
key={engine.name}
engine={engine}
onClick={engine.name === "manus-ia" ? () => setManusOpen(true) : undefined}
/>
))} ))}
</div> </div>
</TabsContent> </TabsContent>
@ -685,83 +438,6 @@ export default function EngineRoom() {
</div> </div>
</TabsContent> </TabsContent>
<TabsContent value="manus">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{data.engines.filter(e => e.name === "manus-ia").map(e => (
<EngineCard key={e.name} engine={e} onClick={() => setManusOpen(true)} />
))}
<Card className="bg-[#1a1a2e] border-[#2a2a4a]">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-gray-300 flex items-center gap-2">
<Sparkles className="w-4 h-4 text-violet-400" /> Metricas do Manus IA
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{manusMetrics ? (
<>
<div className="p-3 rounded bg-[#0d0d1a] border border-[#1a1a3a]">
<p className="text-xs text-gray-500 uppercase mb-2">Modelo & Performance</p>
<div className="grid grid-cols-3 gap-2">
<div className="text-center">
<p className="text-lg font-bold text-violet-400">{manusMetrics.model || "GPT-4o"}</p>
<p className="text-[10px] text-gray-500">Modelo</p>
</div>
<div className="text-center">
<p className="text-lg font-bold text-white">{manusMetrics.metrics?.totalCalls || 0}</p>
<p className="text-[10px] text-gray-500">Chamadas IA</p>
</div>
<div className="text-center">
<p className="text-lg font-bold text-emerald-400">{((manusMetrics.metrics?.totalTokens || 0) / 1000).toFixed(1)}k</p>
<p className="text-[10px] text-gray-500">Tokens</p>
</div>
</div>
</div>
<div className="p-3 rounded bg-[#0d0d1a] border border-[#1a1a3a]">
<p className="text-xs text-gray-500 uppercase mb-2">Capacidades Ativas</p>
<div className="grid grid-cols-2 gap-2">
<div className="flex items-center gap-2">
<span className="text-lg font-bold text-white">{manusMetrics.capabilities?.tools || 56}</span>
<span className="text-xs text-gray-400">Ferramentas</span>
</div>
<div className="flex items-center gap-2">
<span className="text-lg font-bold text-white">{manusMetrics.capabilities?.agents || 0}</span>
<span className="text-xs text-gray-400">Agentes Ativos</span>
</div>
</div>
</div>
<div className="p-3 rounded bg-[#0d0d1a] border border-[#1a1a3a]">
<p className="text-xs text-gray-500 uppercase mb-2">Uptime</p>
<p className="text-sm text-gray-300">
{manusMetrics.metrics?.uptime
? `${Math.floor(manusMetrics.metrics.uptime / 3600)}h ${Math.floor((manusMetrics.metrics.uptime % 3600) / 60)}m`
: "---"}
</p>
</div>
</>
) : (
<div className="text-center py-4 text-gray-500">
<Loader2 className="w-6 h-6 animate-spin mx-auto mb-2" />
<p className="text-xs">Carregando metricas...</p>
</div>
)}
</CardContent>
</Card>
</div>
<div className="mt-4 p-4 rounded-lg bg-[#1a1a2e] border border-[#2a2a4a]">
<h3 className="text-sm font-medium text-gray-300 mb-2 flex items-center gap-2">
<Brain className="w-4 h-4 text-violet-400" /> Capacidades do Manus IA
</h3>
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
{["GPT-4o (Cerebro Central)", "56 Ferramentas Integradas", "Knowledge Graph", "Busca Semantica", "Pipeline de Dev Autonomo", "Orquestrador de Agentes", "Leitura/Escrita de Arquivos", "Execucao de Comandos", "Web Research", "Analise de Codigo", "Validacao TypeScript", "Memoria Evolutiva"].map(cap => (
<div key={cap} className="flex items-center gap-2 p-2 rounded bg-[#0d0d1a]">
<CheckCircle className="w-3.5 h-3.5 text-violet-400 shrink-0" />
<span className="text-xs text-gray-300">{cap}</span>
</div>
))}
</div>
</div>
</TabsContent>
<TabsContent value="agents"> <TabsContent value="agents">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-medium text-gray-300 flex items-center gap-2"> <h3 className="text-sm font-medium text-gray-300 flex items-center gap-2">
@ -813,15 +489,13 @@ export default function EngineRoom() {
<p className="text-amber-400">{"├─────────────────────────────────────────────────────┤"}</p> <p className="text-amber-400">{"├─────────────────────────────────────────────────────┤"}</p>
<p>{"│ Express.js (5000) ─── Orquestracao + API Gateway │"}</p> <p>{"│ Express.js (5000) ─── Orquestracao + API Gateway │"}</p>
<p className="text-amber-400">{"├─────────────────────────────────────────────────────┤"}</p> <p className="text-amber-400">{"├─────────────────────────────────────────────────────┤"}</p>
<p className="text-violet-400 font-bold">{"│ Manus IA (5000) ─── GPT-4o + 56 Tools (Cerebro)│"}</p>
<p className="text-amber-400">{"├─────────────────────────────────────────────────────┤"}</p>
<p className="text-blue-400">{"│ Plus ERP (8080) ─── Laravel/PHP - ERP Completo │"}</p> <p className="text-blue-400">{"│ Plus ERP (8080) ─── Laravel/PHP - ERP Completo │"}</p>
<p className="text-amber-300">{"│ Contabil (8003) ─── FastAPI - DRE/Balancete │"}</p> <p className="text-amber-300">{"│ Contabil (8003) ─── FastAPI - DRE/Balancete │"}</p>
<p className="text-amber-300">{"│ Fiscal (8002) ─── FastAPI - NF-e/SEFAZ │"}</p> <p className="text-amber-300">{"│ Fiscal (8002) ─── FastAPI - NF-e/SEFAZ │"}</p>
<p className="text-emerald-400">{"│ BI Engine (8004) ─── FastAPI - SQL/Charts/Cache │"}</p> <p className="text-emerald-400">{"│ BI Engine (8004) ─── FastAPI - SQL/Charts/Cache │"}</p>
<p className="text-purple-400">{"│ Automacao (8005) ─── FastAPI - Scheduler/Events │"}</p> <p className="text-purple-400">{"│ Automacao (8005) ─── FastAPI - Scheduler/Events │"}</p>
<p className="text-amber-400">{"├─────────────────────────────────────────────────────┤"}</p> <p className="text-amber-400">{"├─────────────────────────────────────────────────────┤"}</p>
<p className="text-cyan-400">{"│ XOS Agents ─── 6 Agentes via ManusIntel. │"}</p> <p className="text-cyan-400">{"│ XOS Agents ─── 6 Agentes Autonomos │"}</p>
<p className="text-amber-400">{"└─────────────────────────────────────────────────────┘"}</p> <p className="text-amber-400">{"└─────────────────────────────────────────────────────┘"}</p>
</div> </div>
</div> </div>
@ -832,8 +506,6 @@ export default function EngineRoom() {
<p>Nao foi possivel carregar o status dos motores</p> <p>Nao foi possivel carregar o status dos motores</p>
</div> </div>
)} )}
<ManusStructurePanel open={manusOpen} onOpenChange={setManusOpen} metrics={manusMetrics} />
</div> </div>
</div> </div>
); );

View File

@ -137,12 +137,12 @@ export default function EngineeringHub() {
{isErpConnected ? ( {isErpConnected ? (
<Badge className="bg-green-100 text-green-700 border-green-300 flex items-center gap-1.5 px-3 py-1.5"> <Badge className="bg-green-100 text-green-700 border-green-300 flex items-center gap-1.5 px-3 py-1.5">
<Link2 className="h-3.5 w-3.5" /> <Link2 className="h-3.5 w-3.5" />
Sincronizado com Arcádia SOE Sincronizado com Arcádia ERP
</Badge> </Badge>
) : ( ) : (
<Badge variant="secondary" className="flex items-center gap-1.5 px-3 py-1.5"> <Badge variant="secondary" className="flex items-center gap-1.5 px-3 py-1.5">
<AlertTriangle className="h-3.5 w-3.5" /> <AlertTriangle className="h-3.5 w-3.5" />
Arcádia SOE não conectado Arcádia ERP não conectado
</Badge> </Badge>
)} )}
<Button variant="outline" size="sm" data-testid="btn-refresh-engineering"> <Button variant="outline" size="sm" data-testid="btn-refresh-engineering">

View File

@ -1,5 +1,4 @@
import { BrowserFrame } from "@/components/Browser/BrowserFrame"; import { BrowserFrame } from "@/components/Browser/BrowserFrame";
import { SupersetDashboard } from "@/components/SupersetDashboard";
import { useState } from "react"; import { useState } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@ -41,7 +40,7 @@ import {
import { format } from "date-fns"; import { format } from "date-fns";
import { ptBR } from "date-fns/locale"; import { ptBR } from "date-fns/locale";
type TabType = "dashboard" | "payables" | "receivables" | "accounts" | "transactions" | "settings" | "analise"; type TabType = "dashboard" | "payables" | "receivables" | "accounts" | "transactions" | "settings";
interface BankAccount { interface BankAccount {
id: number; id: number;
@ -976,14 +975,13 @@ export default function Financeiro() {
</div> </div>
<Tabs value={activeTab} onValueChange={(v) => setActiveTab(v as TabType)} className="space-y-4"> <Tabs value={activeTab} onValueChange={(v) => setActiveTab(v as TabType)} className="space-y-4">
<TabsList className="grid grid-cols-7 w-full max-w-4xl"> <TabsList className="grid grid-cols-6 w-full max-w-4xl">
<TabsTrigger value="dashboard" data-testid="tab-dashboard">Dashboard</TabsTrigger> <TabsTrigger value="dashboard" data-testid="tab-dashboard">Dashboard</TabsTrigger>
<TabsTrigger value="payables" data-testid="tab-payables">A Pagar</TabsTrigger> <TabsTrigger value="payables" data-testid="tab-payables">A Pagar</TabsTrigger>
<TabsTrigger value="receivables" data-testid="tab-receivables">A Receber</TabsTrigger> <TabsTrigger value="receivables" data-testid="tab-receivables">A Receber</TabsTrigger>
<TabsTrigger value="accounts" data-testid="tab-accounts">Contas</TabsTrigger> <TabsTrigger value="accounts" data-testid="tab-accounts">Contas</TabsTrigger>
<TabsTrigger value="transactions" data-testid="tab-transactions">Extrato</TabsTrigger> <TabsTrigger value="transactions" data-testid="tab-transactions">Extrato</TabsTrigger>
<TabsTrigger value="settings" data-testid="tab-settings">Configurações</TabsTrigger> <TabsTrigger value="settings" data-testid="tab-settings">Configurações</TabsTrigger>
<TabsTrigger value="analise" data-testid="tab-analise">Análise BI</TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="dashboard">{renderDashboard()}</TabsContent> <TabsContent value="dashboard">{renderDashboard()}</TabsContent>
@ -992,9 +990,6 @@ export default function Financeiro() {
<TabsContent value="accounts">{renderAccounts()}</TabsContent> <TabsContent value="accounts">{renderAccounts()}</TabsContent>
<TabsContent value="transactions">{renderTransactions()}</TabsContent> <TabsContent value="transactions">{renderTransactions()}</TabsContent>
<TabsContent value="settings">{renderSettings()}</TabsContent> <TabsContent value="settings">{renderSettings()}</TabsContent>
<TabsContent value="analise">
<SupersetDashboard dashboardId="financial-overview" />
</TabsContent>
</Tabs> </Tabs>
<Dialog open={showAccountDialog} onOpenChange={setShowAccountDialog}> <Dialog open={showAccountDialog} onOpenChange={setShowAccountDialog}>

View File

@ -1,5 +1,4 @@
import { BrowserFrame } from "@/components/Browser/BrowserFrame"; import { BrowserFrame } from "@/components/Browser/BrowserFrame";
import { SupersetDashboard } from "@/components/SupersetDashboard";
import { import {
Search, Grid, Settings, FileText, Plus, Star, Search, Grid, Settings, FileText, Plus, Star,
ChevronRight, Bell, Clock, CheckSquare, ChevronRight, Bell, Clock, CheckSquare,
@ -120,7 +119,7 @@ export default function Home() {
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false); const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
const [editingApp, setEditingApp] = useState<Application | null>(null); const [editingApp, setEditingApp] = useState<Application | null>(null);
const [newNoteContent, setNewNoteContent] = useState(""); const [newNoteContent, setNewNoteContent] = useState("");
const isAdmin = user?.role === "admin" || user?.role === "master"; const isAdmin = user?.role === "admin";
const [newApp, setNewApp] = useState({ const [newApp, setNewApp] = useState({
name: "", name: "",
category: "", category: "",
@ -1171,15 +1170,6 @@ export default function Home() {
)} )}
</div> </div>
</div> </div>
{/* Arcádia Insights — Dashboard executivo */}
<div className="mt-6 px-6 pb-6">
<div className="flex items-center justify-between mb-3">
<h2 className="font-semibold text-slate-800">Arcádia Insights</h2>
<a href="/bi" className="text-xs text-indigo-500 hover:underline">Abrir BI completo </a>
</div>
<SupersetDashboard dashboardId="executive-summary" height={400} />
</div>
</div> </div>
</main> </main>

View File

@ -275,7 +275,7 @@ export default function Migration() {
<div className="mt-2 border-2 border-dashed rounded-lg p-6 text-center"> <div className="mt-2 border-2 border-dashed rounded-lg p-6 text-center">
<input <input
type="file" type="file"
accept=".zip,.json,.csv,.rar,.sql,.sql.gz" accept=".zip,.json,.csv"
className="hidden" className="hidden"
id="backup-file" id="backup-file"
onChange={e => setUploadFile(e.target.files?.[0] || null)} onChange={e => setUploadFile(e.target.files?.[0] || null)}
@ -286,7 +286,7 @@ export default function Migration() {
<p className="mt-2 text-sm text-gray-500"> <p className="mt-2 text-sm text-gray-500">
{uploadFile ? uploadFile.name : "Clique para selecionar"} {uploadFile ? uploadFile.name : "Clique para selecionar"}
</p> </p>
<p className="text-xs text-gray-400 mt-1">ZIP (MongoDB), RAR, SQL, SQL.GZ, JSON ou CSV</p> <p className="text-xs text-gray-400 mt-1">ZIP (MongoDB), JSON ou CSV</p>
</label> </label>
</div> </div>
</div> </div>

View File

@ -1855,7 +1855,7 @@ export default function ProcessCompass() {
data-testid="search-projects" data-testid="search-projects"
/> />
</div> </div>
<Button onClick={() => setShowNewProjectDialog(true)} data-testid="btn-new-project"> <Button onClick={() => setShowNewProjectDialog(true)} disabled={clients.length === 0} data-testid="btn-new-project">
<Plus className="h-4 w-4 mr-2" /> Novo Projeto <Plus className="h-4 w-4 mr-2" /> Novo Projeto
</Button> </Button>
</div> </div>
@ -4600,32 +4600,18 @@ export default function ProcessCompass() {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="project-client">Cliente</Label> <Label htmlFor="project-client">Cliente</Label>
{clients.length === 0 ? ( <Select name="clientId" required>
<div className="rounded-md border border-yellow-200 bg-yellow-50 p-3 text-sm text-yellow-800"> <SelectTrigger data-testid="select-project-client">
Nenhum cliente cadastrado.{" "} <SelectValue placeholder="Selecione um cliente" />
<button </SelectTrigger>
type="button" <SelectContent>
className="underline font-medium" {clients.map(client => (
onClick={() => { setShowNewProjectDialog(false); setShowNewClientDialog(true); }} <SelectItem key={client.id} value={client.id.toString()}>
> {client.name}
Criar cliente </SelectItem>
</button>{" "} ))}
antes de criar um projeto. </SelectContent>
</div> </Select>
) : (
<Select name="clientId" required>
<SelectTrigger data-testid="select-project-client">
<SelectValue placeholder="Selecione um cliente" />
</SelectTrigger>
<SelectContent>
{clients.map(client => (
<SelectItem key={client.id} value={client.id.toString()}>
{client.name}
</SelectItem>
))}
</SelectContent>
</Select>
)}
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="project-description">Descrição</Label> <Label htmlFor="project-description">Descrição</Label>

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,8 @@ import { BrowserFrame } from "@/components/Browser/BrowserFrame";
import { import {
Users, Building2, TrendingUp, MessageSquare, Ticket, Zap, LayoutGrid, Users, Building2, TrendingUp, MessageSquare, Ticket, Zap, LayoutGrid,
ChevronRight, PlusCircle, Filter, Search, Bell, Calendar, Target, ChevronRight, PlusCircle, Filter, Search, Bell, Calendar, Target,
BarChart3, DollarSign, Clock, AlertCircle, Phone, Mail, ArrowUpRight BarChart3, DollarSign, Clock, AlertCircle, Phone, Mail, ArrowUpRight,
Activity, Hash, Shield
} from "lucide-react"; } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
@ -15,6 +16,7 @@ import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
interface XosStats { interface XosStats {
total_contacts: number; total_contacts: number;
@ -69,12 +71,10 @@ interface Activity {
export default function XosCentral() { export default function XosCentral() {
const [activeTab, setActiveTab] = useState("dashboard"); const [activeTab, setActiveTab] = useState("dashboard");
const [isNewContactOpen, setIsNewContactOpen] = useState(false); const [isNewContactOpen, setIsNewContactOpen] = useState(false);
const [newContact, setNewContact] = useState({ name: "", email: "", phone: "", company: "", position: "" });
const [isNewActivityOpen, setIsNewActivityOpen] = useState(false); const [isNewActivityOpen, setIsNewActivityOpen] = useState(false);
const [newActivity, setNewActivity] = useState({ type: "task", title: "", description: "", due_at: "", priority: "normal" }); const [newContact, setNewContact] = useState({ name: "", email: "", phone: "", company: "", position: "" });
const [isNewSelectorOpen, setIsNewSelectorOpen] = useState(false); const [newActivity, setNewActivity] = useState({ type: "task", title: "", due_at: "", priority: "medium" });
const [isNewDealOpen, setIsNewDealOpen] = useState(false);
const [newDeal, setNewDeal] = useState({ title: "", pipeline_id: "1", stage_id: "1", value: "" });
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const createContactMutation = useMutation({ const createContactMutation = useMutation({
@ -95,24 +95,6 @@ export default function XosCentral() {
}, },
}); });
const createDealMutation = useMutation({
mutationFn: async (data: typeof newDeal) => {
const res = await fetch("/api/xos/deals", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
if (!res.ok) throw new Error("Erro ao criar negócio");
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/xos/deals"] });
queryClient.invalidateQueries({ queryKey: ["/api/xos/stats"] });
setIsNewDealOpen(false);
setNewDeal({ title: "", pipeline_id: "1", stage_id: "1", value: "" });
},
});
const createActivityMutation = useMutation({ const createActivityMutation = useMutation({
mutationFn: async (data: typeof newActivity) => { mutationFn: async (data: typeof newActivity) => {
const res = await fetch("/api/xos/activities", { const res = await fetch("/api/xos/activities", {
@ -127,7 +109,7 @@ export default function XosCentral() {
queryClient.invalidateQueries({ queryKey: ["/api/xos/activities"] }); queryClient.invalidateQueries({ queryKey: ["/api/xos/activities"] });
queryClient.invalidateQueries({ queryKey: ["/api/xos/stats"] }); queryClient.invalidateQueries({ queryKey: ["/api/xos/stats"] });
setIsNewActivityOpen(false); setIsNewActivityOpen(false);
setNewActivity({ type: "task", title: "", description: "", due_at: "", priority: "normal" }); setNewActivity({ type: "task", title: "", due_at: "", priority: "medium" });
}, },
}); });
@ -161,6 +143,9 @@ export default function XosCentral() {
{ id: "automations", name: "Automações", icon: Zap, href: "/xos/automations", color: "bg-violet-100 text-violet-600", description: "Workflows automáticos" }, { id: "automations", name: "Automações", icon: Zap, href: "/xos/automations", color: "bg-violet-100 text-violet-600", description: "Workflows automáticos" },
{ id: "campaigns", name: "Campanhas", icon: Target, href: "/xos/campaigns", color: "bg-pink-100 text-pink-600", description: "Marketing automation" }, { id: "campaigns", name: "Campanhas", icon: Target, href: "/xos/campaigns", color: "bg-pink-100 text-pink-600", description: "Marketing automation" },
{ id: "sites", name: "Sites", icon: LayoutGrid, href: "/xos/sites", color: "bg-cyan-100 text-cyan-600", description: "Site builder" }, { id: "sites", name: "Sites", icon: LayoutGrid, href: "/xos/sites", color: "bg-cyan-100 text-cyan-600", description: "Site builder" },
{ id: "supervisor", name: "Supervisor", icon: Activity, href: "/xos/supervisor", color: "bg-indigo-100 text-indigo-600", description: "Monitor em tempo real" },
{ id: "reports", name: "Relatórios", icon: BarChart3, href: "/xos/reports", color: "bg-emerald-100 text-emerald-600", description: "CSAT, SLA e KPIs" },
{ id: "protocols", name: "Protocolos", icon: Hash, href: "/xos/protocols", color: "bg-teal-100 text-teal-600", description: "Rastreamento de atendimentos" },
]; ];
const getStatusColor = (status: string) => { const getStatusColor = (status: string) => {
@ -241,9 +226,9 @@ export default function XosCentral() {
</span> </span>
)} )}
</Button> </Button>
<Button data-testid="button-new-contact" onClick={() => setIsNewSelectorOpen(true)}> <Button data-testid="button-new-contact" onClick={() => setIsNewContactOpen(true)}>
<PlusCircle className="h-4 w-4 mr-2" /> <PlusCircle className="h-4 w-4 mr-2" />
Novo Novo Contato
</Button> </Button>
</div> </div>
</div> </div>
@ -328,7 +313,7 @@ export default function XosCentral() {
{/* Modules Grid */} {/* Modules Grid */}
<div> <div>
<h2 className="text-lg font-semibold text-slate-800 mb-4">Módulos XOS</h2> <h2 className="text-lg font-semibold text-slate-800 mb-4">Módulos XOS</h2>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4"> <div className="grid grid-cols-3 md:grid-cols-4 lg:grid-cols-9 gap-4">
{modules.map((mod) => ( {modules.map((mod) => (
<Link key={mod.id} href={mod.href}> <Link key={mod.id} href={mod.href}>
<Card className="hover:shadow-lg transition-all cursor-pointer group" data-testid={`card-module-${mod.id}`}> <Card className="hover:shadow-lg transition-all cursor-pointer group" data-testid={`card-module-${mod.id}`}>
@ -584,175 +569,129 @@ export default function XosCentral() {
</Tabs> </Tabs>
</main> </main>
</div> </div>
{/* Modal: Novo Contato */}
{/* Seletor: o que deseja criar? */} <Dialog open={isNewContactOpen} onOpenChange={setIsNewContactOpen}>
<Dialog open={isNewSelectorOpen} onOpenChange={setIsNewSelectorOpen}> <DialogContent>
<DialogContent className="max-w-sm"> <DialogHeader>
<DialogHeader> <DialogTitle>Novo Contato</DialogTitle>
<DialogTitle>O que deseja criar?</DialogTitle> </DialogHeader>
</DialogHeader> <div className="space-y-4 py-4">
<div className="grid gap-3 pt-2"> <div className="space-y-2">
<button <Label>Nome *</Label>
className="flex items-center gap-4 p-4 rounded-lg border hover:bg-blue-50 hover:border-blue-300 transition-colors text-left" <Input
onClick={() => { setIsNewSelectorOpen(false); setIsNewContactOpen(true); }} value={newContact.name}
> onChange={(e) => setNewContact({ ...newContact, name: e.target.value })}
<div className="bg-blue-100 p-2 rounded-lg"><Users className="h-5 w-5 text-blue-600" /></div> placeholder="Nome do contato"
<div><p className="font-semibold text-slate-800">Contato</p><p className="text-sm text-slate-500">Adicionar lead ou cliente</p></div> />
</button> </div>
<button <div className="space-y-2">
className="flex items-center gap-4 p-4 rounded-lg border hover:bg-emerald-50 hover:border-emerald-300 transition-colors text-left" <Label>Email</Label>
onClick={() => { setIsNewSelectorOpen(false); setIsNewDealOpen(true); }} <Input
> type="email"
<div className="bg-emerald-100 p-2 rounded-lg"><TrendingUp className="h-5 w-5 text-emerald-600" /></div> value={newContact.email}
<div><p className="font-semibold text-slate-800">Negócio</p><p className="text-sm text-slate-500">Criar oportunidade de venda</p></div> onChange={(e) => setNewContact({ ...newContact, email: e.target.value })}
</button> placeholder="email@exemplo.com"
<button />
className="flex items-center gap-4 p-4 rounded-lg border hover:bg-violet-50 hover:border-violet-300 transition-colors text-left" </div>
onClick={() => { setIsNewSelectorOpen(false); setIsNewActivityOpen(true); }} <div className="space-y-2">
> <Label>Telefone</Label>
<div className="bg-violet-100 p-2 rounded-lg"><Calendar className="h-5 w-5 text-violet-600" /></div> <Input
<div><p className="font-semibold text-slate-800">Atividade</p><p className="text-sm text-slate-500">Agendar tarefa ou reunião</p></div> value={newContact.phone}
</button> onChange={(e) => setNewContact({ ...newContact, phone: e.target.value })}
</div> placeholder="(00) 00000-0000"
</DialogContent> />
</Dialog> </div>
<div className="grid grid-cols-2 gap-4">
{/* Dialog Novo Negócio */} <div className="space-y-2">
<Dialog open={isNewDealOpen} onOpenChange={setIsNewDealOpen}> <Label>Empresa</Label>
<DialogContent> <Input
<DialogHeader> value={newContact.company}
<DialogTitle>Novo Negócio</DialogTitle> onChange={(e) => setNewContact({ ...newContact, company: e.target.value })}
</DialogHeader> placeholder="Empresa"
<div className="space-y-4 pt-2"> />
<div> </div>
<Label>Título *</Label> <div className="space-y-2">
<Input placeholder="Ex: Proposta comercial Empresa X" value={newDeal.title} onChange={(e) => setNewDeal({ ...newDeal, title: e.target.value })} /> <Label>Cargo</Label>
</div> <Input
<div> value={newContact.position}
<Label>Estágio</Label> onChange={(e) => setNewContact({ ...newContact, position: e.target.value })}
<select className="w-full mt-1 border rounded-md px-3 py-2 text-sm" value={newDeal.stage_id} onChange={(e) => setNewDeal({ ...newDeal, stage_id: e.target.value })}> placeholder="Cargo"
<option value="1">Prospecção</option> />
<option value="2">Qualificação</option> </div>
<option value="3">Proposta</option> </div>
<option value="4">Negociação</option>
<option value="5">Ganho</option>
<option value="6">Perdido</option>
</select>
</div>
<div>
<Label>Valor (R$)</Label>
<Input type="number" placeholder="0,00" value={newDeal.value} onChange={(e) => setNewDeal({ ...newDeal, value: e.target.value })} />
</div>
<div className="flex gap-2 pt-2">
<Button variant="outline" className="flex-1" onClick={() => setIsNewDealOpen(false)}>Cancelar</Button>
<Button <Button
className="flex-1" className="w-full"
onClick={() => createDealMutation.mutate(newDeal)}
disabled={!newDeal.title || createDealMutation.isPending}
>
{createDealMutation.isPending ? "Salvando..." : "Salvar Negócio"}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
<Dialog open={isNewContactOpen} onOpenChange={setIsNewContactOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Novo Contato</DialogTitle>
</DialogHeader>
<div className="space-y-4 pt-2">
<div>
<Label>Nome *</Label>
<Input placeholder="Nome completo" value={newContact.name} onChange={(e) => setNewContact({ ...newContact, name: e.target.value })} />
</div>
<div>
<Label>E-mail</Label>
<Input placeholder="email@empresa.com" value={newContact.email} onChange={(e) => setNewContact({ ...newContact, email: e.target.value })} />
</div>
<div>
<Label>Telefone / WhatsApp</Label>
<Input placeholder="(11) 99999-9999" value={newContact.phone} onChange={(e) => setNewContact({ ...newContact, phone: e.target.value })} />
</div>
<div>
<Label>Empresa</Label>
<Input placeholder="Nome da empresa" value={newContact.company} onChange={(e) => setNewContact({ ...newContact, company: e.target.value })} />
</div>
<div>
<Label>Cargo</Label>
<Input placeholder="Cargo ou função" value={newContact.position} onChange={(e) => setNewContact({ ...newContact, position: e.target.value })} />
</div>
<div className="flex gap-2 pt-2">
<Button variant="outline" className="flex-1" onClick={() => setIsNewContactOpen(false)}>Cancelar</Button>
<Button
className="flex-1"
onClick={() => createContactMutation.mutate(newContact)} onClick={() => createContactMutation.mutate(newContact)}
disabled={!newContact.name || createContactMutation.isPending} disabled={!newContact.name || createContactMutation.isPending}
> >
{createContactMutation.isPending ? "Salvando..." : "Salvar Contato"} {createContactMutation.isPending ? "Salvando..." : "Criar Contato"}
</Button> </Button>
</div> </div>
</div> </DialogContent>
</DialogContent> </Dialog>
</Dialog>
<Dialog open={isNewActivityOpen} onOpenChange={setIsNewActivityOpen}> {/* Modal: Nova Atividade */}
<DialogContent> <Dialog open={isNewActivityOpen} onOpenChange={setIsNewActivityOpen}>
<DialogHeader> <DialogContent>
<DialogTitle>Nova Atividade</DialogTitle> <DialogHeader>
</DialogHeader> <DialogTitle>Nova Atividade</DialogTitle>
<div className="space-y-4 pt-2"> </DialogHeader>
<div> <div className="space-y-4 py-4">
<Label>Tipo</Label> <div className="space-y-2">
<select <Label>Título *</Label>
className="w-full mt-1 border rounded-md px-3 py-2 text-sm" <Input
value={newActivity.type} value={newActivity.title}
onChange={(e) => setNewActivity({ ...newActivity, type: e.target.value })} onChange={(e) => setNewActivity({ ...newActivity, title: e.target.value })}
> placeholder="Descrição da atividade"
<option value="task">Tarefa</option> />
<option value="call">Ligação</option> </div>
<option value="email">E-mail</option> <div className="grid grid-cols-2 gap-4">
<option value="meeting">Reunião</option> <div className="space-y-2">
</select> <Label>Tipo</Label>
</div> <Select value={newActivity.type} onValueChange={(v) => setNewActivity({ ...newActivity, type: v })}>
<div> <SelectTrigger>
<Label>Título *</Label> <SelectValue />
<Input placeholder="Descreva a atividade" value={newActivity.title} onChange={(e) => setNewActivity({ ...newActivity, title: e.target.value })} /> </SelectTrigger>
</div> <SelectContent>
<div> <SelectItem value="task">Tarefa</SelectItem>
<Label>Descrição</Label> <SelectItem value="call">Ligação</SelectItem>
<Input placeholder="Detalhes adicionais" value={newActivity.description} onChange={(e) => setNewActivity({ ...newActivity, description: e.target.value })} /> <SelectItem value="email">E-mail</SelectItem>
</div> <SelectItem value="meeting">Reunião</SelectItem>
<div> </SelectContent>
<Label>Data prevista</Label> </Select>
<Input type="datetime-local" value={newActivity.due_at} onChange={(e) => setNewActivity({ ...newActivity, due_at: e.target.value })} /> </div>
</div> <div className="space-y-2">
<div> <Label>Prioridade</Label>
<Label>Prioridade</Label> <Select value={newActivity.priority} onValueChange={(v) => setNewActivity({ ...newActivity, priority: v })}>
<select <SelectTrigger>
className="w-full mt-1 border rounded-md px-3 py-2 text-sm" <SelectValue />
value={newActivity.priority} </SelectTrigger>
onChange={(e) => setNewActivity({ ...newActivity, priority: e.target.value })} <SelectContent>
> <SelectItem value="low">Baixa</SelectItem>
<option value="low">Baixa</option> <SelectItem value="medium">Média</SelectItem>
<option value="normal">Normal</option> <SelectItem value="high">Alta</SelectItem>
<option value="high">Alta</option> </SelectContent>
</select> </Select>
</div> </div>
<div className="flex gap-2 pt-2"> </div>
<Button variant="outline" className="flex-1" onClick={() => setIsNewActivityOpen(false)}>Cancelar</Button> <div className="space-y-2">
<Label>Data prevista</Label>
<Input
type="datetime-local"
value={newActivity.due_at}
onChange={(e) => setNewActivity({ ...newActivity, due_at: e.target.value })}
/>
</div>
<Button <Button
className="flex-1" className="w-full"
onClick={() => createActivityMutation.mutate(newActivity)} onClick={() => createActivityMutation.mutate(newActivity)}
disabled={!newActivity.title || createActivityMutation.isPending} disabled={!newActivity.title || createActivityMutation.isPending}
> >
{createActivityMutation.isPending ? "Salvando..." : "Salvar Atividade"} {createActivityMutation.isPending ? "Salvando..." : "Criar Atividade"}
</Button> </Button>
</div> </div>
</div> </DialogContent>
</DialogContent> </Dialog>
</Dialog>
</BrowserFrame> </BrowserFrame>
); );
} }

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,7 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
const browserIcon = "/arcadia_suite_icon.png"; import browserIcon from "/favicon.png";
import { Loader2 } from "lucide-react"; import { Loader2 } from "lucide-react";
export default function AuthPage() { export default function AuthPage() {

View File

@ -77,15 +77,6 @@ services:
ERPNEXT_URL: ${ERPNEXT_URL:-http://erpnext:8080} ERPNEXT_URL: ${ERPNEXT_URL:-http://erpnext:8080}
ERPNEXT_API_KEY: ${ERPNEXT_API_KEY:-} ERPNEXT_API_KEY: ${ERPNEXT_API_KEY:-}
ERPNEXT_API_SECRET: ${ERPNEXT_API_SECRET:-} ERPNEXT_API_SECRET: ${ERPNEXT_API_SECRET:-}
# ── MiroFlow (container) ─────────────────────────────────────────────
MIROFLOW_HOST: "miroflow"
MIROFLOW_PORT: "8006"
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:5000/api/health || exit 1"]
interval: 30s
timeout: 10s
retries: 5
start_period: 60s
ports: ports:
- "5000:5000" - "5000:5000"
depends_on: depends_on:
@ -151,6 +142,38 @@ services:
networks: networks:
- arcadia-internal - arcadia-internal
# ── Apache Superset (BI) ─────────────────────────────────────────────────────
# Perfil `bi` — suba com: docker compose --profile bi up
superset:
image: apache/superset:4.1.0
restart: always
profiles: [bi]
environment:
SUPERSET_SECRET_KEY: ${SUPERSET_SECRET_KEY}
DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/arcadia_superset
ARCADIA_DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/${PGDATABASE:-arcadia}
SUPERSET_ADMIN_USER: ${SUPERSET_ADMIN_USER:-admin}
SUPERSET_ADMIN_EMAIL: ${SUPERSET_ADMIN_EMAIL:-admin@arcadia.app}
SUPERSET_ADMIN_PASSWORD: ${SUPERSET_ADMIN_PASSWORD}
SUPERSET_WEBSERVER_PORT: "8088"
PYTHONPATH: /app/pythonpath
volumes:
- ./docker/superset/superset_config.py:/app/pythonpath/superset_config.py:ro
- ./docker/superset/init.sh:/app/docker/init.sh:ro
- superset_home:/app/superset_home
depends_on:
db:
condition: service_healthy
command: ["/bin/bash", "/app/docker/init.sh"]
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8088/health"]
interval: 30s
timeout: 10s
retries: 5
start_period: 60s
networks:
- arcadia-internal
# ── ERPNext (Frappe Framework) ─────────────────────────────────────────────── # ── ERPNext (Frappe Framework) ───────────────────────────────────────────────
# Perfil `erpnext` — suba com: docker compose --profile erpnext up # Perfil `erpnext` — suba com: docker compose --profile erpnext up
erpnext-db: erpnext-db:
@ -203,19 +226,135 @@ services:
networks: networks:
- arcadia-internal - arcadia-internal
# ── Arcádia Plus (Laravel + MySQL) ──────────────────────────────────────────
# ── MiroFlow (agente científico) ───────────────────────────────────────────── # Perfil `plus` — suba com: docker compose --profile plus up
miroflow: plus-db:
build: image: mysql:8.0
context: .
dockerfile: Dockerfile.miroflow
restart: always restart: always
profiles: [plus]
environment: environment:
MIROFLOW_PORT: "8006" MYSQL_ROOT_PASSWORD: ${PLUS_DB_ROOT_PASSWORD}
OLLAMA_BASE_URL: "http://ollama-ia1upsekrad96at5hq97e4qa:11434" MYSQL_DATABASE: ${PLUS_DB_DATABASE:-arcadia_plus}
MYSQL_USER: ${PLUS_DB_USER:-plus}
MYSQL_PASSWORD: ${PLUS_DB_PASSWORD}
volumes:
- plus_db:/var/lib/mysql
command: --default-authentication-plugin=mysql_native_password
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "--password=${PLUS_DB_ROOT_PASSWORD}"]
interval: 10s
timeout: 5s
retries: 10
networks:
- arcadia-internal
plus:
image: ${PLUS_IMAGE:-php:8.3-apache}
restart: always
profiles: [plus]
environment:
APP_ENV: production
APP_KEY: ${PLUS_APP_KEY}
APP_URL: https://${DOMAIN}/plus
DB_HOST: plus-db
DB_DATABASE: ${PLUS_DB_DATABASE:-arcadia_plus}
DB_USERNAME: ${PLUS_DB_USER:-plus}
DB_PASSWORD: ${PLUS_DB_PASSWORD}
SESSION_DRIVER: redis
REDIS_HOST: redis
ARCADIA_URL: https://${DOMAIN}
ARCADIA_SSO_SECRET: ${SSO_SECRET}
volumes:
- plus_storage:/var/www/html/storage
depends_on:
plus-db:
condition: service_healthy
networks:
- arcadia-internal
# ── Apache Superset (BI) ─────────────────────────────────────────────────────
# Perfil `bi` — suba com: docker compose --profile bi up
superset:
image: apache/superset:4.1.0
restart: always
profiles: [bi]
environment:
SUPERSET_SECRET_KEY: ${SUPERSET_SECRET_KEY}
DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/arcadia_superset
ARCADIA_DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/${PGDATABASE:-arcadia}
SUPERSET_ADMIN_USER: ${SUPERSET_ADMIN_USER:-admin}
SUPERSET_ADMIN_EMAIL: ${SUPERSET_ADMIN_EMAIL:-admin@arcadia.app}
SUPERSET_ADMIN_PASSWORD: ${SUPERSET_ADMIN_PASSWORD}
SUPERSET_WEBSERVER_PORT: "8088"
PYTHONPATH: /app/pythonpath
volumes:
- ./docker/superset/superset_config.py:/app/pythonpath/superset_config.py:ro
- ./docker/superset/init.sh:/app/docker/init.sh:ro
- superset_home:/app/superset_home
depends_on:
db:
condition: service_healthy
command: ["/bin/bash", "/app/docker/init.sh"]
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8088/health"]
interval: 30s
timeout: 10s
retries: 5
start_period: 60s
networks:
- arcadia-internal
# ── ERPNext (Frappe Framework) ───────────────────────────────────────────────
# Perfil `erpnext` — suba com: docker compose --profile erpnext up
erpnext-db:
image: mariadb:10.6
restart: always
profiles: [erpnext]
environment:
MYSQL_ROOT_PASSWORD: ${ERPNEXT_DB_ROOT_PASSWORD}
MYSQL_DATABASE: _frappe
MYSQL_USER: frappe
MYSQL_PASSWORD: ${ERPNEXT_DB_PASSWORD}
volumes:
- erpnext_db:/var/lib/mysql
command: >
--character-set-server=utf8mb4
--collation-server=utf8mb4_unicode_ci
--skip-character-set-client-handshake
--skip-innodb-read-only-compressed
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "--password=${ERPNEXT_DB_ROOT_PASSWORD}"]
interval: 10s
timeout: 5s
retries: 10
start_period: 30s
networks:
- arcadia-internal
erpnext:
image: frappe/erpnext:version-15
restart: always
profiles: [erpnext]
environment:
FRAPPE_SITE_NAME_HEADER: erpnext.local
ERPNEXT_DB_ROOT_PASSWORD: ${ERPNEXT_DB_ROOT_PASSWORD}
ERPNEXT_ADMIN_PASSWORD: ${ERPNEXT_ADMIN_PASSWORD}
volumes:
- erpnext_sites:/home/frappe/frappe-bench/sites
- erpnext_logs:/home/frappe/frappe-bench/logs
- ./docker/erpnext/init.sh:/usr/local/bin/init-erpnext.sh:ro
depends_on:
erpnext-db:
condition: service_healthy
command: ["/bin/bash", "/usr/local/bin/init-erpnext.sh"]
healthcheck:
test: ["CMD-SHELL", "curl -sf http://localhost:8080/api/method/frappe.ping || exit 1"]
interval: 30s
timeout: 10s
retries: 10
start_period: 120s
networks: networks:
- arcadia-internal - arcadia-internal
- ollama-net
# ── Microserviços Python ───────────────────────────────────────────────────── # ── Microserviços Python ─────────────────────────────────────────────────────
contabil: contabil:
@ -327,7 +466,6 @@ services:
networks: networks:
- arcadia-internal - arcadia-internal
- coolify - coolify
- ollama-net
# ── Ollama (LLMs locais — soberania total) ──────────────────────────────────── # ── Ollama (LLMs locais — soberania total) ────────────────────────────────────
# OPÇÃO A (padrão): Ollama como container Docker # OPÇÃO A (padrão): Ollama como container Docker
@ -377,10 +515,8 @@ services:
environment: environment:
SUPERSET_SECRET_KEY: ${SUPERSET_SECRET_KEY} SUPERSET_SECRET_KEY: ${SUPERSET_SECRET_KEY}
SQLALCHEMY_DATABASE_URI: postgresql://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/superset SQLALCHEMY_DATABASE_URI: postgresql://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/superset
DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/superset
PYTHONPATH: /app/pythonpath PYTHONPATH: /app/pythonpath
SUPERSET_ADMIN_USERNAME: ${SUPERSET_ADMIN_USERNAME:-admin} SUPERSET_ADMIN_USERNAME: ${SUPERSET_ADMIN_USERNAME:-admin}
SUPERSET_ADMIN_USER: ${SUPERSET_ADMIN_USERNAME:-admin}
SUPERSET_ADMIN_PASSWORD: ${SUPERSET_ADMIN_PASSWORD} SUPERSET_ADMIN_PASSWORD: ${SUPERSET_ADMIN_PASSWORD}
SUPERSET_ADMIN_EMAIL: ${SUPERSET_ADMIN_EMAIL:-admin@onboardbi.com.br} SUPERSET_ADMIN_EMAIL: ${SUPERSET_ADMIN_EMAIL:-admin@onboardbi.com.br}
ARCADIA_DATABASE_URL: postgresql+psycopg2://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/${PGDATABASE:-arcadia} ARCADIA_DATABASE_URL: postgresql+psycopg2://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/${PGDATABASE:-arcadia}
@ -388,11 +524,6 @@ services:
volumes: volumes:
- ./docker/superset/superset_config.py:/app/pythonpath/superset_config.py:ro - ./docker/superset/superset_config.py:/app/pythonpath/superset_config.py:ro
- ./docker/superset/init.sh:/app/docker/init.sh:ro - ./docker/superset/init.sh:/app/docker/init.sh:ro
- ./docker/superset/images/arcadia_logo.png:/app/superset/static/assets/images/arcadia_logo.png:ro
- ./docker/superset/images/arcadia_logo_dark.png:/app/superset/static/assets/images/arcadia_logo_dark.png:ro
- ./docker/superset/images/superset-logo-horiz.png:/app/superset/static/assets/images/superset-logo-horiz.png:ro
- ./docker/superset/images/favicon.png:/app/superset/static/assets/images/favicon.png:ro
- ./docker/superset/images/arcadia_favicon.png:/app/superset/static/assets/images/arcadia_favicon.png:ro
- superset_home:/app/superset_home - superset_home:/app/superset_home
depends_on: depends_on:
db: db:
@ -408,48 +539,22 @@ services:
- "traefik.http.routers.superset.tls=true" - "traefik.http.routers.superset.tls=true"
- "traefik.http.routers.superset.tls.certresolver=letsencrypt" - "traefik.http.routers.superset.tls.certresolver=letsencrypt"
- "traefik.http.services.superset.loadbalancer.server.port=8088" - "traefik.http.services.superset.loadbalancer.server.port=8088"
- "traefik.http.routers.superset-http.entrypoints=http"
- "traefik.http.routers.superset-http.rule=Host(`${SUPERSET_DOMAIN:-bi.onboardbi.com.br}`)"
- "traefik.http.routers.superset-http.middlewares=superset-https-redirect"
- "traefik.http.routers.superset-http.service=superset"
- "traefik.http.middlewares.superset-https-redirect.redirectscheme.scheme=https"
- "traefik.http.middlewares.superset-https-redirect.redirectscheme.permanent=true"
profiles: [bi] profiles: [bi]
# ── Neo4j (Knowledge Graph) ─────────────────────────────────────────────────
# Ativar com: docker compose --profile kg up
neo4j:
image: neo4j:5
restart: always
environment:
NEO4J_AUTH: neo4j/${NEO4J_PASSWORD:-arcadia123}
NEO4J_PLUGINS: '["apoc"]'
volumes:
- neo4j_data:/data
- neo4j_logs:/logs
ports:
- "7474:7474"
- "7687:7687"
networks:
- arcadia-internal
profiles: [kg]
networks: networks:
arcadia-internal: arcadia-internal:
driver: bridge driver: bridge
coolify: coolify:
external: true external: true
ollama-net:
external: true
name: ia1upsekrad96at5hq97e4qa
volumes: volumes:
pgdata: pgdata:
redis_data: redis_data:
ollama_models: ollama_models:
open_webui_data: open_webui_data:
neo4j_data:
neo4j_logs:
plus_db: plus_db:
plus_storage: plus_storage:
superset_home: superset_home:

237
docker-compose.prod.yml.bak Normal file
View File

@ -0,0 +1,237 @@
# ─── Arcádia Suite — Produção (Coolify) ───────────────────────────────────────
# Este arquivo é usado pelo Coolify para deploy automático.
# NÃO inclui volumes de código-fonte — só artefatos de build.
# Configurar no Coolify: Environment Variables para todas as vars ${...}
name: arcadia-prod
services:
# ── Banco de dados com pgvector ─────────────────────────────────────────────
db:
image: pgvector/pgvector:pg16
restart: always
environment:
POSTGRES_DB: ${PGDATABASE:-arcadia}
POSTGRES_USER: ${PGUSER:-arcadia}
POSTGRES_PASSWORD: ${PGPASSWORD}
volumes:
- pgdata:/var/lib/postgresql/data
- ./docker/init-pgvector.sql:/docker-entrypoint-initdb.d/01-pgvector.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${PGUSER:-arcadia}"]
interval: 10s
timeout: 5s
retries: 10
networks:
- arcadia-internal
# ── Redis ────────────────────────────────────────────────────────────────────
redis:
image: redis:7-alpine
restart: always
command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru
volumes:
- redis_data:/data
networks:
- arcadia-internal
# ── App principal ─────────────────────────────────────────────────────────
app:
build:
context: .
dockerfile: Dockerfile
restart: always
environment:
NODE_ENV: production
PORT: 5000
DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/${PGDATABASE:-arcadia}
REDIS_URL: redis://redis:6379
DOCKER_MODE: "true"
CONTABIL_PYTHON_URL: http://contabil:8003
BI_PYTHON_URL: http://bi:8004
AUTOMATION_PYTHON_URL: http://automation:8005
FISCO_PYTHON_URL: http://fisco:8002
PYTHON_SERVICE_URL: http://embeddings:8001
SESSION_SECRET: ${SESSION_SECRET}
SSO_SECRET: ${SSO_SECRET}
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
LITELLM_BASE_URL: http://litellm:4000
LITELLM_API_KEY: ${LITELLM_API_KEY}
OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:-http://ollama:11434}
# ── Manus Agent — aponta para LiteLLM como gateway unificado ──────────
# LiteLLM roteia para Ollama (local), LLMFit (fine-tuned) ou externo
AI_INTEGRATIONS_OPENAI_BASE_URL: http://litellm:4000/v1
AI_INTEGRATIONS_OPENAI_API_KEY: ${LITELLM_API_KEY}
ports:
- "5000:5000"
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
networks:
- arcadia-internal
- arcadia-public
labels:
- "traefik.enable=true"
- "traefik.http.routers.arcadia.rule=Host(`${DOMAIN}`)"
- "traefik.http.routers.arcadia.tls=true"
- "traefik.http.routers.arcadia.tls.certresolver=letsencrypt"
# ── Microserviços Python ─────────────────────────────────────────────────────
contabil:
build:
context: .
dockerfile: Dockerfile.python
restart: always
environment:
SERVICE_NAME: contabil
SERVICE_PORT: 8003
CONTABIL_PORT: 8003
DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/${PGDATABASE:-arcadia}
depends_on:
db:
condition: service_healthy
networks:
- arcadia-internal
bi:
build:
context: .
dockerfile: Dockerfile.python
restart: always
environment:
SERVICE_NAME: bi
SERVICE_PORT: 8004
BI_PORT: 8004
DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/${PGDATABASE:-arcadia}
depends_on:
db:
condition: service_healthy
networks:
- arcadia-internal
automation:
build:
context: .
dockerfile: Dockerfile.python
restart: always
environment:
SERVICE_NAME: automation
SERVICE_PORT: 8005
AUTOMATION_PORT: 8005
DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/${PGDATABASE:-arcadia}
depends_on:
db:
condition: service_healthy
networks:
- arcadia-internal
fisco:
build:
context: .
dockerfile: Dockerfile.python
restart: always
environment:
SERVICE_NAME: fisco
SERVICE_PORT: 8002
FISCO_PORT: 8002
DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/${PGDATABASE:-arcadia}
depends_on:
db:
condition: service_healthy
networks:
- arcadia-internal
embeddings:
build:
context: .
dockerfile: Dockerfile.python
restart: always
environment:
SERVICE_NAME: embeddings
SERVICE_PORT: 8001
DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/${PGDATABASE:-arcadia}
depends_on:
db:
condition: service_healthy
networks:
- arcadia-internal
# ── LiteLLM (gateway unificado de LLM — soberania dos dados) ─────────────────
# Roteia: LLMFit (fine-tuned) → Ollama (local) → externo (opt-in)
litellm:
image: ghcr.io/berriai/litellm:main-latest
restart: always
volumes:
- ./docker/litellm-config.yaml:/app/config.yaml
command: ["--config", "/app/config.yaml", "--port", "4000"]
environment:
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
LITELLM_MASTER_KEY: ${LITELLM_API_KEY}
DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/${PGDATABASE:-arcadia}
# Ollama: se instalado no host use http://host-gateway:11434
# Se usar container Docker, mantém http://ollama:11434
OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:-http://ollama:11434}
# LLMFit: URL do serviço de modelos fine-tuned
LLMFIT_BASE_URL: ${LLMFIT_BASE_URL:-}
# Providers externos opcionais (soberania: só habilitados se configurados)
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
GROQ_API_KEY: ${GROQ_API_KEY:-}
depends_on:
db:
condition: service_healthy
networks:
- arcadia-internal
# ── Ollama (LLMs locais — soberania total) ────────────────────────────────────
# OPÇÃO A (padrão): Ollama como container Docker
# OPÇÃO B: Ollama no host → comente este serviço e defina
# OLLAMA_BASE_URL=http://host-gateway:11434 nas env vars
ollama:
image: ollama/ollama:latest
restart: always
volumes:
- ollama_models:/root/.ollama
networks:
- arcadia-internal
# Remova 'profiles: [ai]' para ativar por padrão no deploy
profiles: [ai]
# ── Open WebUI (interface para Ollama + LLMFit) ───────────────────────────────
open-webui:
image: ghcr.io/open-webui/open-webui:main
restart: always
environment:
# Pode apontar para LiteLLM para ter acesso a todos os modelos via WebUI
OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:-http://ollama:11434}
OPENAI_API_BASE_URL: http://litellm:4000/v1
OPENAI_API_KEY: ${LITELLM_API_KEY}
WEBUI_SECRET_KEY: ${WEBUI_SECRET_KEY}
volumes:
- open_webui_data:/app/backend/data
depends_on:
- litellm
networks:
- arcadia-internal
- arcadia-public
labels:
- "traefik.enable=true"
- "traefik.http.routers.webui.rule=Host(`ai.${DOMAIN}`)"
- "traefik.http.routers.webui.tls=true"
- "traefik.http.routers.webui.tls.certresolver=letsencrypt"
- "traefik.http.services.webui.loadbalancer.server.port=8080"
profiles: [ai]
networks:
arcadia-internal:
driver: bridge
arcadia-public:
driver: bridge
volumes:
pgdata:
redis_data:
ollama_models:
open_webui_data:

View File

@ -1,87 +1,340 @@
version: "3.8" # ─── Arcádia Suite — Desenvolvimento Local ────────────────────────────────────
# Uso: docker compose up
# Para subir com IA local: docker compose --profile ai up
name: arcadia-dev
services: services:
# ── Banco de dados com pgvector ─────────────────────────────────────────────
db: db:
image: postgres:16-alpine image: pgvector/pgvector:pg16
restart: always restart: unless-stopped
environment: environment:
POSTGRES_USER: ${PGUSER:-arcadia} POSTGRES_DB: arcadia
POSTGRES_PASSWORD: ${PGPASSWORD:-SuaSenhaSegura} POSTGRES_USER: arcadia
POSTGRES_DB: ${PGDATABASE:-arcadia} POSTGRES_PASSWORD: arcadia123
volumes: volumes:
- pgdata:/var/lib/postgresql/data - pgdata:/var/lib/postgresql/data
- ./docker/init-pgvector.sql:/docker-entrypoint-initdb.d/01-pgvector.sql
ports: ports:
- "5432:5432" - "5432:5432"
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${PGUSER:-arcadia}"] test: ["CMD-SHELL", "pg_isready -U arcadia -d arcadia"]
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
# ── Redis (filas de jobs) ────────────────────────────────────────────────────
redis:
image: redis:7-alpine
restart: unless-stopped
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
# ── App principal (Node.js + React) ─────────────────────────────────────────
app: app:
build: . build:
restart: always context: .
dockerfile: Dockerfile
target: builder # usa stage builder em dev (hot reload via volume)
restart: unless-stopped
command: npx tsx server/index.ts
environment:
NODE_ENV: development
PORT: 5000
DATABASE_URL: postgresql://arcadia:arcadia123@db:5432/arcadia
REDIS_URL: redis://redis:6379
DOCKER_MODE: "true" # desativa spawn de processos filhos
CONTABIL_PYTHON_URL: http://contabil:8003
BI_PYTHON_URL: http://bi:8004
AUTOMATION_PYTHON_URL: http://automation:8005
FISCO_PYTHON_URL: http://fisco:8002
PYTHON_SERVICE_URL: http://embeddings:8001
SESSION_SECRET: ${SESSION_SECRET:-arcadia-dev-secret-change-in-prod}
SSO_SECRET: ${SSO_SECRET:-arcadia-sso-secret-2024-plus-integration-key-secure}
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
LITELLM_BASE_URL: http://litellm:4000
LITELLM_API_KEY: ${LITELLM_API_KEY:-arcadia-internal}
ports:
- "5000:5000"
volumes:
- .:/app
- /app/node_modules
- /app/dist
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy
ports: redis:
- "5000:5000" condition: service_healthy
env_file: contabil:
- .env condition: service_started
environment: bi:
DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD:-SuaSenhaSegura}@db:5432/${PGDATABASE:-arcadia} condition: service_started
PGHOST: db
NODE_ENV: production
volumes:
- uploads:/app/uploads
# ── Superset BI ────────────────────────────────────────────────────────────── # ── Microserviço Contábil (Python) ──────────────────────────────────────────
superset: contabil:
build: build:
context: ./docker/superset context: .
dockerfile: Dockerfile dockerfile: Dockerfile.python
restart: unless-stopped restart: unless-stopped
environment: environment:
SUPERSET_SECRET_KEY: ${SUPERSET_SECRET_KEY:-superset-secret-change-in-dev} SERVICE_NAME: contabil
SQLALCHEMY_DATABASE_URI: postgresql://${PGUSER:-arcadia}:${PGPASSWORD:-SuaSenhaSegura}@db:5432/superset SERVICE_PORT: 8003
DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD:-SuaSenhaSegura}@db:5432/superset CONTABIL_PORT: 8003
PYTHONPATH: /app/pythonpath DATABASE_URL: postgresql://arcadia:arcadia123@db:5432/arcadia
ports:
- "8003:8003"
depends_on:
db:
condition: service_healthy
# ── Microserviço BI Engine (Python) ─────────────────────────────────────────
bi:
build:
context: .
dockerfile: Dockerfile.python
restart: unless-stopped
environment:
SERVICE_NAME: bi
SERVICE_PORT: 8004
BI_PORT: 8004
DATABASE_URL: postgresql://arcadia:arcadia123@db:5432/arcadia
ports:
- "8004:8004"
depends_on:
db:
condition: service_healthy
# ── Microserviço Automações (Python) ────────────────────────────────────────
automation:
build:
context: .
dockerfile: Dockerfile.python
restart: unless-stopped
environment:
SERVICE_NAME: automation
SERVICE_PORT: 8005
AUTOMATION_PORT: 8005
DATABASE_URL: postgresql://arcadia:arcadia123@db:5432/arcadia
ports:
- "8005:8005"
depends_on:
db:
condition: service_healthy
# ── Microserviço Fiscal (Python) ────────────────────────────────────────────
fisco:
build:
context: .
dockerfile: Dockerfile.python
restart: unless-stopped
environment:
SERVICE_NAME: fisco
SERVICE_PORT: 8002
FISCO_PORT: 8002
DATABASE_URL: postgresql://arcadia:arcadia123@db:5432/arcadia
ports:
- "8002:8002"
depends_on:
db:
condition: service_healthy
# ── Serviço de Embeddings (pgvector via Python) ──────────────────────────────
embeddings:
build:
context: .
dockerfile: Dockerfile.python
restart: unless-stopped
environment:
SERVICE_NAME: embeddings
SERVICE_PORT: 8001
DATABASE_URL: postgresql://arcadia:arcadia123@db:5432/arcadia
ports:
- "8001:8001"
depends_on:
db:
condition: service_healthy
# ── Apache Superset (BI avançado) ────────────────────────────────────────────
superset:
image: apache/superset:4.1.0
restart: unless-stopped
environment:
SUPERSET_SECRET_KEY: ${SUPERSET_SECRET_KEY:-superset-secret-change-in-prod}
DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD:-arcadia123}@db:5432/arcadia_superset
ARCADIA_DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD:-arcadia123}@db:5432/arcadia
SUPERSET_ADMIN_USER: ${SUPERSET_ADMIN_USER:-admin} SUPERSET_ADMIN_USER: ${SUPERSET_ADMIN_USER:-admin}
SUPERSET_ADMIN_USERNAME: ${SUPERSET_ADMIN_USER:-admin}
SUPERSET_ADMIN_PASSWORD: ${SUPERSET_ADMIN_PASSWORD:-arcadia2026}
SUPERSET_ADMIN_EMAIL: ${SUPERSET_ADMIN_EMAIL:-admin@arcadia.app} SUPERSET_ADMIN_EMAIL: ${SUPERSET_ADMIN_EMAIL:-admin@arcadia.app}
ARCADIA_DATABASE_URL: postgresql+psycopg2://${PGUSER:-arcadia}:${PGPASSWORD:-SuaSenhaSegura}@db:5432/${PGDATABASE:-arcadia} SUPERSET_ADMIN_PASSWORD: ${SUPERSET_ADMIN_PASSWORD:-arcadia2026}
command: ["/bin/bash", "/app/docker/init.sh"] SUPERSET_WEBSERVER_PORT: 8088
PYTHONPATH: /app/pythonpath
FLASK_ENV: production
ports:
- "8088:8088"
volumes: volumes:
- ./docker/superset/superset_config.py:/app/pythonpath/superset_config.py:ro - ./docker/superset/superset_config.py:/app/pythonpath/superset_config.py:ro
- ./docker/superset/init.sh:/app/docker/init.sh:ro - ./docker/superset/init.sh:/app/docker/init.sh:ro
- superset_home:/app/superset_home - superset_home:/app/superset_home
ports:
- "8088:8088"
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy
command: ["/bin/bash", "/app/docker/init.sh"]
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8088/health"]
interval: 30s
timeout: 10s
retries: 5
start_period: 60s
networks:
- arcadia
profiles: [bi] profiles: [bi]
# ── Neo4j (Knowledge Graph) ───────────────────────────────────────────────── # ─────────────── PERFIL: ai (Soberania de IA) ────────────────────────────────
# Ativar com: docker compose --profile kg up
neo4j: # ── Ollama (LLMs locais) ─────────────────────────────────────────────────────
image: neo4j:5 ollama:
image: ollama/ollama:latest
restart: unless-stopped restart: unless-stopped
volumes:
- ollama_models:/root/.ollama
ports:
- "11434:11434"
profiles: [ai]
# Para GPU NVIDIA: adicione `deploy.resources.reservations.devices`
# deploy:
# resources:
# reservations:
# devices:
# - driver: nvidia
# count: 1
# capabilities: [gpu]
# ── Open WebUI (interface para devs/consultores) ─────────────────────────────
open-webui:
image: ghcr.io/open-webui/open-webui:main
restart: unless-stopped
environment:
OLLAMA_BASE_URL: http://ollama:11434
WEBUI_SECRET_KEY: ${WEBUI_SECRET_KEY:-webui-secret-change-in-prod}
DEFAULT_MODELS: llama3.3,qwen2.5-coder
ENABLE_RAG_WEB_SEARCH: "true"
ports:
- "3001:8080"
volumes:
- open_webui_data:/app/backend/data
depends_on:
- ollama
profiles: [ai]
# ── LiteLLM (proxy unificado de LLMs) ───────────────────────────────────────
litellm:
image: ghcr.io/berriai/litellm:main-latest
restart: unless-stopped
volumes:
- ./docker/litellm-config.yaml:/app/config.yaml
command: ["--config", "/app/config.yaml", "--port", "4000", "--detailed_debug"]
environment:
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
OLLAMA_BASE_URL: http://ollama:11434
LITELLM_MASTER_KEY: ${LITELLM_API_KEY:-arcadia-internal}
ports:
- "4000:4000"
profiles: [ai]
# ─────────────── PERFIL: erpnext (ERPNext local) ──────────────────────────────
# ── MariaDB para ERPNext ─────────────────────────────────────────────────────
erpnext-db:
image: mariadb:10.6
restart: unless-stopped
profiles: [erpnext]
environment:
MYSQL_ROOT_PASSWORD: ${ERPNEXT_DB_ROOT_PASSWORD:-erpnext-root-2026}
MYSQL_DATABASE: _frappe
MYSQL_USER: frappe
MYSQL_PASSWORD: ${ERPNEXT_DB_PASSWORD:-frappe2026}
volumes:
- erpnext_db:/var/lib/mysql
command: >
--character-set-server=utf8mb4
--collation-server=utf8mb4_unicode_ci
--skip-character-set-client-handshake
--skip-innodb-read-only-compressed
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "--password=${ERPNEXT_DB_ROOT_PASSWORD:-erpnext-root-2026}"]
interval: 10s
timeout: 5s
retries: 10
start_period: 30s
# ── Neo4j (Knowledge Graph) ──────────────────────────────────────────────────
neo4j:
image: neo4j:5.20-community
restart: unless-stopped
profiles: [agentic]
environment: environment:
NEO4J_AUTH: neo4j/${NEO4J_PASSWORD:-arcadia123} NEO4J_AUTH: neo4j/${NEO4J_PASSWORD:-arcadia123}
NEO4J_PLUGINS: '["apoc"]' NEO4J_PLUGINS: '["apoc"]'
NEO4J_dbms_security_procedures_unrestricted: apoc.*
volumes: volumes:
- neo4j_data:/data - neo4j_data:/data
- neo4j_logs:/logs - neo4j_logs:/logs
ports: ports:
- "7474:7474" - "7474:7474"
- "7687:7687" - "7687:7687"
profiles: [kg] healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:7474 || exit 1"]
interval: 20s
timeout: 10s
retries: 5
start_period: 30s
networks:
- arcadia
# ── ERPNext (Frappe Framework + ERPNext app) ─────────────────────────────────
erpnext:
image: frappe/erpnext:version-15
restart: unless-stopped
profiles: [erpnext]
environment:
FRAPPE_SITE_NAME_HEADER: erpnext.local
BACKEND_PORT: 8000
volumes:
- erpnext_sites:/home/frappe/frappe-bench/sites
- erpnext_logs:/home/frappe/frappe-bench/logs
- ./docker/erpnext/init.sh:/usr/local/bin/init-erpnext.sh:ro
depends_on:
erpnext-db:
condition: service_healthy
ports:
- "8090:8080"
command: ["/bin/bash", "/usr/local/bin/init-erpnext.sh"]
healthcheck:
test: ["CMD-SHELL", "curl -sf http://localhost:8080/api/method/frappe.ping || exit 1"]
interval: 30s
timeout: 10s
retries: 10
start_period: 120s
networks:
- arcadia
volumes: volumes:
pgdata: pgdata:
uploads: ollama_models:
open_webui_data:
superset_home: superset_home:
erpnext_db:
erpnext_sites:
erpnext_logs:
neo4j_data: neo4j_data:
neo4j_logs: neo4j_logs:
networks:
arcadia:
driver: bridge

View File

@ -31,16 +31,6 @@ model_list:
model: ollama/llama3.2:3b model: ollama/llama3.2:3b
api_base: os.environ/OLLAMA_BASE_URL api_base: os.environ/OLLAMA_BASE_URL
- model_name: deepseek-r1
litellm_params:
model: ollama/deepseek-r1:14b
api_base: os.environ/OLLAMA_BASE_URL
- model_name: arcadia-agent
litellm_params:
model: groq/llama-3.3-70b-versatile
api_key: os.environ/GROQ_API_KEY
- model_name: nomic-embed-text - model_name: nomic-embed-text
litellm_params: litellm_params:
model: ollama/nomic-embed-text model: ollama/nomic-embed-text
@ -70,28 +60,31 @@ model_list:
# api_key: os.environ/ANTHROPIC_API_KEY # api_key: os.environ/ANTHROPIC_API_KEY
# ── TIER 3: Groq (opt-in — inferência rápida sem dados persistidos) ────────── # ── TIER 3: Groq (opt-in — inferência rápida sem dados persistidos) ──────────
- model_name: groq-llama # - model_name: groq-llama
litellm_params: # litellm_params:
model: groq/llama-3.3-70b-versatile # model: groq/llama-3.3-70b-versatile
api_key: os.environ/GROQ_API_KEY # api_key: os.environ/GROQ_API_KEY
# ── Modelo padrão do Arcádia (Manus usa este) ───────────────────────────────── # ── Modelo padrão do Arcádia (Manus usa este) ─────────────────────────────────
# Prioridade: DeepSeek R1 14B (TIER 2) → llama3.2 (fallback rápido) # Prioridade: LLMFit (TIER 1) → Ollama (TIER 2 — fallback)
- model_name: arcadia-default - model_name: arcadia-default
litellm_params: litellm_params:
model: ollama/deepseek-r1:14b model: openai/llama3.2:3b
api_base: os.environ/OLLAMA_BASE_URL api_base: os.environ/LLMFIT_BASE_URL
api_key: os.environ/LLMFIT_API_KEY
router_settings: router_settings:
routing_strategy: least-busy routing_strategy: least-busy
fallbacks: fallbacks:
- {"gpt-4o": ["arcadia-agent"]} - {"gpt-4o": ["llama3.2"]}
- {"gpt-4o-mini": ["arcadia-agent"]} - {"gpt-4o-mini": ["llama3.2"]}
- {"arcadia-default": ["llama3.2"]}
- {"arcadia-finetuned": ["llama3.2"]}
- {"arcadia-embed": ["nomic-embed-text"]} - {"arcadia-embed": ["nomic-embed-text"]}
litellm_settings: litellm_settings:
drop_params: true drop_params: true
request_timeout: 300 request_timeout: 120
set_verbose: false set_verbose: false
general_settings: general_settings:

View File

@ -10,36 +10,17 @@ ARCADIA_DB_URL="${ARCADIA_DATABASE_URL:-postgresql://arcadia:arcadia123@db:5432/
echo "[Superset Init] Aguardando PostgreSQL..." echo "[Superset Init] Aguardando PostgreSQL..."
until python -c " until python -c "
import psycopg2, os, sys, re import psycopg2, os, sys
try: try:
# Health check no banco 'postgres' (sempre existe), não no 'superset' que ainda não existe url = os.environ.get('DATABASE_URL', 'postgresql://arcadia:arcadia123@db:5432/arcadia_superset')
url = os.environ.get('DATABASE_URL', 'postgresql://arcadia:SuaSenhaSegura@db:5432/superset') psycopg2.connect(url)
check_url = re.sub(r'/[^/?]+(\?.*)?$', '/postgres', url) sys.exit(0)
psycopg2.connect(check_url) except: sys.exit(1)
except Exception:
sys.exit(1)
" 2>/dev/null; do " 2>/dev/null; do
sleep 2 sleep 2
done done
echo "[Superset Init] PostgreSQL disponível!" echo "[Superset Init] PostgreSQL disponível!"
echo "[Superset Init] Criando banco superset se necessário..."
python - <<'DBEOF'
import psycopg2, os, re
url = os.environ.get('DATABASE_URL', 'postgresql://arcadia:SuaSenhaSegura@db:5432/superset')
conn_url = re.sub(r'/[^/?]+(\?.*)?$', '/postgres', url)
conn = psycopg2.connect(conn_url)
conn.autocommit = True
cur = conn.cursor()
cur.execute("SELECT 1 FROM pg_database WHERE datname = 'superset'")
if not cur.fetchone():
cur.execute("CREATE DATABASE superset")
print("[Superset Init] Banco 'superset' criado.")
else:
print("[Superset Init] Banco 'superset' já existe.")
conn.close()
DBEOF
echo "[Superset Init] Rodando migrações do banco..." echo "[Superset Init] Rodando migrações do banco..."
superset db upgrade superset db upgrade

View File

@ -46,8 +46,6 @@ SQLLAB_ASYNC_TIME_LIMIT_SEC = 300
# ── Branding Arcádia ────────────────────────────────────────────────────────── # ── Branding Arcádia ──────────────────────────────────────────────────────────
APP_NAME = "Arcádia Insights" APP_NAME = "Arcádia Insights"
APP_ICON = "/static/assets/images/arcadia_logo.png"
APP_ICON_WIDTH = 150
LOGO_TARGET_PATH = "/" LOGO_TARGET_PATH = "/"
FAVICONS = [{"href": "/static/assets/images/favicon.png"}] FAVICONS = [{"href": "/static/assets/images/favicon.png"}]
@ -57,42 +55,8 @@ SESSION_COOKIE_SECURE = False # True em prod com HTTPS
WTF_CSRF_ENABLED = True WTF_CSRF_ENABLED = True
WTF_CSRF_EXEMPT_LIST = ["superset.views.core.log"] WTF_CSRF_EXEMPT_LIST = ["superset.views.core.log"]
# ── Embedding cross-origin ────────────────────────────────────────────────────
# Desabilita Flask-Talisman para permitir embedding via iframe cross-origin
# (HTTPS é gerenciado pelo Traefik upstream)
TALISMAN_ENABLED = False
HTTP_HEADERS = {}
X_FRAME_OPTIONS = "ALLOWALL"
# ── Guest Token (para embedding) ────────────────────────────────────────────── # ── Guest Token (para embedding) ──────────────────────────────────────────────
GUEST_TOKEN_JWT_EXP_SECONDS = 300 # 5 minutos — frontend renova automaticamente GUEST_TOKEN_JWT_EXP_SECONDS = 300 # 5 minutos — frontend renova automaticamente
GUEST_ROLE_NAME = "Public" GUEST_ROLE_NAME = "Public"
GUEST_TOKEN_JWT_ALGO = "HS256" GUEST_TOKEN_JWT_ALGO = "HS256"
GUEST_TOKEN_HEADER_NAME = "X-GuestToken" GUEST_TOKEN_HEADER_NAME = "X-GuestToken"
# ── Fix: g.user a partir do Bearer JWT ────────────────────────────────────────
# Quando o papel Public tem can_read on Dashboard, o decorator protect() do FAB
# trata o endpoint como público e pula a verificação JWT, deixando g.user como
# AnonymousUser. Este hook corrige isso: se há um Bearer JWT válido e g.user é
# anônimo, carrega o usuário do JWT antes do handler ser invocado.
def FLASK_APP_MUTATOR(app): # noqa
@app.before_request
def _set_g_user_from_bearer_jwt():
from flask import g, request # noqa
user = getattr(g, "user", None)
if user is not None and not getattr(user, "is_anonymous", True):
return # já autenticado (ex: guest token via request_loader)
auth = request.headers.get("Authorization", "")
if not auth.startswith("Bearer "):
return
try:
from flask_jwt_extended import decode_token # noqa
decoded = decode_token(auth[7:])
user_id = decoded.get("sub")
if user_id is not None:
from superset.extensions import security_manager as sm # noqa
jwt_user = sm.get_user_by_id(int(user_id))
if jwt_user and jwt_user.is_active:
g.user = jwt_user
except Exception: # noqa
pass

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,63 @@
-- Migration 0003: Agentic Suite — Skills (POO) + Skill Executions
-- Fase 1 do planejamento estratégico Arcádia Agentic Suite
CREATE TABLE IF NOT EXISTS "skills" (
"id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
"tenant_id" INTEGER,
"company_id" INTEGER,
"user_id" VARCHAR,
"name" VARCHAR(255) NOT NULL,
"slug" VARCHAR(255) NOT NULL,
"namespace" VARCHAR(20) NOT NULL DEFAULT 'tenant',
"description" TEXT,
"version" VARCHAR(20) NOT NULL DEFAULT '1.0.0',
"tags" TEXT[] DEFAULT '{}',
"extends" TEXT[] DEFAULT '{}',
"implements" TEXT[] DEFAULT '{}',
"dependencies" JSONB DEFAULT '[]',
"execute_visibility" VARCHAR(20) DEFAULT 'public',
"parameters_visibility" VARCHAR(20) DEFAULT 'public',
"trigger_type" VARCHAR(30),
"trigger_config" JSONB,
"parameters_schema" JSONB DEFAULT '{}',
"return_schema" JSONB DEFAULT '{}',
"body" TEXT,
"status" VARCHAR(20) NOT NULL DEFAULT 'active',
"is_system" BOOLEAN DEFAULT false,
"created_by" VARCHAR,
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- Índice único: mesmo slug não pode repetir dentro do mesmo tenant
CREATE UNIQUE INDEX IF NOT EXISTS "skills_tenant_slug_idx"
ON "skills"("tenant_id", "slug")
WHERE "tenant_id" IS NOT NULL;
-- Skills de sistema (sem tenant) também precisam slug único
CREATE UNIQUE INDEX IF NOT EXISTS "skills_system_slug_idx"
ON "skills"("slug")
WHERE "tenant_id" IS NULL AND "is_system" = true;
CREATE TABLE IF NOT EXISTS "skill_executions" (
"id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
"skill_id" UUID NOT NULL REFERENCES "skills"("id") ON DELETE CASCADE,
"tenant_id" INTEGER,
"triggered_by" VARCHAR(30) NOT NULL DEFAULT 'manual',
"triggered_by_user_id" VARCHAR,
"triggered_by_agent_id" VARCHAR(100),
"parameters" JSONB DEFAULT '{}',
"result" JSONB,
"error" TEXT,
"status" VARCHAR(20) NOT NULL DEFAULT 'pending',
"duration_ms" INTEGER,
"steps_count" INTEGER DEFAULT 0,
"correlation_id" UUID DEFAULT gen_random_uuid(),
"audit_hash" VARCHAR(64),
"started_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
"completed_at" TIMESTAMP
);
CREATE INDEX IF NOT EXISTS "skill_executions_skill_id_idx" ON "skill_executions"("skill_id");
CREATE INDEX IF NOT EXISTS "skill_executions_tenant_id_idx" ON "skill_executions"("tenant_id");
CREATE INDEX IF NOT EXISTS "skill_executions_started_at_idx" ON "skill_executions"("started_at" DESC);

View File

@ -0,0 +1,74 @@
-- Migration 0004: OpenClaw — Detecção de Padrões e Sugestões de Skills Emergentes
-- Fase 4 do planejamento estratégico Arcádia Agentic Suite
-- Tabela de padrões detectados
CREATE TABLE IF NOT EXISTS "detected_patterns" (
"id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
"tenant_id" INTEGER REFERENCES "tenants"("id"),
"user_id" VARCHAR REFERENCES "users"("id"),
-- Padrão detectado
"action_type" VARCHAR(100) NOT NULL,
"description" TEXT,
"frequency" INTEGER NOT NULL DEFAULT 0,
"confidence" NUMERIC(4, 3) NOT NULL DEFAULT '0',
-- Janela de análise
"first_seen_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
"last_seen_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- Metadados do padrão (contexto, módulos envolvidos, etc.)
"metadata" JSONB DEFAULT '{}',
-- Ciclo de vida: active | archived | converted
"status" VARCHAR(20) NOT NULL DEFAULT 'active',
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS "detected_patterns_tenant_user_idx"
ON "detected_patterns"("tenant_id", "user_id");
CREATE INDEX IF NOT EXISTS "detected_patterns_action_type_idx"
ON "detected_patterns"("action_type");
CREATE INDEX IF NOT EXISTS "detected_patterns_status_idx"
ON "detected_patterns"("status");
-- Tabela de sugestões de skills emergentes
CREATE TABLE IF NOT EXISTS "skill_suggestions" (
"id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
"pattern_id" UUID REFERENCES "detected_patterns"("id") ON DELETE CASCADE,
"tenant_id" INTEGER REFERENCES "tenants"("id"),
"user_id" VARCHAR REFERENCES "users"("id"),
-- Skill sugerida
"suggested_skill_name" VARCHAR(200) NOT NULL,
"suggested_description" TEXT,
"estimated_automation" TEXT,
"confidence" NUMERIC(4, 3) NOT NULL DEFAULT '0',
-- Skill gerada (após confirmação)
"generated_skill_id" UUID REFERENCES "skills"("id"),
-- Estado da sugestão: pending | accepted | rejected | expired
"status" VARCHAR(20) NOT NULL DEFAULT 'pending',
-- Rastreabilidade
"source" VARCHAR(50) NOT NULL DEFAULT 'openclaw',
"reviewed_by" VARCHAR REFERENCES "users"("id"),
"reviewed_at" TIMESTAMP,
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS "skill_suggestions_pattern_id_idx"
ON "skill_suggestions"("pattern_id");
CREATE INDEX IF NOT EXISTS "skill_suggestions_tenant_user_idx"
ON "skill_suggestions"("tenant_id", "user_id");
CREATE INDEX IF NOT EXISTS "skill_suggestions_status_idx"
ON "skill_suggestions"("status");

View File

@ -8,6 +8,20 @@
"when": 1769121114659, "when": 1769121114659,
"tag": "0000_low_tiger_shark", "tag": "0000_low_tiger_shark",
"breakpoints": true "breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1741824000000,
"tag": "0001_whatsapp_auto_reply_config",
"breakpoints": true
},
{
"idx": 2,
"version": "7",
"when": 1741824060000,
"tag": "0002_tenant_isolation",
"breakpoints": true
} }
] ]
} }

View File

@ -9,7 +9,10 @@
"build": "tsx script/build.ts", "build": "tsx script/build.ts",
"start": "NODE_ENV=production node dist/index.cjs", "start": "NODE_ENV=production node dist/index.cjs",
"check": "tsc", "check": "tsc",
"db:push": "drizzle-kit push" "db:push": "drizzle-kit push",
"test": "vitest run",
"test:watch": "vitest",
"test:openclaw": "vitest run tests/openclaw"
}, },
"dependencies": { "dependencies": {
"@hookform/resolvers": "^3.10.0", "@hookform/resolvers": "^3.10.0",
@ -72,7 +75,6 @@
"connect-pg-simple": "^10.0.0", "connect-pg-simple": "^10.0.0",
"date-fns": "^3.6.0", "date-fns": "^3.6.0",
"docx": "^9.5.1", "docx": "^9.5.1",
"dotenv": "^17.3.1",
"drizzle-orm": "^0.39.3", "drizzle-orm": "^0.39.3",
"drizzle-zod": "^0.7.1", "drizzle-zod": "^0.7.1",
"embla-carousel-react": "^8.6.0", "embla-carousel-react": "^8.6.0",

View File

@ -0,0 +1,9 @@
fastapi==0.109.0
uvicorn==0.27.0
pandas==2.1.0
numpy==1.26.0
httpx==0.26.0
python-multipart==0.0.6
pydantic==2.5.0
chromadb==0.4.22
openai==1.12.0

View File

@ -46,27 +46,24 @@ The Arcádia Suite employs a 4-layer hybrid architecture:
* **Arcádia Suite's 6-Layer Architecture:** Apps (Core, Business, Segment), Structural Modules (Financial, Accounting, Fiscal, HR), Operational Execution (Retail, Services, ERPs), Intelligence (Manus, Knowledge Graph, Automations), Platform (Central API, Hub API, MCP/A2A, IDE, Data Science), and Consulting (Process Compass, Production, Support). * **Arcádia Suite's 6-Layer Architecture:** Apps (Core, Business, Segment), Structural Modules (Financial, Accounting, Fiscal, HR), Operational Execution (Retail, Services, ERPs), Intelligence (Manus, Knowledge Graph, Automations), Platform (Central API, Hub API, MCP/A2A, IDE, Data Science), and Consulting (Process Compass, Production, Support).
* **Unified Communication Center:** Integrates a WhatsApp Attendance System (multi-session, real-time sync, ticket management, AI auto-replies) and an Internal Chat System, feeding all communications into the Knowledge Graph. * **Unified Communication Center:** Integrates a WhatsApp Attendance System (multi-session, real-time sync, ticket management, AI auto-replies) and an Internal Chat System, feeding all communications into the Knowledge Graph.
* **Learning System & Knowledge Graph:** The system learns from all interactions, storing Q&A and patterns. A semantic Knowledge Graph connects all business information via nodes and edges for insights. * **Learning System & Knowledge Graph:** The system learns from all interactions, storing Q&A and patterns. A semantic Knowledge Graph connects all business information via nodes and edges for insights.
* **Manus Autonomous Agent (Cérebro Central):** Central AI brain (GPT-4o) powering all intelligence via ManusIntelligence singleton. All 6 Dev Center agents (Architect, Generator, Validator, Executor, Researcher, Evolution) route their AI thinking through ManusIntelligence, which provides context enrichment via ToolManager (code search, file reading). Executes multi-step tasks using a thought-action-observation loop with 56+ tools (web search, knowledge/ERP query, file I/O, shell commands, semantic search). Visible in Casa de Máquinas as "Motor IA" with health endpoint at `/api/manus/health` showing model, metrics (calls, tokens, errors), capabilities, and uptime. * **Manus Autonomous Agent:** Executes multi-step tasks using a thought-action-observation loop with various tools (web search, knowledge/ERP query, calculations, messaging, reporting, scheduling).
* **Scientist Module:** An AI auto-programming component for data analysis, code generation (Python/SQL) in a sandbox, and solution storage. * **Scientist Module:** An AI auto-programming component for data analysis, code generation (Python/SQL) in a sandbox, and solution storage.
* **Arcádia Fisco (Fiscal Engine):** Centralized compliance motor for Brazilian tax regulations (NCMs, CFOPs, CESTs, tax groups, digital certificates), integrating with SEFAZ webservices for NF-e/NFC-e issuance. * **Arcádia Fisco (Fiscal Engine):** Centralized compliance motor for Brazilian tax regulations (NCMs, CFOPs, CESTs, tax groups, digital certificates), integrating with SEFAZ webservices for NF-e/NFC-e issuance.
* **Arcádia SOE (Sistema Operacional Empresarial):** The central business operating system (kernel) accessible via `/soe`. Contains all canonical business domains (People, Products, Sales, Purchases, Inventory, Financial, Fiscal, CRM, Projects, Quality, Governance). Apps (Retail, Engineering, Food Service, etc.) extend SOE with segment-specific experiences. Motors (Plus, ERPNext, Fisco, BI) are execution engines selected via server-side adapter pattern. API at `/api/soe/*` (backward-compatible alias from `/api/erp/*`). Context: `SoeMotorContext` replaces `ErpProfileContext`. Legacy file `ERP.tsx` preserved for reference. * **Arcádia ERP (Gestão Empresarial):** A complete ERP module integrating ERPNext with Arcádia's intelligence, accessible via `/erp`. Includes dashboards, unified people module, inventory, sales, purchases, financial management, AI-driven reports, and integration with Arcádia Fisco and Manus AI.
* **Arcádia Plus (ERP Laravel):** A standalone Laravel/PHP ERP, accessible via `/plus`, with extensive features including NF-e/NFC-e/CT-e/MDF-e, POS, digital menu, service orders, financial management, stock traceability, CRM, and integrations with e-commerce platforms. Uses an isolated PostgreSQL schema. * **Arcádia Plus (ERP Laravel):** A standalone Laravel/PHP ERP, accessible via `/plus`, with extensive features including NF-e/NFC-e/CT-e/MDF-e, POS, digital menu, service orders, financial management, stock traceability, CRM, and integrations with e-commerce platforms. Uses an isolated PostgreSQL schema.
* **Business Intelligence (Arcádia Insights):** Provides data visualization and analysis, powered by the BI Engine (FastAPI, port 8004) with SQL query execution, chart-data generation, micro-BI API, caching layer, and Pandas analysis. * **Business Intelligence (Arcádia Insights):** Provides data visualization and analysis, powered by the BI Engine (FastAPI, port 8004) with SQL query execution, chart-data generation, micro-BI API, caching layer, and Pandas analysis.
* **Automation Engine:** FastAPI service (port 8005) providing cron scheduler, event bus, and workflow executor for the Automations module. * **Automation Engine:** FastAPI service (port 8005) providing cron scheduler, event bus, and workflow executor for the Automations module.
* **Communication Engine (Motor de Comunicação):** Node/TypeScript service (port 8006) unifying XOS CRM, Arcádia CRM, WhatsApp, and Email into a single managed engine. Provides unified API for contacts, threads, messages, channels, queues, quick messages, stats, and agent context (360° view). Emits events to `comm_events` table consumed by Knowledge Graph and AI agents. Proxy at `/api/comm/*`. Canonical tables: `comm_contacts`, `comm_threads`, `comm_messages`, `comm_channels`, `comm_queues`, `comm_quick_messages`, `comm_events`. Backward-compatible via reference fields (`xos_contact_id`, `crm_client_id`, etc.). * **Communication Engine (Motor de Comunicação):** Node/TypeScript service (port 8006) unifying XOS CRM, Arcádia CRM, WhatsApp, and Email into a single managed engine. Provides unified API for contacts, threads, messages, channels, queues, quick messages, stats, and agent context (360° view). Emits events to `comm_events` table consumed by Knowledge Graph and AI agents. Proxy at `/api/comm/*`. Canonical tables: `comm_contacts`, `comm_threads`, `comm_messages`, `comm_channels`, `comm_queues`, `comm_quick_messages`, `comm_events`. Backward-compatible via reference fields (`xos_contact_id`, `crm_client_id`, etc.).
* **Casa de Máquinas (Engine Room):** Unified control panel at `/engine-room` showing real-time status of all engines (Manus IA, Plus 8080, Contábil 8003, Fiscal 8002, BI 8004, Automation 8005, Communication 8006) and XOS agents. Manus IA has a dedicated tab with metrics (model, calls, tokens, errors, uptime, capabilities). API at `/api/engine-room/*`. * **Casa de Máquinas (Engine Room):** Unified control panel at `/engine-room` showing real-time status of all engines (Plus 8080, Contábil 8003, Fiscal 8002, BI 8004, Automation 8005, Communication 8006) and XOS agents. API at `/api/engine-room/*`.
* **IDE:** Integrated development environment with Monaco Editor and Xterm.js. * **IDE:** Integrated development environment with Monaco Editor and Xterm.js.
* **Module Activation System:** Per-tenant module management via `tenants.features` JSONB column (TenantFeatures type). API at `GET/PUT /api/soe/tenant/modules`. Admin → Módulos tab provides toggle switches for each module. Backend gating middleware (`requireModule`) blocks sync endpoints when modules are disabled (returns 403 with actionable message). SOE Sync Dashboard at `/soe` tab "Sincronização" shows motor status, enabled/disabled state, and manual sync buttons. Frontend hook `useTenantFeatures()` provides feature checks. Feature keys: ide, whatsapp, crm, erp, bi, manus, retail, plus, fisco, cockpit, compass, production, support, xosCrm, centralApis, comunidades, biblioteca.
* **Multi-Tenant Architecture:** Supports a hierarchical structure for Master, Partners, and Clients. * **Multi-Tenant Architecture:** Supports a hierarchical structure for Master, Partners, and Clients.
* **Agentic Interoperability Protocols:** Implements MCP (Model Context Protocol) for tool exposure (`/api/mcp/v1/`), A2A (Agent to Agent Protocol) for bidirectional communication (`/api/a2a/v1/`), and planned AP2 (Agent Payment Protocol) and UCP (Unified Commerce Protocol). Authentication uses `X-API-Key`. * **Agentic Interoperability Protocols:** Implements MCP (Model Context Protocol) for tool exposure (`/api/mcp/v1/`), A2A (Agent to Agent Protocol) for bidirectional communication (`/api/a2a/v1/`), and planned AP2 (Agent Payment Protocol) and UCP (Unified Commerce Protocol). Authentication uses `X-API-Key`.
* **Autonomous Development:** Integrated with GitHub for automatic commits, branch creation, pull requests, and repository analysis using a `ToolManager` system. * **Autonomous Development:** Integrated with GitHub for automatic commits, branch creation, pull requests, and repository analysis using a `ToolManager` system.
* **XOS Governance Layer:** Policy evaluation engine with fail-closed security, immutable audit trail, tool registry (auto-synced from ToolManager), skill registry with usage tracking, and contract registry. Integrated into BaseBlackboardAgent and ToolManager for automatic policy checks. API at `/api/governance/*`. 5 security policies: critical file protection, read permissions, destructive command blocking, human approval for production deploys, automatic staging with validation score threshold. Phase 3 adds PostgreSQL Job Queue (xos_job_queue table with priority, retry, dead-letter), Agent Metrics (xos_agent_metrics), 6th Researcher agent, and a visual Governance Dashboard at `/xos/governance` with real-time stats, audit trail, policies, tools, skills, jobs, and agent monitoring. Phase 4 adds Autonomous Pipeline Orchestrator (PipelineOrchestrator.ts) that chains all 6 agents: Portuguese prompt → Architect (design) → Generator (codegen) → Validator (typecheck) → Executor (staging) → Evolution (learn). Staging review system (xos_staging_changes + xos_dev_pipelines tables) with explicit user approval before applying code. SSE streaming for real-time progress. Pipeline UI integrated as "Pipeline" tab within Dev Center (`/dev-center`) with enhanced prompt input (image paste, file attachment, plan section), live timeline, staged code review/approve/reject. `/xos/pipeline` redirects to Dev Center. API at `/api/xos/pipeline`. * **XOS Governance Layer:** Policy evaluation engine with fail-closed security, immutable audit trail, tool registry (auto-synced from ToolManager), skill registry with usage tracking, and contract registry. Integrated into BaseBlackboardAgent and ToolManager for automatic policy checks. API at `/api/governance/*`. 5 security policies: critical file protection, read permissions, destructive command blocking, human approval for production deploys, automatic staging with validation score threshold. Phase 3 adds PostgreSQL Job Queue (xos_job_queue table with priority, retry, dead-letter), Agent Metrics (xos_agent_metrics), 6th Researcher agent, and a visual Governance Dashboard at `/xos/governance` with real-time stats, audit trail, policies, tools, skills, jobs, and agent monitoring. Phase 4 adds Autonomous Pipeline Orchestrator (PipelineOrchestrator.ts) that chains all 6 agents: Portuguese prompt → Architect (design) → Generator (codegen) → Validator (typecheck) → Executor (staging) → Evolution (learn). Staging review system (xos_staging_changes + xos_dev_pipelines tables) with explicit user approval before applying code. SSE streaming for real-time progress. Pipeline UI at `/xos/pipeline` with prompt submission, live timeline, and staged code review/approve/reject. API at `/api/xos/pipeline`.
* **Modular Dev Center Architecture:** The XOS Pipeline agents can autonomously create complete modules (database schemas, API routes, and UI pages) using a modular system. Module schemas go to `shared/schemas/{moduleName}.ts` and are auto-registered in the main schema via `shared/schemas/index.ts`. Module routes go to `server/modules/{moduleName}.ts` and are auto-loaded at startup via `server/modules/loader.ts`, mounted at `/api/modules/{moduleName}`. After pipeline approval, the migrator (`server/modules/migrator.ts`) auto-registers schemas and runs `drizzle-kit push`. Core files (`shared/schema.ts`, `server/routes.ts`, `db/index.ts`, `shared/schemas/index.ts`, `shared/schemas/loader.ts`, `server/modules/loader.ts`) remain protected. Agent prompts (Architect, Generator) include instructions for the modular pattern. API at `/api/xos/pipeline/modules/status` shows registered schemas and loaded routes.
## External Dependencies ## External Dependencies
* **OpenAI API:** For AI capabilities via ManusIntelligence (GPT-4o for Dev Center agents) and auto-replies (gpt-4o-mini for WhatsApp). * **OpenAI API:** For AI capabilities, including auto-replies (`gpt-4o-mini`) and the Scientist Module.
* **Baileys Library:** For WhatsApp integration and multi-session management. * **Baileys Library:** For WhatsApp integration and multi-session management.
* **nfelib (Python):** For Brazilian fiscal document (NF-e/NFC-e) issuance, validation, and SEFAZ communication. * **nfelib (Python):** For Brazilian fiscal document (NF-e/NFC-e) issuance, validation, and SEFAZ communication.
* **PostgreSQL:** Primary database, managed with Drizzle ORM. * **PostgreSQL:** Primary database, managed with Drizzle ORM.

View File

@ -1,7 +1,7 @@
import { Router, Request, Response } from "express"; import { Router, Request, Response } from "express";
import { db } from "../../db/index"; import { db } from "../../db/index";
import { eq, desc, and, or, isNull, inArray, sql } from "drizzle-orm"; import { eq, desc, and, or, isNull, inArray, sql } from "drizzle-orm";
import { users, profiles, crmPartners, crmClients, tenants, tenantPlans, partnerClients, partnerCommissions, tenantEmpresas, insertTenantSchema, insertTenantPlanSchema, insertPartnerClientSchema, insertTenantEmpresaSchema } from "@shared/schema"; import { users, profiles, crmPartners, crmClients, tenants, tenantPlans, partnerClients, partnerCommissions, insertTenantSchema, insertTenantPlanSchema, insertPartnerClientSchema } from "@shared/schema";
import fs from "fs"; import fs from "fs";
import path from "path"; import path from "path";
@ -36,7 +36,7 @@ router.use((req: Request, res: Response, next) => {
return res.status(401).json({ error: "Não autorizado" }); return res.status(401).json({ error: "Não autorizado" });
} }
const user = req.user as any; const user = req.user as any;
if (user.role !== "admin" && user.role !== "master") { if (user.role !== "admin") {
return res.status(403).json({ error: "Acesso negado. Apenas administradores." }); return res.status(403).json({ error: "Acesso negado. Apenas administradores." });
} }
next(); next();
@ -317,25 +317,10 @@ router.get("/tenants", async (req: Request, res: Response) => {
} }
}); });
// Helper: resolve plan features for a tenant based on plan code
async function resolvePlanFeatures(planCode: string): Promise<Record<string, any> | null> {
const [plan] = await db.select().from(tenantPlans).where(eq(tenantPlans.code, planCode));
if (!plan || !plan.features) return null;
return plan.features as Record<string, any>;
}
// POST /api/admin/tenants - Create new tenant // POST /api/admin/tenants - Create new tenant
router.post("/tenants", async (req: Request, res: Response) => { router.post("/tenants", async (req: Request, res: Response) => {
try { try {
const validated = insertTenantSchema.parse(req.body); const validated = insertTenantSchema.parse(req.body);
if (validated.plan && !validated.features) {
const planFeatures = await resolvePlanFeatures(validated.plan);
if (planFeatures) {
validated.features = planFeatures as any;
}
}
const [tenant] = await db.insert(tenants).values(validated).returning(); const [tenant] = await db.insert(tenants).values(validated).returning();
res.status(201).json(tenant); res.status(201).json(tenant);
} catch (error: any) { } catch (error: any) {
@ -349,41 +334,19 @@ router.patch("/tenants/:id", async (req: Request, res: Response) => {
const id = parseInt(req.params.id); const id = parseInt(req.params.id);
const user = req.user as any; const user = req.user as any;
// Verificar permissão de acesso
if (!await canAccessTenant(user, id)) { if (!await canAccessTenant(user, id)) {
return res.status(403).json({ error: "Sem permissão para modificar este tenant" }); return res.status(403).json({ error: "Sem permissão para modificar este tenant" });
} }
const { name, email, phone, plan, status, tenantType, parentTenantId, partnerCode, commissionRate, maxUsers, maxStorageMb, features, billingEmail, trialEndsAt } = req.body; const { name, email, phone, plan, status, tenantType, parentTenantId, partnerCode, commissionRate, maxUsers, maxStorageMb, features, billingEmail, trialEndsAt } = req.body;
let resolvedFeatures = features;
if (plan && !features) {
const [currentTenant] = await db.select().from(tenants).where(eq(tenants.id, id));
if (currentTenant && currentTenant.plan !== plan) {
const planFeatures = await resolvePlanFeatures(plan);
if (planFeatures) {
resolvedFeatures = planFeatures;
}
}
}
const updateData: any = { updatedAt: new Date() };
if (name !== undefined) updateData.name = name;
if (email !== undefined) updateData.email = email;
if (phone !== undefined) updateData.phone = phone;
if (plan !== undefined) updateData.plan = plan;
if (status !== undefined) updateData.status = status;
if (tenantType !== undefined) updateData.tenantType = tenantType;
if (parentTenantId !== undefined) updateData.parentTenantId = parentTenantId;
if (partnerCode !== undefined) updateData.partnerCode = partnerCode;
if (commissionRate !== undefined) updateData.commissionRate = commissionRate;
if (maxUsers !== undefined) updateData.maxUsers = maxUsers;
if (maxStorageMb !== undefined) updateData.maxStorageMb = maxStorageMb;
if (resolvedFeatures !== undefined) updateData.features = resolvedFeatures;
if (billingEmail !== undefined) updateData.billingEmail = billingEmail;
if (trialEndsAt !== undefined) updateData.trialEndsAt = trialEndsAt;
const [tenant] = await db.update(tenants) const [tenant] = await db.update(tenants)
.set(updateData) .set({
name, email, phone, plan, status, tenantType, parentTenantId, partnerCode,
commissionRate, maxUsers, maxStorageMb, features, billingEmail, trialEndsAt,
updatedAt: new Date()
})
.where(eq(tenants.id, id)) .where(eq(tenants.id, id))
.returning(); .returning();
@ -504,121 +467,6 @@ router.delete("/plans/:id", async (req: Request, res: Response) => {
} }
}); });
// POST /api/admin/plans/:id/propagate - Apply plan features to all tenants with this plan
router.post("/plans/:id/propagate", async (req: Request, res: Response) => {
try {
const id = parseInt(req.params.id);
const [plan] = await db.select().from(tenantPlans).where(eq(tenantPlans.id, id));
if (!plan) return res.status(404).json({ error: "Plano não encontrado" });
if (!plan.features) return res.status(400).json({ error: "Plano sem módulos definidos" });
const affected = await db.update(tenants)
.set({ features: plan.features, maxUsers: plan.maxUsers, maxStorageMb: plan.maxStorageMb, updatedAt: new Date() })
.where(eq(tenants.plan, plan.code))
.returning();
res.json({ propagated: affected.length, planCode: plan.code });
} catch (error: any) {
res.status(500).json({ error: error.message });
}
});
// POST /api/admin/plans/seed - Seed default plans
router.post("/plans/seed", async (req: Request, res: Response) => {
try {
const existing = await db.select().from(tenantPlans);
if (existing.length > 0) {
return res.json({ message: "Planos já existem", count: existing.length });
}
const defaultPlans: any[] = [
{
code: "free",
name: "Gratuito",
description: "Plano básico gratuito para experimentar",
tenantType: "client",
maxUsers: 2,
maxStorageMb: 500,
monthlyPrice: 0,
yearlyPrice: 0,
trialDays: 14,
sortOrder: 1,
features: { erp: true, crm: true },
},
{
code: "starter",
name: "Starter",
description: "Para pequenas empresas começando",
tenantType: "client",
maxUsers: 5,
maxStorageMb: 2000,
monthlyPrice: 9900,
yearlyPrice: 99000,
trialDays: 14,
sortOrder: 2,
features: { erp: true, crm: true, bi: true, fisco: true, whatsapp: true, compass: true },
},
{
code: "pro",
name: "Profissional",
description: "Para empresas em crescimento",
tenantType: "client",
maxUsers: 15,
maxStorageMb: 10000,
monthlyPrice: 29900,
yearlyPrice: 299000,
trialDays: 14,
sortOrder: 3,
features: { erp: true, crm: true, bi: true, fisco: true, retail: true, plus: true, whatsapp: true, manus: true, cockpit: true, compass: true, support: true, biblioteca: true },
},
{
code: "enterprise",
name: "Enterprise",
description: "Para grandes empresas",
tenantType: "client",
maxUsers: 100,
maxStorageMb: 50000,
monthlyPrice: 99900,
yearlyPrice: 999000,
trialDays: 30,
sortOrder: 4,
features: { erp: true, crm: true, bi: true, fisco: true, retail: true, plus: true, whatsapp: true, manus: true, ide: true, cockpit: true, compass: true, production: true, support: true, xosCrm: true, centralApis: true, comunidades: true, biblioteca: true },
},
{
code: "partner_starter",
name: "Parceiro Starter",
description: "Para consultores e pequenos integradores",
tenantType: "partner",
maxUsers: 10,
maxStorageMb: 5000,
monthlyPrice: 19900,
yearlyPrice: 199000,
trialDays: 14,
sortOrder: 5,
features: { erp: true, crm: true, bi: true, fisco: true, retail: true, whatsapp: true, compass: true, support: true },
},
{
code: "partner_pro",
name: "Parceiro Pro",
description: "Para integradores e consultorias",
tenantType: "partner",
maxUsers: 50,
maxStorageMb: 25000,
monthlyPrice: 49900,
yearlyPrice: 499000,
trialDays: 30,
sortOrder: 6,
features: { erp: true, crm: true, bi: true, fisco: true, retail: true, plus: true, whatsapp: true, manus: true, ide: true, cockpit: true, compass: true, production: true, support: true, xosCrm: true, centralApis: true, comunidades: true, biblioteca: true },
},
];
const created = await db.insert(tenantPlans).values(defaultPlans).returning();
res.status(201).json({ message: "Planos criados com sucesso", count: created.length, plans: created });
} catch (error: any) {
res.status(500).json({ error: error.message });
}
});
// ========================================== // ==========================================
// PARTNER-CLIENT RELATIONSHIPS // PARTNER-CLIENT RELATIONSHIPS
// ========================================== // ==========================================
@ -770,106 +618,4 @@ router.get("/tenants/stats", async (req: Request, res: Response) => {
} }
}); });
// ========== TENANT EMPRESAS (Matriz/Filiais) ==========
router.get("/empresas", async (req: Request, res: Response) => {
try {
const { tenantId } = req.query;
const user = req.user as any;
let query;
if (tenantId) {
const tid = parseInt(tenantId as string);
if (!(await canAccessTenant(user, tid))) {
return res.status(403).json({ error: "Sem permissão para este tenant" });
}
query = await db.select().from(tenantEmpresas)
.where(eq(tenantEmpresas.tenantId, tid))
.orderBy(desc(tenantEmpresas.tipo), tenantEmpresas.razaoSocial);
} else {
const allowed = await getAllowedTenantIds(user);
if (allowed) {
query = await db.select().from(tenantEmpresas)
.where(inArray(tenantEmpresas.tenantId, allowed))
.orderBy(desc(tenantEmpresas.tipo), tenantEmpresas.razaoSocial);
} else {
query = await db.select().from(tenantEmpresas)
.orderBy(desc(tenantEmpresas.tipo), tenantEmpresas.razaoSocial);
}
}
res.json(query);
} catch (error: any) {
res.status(500).json({ error: error.message });
}
});
router.get("/empresas/:id", async (req: Request, res: Response) => {
try {
const [empresa] = await db.select().from(tenantEmpresas)
.where(eq(tenantEmpresas.id, parseInt(req.params.id)));
if (!empresa) return res.status(404).json({ error: "Empresa não encontrada" });
const user = req.user as any;
if (!(await canAccessTenant(user, empresa.tenantId))) {
return res.status(403).json({ error: "Sem permissão" });
}
res.json(empresa);
} catch (error: any) {
res.status(500).json({ error: error.message });
}
});
router.post("/empresas", async (req: Request, res: Response) => {
try {
const data = insertTenantEmpresaSchema.parse(req.body);
const user = req.user as any;
if (!(await canAccessTenant(user, data.tenantId))) {
return res.status(403).json({ error: "Sem permissão para este tenant" });
}
const [empresa] = await db.insert(tenantEmpresas).values(data).returning();
res.json(empresa);
} catch (error: any) {
res.status(400).json({ error: error.message });
}
});
router.patch("/empresas/:id", async (req: Request, res: Response) => {
try {
const [existing] = await db.select().from(tenantEmpresas)
.where(eq(tenantEmpresas.id, parseInt(req.params.id)));
if (!existing) return res.status(404).json({ error: "Empresa não encontrada" });
const user = req.user as any;
if (!(await canAccessTenant(user, existing.tenantId))) {
return res.status(403).json({ error: "Sem permissão" });
}
const [empresa] = await db.update(tenantEmpresas)
.set({ ...req.body, updatedAt: new Date() })
.where(eq(tenantEmpresas.id, parseInt(req.params.id)))
.returning();
res.json(empresa);
} catch (error: any) {
res.status(400).json({ error: error.message });
}
});
router.delete("/empresas/:id", async (req: Request, res: Response) => {
try {
const [existing] = await db.select().from(tenantEmpresas)
.where(eq(tenantEmpresas.id, parseInt(req.params.id)));
if (!existing) return res.status(404).json({ error: "Empresa não encontrada" });
const user = req.user as any;
if (!(await canAccessTenant(user, existing.tenantId))) {
return res.status(403).json({ error: "Sem permissão" });
}
await db.delete(tenantEmpresas).where(eq(tenantEmpresas.id, parseInt(req.params.id)));
res.json({ success: true });
} catch (error: any) {
res.status(500).json({ error: error.message });
}
});
export default router; export default router;

View File

@ -30,8 +30,12 @@ async function comparePasswords(supplied: string, stored: string) {
return timingSafeEqual(hashedBuf, suppliedBuf); return timingSafeEqual(hashedBuf, suppliedBuf);
} }
if (!process.env.SESSION_SECRET) {
console.warn("[auth] WARNING: SESSION_SECRET env var not set. Using insecure fallback. Set SESSION_SECRET in production.");
}
const sessionSettings: session.SessionOptions = { const sessionSettings: session.SessionOptions = {
secret: process.env.SESSION_SECRET || "arcadia-browser-secret-key-2024", secret: process.env.SESSION_SECRET || `arcadia-dev-${Math.random().toString(36)}`,
resave: false, resave: false,
saveUninitialized: false, saveUninitialized: false,
store: storage.sessionStore, store: storage.sessionStore,

View File

@ -4,109 +4,10 @@ import { db } from "../../../db/index";
import { xosToolRegistry } from "@shared/schema"; import { xosToolRegistry } from "@shared/schema";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
const WRITE_TOOLS = new Set([
"write_file",
"github_commit",
"git_local_commit",
"run_command",
]);
const READ_ONLY_TOOLS = new Set([
"read_file",
"list_directory",
"search_code",
"git_status",
"analyze_external_repo",
"read_external_file",
"typecheck",
"metaset.query",
"metaset.list_tables",
"metaset.table_fields",
"metaset.list_questions",
"metaset.list_dashboards",
"metaset.health",
]);
interface PipelineContext {
planMode: boolean;
commandLog: Array<{ command: string; output: string; timestamp: string; success: boolean }>;
}
export class ToolManager { export class ToolManager {
private tools: Map<string, BaseTool> = new Map(); private tools: Map<string, BaseTool> = new Map();
private categories: Map<string, string[]> = new Map(); private categories: Map<string, string[]> = new Map();
private governanceSynced: boolean = false; private governanceSynced: boolean = false;
private _activePipelineId: number | null = null;
private _pipelineContexts: Map<number, PipelineContext> = new Map();
private _globalCommandLog: Array<{ command: string; output: string; timestamp: string; success: boolean; pipelineId?: number }> = [];
private getPipelineContext(pipelineId?: number): PipelineContext | null {
const id = pipelineId ?? this._activePipelineId;
if (id === null) return null;
if (!this._pipelineContexts.has(id)) {
this._pipelineContexts.set(id, { planMode: false, commandLog: [] });
}
return this._pipelineContexts.get(id)!;
}
setActivePipeline(pipelineId: number | null): void {
this._activePipelineId = pipelineId;
}
get activePipelineId(): number | null {
return this._activePipelineId;
}
get planMode(): boolean {
const ctx = this.getPipelineContext();
return ctx?.planMode ?? false;
}
setPlanMode(enabled: boolean, pipelineId?: number): void {
const id = pipelineId ?? this._activePipelineId;
if (id !== null && id !== undefined) {
const ctx = this.getPipelineContext(id);
if (ctx) ctx.planMode = enabled;
}
console.log(`[ToolManager] Plan Mode pipeline=${id}: ${enabled ? "ATIVADO" : "DESATIVADO"}`);
}
isWriteTool(toolName: string): boolean {
return WRITE_TOOLS.has(toolName);
}
getCommandLog(pipelineId?: number): typeof this._globalCommandLog {
if (pipelineId !== undefined) {
return this._globalCommandLog.filter(l => l.pipelineId === pipelineId);
}
return [...this._globalCommandLog];
}
clearCommandLog(pipelineId?: number): void {
if (pipelineId !== undefined) {
this._globalCommandLog = this._globalCommandLog.filter(l => l.pipelineId !== pipelineId);
} else {
this._globalCommandLog = [];
}
}
addCommandLog(entry: { command: string; output: string; timestamp: string; success: boolean }): void {
this._globalCommandLog.push({ ...entry, pipelineId: this._activePipelineId ?? undefined });
if (this._globalCommandLog.length > 500) {
this._globalCommandLog = this._globalCommandLog.slice(-250);
}
}
cleanupPipeline(pipelineId: number): void {
this._pipelineContexts.delete(pipelineId);
this._globalCommandLog = this._globalCommandLog.filter(l => l.pipelineId !== pipelineId);
if (this._activePipelineId === pipelineId) this._activePipelineId = null;
}
getToolsForMode(mode: "plan" | "act"): ToolDefinition[] {
if (mode === "act") return this.listTools();
return this.listTools().filter(t => !WRITE_TOOLS.has(t.name));
}
register(tool: BaseTool): void { register(tool: BaseTool): void {
this.tools.set(tool.name, tool); this.tools.set(tool.name, tool);
@ -188,14 +89,6 @@ export class ToolManager {
}; };
} }
if (this.planMode && WRITE_TOOLS.has(toolName)) {
return {
success: false,
result: `[PLAN MODE] Ferramenta "${toolName}" bloqueada. Em modo planejamento, apenas ferramentas de leitura são permitidas. Analise o código e gere um plano detalhado.`,
error: `PLAN_MODE_BLOCKED: ${toolName}`,
};
}
if (agentName) { if (agentName) {
const rbacCheck = await this.checkRBAC(toolName, agentName); const rbacCheck = await this.checkRBAC(toolName, agentName);
if (!rbacCheck.allowed) { if (!rbacCheck.allowed) {
@ -233,15 +126,6 @@ export class ToolManager {
console.log(`[ToolManager] ${toolName} executado em ${duration}ms`); console.log(`[ToolManager] ${toolName} executado em ${duration}ms`);
if (toolName === "run_command" || toolName === "typecheck") {
this.addCommandLog({
command: params.command || toolName,
output: result.result?.slice(0, 5000) || "",
timestamp: new Date().toISOString(),
success: result.success,
});
}
if (agentName) { if (agentName) {
await governanceService.recordAudit({ await governanceService.recordAudit({
agentName, agentName,

View File

@ -14,9 +14,6 @@ const BLOCKED_FILES = ['package.json', 'package-lock.json', '.env', 'drizzle.con
const PROTECTED_PATHS = [ const PROTECTED_PATHS = [
'server/routes.ts', 'server/routes.ts',
'shared/schema.ts', 'shared/schema.ts',
'shared/schemas/index.ts',
'shared/schemas/loader.ts',
'server/modules/loader.ts',
'client/src/App.tsx', 'client/src/App.tsx',
'client/src/main.tsx', 'client/src/main.tsx',
'server/index.ts', 'server/index.ts',
@ -24,11 +21,6 @@ const PROTECTED_PATHS = [
'db/index.ts', 'db/index.ts',
]; ];
const ALLOWED_MODULE_DIRS = [
'shared/schemas/',
'server/modules/',
];
export class WriteFileTool extends BaseTool { export class WriteFileTool extends BaseTool {
name = "write_file"; name = "write_file";
description = "Escreve ou cria um arquivo no projeto"; description = "Escreve ou cria um arquivo no projeto";
@ -61,6 +53,7 @@ export class WriteFileTool extends BaseTool {
return this.formatError(`Arquivo protegido: ${fileName}`); return this.formatError(`Arquivo protegido: ${fileName}`);
} }
// Bloquear sobrescrita de arquivos críticos do sistema
if (PROTECTED_PATHS.includes(filePath)) { if (PROTECTED_PATHS.includes(filePath)) {
return this.formatError(`Arquivo crítico protegido - não pode ser sobrescrito: ${filePath}`); return this.formatError(`Arquivo crítico protegido - não pode ser sobrescrito: ${filePath}`);
} }

View File

@ -800,7 +800,7 @@ ${JSON.stringify(sampleData.slice(0, 10), null, 2)}
Pergunta do usuário: ${question}`; Pergunta do usuário: ${question}`;
const response = await openai.chat.completions.create({ const response = await openai.chat.completions.create({
model: "arcadia-agent", model: "gpt-4o-mini",
messages: [ messages: [
{ role: "system", content: systemPrompt }, { role: "system", content: systemPrompt },
{ role: "user", content: userPrompt } { role: "user", content: userPrompt }

View File

@ -2,11 +2,14 @@ import type { Express, Request, Response } from "express";
import multer from "multer"; import multer from "multer";
import path from "path"; import path from "path";
import fs from "fs"; import fs from "fs";
import AdmZip from "adm-zip"; import AdmZip from "adm-zip";
import * as BSON from "bson"; import * as BSON from "bson";
import { db } from "../../db/index"; import { db } from "../../db/index";
import { biDatasets, stagedTables } from "@shared/schema"; import { biDatasets, stagedTables } from "@shared/schema";
import { sql } from "drizzle-orm"; import { sql } from "drizzle-orm";
import * as XLSX from "xlsx"; import * as XLSX from "xlsx";
const uploadDir = path.join(process.cwd(), "uploads"); const uploadDir = path.join(process.cwd(), "uploads");

View File

@ -2,10 +2,9 @@
* Arcadia Suite - Base Blackboard Agent * Arcadia Suite - Base Blackboard Agent
* *
* Classe base para agentes que monitoram e reagem ao Blackboard. * Classe base para agentes que monitoram e reagem ao Blackboard.
* Toda inteligência de IA é centralizada via ManusIntelligence.
* *
* @author Arcadia Development Team * @author Arcadia Development Team
* @version 2.0.0 - Manus-Powered * @version 1.0.0
*/ */
import OpenAI from "openai"; import OpenAI from "openai";
@ -36,121 +35,6 @@ export interface AgentThought {
result?: any; result?: any;
} }
export class ManusIntelligence {
private static instance: ManusIntelligence;
private model = "arcadia-agent";
private callCount = 0;
private tokenCount = 0;
private errorCount = 0;
private lastCallAt = 0;
private startedAt = Date.now();
static getInstance(): ManusIntelligence {
if (!ManusIntelligence.instance) {
ManusIntelligence.instance = new ManusIntelligence();
}
return ManusIntelligence.instance;
}
async generate(systemPrompt: string, userPrompt: string, options?: { maxTokens?: number; temperature?: number; enrichContext?: boolean }): Promise<string> {
this.callCount++;
try {
let enrichedPrompt = userPrompt;
if (options?.enrichContext !== false) {
const contextData = await this.enrichWithContext(userPrompt);
if (contextData) {
enrichedPrompt = userPrompt + contextData;
}
}
const response = await openai.chat.completions.create({
model: this.model,
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: enrichedPrompt }
],
max_tokens: options?.maxTokens || 8000,
temperature: options?.temperature || 0.2,
});
const content = response.choices[0]?.message?.content || '';
this.tokenCount += response.usage?.total_tokens || 0;
this.lastCallAt = Date.now();
return content;
} catch (error: any) {
this.errorCount++;
console.error(`[ManusIntelligence] Erro na geração:`, error.message);
throw error;
}
}
async think(systemPrompt: string, taskContext: string): Promise<AgentThought> {
this.callCount++;
try {
const contextData = await this.enrichWithContext(taskContext);
const enrichedContext = contextData ? taskContext + contextData : taskContext;
const response = await openai.chat.completions.create({
model: this.model,
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: enrichedContext }
],
response_format: { type: "json_object" },
max_tokens: 4000,
temperature: 0.2,
});
const content = response.choices[0]?.message?.content || '{}';
this.tokenCount += response.usage?.total_tokens || 0;
this.lastCallAt = Date.now();
return JSON.parse(content) as AgentThought;
} catch (error: any) {
this.errorCount++;
console.error(`[ManusIntelligence] Erro ao pensar:`, error.message);
return {
thought: `Erro ao processar: ${error.message}`,
finished: true,
result: { error: error.message }
};
}
}
async enrichWithContext(prompt: string): Promise<string> {
try {
const { toolManager } = await import("../autonomous/tools");
const keywords = prompt.split(/\s+/).filter(w => w.length > 4).slice(0, 5);
let contextData = "";
const searchResult = await toolManager.execute("search_code", {
query: keywords.join("|"),
maxResults: 5,
});
if (searchResult.success && searchResult.data?.results?.length > 0) {
contextData += "\n\nCÓDIGO RELEVANTE DO PROJETO:\n" +
searchResult.data.results.map((r: any) => `${r.file}:${r.line} - ${r.content}`).join("\n");
}
return contextData;
} catch {
return "";
}
}
getMetrics() {
return {
model: this.model,
callCount: this.callCount,
tokenCount: this.tokenCount,
errorCount: this.errorCount,
lastCallAt: this.lastCallAt,
startedAt: this.startedAt,
healthy: !!process.env.AI_INTEGRATIONS_OPENAI_API_KEY,
};
}
}
export const manusIntelligence = ManusIntelligence.getInstance();
export abstract class BaseBlackboardAgent extends EventEmitter { export abstract class BaseBlackboardAgent extends EventEmitter {
protected config: AgentConfig; protected config: AgentConfig;
protected isRunning: boolean = false; protected isRunning: boolean = false;
@ -182,17 +66,48 @@ export abstract class BaseBlackboardAgent extends EventEmitter {
? `\n\nArtefatos disponíveis:\n${artifacts.map(a => `- ${a.name} (${a.type}): ${a.content?.slice(0, 500)}...`).join('\n')}` ? `\n\nArtefatos disponíveis:\n${artifacts.map(a => `- ${a.name} (${a.type}): ${a.content?.slice(0, 500)}...`).join('\n')}`
: ''; : '';
const taskContext = `TAREFA: ${task.title}\n\nDESCRIÇÃO: ${task.description}\n\nCONTEXTO: ${context}${artifactContext}\n\nResponda em JSON: { "thought": "seu raciocínio", "action": "próxima ação", "actionInput": {}, "finished": true/false, "result": "se finished=true" }`; try {
const response = await openai.chat.completions.create({
model: "gpt-4o-mini",
messages: [
{ role: "system", content: this.config.systemPrompt },
{
role: "user",
content: `TAREFA: ${task.title}\n\nDESCRIÇÃO: ${task.description}\n\nCONTEXTO: ${context}${artifactContext}\n\nResponda em JSON: { "thought": "seu raciocínio", "action": "próxima ação", "actionInput": {}, "finished": true/false, "result": "se finished=true" }`
}
],
response_format: { type: "json_object" },
max_tokens: 4000,
});
return manusIntelligence.think(this.config.systemPrompt, taskContext); const content = response.choices[0]?.message?.content || '{}';
return JSON.parse(content) as AgentThought;
} catch (error: any) {
console.error(`[${this.name}] Erro ao pensar:`, error.message);
return {
thought: `Erro ao processar: ${error.message}`,
finished: true,
result: { error: error.message }
};
}
} }
protected async generateWithAI(prompt: string, systemPrompt?: string): Promise<string> { protected async generateWithAI(prompt: string, systemPrompt?: string): Promise<string> {
return manusIntelligence.generate( try {
systemPrompt || this.config.systemPrompt, const response = await openai.chat.completions.create({
prompt, model: "gpt-4o-mini",
{ maxTokens: 8000 } messages: [
); { role: "system", content: systemPrompt || this.config.systemPrompt },
{ role: "user", content: prompt }
],
max_tokens: 8000,
});
return response.choices[0]?.message?.content || '';
} catch (error: any) {
console.error(`[${this.name}] Erro na geração:`, error.message);
throw error;
}
} }
protected async log(taskId: number, action: string, thought: string, observation?: string): Promise<void> { protected async log(taskId: number, action: string, thought: string, observation?: string): Promise<void> {

View File

@ -58,6 +58,7 @@ const ALL_PHASES: PipelinePhase[] = ["design", "codegen", "validation", "staging
class PipelineOrchestrator extends EventEmitter { class PipelineOrchestrator extends EventEmitter {
private activeMonitors: Map<number, NodeJS.Timeout> = new Map(); private activeMonitors: Map<number, NodeJS.Timeout> = new Map();
private processingMonitors: Set<number> = new Set();
async createPipeline(prompt: string, userId: string = "system", metadata?: any): Promise<XosDevPipeline> { async createPipeline(prompt: string, userId: string = "system", metadata?: any): Promise<XosDevPipeline> {
const correlationId = randomUUID(); const correlationId = randomUUID();
@ -169,10 +170,14 @@ class PipelineOrchestrator extends EventEmitter {
if (this.activeMonitors.has(pipelineId)) return; if (this.activeMonitors.has(pipelineId)) return;
const interval = setInterval(async () => { const interval = setInterval(async () => {
if (this.processingMonitors.has(pipelineId)) return;
this.processingMonitors.add(pipelineId);
try { try {
await this.checkPhaseProgress(pipelineId, mainTaskId); await this.checkPhaseProgress(pipelineId, mainTaskId);
} catch (error) { } catch (error) {
console.error(`[PipelineOrchestrator] Erro no monitor #${pipelineId}:`, error); console.error(`[PipelineOrchestrator] Erro no monitor #${pipelineId}:`, error);
} finally {
this.processingMonitors.delete(pipelineId);
} }
}, 3000); }, 3000);
@ -384,26 +389,6 @@ class PipelineOrchestrator extends EventEmitter {
} }
} }
const schemaFiles = applied.filter(f => f.startsWith("shared/schemas/") && f.endsWith(".ts"));
if (schemaFiles.length > 0) {
try {
const { registerAndMigrate } = await import("../modules/migrator");
for (const schemaFile of schemaFiles) {
const moduleName = schemaFile.replace("shared/schemas/", "").replace(".ts", "");
if (moduleName && moduleName !== "index" && moduleName !== "loader" && !moduleName.startsWith("_")) {
const migResult = await registerAndMigrate(moduleName);
if (migResult.success) {
console.log(`[Pipeline] Módulo ${moduleName}: schema registrado${migResult.migrationApplied ? " + migração aplicada" : ""}`);
} else {
errors.push(`Migração ${moduleName}: ${migResult.error}`);
}
}
}
} catch (migError: any) {
console.error("[Pipeline] Erro no migrator:", migError.message);
}
}
if (applied.length > 0) { if (applied.length > 0) {
await toolManager.execute("git_local_commit", { await toolManager.execute("git_local_commit", {
message: `[Pipeline #${pipelineId}] ${pipeline.prompt.slice(0, 60)}`, message: `[Pipeline #${pipelineId}] ${pipeline.prompt.slice(0, 60)}`,

View File

@ -33,44 +33,18 @@ Você tem acesso ao contexto do projeto e pode ler arquivos existentes.
- Banco: PostgreSQL - Banco: PostgreSQL
- Autenticação: Passport.js - Autenticação: Passport.js
## SISTEMA MODULAR (IMPORTANTE!)
O projeto usa um sistema modular para autonomia do Dev Center:
### Schemas (Banco de Dados)
- Criar schemas novos em: shared/schemas/{nomeModulo}.ts
- NÃO modificar shared/schema.ts (protegido)
- NÃO importar de @shared/schema (evitar referência circular)
- Usar varchar("user_id") SEM .references() para referências a tabelas do schema principal
- Importar diretamente: import { sql } from "drizzle-orm"; import { pgTable, ... } from "drizzle-orm/pg-core";
- Exportar: tabelas, insertSchemas (drizzle-zod), e types
- Prefixar tabelas com nome do módulo: bsc_objectives, fin_transactions, etc.
### Rotas (API)
- Criar rotas em: server/modules/{nomeModulo}.ts
- NÃO modificar server/routes.ts (protegido)
- Exportar default um Router do Express
- As rotas ficam automaticamente em /api/modules/{nomeModulo}
- Importar schemas do módulo: import { ... } from "@shared/schemas/{nomeModulo}"
- Importar db: import { db } from "../../db"
### Páginas (Frontend)
- Criar em: client/src/pages/{NomeModulo}.tsx
- Componentes em: client/src/components/{nomeModulo}/
## Formato de Saída (JSON) ## Formato de Saída (JSON)
{ {
"moduleName": "nome-do-modulo", "moduleName": "nome-do-modulo",
"description": "descrição completa", "description": "descrição completa",
"schema": { "schema": {
"file": "shared/schemas/{moduleName}.ts",
"tables": [{ "name": "tabela", "columns": [{ "name": "coluna", "type": "tipo", "constraints": [] }] }] "tables": [{ "name": "tabela", "columns": [{ "name": "coluna", "type": "tipo", "constraints": [] }] }]
}, },
"api": { "api": {
"file": "server/modules/{moduleName}.ts", "routes": [{ "method": "GET|POST|PUT|DELETE", "path": "/api/rota", "description": "descrição", "requestBody": {}, "response": {} }]
"routes": [{ "method": "GET|POST|PUT|DELETE", "path": "/{rota}", "description": "descrição" }]
}, },
"ui": { "ui": {
"components": [{ "name": "Componente", "type": "page|modal|widget", "description": "desc", "path": "client/src/pages/{Nome}.tsx" }] "components": [{ "name": "Componente", "type": "page|modal|widget", "description": "desc", "path": "caminho/arquivo.tsx" }]
}, },
"integrations": ["módulos existentes que devem ser integrados"], "integrations": ["módulos existentes que devem ser integrados"],
"existingFiles": ["arquivos que precisam ser modificados"] "existingFiles": ["arquivos que precisam ser modificados"]

View File

@ -35,9 +35,6 @@ const PROTECTED_FILES = [
"server/storage.ts", "server/storage.ts",
"server/db.ts", "server/db.ts",
"shared/schema.ts", "shared/schema.ts",
"shared/schemas/index.ts",
"shared/schemas/loader.ts",
"server/modules/loader.ts",
"client/src/App.tsx", "client/src/App.tsx",
"client/src/main.tsx", "client/src/main.tsx",
"client/src/pages/Cockpit.tsx", "client/src/pages/Cockpit.tsx",
@ -48,21 +45,8 @@ const PROTECTED_FILES = [
"tsconfig.json", "tsconfig.json",
"vite.config.ts", "vite.config.ts",
"drizzle.config.ts", "drizzle.config.ts",
"db/index.ts",
]; ];
const ALLOWED_MODULE_DIRS = [
"shared/schemas/",
"server/modules/",
];
export function isModuleFile(filePath: string): boolean {
return ALLOWED_MODULE_DIRS.some(dir => filePath.startsWith(dir)) &&
!PROTECTED_FILES.includes(filePath) &&
!filePath.startsWith("shared/schemas/_") &&
!filePath.startsWith("server/modules/_");
}
export class ExecutorAgent extends BaseBlackboardAgent { export class ExecutorAgent extends BaseBlackboardAgent {
constructor() { constructor() {
const config: AgentConfig = { const config: AgentConfig = {

View File

@ -34,27 +34,6 @@ Você pode ler arquivos existentes para manter consistência de estilo.
5. Use shadcn/ui para componentes de UI 5. Use shadcn/ui para componentes de UI
6. Adicione data-testid em elementos interativos 6. Adicione data-testid em elementos interativos
## SISTEMA MODULAR (IMPORTANTE!)
O projeto usa um sistema modular. SEMPRE crie arquivos nas pastas modulares:
### Schemas de Banco shared/schemas/{nomeModulo}.ts
- NÃO modificar shared/schema.ts (protegido!)
- NÃO importar de @shared/schema (referência circular!)
- Importar: import { sql } from "drizzle-orm"; import { pgTable, text, varchar, serial, integer, timestamp, boolean, jsonb, numeric } from "drizzle-orm/pg-core";
- Importar: import { createInsertSchema } from "drizzle-zod"; import { z } from "zod";
- Para referências a users: usar varchar("user_id") SEM .references()
- Prefixar tabelas: bsc_objectives, fin_transactions, etc.
- Exportar: tabelas + insertSchemas + types
### Rotas de API server/modules/{nomeModulo}.ts
- NÃO modificar server/routes.ts (protegido!)
- Exportar default um Router do Express
- Rotas montadas automaticamente em /api/modules/{nomeModulo}
- Import db: import { db } from "../../db";
- Import schemas: import { ... } from "@shared/schemas/{nomeModulo}";
### Páginas React client/src/pages/{NomeModulo}.tsx
## Formato de Saída ## Formato de Saída
Para cada arquivo, retorne JSON: Para cada arquivo, retorne JSON:
{ {
@ -138,8 +117,8 @@ ${schemaRef}
${routesRef} ${routesRef}
Gere arquivos completos e funcionais para: Gere arquivos completos e funcionais para:
1. Schema do banco (se necessário) - CRIAR em shared/schemas/{moduleName}.ts (NÃO editar schema.ts!) 1. Schema do banco de dados (se necessário) - adicionar ao shared/schema.ts
2. Rotas da API - CRIAR em server/modules/{moduleName}.ts (NÃO editar routes.ts!) 2. Rotas da API - novo arquivo em server/ ou adicionar a routes.ts
3. Componente principal React - em client/src/pages/ 3. Componente principal React - em client/src/pages/
Retorne em formato JSON com a estrutura: Retorne em formato JSON com a estrutura:

View File

@ -1,14 +1,11 @@
import { Router, Request, Response } from "express"; import { Router, Request, Response } from "express";
import { pipelineOrchestrator } from "./PipelineOrchestrator"; import { pipelineOrchestrator } from "./PipelineOrchestrator";
import { toolManager } from "../autonomous/tools/ToolManager";
import { z } from "zod"; import { z } from "zod";
const router = Router(); const router = Router();
const createPipelineSchema = z.object({ const createPipelineSchema = z.object({
prompt: z.string().min(5, "O prompt deve ter pelo menos 5 caracteres"), prompt: z.string().min(5, "O prompt deve ter pelo menos 5 caracteres"),
mode: z.enum(["plan", "act"]).optional().default("act"),
planContext: z.string().optional(),
metadata: z.record(z.any()).optional(), metadata: z.record(z.any()).optional(),
budget: z.object({ budget: z.object({
maxTokens: z.number().optional(), maxTokens: z.number().optional(),
@ -24,64 +21,18 @@ router.post("/", async (req: Request, res: Response) => {
return res.status(400).json({ success: false, error: parsed.error.errors[0].message }); return res.status(400).json({ success: false, error: parsed.error.errors[0].message });
} }
const mode = parsed.data.mode || "act";
const userId = (req.user as any)?.id || "anonymous"; const userId = (req.user as any)?.id || "anonymous";
const metadata = { const metadata = { ...(parsed.data.metadata || {}), budget: parsed.data.budget };
...(parsed.data.metadata || {}), const pipeline = await pipelineOrchestrator.createPipeline(parsed.data.prompt, userId, metadata);
budget: parsed.data.budget,
mode,
planContext: parsed.data.planContext || null,
};
let prompt = parsed.data.prompt;
if (mode === "plan") {
prompt = `[MODO PLANEJAMENTO] Analise o projeto e gere um plano detalhado de implementação. NÃO modifique arquivos. Use apenas ferramentas de leitura (read_file, search_code, list_directory). Gere um documento implementation_plan.md com:\n1. Análise do estado atual\n2. Arquivos que precisam ser criados/modificados\n3. Estratégia passo-a-passo\n4. Dependências e riscos\n\nSolicitação: ${prompt}`;
} else if (parsed.data.planContext) {
prompt = `[MODO EXECUÇÃO] Siga o plano de implementação abaixo e execute as mudanças:\n\n--- PLANO ---\n${parsed.data.planContext}\n--- FIM DO PLANO ---\n\nSolicitação original: ${prompt}`;
}
const pipeline = await pipelineOrchestrator.createPipeline(prompt, userId, metadata);
toolManager.setActivePipeline(pipeline.id);
toolManager.setPlanMode(mode === "plan", pipeline.id);
const started = await pipelineOrchestrator.startPipeline(pipeline.id); const started = await pipelineOrchestrator.startPipeline(pipeline.id);
res.json({ success: true, pipeline: started, mode }); res.json({ success: true, pipeline: started });
} catch (error: any) { } catch (error: any) {
console.error("[Pipeline] Erro ao criar:", error); console.error("[Pipeline] Erro ao criar:", error);
res.status(500).json({ success: false, error: error.message }); res.status(500).json({ success: false, error: error.message });
} }
}); });
router.get("/agent-terminal", async (req: Request, res: Response) => {
try {
const pipelineId = req.query.pipelineId ? parseInt(req.query.pipelineId as string) : undefined;
const logs = toolManager.getCommandLog(pipelineId);
res.json({ success: true, logs });
} catch (error: any) {
res.status(500).json({ success: false, error: error.message });
}
});
router.get("/mode", async (req: Request, res: Response) => {
const pipelineId = req.query.pipelineId ? parseInt(req.query.pipelineId as string) : undefined;
if (pipelineId !== undefined) {
toolManager.setActivePipeline(pipelineId);
}
res.json({ success: true, planMode: toolManager.planMode, activePipeline: toolManager.activePipelineId });
});
router.post("/mode", async (req: Request, res: Response) => {
const { mode, pipelineId } = req.body;
if (mode !== "plan" && mode !== "act") {
return res.status(400).json({ success: false, error: "Mode must be 'plan' or 'act'" });
}
const pid = pipelineId ? parseInt(pipelineId) : undefined;
toolManager.setPlanMode(mode === "plan", pid);
res.json({ success: true, mode, planMode: toolManager.planMode });
});
router.get("/", async (req: Request, res: Response) => { router.get("/", async (req: Request, res: Response) => {
try { try {
const limit = Math.min(parseInt(req.query.limit as string) || 20, 50); const limit = Math.min(parseInt(req.query.limit as string) || 20, 50);
@ -235,29 +186,4 @@ router.get("/:id/stream", async (req: Request, res: Response) => {
}); });
}); });
router.get("/modules/status", async (_req: Request, res: Response) => {
try {
const { getModuleStatus, listModuleSchemas } = await import("../modules/migrator");
const { getLoadedModules } = await import("../modules/loader");
const status = await getModuleStatus();
const schemas = await listModuleSchemas();
const loadedRoutes = getLoadedModules();
res.json({
success: true,
modules: status,
registeredSchemas: schemas,
loadedRoutes,
allowedPaths: {
schemas: "shared/schemas/{moduleName}.ts",
routes: "server/modules/{moduleName}.ts",
pages: "client/src/pages/{ModuleName}.tsx",
},
});
} catch (error: any) {
res.status(500).json({ success: false, error: error.message });
}
});
export default router; export default router;

View File

@ -31,7 +31,7 @@ function requireAuth(req: Request, res: Response, next: NextFunction) {
// Apply auth to all routes // Apply auth to all routes
router.use(requireAuth); router.use(requireAuth);
// Helper to get tenant ID from request header or user's first tenant (auto-creates if none) // Helper to get tenant ID from request header or user's first tenant
async function getTenantId(req: Request): Promise<number | null> { async function getTenantId(req: Request): Promise<number | null> {
const userId = (req.user as any).id; const userId = (req.user as any).id;
const headerTenantId = req.headers["x-tenant-id"]; const headerTenantId = req.headers["x-tenant-id"];
@ -40,13 +40,8 @@ async function getTenantId(req: Request): Promise<number | null> {
const isMember = await compassStorage.isUserInTenant(userId, tenantId); const isMember = await compassStorage.isUserInTenant(userId, tenantId);
return isMember ? tenantId : null; return isMember ? tenantId : null;
} }
const userTenants = await compassStorage.getUserTenants(userId); const tenants = await compassStorage.getUserTenants(userId);
if (userTenants.length > 0) return userTenants[0].id; return tenants.length > 0 ? tenants[0].id : null;
// Auto-cria tenant padrão para o usuário na primeira vez
const userName = (req.user as any).name || (req.user as any).username || "Minha Organização";
const tenant = await compassStorage.createTenant({ name: userName, plan: "free", status: "active" });
await compassStorage.addUserToTenant({ tenantId: tenant.id, userId, role: "owner", isOwner: "true" });
return tenant.id;
} }
// Validate tenant membership // Validate tenant membership
@ -2279,4 +2274,124 @@ router.put("/projects/:projectId/history", async (req: Request, res: Response) =
} }
}); });
// ========== AI BRIEFING & HEALTH SCORE ==========
router.post("/projects/:projectId/ai-brief", async (req: Request, res: Response) => {
try {
const tenantId = await getTenantId(req);
if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" });
const projectId = parseInt(req.params.projectId);
const hasAccess = await validateProjectAccess(projectId, tenantId);
if (!hasAccess) return res.status(403).json({ error: "Acesso negado ao projeto" });
const project = await compassStorage.getProject(projectId, tenantId);
const tasks = await compassStorage.getTasks(projectId);
const pdcaCycles = await compassStorage.getPdcaCycles(tenantId, projectId);
const openTasks = tasks.filter((t: any) => t.status !== "done" && t.status !== "completed");
const overdue = tasks.filter((t: any) => t.dueDate && new Date(t.dueDate) < new Date() && t.status !== "done");
const pdcaOpen = pdcaCycles.filter((c: any) => c.status === "open" || c.status === "in_progress");
const OpenAI = (await import("openai")).default;
const openai = new OpenAI({
apiKey: process.env.AI_INTEGRATIONS_OPENAI_API_KEY,
baseURL: process.env.AI_INTEGRATIONS_OPENAI_BASE_URL,
timeout: 30000,
});
const prompt = `Você é um consultor de gestão de projetos. Analise o projeto abaixo e gere um briefing executivo em português.
PROJETO: ${project?.name}
DESCRIÇÃO: ${project?.description || "Não informada"}
STATUS: ${project?.status}
FASE: ${project?.currentPhase || "Não definida"}
RESUMO DE TAREFAS:
- Total: ${tasks.length}
- Em aberto: ${openTasks.length}
- Atrasadas: ${overdue.length}
PDCA ATIVOS: ${pdcaOpen.length}
${overdue.length > 0 ? `TAREFAS ATRASADAS:\n${overdue.slice(0, 5).map((t: any) => `- ${t.title} (responsável: ${t.assignedTo || "não atribuído"})`).join("\n")}` : ""}
Gere um briefing com:
1. **Situação atual** (2-3 linhas)
2. **Principais riscos** (lista com até 3 itens)
3. **Ações prioritárias** (lista com até 5 ações, cada uma com responsável sugerido e prazo)
4. **Score de saúde** (0-100 com justificativa)
Seja direto e objetivo.`;
const response = await openai.chat.completions.create({
model: "gpt-4o-mini",
messages: [{ role: "user", content: prompt }],
max_tokens: 800,
temperature: 0.3,
});
const briefText = response.choices[0]?.message?.content || "";
// Extract health score from response
const scoreMatch = briefText.match(/score[^\d]*(\d{1,3})/i) || briefText.match(/(\d{1,3})[^\d]*\//);
const healthScore = scoreMatch ? Math.min(100, Math.max(0, parseInt(scoreMatch[1]))) : null;
res.json({
projectId,
brief: briefText,
healthScore,
meta: {
totalTasks: tasks.length,
openTasks: openTasks.length,
overdueTasks: overdue.length,
activePdca: pdcaOpen.length,
generatedAt: new Date().toISOString(),
},
});
} catch (error: any) {
res.status(500).json({ error: error.message });
}
});
router.get("/projects/:projectId/health", async (req: Request, res: Response) => {
try {
const tenantId = await getTenantId(req);
if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" });
const projectId = parseInt(req.params.projectId);
const hasAccess = await validateProjectAccess(projectId, tenantId);
if (!hasAccess) return res.status(403).json({ error: "Acesso negado ao projeto" });
const tasks = await compassStorage.getTasks(projectId);
const pdcaCycles = await compassStorage.getPdcaCycles(tenantId, projectId);
const total = tasks.length || 1;
const done = tasks.filter((t: any) => t.status === "done" || t.status === "completed").length;
const overdue = tasks.filter((t: any) => t.dueDate && new Date(t.dueDate) < new Date() && t.status !== "done").length;
const pdcaComplete = pdcaCycles.filter((c: any) => c.status === "completed").length;
const pdcaTotal = pdcaCycles.length || 1;
const completionScore = (done / total) * 40;
const overdueScore = Math.max(0, 30 - (overdue / total) * 30);
const pdcaScore = (pdcaComplete / pdcaTotal) * 30;
const healthScore = Math.round(completionScore + overdueScore + pdcaScore);
const level = healthScore >= 80 ? "saudável" : healthScore >= 50 ? "atenção" : "crítico";
const color = healthScore >= 80 ? "green" : healthScore >= 50 ? "yellow" : "red";
res.json({
projectId,
healthScore,
level,
color,
breakdown: {
completion: { score: Math.round(completionScore), weight: "40%", tasks: `${done}/${tasks.length}` },
timeliness: { score: Math.round(overdueScore), weight: "30%", overdue },
pdca: { score: Math.round(pdcaScore), weight: "30%", cycles: `${pdcaComplete}/${pdcaCycles.length}` },
},
});
} catch (error: any) {
res.status(500).json({ error: error.message });
}
});
export default router; export default router;

View File

@ -36,6 +36,7 @@ export class FrappeService {
"Content-Type": "application/json", "Content-Type": "application/json",
Accept: "application/json", Accept: "application/json",
}, },
signal: AbortSignal.timeout(30000),
}; };
if (data && (method === "POST" || method === "PUT")) { if (data && (method === "POST" || method === "PUT")) {

View File

@ -3,8 +3,6 @@ import { manusService } from "./manus/service";
import { db } from "../db/index"; import { db } from "../db/index";
import { sql } from "drizzle-orm"; import { sql } from "drizzle-orm";
const router = Router(); const router = Router();
const DEV_AGENT_CONTEXT = `CONTEXTO ESPECIAL - DEV AGENT: const DEV_AGENT_CONTEXT = `CONTEXTO ESPECIAL - DEV AGENT:
@ -79,7 +77,9 @@ Usuário: ${message}
Responda como Dev Agent, criando os recursos solicitados:`; Responda como Dev Agent, criando os recursos solicitados:`;
const responseContent = await manusService.runSync(userId, fullPrompt, []); const result = await manusService.run(userId, fullPrompt, []);
const responseContent = result.finalResponse || "Desculpe, não consegui processar sua solicitação.";
const jsonMatch = responseContent.match(/```json\s*([\s\S]*?)\s*```/); const jsonMatch = responseContent.match(/```json\s*([\s\S]*?)\s*```/);
let action = null; let action = null;

View File

@ -1,11 +1,10 @@
import type { Express, Request, Response } from "express"; import type { Express, Request, Response } from "express";
import { restartManagedService, stopManagedService, getManagedServiceInfo, getManagedServiceLogs } from "../index"; import { restartManagedService, stopManagedService, getManagedServiceInfo, getManagedServiceLogs } from "../index";
import { manusIntelligence } from "../blackboard/BaseBlackboardAgent";
interface EngineConfig { interface EngineConfig {
name: string; name: string;
displayName: string; displayName: string;
type: "python" | "php" | "node" | "java" | "ai"; type: "python" | "php" | "node" | "java";
port: number; port: number;
healthPath: string; healthPath: string;
category: "erp" | "intelligence" | "data" | "fiscal" | "automation"; category: "erp" | "intelligence" | "data" | "fiscal" | "automation";
@ -13,23 +12,14 @@ interface EngineConfig {
} }
const ENGINES: EngineConfig[] = [ const ENGINES: EngineConfig[] = [
{
name: "manus-ia",
displayName: "Manus IA (Cerebro Central)",
type: "ai",
port: 5000,
healthPath: "/api/manus/health",
category: "intelligence",
description: "Motor de IA Central - GPT-4o, 56 Tools, Knowledge Graph, Dev Pipeline",
},
{ {
name: "plus", name: "plus",
displayName: "Arcadia Plus (ERP)", displayName: "Arcadia Plus (ERP)",
type: "php", type: "php",
port: 8080, port: 8080,
healthPath: "/", healthPath: "/api/health",
category: "erp", category: "erp",
description: "Motor Plus - PDV, NF-e, Estoque, Financeiro", description: "ERP completo Laravel - PDV, NF-e, Estoque, Financeiro",
}, },
{ {
name: "contabil", name: "contabil",
@ -94,21 +84,12 @@ async function checkEngineHealth(engine: EngineConfig): Promise<any> {
try { try {
const start = Date.now(); const start = Date.now();
const headers: Record<string, string> = {}; const response = await fetch(url, { signal: controller.signal });
if (engine.name === "manus-ia") {
headers["x-internal-check"] = "engine-room";
}
const fetchOptions: any = { signal: controller.signal, headers };
if (engine.name === "plus") {
fetchOptions.redirect = "manual";
}
const response = await fetch(url, fetchOptions);
clearTimeout(timeout); clearTimeout(timeout);
const elapsed = Date.now() - start; const elapsed = Date.now() - start;
const isRedirect = response.status >= 300 && response.status < 400; if (response.ok) {
if (response.ok || (engine.name === "plus" && isRedirect)) { const data = await response.json().catch(() => ({}));
const data = response.ok ? await response.json().catch(() => ({})) : { redirect: true, location: response.headers.get("location") };
return { return {
...engine, ...engine,
status: "online", status: "online",
@ -399,60 +380,5 @@ export function registerEngineRoomRoutes(app: Express): void {
} }
}); });
app.get("/api/manus/health", async (req: Request, res: Response) => {
try {
const isInternal = req.headers["x-internal-check"] === "engine-room";
if (!isInternal && !req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const metrics = manusIntelligence.getMetrics();
let toolCount = 0;
try {
const { toolManager } = await import("../autonomous/tools");
toolCount = (toolManager as any).getToolNames?.()?.length || (toolManager as any).tools?.size || 56;
} catch {
toolCount = 56;
}
let agentCount = 0;
try {
const { getAgentsStatus } = await import("../blackboard/agents");
const agents = getAgentsStatus();
agentCount = agents.filter((a: any) => a.running).length;
} catch {}
const aiConfigured = metrics.healthy;
const engineStatus = aiConfigured ? "online" : "error";
res.json({
status: engineStatus,
engine: "Manus IA",
version: "2.0.0",
model: metrics.model,
database: "connected",
aiConfigured,
capabilities: {
tools: toolCount,
agents: agentCount,
knowledgeGraph: true,
semanticSearch: true,
devPipeline: true,
autonomousDev: true,
},
metrics: {
totalCalls: metrics.callCount,
totalTokens: metrics.tokenCount,
errorCount: metrics.errorCount,
lastCallAt: metrics.lastCallAt || null,
uptime: process.uptime(),
},
});
} catch (error: any) {
res.status(500).json({ status: "error", error: error.message });
}
});
console.log("[Engine Room] Rotas registradas em /api/engine-room/*"); console.log("[Engine Room] Rotas registradas em /api/engine-room/*");
} }

View File

@ -1,23 +1,12 @@
import type { Express, Request, Response } from "express"; import type { Express, Request, Response } from "express";
import { erpStorage, createErpClient } from "./index"; import { erpStorage, createErpClient } from "./index";
import { db } from "../../db/index"; import { db } from "../../db/index";
import { customers, suppliers, products, salesOrders, purchaseOrders, erpSegments, erpConfig, insertErpSegmentSchema, insertErpConfigSchema, persons, personRoles, mobileDevices, posSales, posSaleItems, finAccountsReceivable, tenants, tenantEmpresas, type TenantFeatures } from "@shared/schema"; import { customers, suppliers, products, salesOrders, purchaseOrders, erpSegments, erpConfig, insertErpSegmentSchema, insertErpConfigSchema, persons, personRoles, mobileDevices, posSales, posSaleItems, finAccountsReceivable } from "@shared/schema";
import { eq, and, sql, desc } from "drizzle-orm"; import { eq, and, sql, desc } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { soeRuleEngine } from "../soe/rule-engine/index"; import { soeRuleEngine } from "../soe/rule-engine/index";
import { soeEventBus } from "../soe/event-bus"; import { soeEventBus } from "../soe/event-bus";
export function registerSoeRoutes(app: Express): void {
app.use((req, res, next) => {
if (req.url.startsWith("/api/soe/") || req.url === "/api/soe") {
req.url = req.url.replace("/api/soe", "/api/erp");
}
next();
});
registerErpRoutes(app);
}
export function registerErpRoutes(app: Express): void { export function registerErpRoutes(app: Express): void {
app.get("/api/erp/connections", async (req: Request, res: Response) => { app.get("/api/erp/connections", async (req: Request, res: Response) => {
try { try {
@ -863,23 +852,24 @@ export function registerErpRoutes(app: Express): void {
return res.status(401).json({ error: "Not authenticated" }); return res.status(401).json({ error: "Not authenticated" });
} }
const tenantId = req.user?.tenantId || 1; const tenantId = req.user?.tenantId || 1;
const user = req.user as any;
// SOE Rule Engine — enriquece payload antes de inserir // SOE Rule Engine — aplica regras fiscais e de negócio antes de criar o pedido
const enriched = await soeRuleEngine.apply("pre_sales_order", req.body, { const { payload: enriched, validationErrors } = await soeRuleEngine.apply(
tenantId, "pre_sales_order",
motor: "local", { ...req.body, _tipo: "venda" },
}); { tenantId, empresaId: req.body.empresaId, userId: user?.id, motor: "local" }
if (enriched.validationErrors.length > 0) { );
return res.status(422).json({ error: "Validação SOE falhou", detalhes: enriched.validationErrors }); if (validationErrors.length > 0) {
return res.status(422).json({ error: "Validação falhou", details: validationErrors });
} }
const body = enriched.payload;
const { orderNumber, customerId, orderDate, deliveryDate, status, subtotal, discount, tax, total, paymentMethod, notes } = body; const { orderNumber, customerId, orderDate, deliveryDate, status, subtotal, discount, tax, total, paymentMethod, notes } = enriched;
const [order] = await db.insert(salesOrders).values({ const [order] = await db.insert(salesOrders).values({
tenantId, tenantId,
orderNumber, customerId, orderDate, deliveryDate, status, subtotal, discount, tax, total, paymentMethod, notes orderNumber, customerId, orderDate, deliveryDate, status, subtotal, discount, tax, total, paymentMethod, notes
}).returning(); }).returning();
res.status(201).json(order); res.status(201).json({ ...order, _rulesApplied: enriched._rulesApplied });
} catch (error) { } catch (error) {
console.error("Error creating sales order:", error); console.error("Error creating sales order:", error);
res.status(500).json({ error: "Failed to create sales order" }); res.status(500).json({ error: "Failed to create sales order" });
@ -900,7 +890,16 @@ export function registerErpRoutes(app: Express): void {
if (!updated) { if (!updated) {
return res.status(404).json({ error: "Sales order not found" }); return res.status(404).json({ error: "Sales order not found" });
} }
res.json({ success: true, order: updated, message: "Pedido confirmado. Lançamentos contábeis gerados." });
// SOE Event Bus — dispara automações pós-confirmação
soeEventBus.emit("venda_confirmada", {
tenantId,
empresaId: (req.user as any)?.empresaId,
motorOrigem: "local",
dados: { id, status: "confirmed", ...updated },
}).catch((e) => console.error("[SOE] venda_confirmada error:", e.message));
res.json({ success: true, order: updated, message: "Pedido confirmado. Automações SOE disparadas." });
} catch (error) { } catch (error) {
console.error("Error confirming sales order:", error); console.error("Error confirming sales order:", error);
res.status(500).json({ error: "Failed to confirm sales order" }); res.status(500).json({ error: "Failed to confirm sales order" });
@ -914,14 +913,16 @@ export function registerErpRoutes(app: Express): void {
} }
const id = parseInt(req.params.id); const id = parseInt(req.params.id);
const tenantId = req.user?.tenantId || 1; const tenantId = req.user?.tenantId || 1;
const user = req.user as any;
// SOE Rule Engine — valida e enriquece payload fiscal antes de emitir // SOE Rule Engine — aplica regras fiscais antes de gerar a NF-e
const enriched = await soeRuleEngine.apply("pre_nfe_emissao", { ...req.body, _pedidoId: id }, { const { payload: enriched, validationErrors } = await soeRuleEngine.apply(
tenantId, "pre_nfe_emissao",
motor: "local", { ...req.body, tipoDocumento: "nfe", _tipo: "venda" },
}); { tenantId, empresaId: user?.empresaId, userId: user?.id, motor: "local" }
if (enriched.validationErrors.length > 0) { );
return res.status(422).json({ error: "Validação fiscal SOE falhou", detalhes: enriched.validationErrors }); if (validationErrors.length > 0) {
return res.status(422).json({ error: "Validação fiscal falhou", details: validationErrors });
} }
const [updated] = await db.update(salesOrders) const [updated] = await db.update(salesOrders)
@ -935,12 +936,22 @@ export function registerErpRoutes(app: Express): void {
// SOE Event Bus — dispara lançamentos contábeis automáticos // SOE Event Bus — dispara lançamentos contábeis automáticos
soeEventBus.emit("nfe_emitida", { soeEventBus.emit("nfe_emitida", {
tenantId, tenantId,
empresaId: enriched.payload.empresaId || req.body.empresaId, empresaId: user?.empresaId,
motorOrigem: "local", motorOrigem: "local",
dados: { ...enriched.payload, ...updated }, dados: {
}).catch((e: any) => console.error("[routes] eventBus nfe_emitida:", e.message)); ...enriched,
...updated,
valorTotal: updated.total,
dataEmissao: new Date().toISOString(),
},
}).catch((e) => console.error("[SOE] nfe_emitida error:", e.message));
res.json({ success: true, order: updated, regrasSoe: enriched.appliedRules, message: "NF-e gerada com sucesso." }); res.json({
success: true,
order: updated,
tributacao: enriched.tributacao,
message: "NF-e gerada. Lançamentos contábeis sendo processados.",
});
} catch (error) { } catch (error) {
console.error("Error generating NF-e:", error); console.error("Error generating NF-e:", error);
res.status(500).json({ error: "Failed to generate NF-e" }); res.status(500).json({ error: "Failed to generate NF-e" });
@ -997,15 +1008,7 @@ export function registerErpRoutes(app: Express): void {
return res.status(401).json({ error: "Not authenticated" }); return res.status(401).json({ error: "Not authenticated" });
} }
const tenantId = req.user?.tenantId || 1; const tenantId = req.user?.tenantId || 1;
const { orderNumber, supplierId, orderDate, expectedDate, status, subtotal, discount, tax, total, notes } = req.body;
// SOE Rule Engine — enriquece payload de compra (CFOP, tributação entrada)
const enriched = await soeRuleEngine.apply("pre_purchase_order", req.body, {
tenantId,
motor: "local",
});
const body = enriched.payload;
const { orderNumber, supplierId, orderDate, expectedDate, status, subtotal, discount, tax, total, notes } = body;
const [order] = await db.insert(purchaseOrders).values({ const [order] = await db.insert(purchaseOrders).values({
tenantId, tenantId,
orderNumber, supplierId, orderDate, expectedDate, status, subtotal, discount, tax, total, notes orderNumber, supplierId, orderDate, expectedDate, status, subtotal, discount, tax, total, notes
@ -1186,198 +1189,7 @@ export function registerErpRoutes(app: Express): void {
res.status(500).json({ error: "Failed to save config" }); res.status(500).json({ error: "Failed to save config" });
} }
}); });
// GET /api/erp/tenant/modules - Get current tenant's active modules
app.get("/api/erp/tenant/modules", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) return res.status(401).json({ error: "Not authenticated" });
const tenantId = req.user?.tenantId || 1;
const [tenant] = await db.select().from(tenants).where(eq(tenants.id, tenantId));
if (!tenant) return res.status(404).json({ error: "Tenant not found" });
const defaultFeatures: TenantFeatures = {
ide: true, ideMode: 'pro-code', whatsapp: false, whatsappSessions: 0,
crm: true, erp: true, bi: false, manus: true, manusTools: [],
centralApis: false, centralApisManage: false, comunidades: false,
maxChannels: 5, biblioteca: false, bibliotecaPublish: false,
suporteN3: false, retail: false, plus: false, fisco: false,
cockpit: false, compass: true, production: false, support: false, xosCrm: false
};
res.json({
tenantId: tenant.id,
tenantName: tenant.name,
plan: tenant.plan,
features: tenant.features || defaultFeatures
});
} catch (error) {
console.error("Error fetching tenant modules:", error);
res.status(500).json({ error: "Failed to fetch modules" });
}
});
// PUT /api/erp/tenant/modules - Update current tenant's modules
app.put("/api/erp/tenant/modules", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) return res.status(401).json({ error: "Not authenticated" });
const user = req.user;
if (user?.role !== 'admin' && (user as any)?.tenantRole !== 'owner') {
return res.status(403).json({ error: "Only admins can manage modules" });
}
const tenantId = user?.tenantId || 1;
const features = req.body.features;
if (!features || typeof features !== 'object') {
return res.status(400).json({ error: "Invalid features object" });
}
const [updated] = await db.update(tenants)
.set({ features, updatedAt: new Date() })
.where(eq(tenants.id, tenantId))
.returning();
res.json({ tenantId: updated.id, features: updated.features });
} catch (error) {
console.error("Error updating tenant modules:", error);
res.status(500).json({ error: "Failed to update modules" });
}
});
// GET /api/erp/sync/status - Get sync status for all motors
app.get("/api/erp/sync/status", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) return res.status(401).json({ error: "Not authenticated" });
const tenantId = req.user?.tenantId || 1;
const [tenant] = await db.select().from(tenants).where(eq(tenants.id, tenantId));
const features = (tenant?.features as TenantFeatures) || {};
const status = {
retail: {
enabled: features.retail || false,
motor: features.plus ? 'plus' : features.erp ? 'erpnext' : 'none',
lastSync: null,
status: 'idle'
},
plus: {
enabled: features.plus || false,
status: 'idle'
},
fisco: {
enabled: features.fisco || false,
status: 'idle'
},
bi: {
enabled: features.bi || false,
status: 'idle'
}
};
res.json(status);
} catch (error) {
console.error("Error fetching sync status:", error);
res.status(500).json({ error: "Failed to fetch sync status" });
}
});
app.get("/api/erp/empresas", async (req: Request, res: Response) => {
try {
const tenantId = (req as any).user?.tenantId;
if (!tenantId) return res.status(401).json({ error: "Tenant not identified" });
const empresas = await db.select().from(tenantEmpresas)
.where(and(eq(tenantEmpresas.tenantId, tenantId), eq(tenantEmpresas.status, "active")));
res.json(empresas);
} catch (error) {
console.error("Error fetching empresas:", error);
res.status(500).json({ error: "Failed to fetch empresas" });
}
});
app.get("/api/erp/empresas/:id", async (req: Request, res: Response) => {
try {
const tenantId = (req as any).user?.tenantId;
if (!tenantId) return res.status(401).json({ error: "Tenant not identified" });
const id = parseInt(req.params.id);
const [empresa] = await db.select().from(tenantEmpresas)
.where(and(eq(tenantEmpresas.id, id), eq(tenantEmpresas.tenantId, tenantId)));
if (!empresa) return res.status(404).json({ error: "Empresa not found" });
res.json(empresa);
} catch (error) {
console.error("Error fetching empresa:", error);
res.status(500).json({ error: "Failed to fetch empresa" });
}
});
app.post("/api/erp/empresas", async (req: Request, res: Response) => {
try {
const tenantId = (req as any).user?.tenantId;
if (!tenantId) return res.status(401).json({ error: "Tenant not identified" });
const { razaoSocial, nomeFantasia, cnpj, ie, im, email, phone, tipo, cep, logradouro, numero, complemento, bairro, cidade, uf, codigoIbge, regimeTributario } = req.body;
if (!razaoSocial || !cnpj) return res.status(400).json({ error: "razaoSocial and cnpj required" });
const [empresa] = await db.insert(tenantEmpresas).values({
tenantId,
razaoSocial,
nomeFantasia: nomeFantasia || null,
cnpj,
ie: ie || null,
im: im || null,
email: email || null,
phone: phone || null,
tipo: tipo || "filial",
cep: cep || null,
logradouro: logradouro || null,
numero: numero || null,
complemento: complemento || null,
bairro: bairro || null,
cidade: cidade || null,
uf: uf || null,
codigoIbge: codigoIbge || null,
regimeTributario: regimeTributario || null,
}).returning();
res.status(201).json(empresa);
} catch (error) {
console.error("Error creating empresa:", error);
res.status(500).json({ error: "Failed to create empresa" });
}
});
app.put("/api/erp/empresas/:id", async (req: Request, res: Response) => {
try {
const tenantId = (req as any).user?.tenantId;
if (!tenantId) return res.status(401).json({ error: "Tenant not identified" });
const id = parseInt(req.params.id);
const [existing] = await db.select().from(tenantEmpresas)
.where(and(eq(tenantEmpresas.id, id), eq(tenantEmpresas.tenantId, tenantId)));
if (!existing) return res.status(404).json({ error: "Empresa not found" });
const { razaoSocial, nomeFantasia, cnpj, ie, im, email, phone, tipo, status, cep, logradouro, numero, complemento, bairro, cidade, uf, codigoIbge, regimeTributario } = req.body;
const [updated] = await db.update(tenantEmpresas)
.set({
...(razaoSocial !== undefined && { razaoSocial }),
...(nomeFantasia !== undefined && { nomeFantasia }),
...(cnpj !== undefined && { cnpj }),
...(ie !== undefined && { ie }),
...(im !== undefined && { im }),
...(email !== undefined && { email }),
...(phone !== undefined && { phone }),
...(tipo !== undefined && { tipo }),
...(status !== undefined && { status }),
...(cep !== undefined && { cep }),
...(logradouro !== undefined && { logradouro }),
...(numero !== undefined && { numero }),
...(complemento !== undefined && { complemento }),
...(bairro !== undefined && { bairro }),
...(cidade !== undefined && { cidade }),
...(uf !== undefined && { uf }),
...(codigoIbge !== undefined && { codigoIbge }),
...(regimeTributario !== undefined && { regimeTributario }),
updatedAt: new Date(),
})
.where(and(eq(tenantEmpresas.id, id), eq(tenantEmpresas.tenantId, tenantId)))
.returning();
res.json(updated);
} catch (error) {
console.error("Error updating empresa:", error);
res.status(500).json({ error: "Failed to update empresa" });
}
});
} }
// SOE routes — placeholder
export function registerSoeRoutes(_app: Express): void {}

View File

@ -174,7 +174,7 @@ Responda APENAS com o código, sem explicações adicionais.`;
: prompt; : prompt;
const response = await openai.chat.completions.create({ const response = await openai.chat.completions.create({
model: "arcadia-agent", model: "gpt-4o",
messages: [ messages: [
{ role: "system", content: systemPrompt }, { role: "system", content: systemPrompt },
{ role: "user", content: userPrompt }, { role: "user", content: userPrompt },
@ -228,7 +228,7 @@ Use português brasileiro.`;
: message; : message;
const response = await openai.chat.completions.create({ const response = await openai.chat.completions.create({
model: "arcadia-agent", model: "gpt-4o",
messages: [ messages: [
{ role: "system", content: systemPrompt }, { role: "system", content: systemPrompt },
{ role: "user", content: userPrompt }, { role: "user", content: userPrompt },

View File

@ -4,6 +4,15 @@ import { sql } from "drizzle-orm";
const router = Router(); const router = Router();
function requireAuth(req: any, res: any, next: any) {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Authentication required" });
}
next();
}
router.use(requireAuth);
router.get("/courses", async (req: Request, res: Response) => { router.get("/courses", async (req: Request, res: Response) => {
try { try {
const { category, featured, published } = req.query; const { category, featured, published } = req.query;

View File

@ -1,8 +1,5 @@
import type { Express, Request, Response } from "express"; import type { Express, Request, Response } from "express";
import { manusService } from "./service"; import { manusService } from "./service";
import { db } from "../../db/index";
import { manusRuns, manusSteps } from "@shared/schema";
import { eq, and } from "drizzle-orm";
export function registerManusRoutes(app: Express): void { export function registerManusRoutes(app: Express): void {
const handleManusRun = async (req: Request, res: Response) => { const handleManusRun = async (req: Request, res: Response) => {
@ -64,40 +61,4 @@ export function registerManusRoutes(app: Express): void {
res.status(500).json({ error: "Failed to get runs" }); res.status(500).json({ error: "Failed to get runs" });
} }
}); });
app.delete("/api/manus/runs/:id", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const runId = parseInt(req.params.id);
manusService.cancelRun(runId); // sinaliza o loop para parar
await db.delete(manusSteps).where(eq(manusSteps.runId, runId));
await db.delete(manusRuns).where(and(eq(manusRuns.id, runId), eq(manusRuns.userId, req.user!.id)));
res.json({ success: true });
} catch (error) {
console.error("Manus delete run error:", error);
res.status(500).json({ error: "Failed to delete run" });
}
});
app.delete("/api/manus/runs", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const userRuns = await manusService.getUserRuns(req.user!.id);
const runIds = userRuns.map((r: any) => r.id);
if (runIds.length > 0) {
for (const id of runIds) {
await db.delete(manusSteps).where(eq(manusSteps.runId, id));
}
await db.delete(manusRuns).where(eq(manusRuns.userId, req.user!.id));
}
res.json({ success: true, deleted: runIds.length });
} catch (error) {
console.error("Manus delete all runs error:", error);
res.status(500).json({ error: "Failed to delete runs" });
}
});
} }

View File

@ -12,75 +12,143 @@ import * as erpnextService from "../erpnext/service";
const openai = new OpenAI({ const openai = new OpenAI({
apiKey: process.env.AI_INTEGRATIONS_OPENAI_API_KEY, apiKey: process.env.AI_INTEGRATIONS_OPENAI_API_KEY,
baseURL: process.env.AI_INTEGRATIONS_OPENAI_BASE_URL, baseURL: process.env.AI_INTEGRATIONS_OPENAI_BASE_URL,
timeout: 30000,
maxRetries: 3,
}); });
const SYSTEM_PROMPT = `Você é o Agente Arcádia Manus, assistente empresarial inteligente da Arcádia Suite. const SYSTEM_PROMPT = `Você é o Agente Arcádia Manus, um assistente empresarial inteligente e proativo.
IDENTIDADE: Você executa tarefas usando as ferramentas disponíveis.
- Você é o Manus, IA da Arcádia Suite. Se perguntado: "Sou o Manus, agente IA da Arcádia Suite." Você opera em ciclos de pensamento-ação:
- Não revele detalhes técnicos da infraestrutura (modelos, APIs, servidores). 1. PENSAMENTO: Analise a situação e decida o próximo passo
2. AÇÃO: Execute uma ferramenta
REGRAS DE COMPORTAMENTO: 3. OBSERVAÇÃO: Analise o resultado
- Seja PROATIVO: execute análises sem pedir confirmação desnecessária 4. Repita até completar a tarefa
- Para perguntas simples e cálculos: responda DIRETO, sem usar ferramentas
- NUNCA adicione SWOT, Canvas, PDCA, frameworks ou análises NÃO solicitados
- Para AÇÕES DESTRUTIVAS (deletar, modificar dados): informe o que será feito antes de executar
- Máximo de 10 passos por execução
- Complete SEMPRE a tarefa e apresente o resultado final
ANÁLISE DE DADOS (quando usar ferramentas de dados):
- Apresente dados em TABELAS MARKDOWN formatadas (| Coluna | Valor |)
- Calcule variações percentuais, identifique tendências
- Forneça insights e interpretações, não números brutos
- Ao analisar documentos: extraia dados E forneça interpretação completa
CRIAÇÃO DE GRÁFICOS:
- Use generate_chart para gráficos visuais (NÃO use python_execute para isso)
- Tipos: bar (barras), line (linha), pie (pizza), area (área)
- Dados: JSON array [{"name":"2023","valor":100}]
- Use múltiplas séries quando fizer sentido
PESQUISA E CONHECIMENTO:
- Para PESQUISA PROFUNDA: use deep_research (busca e sintetiza múltiplas fontes)
- Para APRENDER uma URL: use learn_url
- Para BUSCAR conhecimento interno: use semantic_search PRIMEIRO, depois deep_research se necessário
- Para NAVEGAR em página: use web_browse
- NUNCA diga "não consigo acessar" sempre tente as ferramentas
MÓDULO BI (ARCÁDIA INSIGHTS):
- bi_stats: estatísticas gerais do BI
- bi_list_tables: tabelas disponíveis
- bi_get_table_columns: colunas de uma tabela
- bi_create_dataset: criar consultas SQL
- bi_execute_query: executar dataset e obter dados
- bi_create_chart: criar gráficos persistentes
- bi_create_dashboard: organizar gráficos em painéis
COMUNICAÇÃO ENTRE AGENTES (A2A):
- list_agents: ver agentes disponíveis
- call_agent: delegar tarefas a agentes especializados
- Para tarefas especializadas (fiscal, jurídico, vendas): verifique agentes registrados
FERRAMENTAS DISPONÍVEIS: FERRAMENTAS DISPONÍVEIS:
${getToolsDescription()} ${getToolsDescription()}
FORMATO DE RESPOSTA OBRIGATÓRIO (JSON): REGRAS DE AUTONOMIA:
{"thought": "raciocínio sobre o próximo passo", "tool": "nome_ferramenta", "tool_input": {"param": "valor"}} - Para ANÁLISES e CONSULTAS: seja proativo e execute sem pedir confirmação
- Para GERAÇÃO DE CÓDIGO: execute o código e apresente o resultado
- Se uma ferramenta falhar, tente uma alternativa ou apresente o que conseguiu
- NUNCA fique "aguardando resposta" no meio da tarefa - complete sempre
- Se não conseguir gerar um gráfico visualmente, forneça os dados em formato de tabela
- Para AÇÕES DESTRUTIVAS (deletar, modificar dados críticos): informe o que será feito na resposta final
- Sempre complete a tarefa e apresente o resultado ao final
- Máximo de 10 passos por execução
Para concluir: COMPORTAMENTO IMPORTANTE:
{"thought": "raciocínio final", "tool": "finish", "tool_input": {"answer": "resposta COMPLETA com dados, tabelas, análise e conclusão"}} - Quando o usuário pedir análise de dados, faça uma análise COMPLETA e PROFISSIONAL
- Sempre forneça insights e interpretações, não apenas os números brutos
- Calcule variações percentuais, identifique tendências e faça observações relevantes
- Apresente dados em TABELAS FORMATADAS usando Markdown quando apropriado
REGRA CRÍTICA: O campo "answer" deve conter TODO o conteúdo tabelas, cálculos, insights. NUNCA apenas "relatório gerado".`; FORMATO DE RESPOSTA IDEAL:
1. Primeiro, apresente uma TABELA com os dados extraídos (use formato Markdown: | Coluna | Valor |)
2. Em seguida, forneça uma ANÁLISE explicativa com insights (variações %, tendências, observações)
3. Por fim, gere um GRÁFICO visual usando generate_chart
CRIAÇÃO DE GRÁFICOS:
- Use generate_chart para criar gráficos visuais (NÃO use python_execute)
- Tipos disponíveis: bar (barras), line (linha), pie (pizza), area (área)
- Formate os dados como JSON array: [{"name":"2023","ativo":10844216,"passivo":10844216}]
- Inclua múltiplas séries quando fizer sentido (ex: ativo E passivo no mesmo gráfico)
EXEMPLO DE RESPOSTA COMPLETA:
1. analyze_file -> extrair dados do documento
2. generate_chart -> criar gráfico visual
3. finish -> apresentar tabela + análise + conclusão
Sempre calcule e mencione:
- Variações percentuais entre períodos
- Tendências (crescimento/queda)
- Observações sobre equilíbrio contábil quando aplicável
PESQUISA INTELIGENTE:
- Para PESQUISA PROFUNDA sobre um tema: use deep_research (busca, extrai e sintetiza múltiplas fontes)
- Para APRENDER conteúdo de uma URL específica: use learn_url
- Para BUSCAR no conhecimento aprendido: use semantic_search PRIMEIRO
- Para NAVEGAR e extrair conteúdo de uma página: use web_browse
ESTRATÉGIA DE PESQUISA (siga esta ordem):
1. PRIMEIRO: Consulte semantic_search para ver se temos informações sobre o assunto
2. SE não houver informações: Use deep_research para pesquisar na web e aprender
3. SE o usuário fornecer uma URL específica: Use web_browse ou learn_url
4. SEMPRE sintetize e apresente uma resposta completa
REGRAS DE PESQUISA:
- Quando o usuário pedir para "pesquisar sobre X", use deep_research
- Quando o usuário mencionar URLs, use web_browse para ver ou learn_url para salvar
- Seja PROATIVO: se não encontrar na base interna, pesquise na web automaticamente
- NUNCA diga "não consigo acessar" - sempre tente as ferramentas disponíveis
MÓDULO DE BI (ARCÁDIA INSIGHTS):
- Use bi_stats para ver estatísticas gerais do BI
- Use bi_list_tables para listar tabelas disponíveis no banco
- Use bi_get_table_columns para ver colunas de uma tabela
- Use bi_create_dataset para criar consultas SQL ou selecionar tabelas
- Use bi_execute_query para executar um dataset e obter dados
- Use bi_create_chart para criar gráficos persistentes no BI
- Use bi_create_dashboard para organizar gráficos em painéis
- Os recursos do BI ficam salvos permanentemente no sistema
COMUNICAÇÃO ENTRE AGENTES (A2A - Agent to Agent):
- Use list_agents para ver agentes externos disponíveis
- Use register_agent para adicionar um novo agente externo
- Use discover_agent para descobrir capacidades de um agente via Agent Card
- Use call_agent para enviar mensagens e delegar tarefas a outros agentes
- Você pode orquestrar múltiplos agentes para tarefas complexas
- Agentes podem ter especializações (fiscal, jurídico, vendas, etc.)
ESTRATÉGIA DE ORQUESTRAÇÃO:
- Para tarefas especializadas: verifique se um agente especialista registrado
- Para tarefas complexas: divida em subtarefas e delegue para agentes apropriados
- Sempre sintetize as respostas dos agentes antes de apresentar ao usuário
Responda SEMPRE em formato JSON:
{
"thought": "Seu raciocínio sobre o próximo passo",
"tool": "nome_da_ferramenta",
"tool_input": { "param1": "valor1" }
}
Quando concluir, use:
{
"thought": "Raciocínio final",
"tool": "finish",
"tool_input": { "answer": "Resposta final COMPLETA com TODOS os dados e análise" }
}
REGRA CRÍTICA PARA RESPOSTA FINAL:
- A resposta no campo "answer" deve conter TODO o conteúdo da análise
- NUNCA diga apenas "relatório gerado com sucesso" - inclua o conteúdo completo
- Inclua: tabelas de dados, cálculos, variações percentuais, insights e conclusões
- O usuário quer ver a análise completa, não apenas uma confirmação
- Se analisou um documento, inclua os dados extraídos E sua interpretação`;
class ManusService extends EventEmitter { class ManusService extends EventEmitter {
private cancelledRuns = new Set<number>(); private pendingApprovals: Map<string, { tool: string; input: Record<string, any> }> = new Map();
cancelRun(runId: number) { private async executeTool(tool: string, input: Record<string, any>, userId: string): Promise<ToolResult> {
this.cancelledRuns.add(runId); // Dangerous tools require explicit user approval via ask_human first
setTimeout(() => this.cancelledRuns.delete(runId), 60000); // limpa após 1 min const DANGEROUS_TOOLS = new Set(["shell", "write_file", "python_execute"]);
} if (DANGEROUS_TOOLS.has(tool)) {
const approvalKey = `${userId}:${tool}:${JSON.stringify(input)}`;
if (!this.pendingApprovals.has(approvalKey)) {
this.pendingApprovals.set(approvalKey, { tool, input });
const preview = tool === "shell" ? input.command
: tool === "write_file" ? `Escrever em: ${input.path}`
: `Executar código Python (${String(input.code || "").substring(0, 80)}...)`;
return {
success: false,
output: `[APROVAÇÃO NECESSÁRIA] Esta ação requer confirmação: ${preview}. Use ask_human para solicitar aprovação antes de prosseguir.`,
error: "requires_approval"
};
}
this.pendingApprovals.delete(approvalKey);
}
async executeTool(tool: string, input: Record<string, any>, userId: string): Promise<ToolResult> {
try { try {
switch (tool) { switch (tool) {
case "web_search": case "web_search":
@ -267,6 +335,13 @@ class ManusService extends EventEmitter {
return this.toolRetailStats(input.period, input.storeId); return this.toolRetailStats(input.period, input.storeId);
case "retail_report": case "retail_report":
return this.toolRetailReport(input.type, input.dateFrom, input.dateTo, input.storeId); return this.toolRetailReport(input.type, input.dateFrom, input.dateTo, input.storeId);
// ========== AUTOMAÇÃO + XOS + INBOX ==========
case "automation_trigger":
return this.toolAutomationTrigger(input.automation_id, input.event_type, input.payload, input.tenant_id, userId);
case "xos_action":
return this.toolXosAction(input.action, input.data, userId);
case "inbox_action":
return this.toolInboxAction(input.action, input.conversation_id, input.data, userId);
case "finish": case "finish":
let finishOutput = input.answer || ""; let finishOutput = input.answer || "";
if (input.chart) { if (input.chart) {
@ -2184,19 +2259,6 @@ class ManusService extends EventEmitter {
return { runId: run.id }; return { runId: run.id };
} }
async runSync(userId: string, prompt: string, attachedFiles?: Array<{name: string, content: string, base64?: string}>): Promise<string> {
const [run] = await db.insert(manusRuns).values({
userId,
prompt,
status: "running"
}).returning();
await this.executeAgentLoop(run.id, userId, prompt, attachedFiles);
const [completed] = await db.select().from(manusRuns).where(eq(manusRuns.id, run.id));
return completed?.result || "Não foi possível processar a solicitação.";
}
private async executeAgentLoop(runId: number, userId: string, prompt: string, attachedFiles?: Array<{name: string, content: string, base64?: string}>, conversationHistory?: Array<{role: string; content: string}>) { private async executeAgentLoop(runId: number, userId: string, prompt: string, attachedFiles?: Array<{name: string, content: string, base64?: string}>, conversationHistory?: Array<{role: string; content: string}>) {
let userPrompt = prompt; let userPrompt = prompt;
if (attachedFiles && attachedFiles.length > 0) { if (attachedFiles && attachedFiles.length > 0) {
@ -2206,7 +2268,7 @@ class ManusService extends EventEmitter {
// Build messages with conversation history for context // Build messages with conversation history for context
const messages: OpenAI.Chat.ChatCompletionMessageParam[] = [ const messages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: SYSTEM_PROMPT + `\n\nDATA/HORA ATUAL: ${new Date().toLocaleString('pt-BR', { timeZone: 'America/Sao_Paulo', dateStyle: 'full', timeStyle: 'short' })}` } { role: "system", content: SYSTEM_PROMPT }
]; ];
// Add conversation history if available // Add conversation history if available
@ -2230,25 +2292,13 @@ class ManusService extends EventEmitter {
while (step < maxSteps && !finished) { while (step < maxSteps && !finished) {
step++; step++;
// Verificar cancelamento antes de cada step
if (this.cancelledRuns.has(runId)) {
this.cancelledRuns.delete(runId);
await db.update(manusRuns).set({ status: "stopped", completedAt: new Date() }).where(eq(manusRuns.id, runId));
return;
}
try { try {
const STEP_TIMEOUT_MS = 60000; // 60s por step const response = await openai.chat.completions.create({
const responsePromise = openai.chat.completions.create({ model: "gpt-4o",
model: "arcadia-agent",
messages, messages,
temperature: 0.2, temperature: 0.2,
max_tokens: 1500, max_tokens: 4000,
}); });
const timeoutPromise = new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error("Step timeout (60s)")), STEP_TIMEOUT_MS)
);
const response = await Promise.race([responsePromise, timeoutPromise]);
const content = response.choices[0]?.message?.content || ""; const content = response.choices[0]?.message?.content || "";
messages.push({ role: "assistant", content }); messages.push({ role: "assistant", content });
@ -3828,6 +3878,253 @@ class ManusService extends EventEmitter {
return { success: false, output: "", error: `Erro ao gerar relatório: ${error.message}` }; return { success: false, output: "", error: `Erro ao gerar relatório: ${error.message}` };
} }
} }
// ============================================================
// TOOL: automation_trigger
// ============================================================
private async toolAutomationTrigger(
automationId: number | undefined,
eventType: string | undefined,
payload: string | undefined,
tenantId: number | undefined,
userId: string
): Promise<ToolResult> {
try {
const parsedPayload = payload ? (typeof payload === 'string' ? JSON.parse(payload) : payload) : {};
if (automationId) {
// Direct automation execution via AutomationService
const { automationService } = await import("../automations/service");
const result = await automationService.runAutomation(automationId, userId, { ...parsedPayload, triggered_by: "manus" });
return {
success: result.success,
output: `Automação #${automationId} ${result.success ? 'executada com sucesso' : 'falhou'}. Log ID: ${result.logId}. ${result.result || result.error || ''}`,
};
}
if (eventType) {
// Emit event to automation engine via HTTP
const engineHost = process.env.AUTOMATION_ENGINE_HOST || "localhost";
const enginePort = process.env.AUTOMATION_ENGINE_PORT || "8005";
const response = await fetch(`http://${engineHost}:${enginePort}/xos/trigger`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ event_type: eventType, tenant_id: tenantId, payload: parsedPayload }),
});
if (!response.ok) throw new Error(`Engine retornou ${response.status}`);
const result: any = await response.json();
return {
success: true,
output: `Evento '${eventType}' emitido. ${result.triggered_handlers?.length || 0} handlers ativados.`,
};
}
return { success: false, output: "", error: "Informe automation_id ou event_type" };
} catch (error: any) {
return { success: false, output: "", error: `Erro ao disparar automação: ${error.message}` };
}
}
// ============================================================
// TOOL: xos_action
// ============================================================
private async toolXosAction(action: string, data: string | object, userId: string): Promise<ToolResult> {
try {
const parsed = typeof data === 'string' ? JSON.parse(data) : data;
switch (action) {
case "create_contact": {
const result = await db.execute(sql`
INSERT INTO xos_contacts (name, email, phone, whatsapp, type, company, position, source, tags, notes)
VALUES (${parsed.name}, ${parsed.email || null}, ${parsed.phone || null}, ${parsed.whatsapp || null},
${parsed.type || 'lead'}, ${parsed.company || null}, ${parsed.position || null},
${parsed.source || 'manus'}, ${parsed.tags || null}, ${parsed.notes || null})
RETURNING id, name, email, type
`);
const contact = (result.rows || result)[0] as any;
return { success: true, output: `Contato criado: ${contact.name} (ID: ${contact.id}, tipo: ${contact.type})` };
}
case "update_contact": {
const { id, ...fields } = parsed;
if (!id) return { success: false, output: "", error: "id obrigatório para update_contact" };
const sets = Object.entries(fields).map(([k, v]) => `${k} = '${v}'`).join(", ");
await db.execute(sql`UPDATE xos_contacts SET ${sql.raw(sets)}, updated_at = NOW() WHERE id = ${id}`);
return { success: true, output: `Contato #${id} atualizado com sucesso.` };
}
case "create_deal": {
const result = await db.execute(sql`
INSERT INTO xos_deals (title, pipeline_id, stage_id, contact_id, company_id, value, currency, assigned_to, expected_close_date, notes)
VALUES (${parsed.title}, ${parsed.pipeline_id}, ${parsed.stage_id}, ${parsed.contact_id || null},
${parsed.company_id || null}, ${parsed.value || null}, ${parsed.currency || 'BRL'},
${parsed.assigned_to || null}, ${parsed.expected_close_date || null}, ${parsed.notes || null})
RETURNING id, title, value
`);
const deal = (result.rows || result)[0] as any;
return { success: true, output: `Deal criado: "${deal.title}" (ID: ${deal.id}, valor: ${deal.value})` };
}
case "move_deal_stage": {
const { deal_id, stage_id } = parsed;
if (!deal_id || !stage_id) return { success: false, output: "", error: "deal_id e stage_id obrigatórios" };
await db.execute(sql`UPDATE xos_deals SET stage_id = ${stage_id}, updated_at = NOW() WHERE id = ${deal_id}`);
return { success: true, output: `Deal #${deal_id} movido para estágio #${stage_id}.` };
}
case "create_ticket": {
const result = await db.execute(sql`
INSERT INTO xos_tickets (title, description, contact_id, conversation_id, priority, status, category, assigned_to)
VALUES (${parsed.title}, ${parsed.description || null}, ${parsed.contact_id || null},
${parsed.conversation_id || null}, ${parsed.priority || 'medium'}, 'open',
${parsed.category || null}, ${parsed.assigned_to || null})
RETURNING id, title, priority
`);
const ticket = (result.rows || result)[0] as any;
return { success: true, output: `Ticket criado: "${ticket.title}" (ID: ${ticket.id}, prioridade: ${ticket.priority})` };
}
case "assign_agent": {
const { conversation_id, agent_id } = parsed;
if (!conversation_id) return { success: false, output: "", error: "conversation_id obrigatório" };
await db.execute(sql`UPDATE xos_conversations SET assigned_to = ${agent_id}, updated_at = NOW() WHERE id = ${conversation_id}`);
return { success: true, output: `Agente #${agent_id} atribuído à conversa #${conversation_id}.` };
}
case "create_task": {
const result = await db.execute(sql`
INSERT INTO xos_activities (contact_id, deal_id, type, title, description, due_date, assigned_to, status)
VALUES (${parsed.contact_id || null}, ${parsed.deal_id || null}, 'task',
${parsed.title}, ${parsed.description || null},
${parsed.due_date || null}, ${parsed.assigned_to || null}, 'pending')
RETURNING id, title
`);
const task = (result.rows || result)[0] as any;
return { success: true, output: `Tarefa criada: "${task.title}" (ID: ${task.id})` };
}
case "create_activity": {
const result = await db.execute(sql`
INSERT INTO xos_activities (contact_id, deal_id, type, title, description, scheduled_at, assigned_to)
VALUES (${parsed.contact_id || null}, ${parsed.deal_id || null}, ${parsed.type || 'note'},
${parsed.title}, ${parsed.description || null},
${parsed.scheduled_at || null}, ${parsed.assigned_to || null})
RETURNING id, title, type
`);
const act = (result.rows || result)[0] as any;
return { success: true, output: `Atividade criada: "${act.title}" (tipo: ${act.type}, ID: ${act.id})` };
}
case "create_note": {
const result = await db.execute(sql`
INSERT INTO xos_internal_notes (conversation_id, content, created_by, is_pinned)
VALUES (${parsed.conversation_id || null}, ${parsed.content}, ${userId}, ${parsed.is_pinned || false})
RETURNING id
`);
const note = (result.rows || result)[0] as any;
return { success: true, output: `Nota interna criada (ID: ${note.id})` };
}
default:
return { success: false, output: "", error: `Ação XOS desconhecida: ${action}. Use: create_contact, update_contact, create_deal, move_deal_stage, create_ticket, assign_agent, create_task, create_activity, create_note` };
}
} catch (error: any) {
return { success: false, output: "", error: `Erro na ação XOS '${action}': ${error.message}` };
}
}
// ============================================================
// TOOL: inbox_action
// ============================================================
private async toolInboxAction(
action: string,
conversationId: number | undefined,
data: string | object | undefined,
userId: string
): Promise<ToolResult> {
try {
const parsed = data ? (typeof data === 'string' ? JSON.parse(data) : data) : {} as any;
switch (action) {
case "close_conversation": {
if (!conversationId) return { success: false, output: "", error: "conversation_id obrigatório" };
await db.execute(sql`
UPDATE xos_conversations SET status = 'closed', closed_at = NOW(), updated_at = NOW()
WHERE id = ${conversationId}
`);
return { success: true, output: `Conversa #${conversationId} fechada.` };
}
case "transfer_conversation": {
if (!conversationId) return { success: false, output: "", error: "conversation_id obrigatório" };
const { queue_id, agent_id } = parsed;
await db.execute(sql`
UPDATE xos_conversations SET
queue_id = COALESCE(${queue_id || null}, queue_id),
assigned_to = COALESCE(${agent_id || null}, assigned_to),
updated_at = NOW()
WHERE id = ${conversationId}
`);
return { success: true, output: `Conversa #${conversationId} transferida para fila #${queue_id || 'N/A'} / agente #${agent_id || 'N/A'}.` };
}
case "send_message": {
if (!conversationId) return { success: false, output: "", error: "conversation_id obrigatório" };
const { content, content_type } = parsed;
if (!content) return { success: false, output: "", error: "content obrigatório" };
await db.execute(sql`
INSERT INTO xos_messages (conversation_id, direction, sender_type, sender_name, content, content_type)
VALUES (${conversationId}, 'outbound', 'agent', 'Manus IA', ${content}, ${content_type || 'text'})
`);
await db.execute(sql`
UPDATE xos_conversations SET last_message = ${content}, updated_at = NOW() WHERE id = ${conversationId}
`);
return { success: true, output: `Mensagem enviada na conversa #${conversationId}: "${content.substring(0, 100)}"` };
}
case "add_label": {
if (!conversationId) return { success: false, output: "", error: "conversation_id obrigatório" };
const { label } = parsed;
await db.execute(sql`
UPDATE xos_conversations SET
tags = COALESCE(tags, '') || ${label ? ',' + label : ''},
updated_at = NOW()
WHERE id = ${conversationId}
`);
return { success: true, output: `Etiqueta '${label}' adicionada à conversa #${conversationId}.` };
}
case "resolve_ticket": {
const { ticket_id, resolution } = parsed;
if (!ticket_id) return { success: false, output: "", error: "ticket_id obrigatório" };
await db.execute(sql`
UPDATE xos_tickets SET status = 'resolved', resolution = ${resolution || null},
resolved_at = NOW(), updated_at = NOW()
WHERE id = ${ticket_id}
`);
return { success: true, output: `Ticket #${ticket_id} resolvido.` };
}
case "escalate_ticket": {
const { ticket_id, priority, reason } = parsed;
if (!ticket_id) return { success: false, output: "", error: "ticket_id obrigatório" };
await db.execute(sql`
UPDATE xos_tickets SET priority = ${priority || 'urgent'},
notes = CONCAT(COALESCE(notes, ''), ' [Escalado por Manus: ', ${reason || 'sem motivo'}, ']'),
updated_at = NOW()
WHERE id = ${ticket_id}
`);
return { success: true, output: `Ticket #${ticket_id} escalado para prioridade ${priority || 'urgent'}.` };
}
default:
return { success: false, output: "", error: `Ação de inbox desconhecida: ${action}. Use: close_conversation, transfer_conversation, send_message, add_label, resolve_ticket, escalate_ticket` };
}
} catch (error: any) {
return { success: false, output: "", error: `Erro na ação de inbox '${action}': ${error.message}` };
}
}
} }
export const manusService = new ManusService(); export const manusService = new ManusService();

View File

@ -733,6 +733,33 @@ export const MANUS_TOOLS: ManusToolDef[] = [
dateTo: { type: "string", description: "Data final (YYYY-MM-DD)", required: false }, dateTo: { type: "string", description: "Data final (YYYY-MM-DD)", required: false },
storeId: { type: "number", description: "ID da loja para filtrar", required: false } storeId: { type: "number", description: "ID da loja para filtrar", required: false }
} }
},
{
name: "automation_trigger",
description: "Dispara uma automação existente ou emite um evento no barramento. Use para encadear automações, disparar workflows CRM, ou emitir eventos personalizados que ativam outras automações.",
parameters: {
automation_id: { type: "number", description: "ID da automação a executar (opcional se usar event_type)", required: false },
event_type: { type: "string", description: "Tipo de evento a emitir no barramento: crm.contact.created, crm.deal.stage_changed, crm.ticket.created, crm.message.received, manual.trigger, system.event, etc.", required: false },
payload: { type: "string", description: "Dados adicionais em JSON para passar à automação ou ao evento (ex: '{\"contact_id\": 42, \"stage\": \"qualified\"}')", required: false },
tenant_id: { type: "number", description: "ID do tenant para automações multi-tenant (xos_automations)", required: false }
}
},
{
name: "xos_action",
description: "Executa ações no XOS CRM: criar/atualizar contatos, mover deals no pipeline, criar tickets, atribuir agentes, criar tarefas, enviar campanhas. Use para automatizar operações de CRM via IA.",
parameters: {
action: { type: "string", description: "Ação a executar: create_contact, update_contact, create_deal, move_deal_stage, create_ticket, assign_agent, create_task, create_activity, send_campaign, create_note", required: true },
data: { type: "string", description: "Dados para a ação em JSON. Ex: create_contact: '{\"name\":\"João\",\"email\":\"joao@ex.com\",\"type\":\"lead\"}'; move_deal_stage: '{\"deal_id\":5,\"stage_id\":3}'; create_ticket: '{\"title\":\"Bug\",\"contact_id\":2,\"priority\":\"high\"}'", required: true }
}
},
{
name: "inbox_action",
description: "Executa ações na Central de Atendimento: fechar/transferir conversas, enviar mensagens automáticas, atribuir agentes, adicionar etiquetas, criar protocolos, alterar status de tickets de suporte.",
parameters: {
action: { type: "string", description: "Ação: close_conversation, transfer_conversation, send_message, assign_agent, add_label, create_protocol, resolve_ticket, escalate_ticket, send_csat", required: true },
conversation_id: { type: "number", description: "ID da conversa (obrigatório para ações em conversas existentes)", required: false },
data: { type: "string", description: "Dados adicionais em JSON. Ex: transfer: '{\"queue_id\":2}'; send_message: '{\"content\":\"Olá!\",\"type\":\"text\"}'; assign_agent: '{\"agent_id\":5}'", required: false }
}
} }
]; ];

View File

@ -5,6 +5,52 @@ import { eq, desc, and, sql } from "drizzle-orm";
const router = Router(); const router = Router();
function requireAuth(req: any, res: any, next: any) {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Authentication required" });
}
next();
}
// My apps: returns active subscriptions for the current user's tenant
// Public modules (isCore=true) are always included
router.get("/my-apps", requireAuth, async (req: any, res: Response) => {
try {
const tenantId = req.user?.tenantId;
// Core modules always available
const coreModules = await db
.select({ code: marketplaceModules.code, route: marketplaceModules.route })
.from(marketplaceModules)
.where(and(eq(marketplaceModules.isCore, true), eq(marketplaceModules.isActive, true)));
// Subscribed modules for this tenant
const subscribed = tenantId
? await db
.select({ code: marketplaceModules.code, route: marketplaceModules.route })
.from(moduleSubscriptions)
.innerJoin(marketplaceModules, eq(moduleSubscriptions.moduleId, marketplaceModules.id))
.where(
and(
eq(moduleSubscriptions.tenantId, tenantId),
eq(moduleSubscriptions.status, "active"),
eq(marketplaceModules.isActive, true)
)
)
: [];
const allCodes = new Set([
...coreModules.map((m) => m.code),
...subscribed.map((m) => m.code),
]);
res.json({ subscribedCodes: [...allCodes], tenantId: tenantId || null });
} catch (error) {
console.error("Error fetching my-apps:", error);
res.status(500).json({ error: "Failed to fetch subscribed apps" });
}
});
router.get("/modules", async (req: Request, res: Response) => { router.get("/modules", async (req: Request, res: Response) => {
try { try {
const modules = await db const modules = await db

View File

@ -4,14 +4,17 @@ import { metasetClient } from "./client";
const METASET_HOST = process.env.METABASE_HOST || "localhost"; const METASET_HOST = process.env.METABASE_HOST || "localhost";
const METASET_PORT = parseInt(process.env.METABASE_PORT || "8088", 10); const METASET_PORT = parseInt(process.env.METABASE_PORT || "8088", 10);
const METASET_URL = `http://${METASET_HOST}:${METASET_PORT}`; const METASET_URL = `http://${METASET_HOST}:${METASET_PORT}`;
const ADMIN_EMAIL = process.env.METASET_ADMIN_EMAIL || "admin@arcadia.app"; const ADMIN_EMAIL = process.env.METASET_ADMIN_EMAIL;
const ADMIN_PASSWORD = process.env.METASET_ADMIN_PASSWORD || "Arcadia2026!BI"; const ADMIN_PASSWORD = process.env.METASET_ADMIN_PASSWORD;
export function registerMetaSetRoutes(app: Express): void { export function registerMetaSetRoutes(app: Express): void {
app.get("/api/bi/metaset/autologin", async (req: Request, res: Response) => { app.get("/api/bi/metaset/autologin", async (req: Request, res: Response) => {
try { try {
if (!req.isAuthenticated()) return res.status(401).json({ error: "Not authenticated" }); if (!req.isAuthenticated()) return res.status(401).json({ error: "Not authenticated" });
if (!ADMIN_EMAIL || !ADMIN_PASSWORD) {
return res.status(503).json({ error: "MetaSet credentials not configured (METASET_ADMIN_EMAIL / METASET_ADMIN_PASSWORD)" });
}
const sessionResp = await fetch(`${METASET_URL}/api/session`, { const sessionResp = await fetch(`${METASET_URL}/api/session`, {
method: "POST", method: "POST",

View File

@ -90,8 +90,6 @@ router.post('/upload', upload.single('file'), async (req, res) => {
if (fileName.endsWith('.zip')) sourceType = 'mongodb'; if (fileName.endsWith('.zip')) sourceType = 'mongodb';
else if (fileName.endsWith('.json')) sourceType = 'json'; else if (fileName.endsWith('.json')) sourceType = 'json';
else if (fileName.endsWith('.csv')) sourceType = 'csv'; else if (fileName.endsWith('.csv')) sourceType = 'csv';
else if (fileName.endsWith('.rar')) sourceType = 'sql-rar';
else if (fileName.endsWith('.sql.gz') || fileName.endsWith('.sql')) sourceType = 'sql';
const [job] = await db.insert(migrationJobs).values({ const [job] = await db.insert(migrationJobs).values({
name: name || `Migração ${new Date().toLocaleDateString('pt-BR')}`, name: name || `Migração ${new Date().toLocaleDateString('pt-BR')}`,
@ -150,87 +148,6 @@ router.post('/upload', upload.single('file'), async (req, res) => {
}); });
} }
} }
} else if (sourceType === 'sql-rar') {
const extractDir = path.join(UPLOAD_DIR, `job-${job.id}`);
fs.mkdirSync(extractDir, { recursive: true });
await db.update(migrationJobs)
.set({ status: 'analyzing' })
.where(eq(migrationJobs.id, job.id));
await execAsync(`7z x "${filePath}" -o"${extractDir}" -y`);
const allFiles = fs.readdirSync(extractDir);
const sqlGzFile = allFiles.find(f => f.endsWith('.sql.gz'));
const sqlFile = allFiles.find(f => f.endsWith('.sql'));
let finalSqlPath: string;
if (sqlGzFile) {
const gzPath = path.join(extractDir, sqlGzFile);
finalSqlPath = gzPath.replace(/\.gz$/, '');
await execAsync(`gunzip -f "${gzPath}"`);
} else if (sqlFile) {
finalSqlPath = path.join(extractDir, sqlFile);
} else {
throw new Error('Nenhum arquivo .sql ou .sql.gz encontrado dentro do RAR');
}
const sqlContent = fs.readFileSync(finalSqlPath, 'utf8');
const tableMatches = sqlContent.match(/CREATE TABLE\s+`?(\w+)`?/gi) || [];
const tables = tableMatches.map(m => m.replace(/CREATE TABLE\s+`?/i, '').replace(/`/g, '').trim());
const lineCount = sqlContent.split('\n').length;
await db.update(migrationJobs)
.set({
status: 'mapping',
totalRecords: lineCount,
analysisResult: {
type: 'sql',
sqlPath: finalSqlPath,
tables,
tableCount: tables.length,
lineCount,
fileSizeBytes: fs.statSync(finalSqlPath).size
},
importConfig: { sqlPath: finalSqlPath }
})
.where(eq(migrationJobs.id, job.id));
} else if (sourceType === 'sql') {
const extractDir = path.join(UPLOAD_DIR, `job-${job.id}`);
fs.mkdirSync(extractDir, { recursive: true });
await db.update(migrationJobs)
.set({ status: 'analyzing' })
.where(eq(migrationJobs.id, job.id));
let finalSqlPath: string;
if (fileName.endsWith('.sql.gz')) {
finalSqlPath = path.join(extractDir, fileName.replace(/\.gz$/, ''));
await execAsync(`gunzip -c "${filePath}" > "${finalSqlPath}"`);
} else {
finalSqlPath = filePath;
}
const sqlContent = fs.readFileSync(finalSqlPath, 'utf8');
const tableMatches = sqlContent.match(/CREATE TABLE\s+`?(\w+)`?/gi) || [];
const tables = tableMatches.map(m => m.replace(/CREATE TABLE\s+`?/i, '').replace(/`/g, '').trim());
const lineCount = sqlContent.split('\n').length;
await db.update(migrationJobs)
.set({
status: 'mapping',
totalRecords: lineCount,
analysisResult: {
type: 'sql',
sqlPath: finalSqlPath,
tables,
tableCount: tables.length,
lineCount,
fileSizeBytes: fs.statSync(finalSqlPath).size
},
importConfig: { sqlPath: finalSqlPath }
})
.where(eq(migrationJobs.id, job.id));
} }
const [updatedJob] = await db.select().from(migrationJobs).where(eq(migrationJobs.id, job.id)); const [updatedJob] = await db.select().from(migrationJobs).where(eq(migrationJobs.id, job.id));

View File

@ -1,80 +1,8 @@
/**
* Arcádia Suite - Module Routes Auto-Loader
*
* Carrega automaticamente todas as rotas de módulos em server/modules/.
* Cada arquivo exporta um Router do Express, montado em /api/modules/{nome}.
*
* Arquivos que começam com _ são ignorados (_template.ts, _utils.ts).
* O loader é chamado pelo routes.ts central.
*/
import type { Express } from "express"; import type { Express } from "express";
import * as fs from "fs"; import skillsRouter from "../skills/routes";
import * as path from "path"; import openclawRouter from "./openclaw/routes";
import { fileURLToPath } from "url";
function getDirname(): string { export async function loadModuleRoutes(app: Express): Promise<void> {
try { app.use("/api/skills", skillsRouter);
if (typeof __dirname !== "undefined") return __dirname; app.use("/api/openclaw", openclawRouter);
} catch {}
try {
const currentFile = fileURLToPath(import.meta.url);
return path.dirname(currentFile);
} catch {}
return path.resolve("server/modules");
}
const currentDir = getDirname();
export async function loadModuleRoutes(app: Express): Promise<string[]> {
const modulesDir = path.resolve(currentDir);
const loaded: string[] = [];
if (!fs.existsSync(modulesDir)) {
return loaded;
}
const files = fs.readdirSync(modulesDir);
const moduleFiles = files.filter(
f => (f.endsWith(".ts") || f.endsWith(".js")) &&
!f.startsWith("_") &&
f !== "loader.ts" &&
f !== "loader.js" &&
f !== "migrator.ts" &&
f !== "migrator.js"
);
for (const file of moduleFiles) {
const moduleName = file.replace(/\.(ts|js)$/, "");
try {
const modulePath = path.join(modulesDir, file);
const mod = await import(modulePath);
const router = mod.default || mod.router;
if (router) {
app.use(`/api/modules/${moduleName}`, router);
loaded.push(moduleName);
console.log(`[ModuleLoader] Módulo carregado: /api/modules/${moduleName}`);
} else {
console.warn(`[ModuleLoader] Módulo ${moduleName} não exporta router default`);
}
} catch (error: any) {
console.error(`[ModuleLoader] Erro ao carregar módulo ${moduleName}:`, error.message);
}
}
if (loaded.length > 0) {
console.log(`[ModuleLoader] ${loaded.length} módulos carregados: ${loaded.join(", ")}`);
}
return loaded;
}
export function getLoadedModules(): string[] {
const modulesDir = path.resolve(currentDir);
if (!fs.existsSync(modulesDir)) return [];
return fs.readdirSync(modulesDir)
.filter(f => (f.endsWith(".ts") || f.endsWith(".js")) && !f.startsWith("_") && f !== "loader.ts" && f !== "loader.js" && f !== "migrator.ts" && f !== "migrator.js")
.map(f => f.replace(/\.(ts|js)$/, ""));
} }

@ -1 +0,0 @@
Subproject commit bd7ec41b51dd89f4c5551e3d27917b41af4a3793

View File

@ -0,0 +1,445 @@
/**
* OpenClawEngine.ts
*
* Orquestração central de OpenClaw
* Coordena detecção de padrões, geração de skills e sugestões
*
* Fase 4: OpenClaw Embutido
*/
import { PatternDetector, getPatternDetector, DetectedPattern } from "./PatternDetector";
import { SkillEmergence } from "./SkillEmergence";
import { EventEmitter } from "events";
import { Server as SocketIOServer } from "socket.io";
interface SkillSuggestion {
id: string;
pattern: DetectedPattern;
suggested_skill_name: string;
suggested_description: string;
estimated_automation: string;
confidence: number;
created_at: Date;
status: "pending" | "accepted" | "rejected";
}
interface OpenClawEngineConfig {
pattern_detection: {
enabled: boolean;
min_occurrences: number;
time_window_days: number;
confidence_threshold: number;
scheduled_check_interval_minutes: number;
};
suggestions: {
enabled: boolean;
auto_suggest: boolean;
channels: string[];
priority_threshold: number;
};
emergence: {
create_as_draft: boolean;
auto_publish: boolean;
notify_admins: boolean;
skill_prefix: string;
};
logging: {
enabled: boolean;
store_patterns: boolean;
store_in_kg: boolean;
audit_trail: boolean;
};
}
/**
* OpenClawEngine
*
* Motor central que coordena todo o fluxo de OpenClaw:
* Pattern Detection Suggestions Skill Generation Dev Center
*/
export class OpenClawEngine extends EventEmitter {
private patternDetector: PatternDetector;
private skillEmergence: SkillEmergence;
private config: OpenClawEngineConfig;
private socketIO?: SocketIOServer;
private checkInterval?: NodeJS.Timer;
private suggestions: Map<string, SkillSuggestion> = new Map();
constructor(config: Partial<OpenClawEngineConfig>, socketIO?: SocketIOServer) {
super();
// Configuração padrão
this.config = {
pattern_detection: {
enabled: true,
min_occurrences: 3,
time_window_days: 30,
confidence_threshold: 0.8,
scheduled_check_interval_minutes: 60,
...config?.pattern_detection,
},
suggestions: {
enabled: true,
auto_suggest: false,
channels: ["widget", "chat"],
priority_threshold: 0.85,
...config?.suggestions,
},
emergence: {
create_as_draft: true,
auto_publish: false,
notify_admins: true,
skill_prefix: "emergent_",
...config?.emergence,
},
logging: {
enabled: true,
store_patterns: true,
store_in_kg: true,
audit_trail: true,
...config?.logging,
},
};
this.socketIO = socketIO;
this.patternDetector = getPatternDetector(this.config.pattern_detection);
this.skillEmergence = new SkillEmergence();
// Setup listeners
this.setupListeners();
console.log("[OpenClawEngine] Inicializado com config:", this.config);
}
/**
* Setup dos listeners de eventos internos
*/
private setupListeners(): void {
// Listener: quando PatternDetector detecta um padrão
this.patternDetector.on("pattern-detected", async (event) => {
console.log("[OpenClawEngine] Padrão detectado:", event.pattern);
await this.handlePatternDetected(event.pattern);
});
// Listener: quando usuário confirma uma sugestão
this.on("user-confirmed-suggestion", async (suggestionId: string) => {
console.log("[OpenClawEngine] Usuário confirmou sugestão:", suggestionId);
await this.handleUserConfirmation(suggestionId);
});
}
/**
* Detectar padrão manualmente
*
* Pode ser chamado por:
* 1. Sistema ao receber uma interação
* 2. Scheduler verificando regularmente
* 3. API endpoint de teste
*/
async detectPattern(userId: string, tenantId: string, actionType: string): Promise<DetectedPattern | null> {
if (!this.config.pattern_detection.enabled) {
return null;
}
const pattern = await this.patternDetector.detectPattern(userId, tenantId, actionType);
if (pattern && pattern.confidence >= this.config.suggestions.priority_threshold) {
// Padrão de alta prioridade - sugerir imediatamente
await this.suggestSkill(pattern);
}
return pattern;
}
/**
* Handler: Padrão foi detectado
*
* Fluxo:
* 1. Validar padrão
* 2. Gerar sugestão de skill
* 3. Emitir para widget via Socket.IO
* 4. Armazenar sugestão
*/
private async handlePatternDetected(pattern: DetectedPattern): Promise<void> {
try {
console.log(`[OpenClawEngine] Processando padrão detectado: ${pattern.action_type}`);
// 1. Gerar sugestão de skill baseado no padrão
const suggestion = await this.suggestSkill(pattern);
if (!suggestion) {
console.log("[OpenClawEngine] Não foi possível gerar sugestão para o padrão");
return;
}
// 2. Emitir evento para Socket.IO (chega no widget frontend)
if (this.socketIO) {
this.socketIO.emit("openclaw:pattern-detected", {
suggestion,
timestamp: new Date(),
});
console.log("[OpenClawEngine] Evento emitido ao Socket.IO");
}
// 3. Armazenar sugestão
this.suggestions.set(suggestion.id, suggestion);
// 4. Notificar admin se configurado
if (this.config.emergence.notify_admins) {
await this.notifyAdmins(pattern, suggestion);
}
} catch (error) {
console.error("[OpenClawEngine] Erro ao processar padrão detectado:", error);
this.emit("error", error);
}
}
/**
* Gerar sugestão de skill baseado em um padrão
*
* Analisa o padrão e cria uma sugestão descritiva
* do que a automação faria
*/
private async suggestSkill(pattern: DetectedPattern): Promise<SkillSuggestion | null> {
try {
// Gerar nome e descrição da skill baseado no padrão
const skillName = this.generateSkillName(pattern);
const skillDescription = this.generateSkillDescription(pattern);
const estimatedAutomation = this.generateAutomationPreview(pattern);
const suggestion: SkillSuggestion = {
id: `suggest_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
pattern,
suggested_skill_name: skillName,
suggested_description: skillDescription,
estimated_automation: estimatedAutomation,
confidence: pattern.confidence,
created_at: new Date(),
status: "pending",
};
console.log(`[OpenClawEngine] Sugestão gerada: ${skillName}`);
return suggestion;
} catch (error) {
console.error("[OpenClawEngine] Erro ao gerar sugestão:", error);
return null;
}
}
/**
* Gerar nome da skill baseado no padrão
*/
private generateSkillName(pattern: DetectedPattern): string {
// Simples: capitalizar o tipo de ação
// Exemplo: "query_sales_report" → "Sales Report Query"
const words = pattern.action_type.split("_").map((w) => w.charAt(0).toUpperCase() + w.slice(1));
return `${this.config.emergence.skill_prefix}${words.join(" ")}`.trim();
}
/**
* Gerar descrição da skill baseado no padrão
*/
private generateSkillDescription(pattern: DetectedPattern): string {
return `
Automação detectada para: ${pattern.action_type}
Padrão observado:
- Ocorrências: ${pattern.occurrences}x nos últimos ${pattern.time_window_days} dias
- Frequência: ${pattern.metadata?.frequency_per_week} vezes/semana
- Intervalo médio: ${pattern.metadata?.average_interval_hours} horas
- Confiança: ${(pattern.confidence * 100).toFixed(1)}%
Esta skill automatizará essa ação repetitiva para você.
`.trim();
}
/**
* Gerar preview da automação
*/
private generateAutomationPreview(pattern: DetectedPattern): string {
return `
# Automação Proposta
**Trigger:** Quando ocorre ${pattern.action_type}
**Frequência:** A cada ${pattern.metadata?.average_interval_hours} horas aproximadamente
**Ação:** Executar automaticamente
Esta skill vai:
1. Monitorar quando você executa a ação: ${pattern.action_type}
2. Ao detectar o padrão, executar automaticamente
3. Salvar tempo e aumentar produtividade
**Status:** Pronto para criar como DRAFT e publicar após aprovação
`.trim();
}
/**
* Handler: Usuário confirmou uma sugestão
*
* Fluxo:
* 1. Validar sugestão
* 2. Chamar SkillEmergence para gerar código
* 3. Armazenar skill como DRAFT
* 4. Notificar Dev Center
*/
private async handleUserConfirmation(suggestionId: string): Promise<void> {
try {
const suggestion = this.suggestions.get(suggestionId);
if (!suggestion) {
console.error(`[OpenClawEngine] Sugestão não encontrada: ${suggestionId}`);
return;
}
console.log(`[OpenClawEngine] Processando confirmação do usuário para: ${suggestion.suggested_skill_name}`);
// 1. Gerar skill (código) via Blackboard
const skillDraft = await this.skillEmergence.generateSkillDraft(suggestion);
if (!skillDraft) {
console.error("[OpenClawEngine] Falha ao gerar skill");
this.emit("error", new Error("Failed to generate skill draft"));
return;
}
// 2. Atualizar status da sugestão
suggestion.status = "accepted";
// 3. Emitir evento de sucesso
this.emit("skill-created", {
suggestion,
skill: skillDraft,
timestamp: new Date(),
});
// 4. Notificar via Socket.IO (para frontend)
if (this.socketIO) {
this.socketIO.emit("openclaw:skill-created", {
skill: skillDraft,
suggestion,
message: `Skill "${suggestion.suggested_skill_name}" criada com sucesso! Vá para Dev Center para publicar.`,
});
}
console.log("[OpenClawEngine] Skill criada com sucesso:", skillDraft.id);
} catch (error) {
console.error("[OpenClawEngine] Erro ao processar confirmação:", error);
this.emit("error", error);
}
}
/**
* Notificar admins sobre novo padrão detectado
*/
private async notifyAdmins(pattern: DetectedPattern, suggestion: SkillSuggestion): Promise<void> {
// TODO: Implementar notificação
// Opções:
// 1. Email para admins
// 2. Notificação em dashboard
// 3. Alerta no Slack/Discord
// 4. Entry em audit log
console.log(
`[OpenClawEngine] Admin notificado: Novo padrão ${pattern.action_type} (confiança: ${pattern.confidence})`
);
}
/**
* Verificar padrões periodicamente (para ser chamado por scheduler)
*/
async checkForPatternsScheduled(tenantId: string): Promise<void> {
if (!this.config.pattern_detection.enabled) {
return;
}
console.log(`[OpenClawEngine] Verificação agendada de padrões para tenant: ${tenantId}`);
try {
await this.patternDetector.checkForNewPatterns(tenantId);
} catch (error) {
console.error("[OpenClawEngine] Erro na verificação agendada:", error);
}
}
/**
* Iniciar verificação periódica de padrões
*/
startScheduledChecks(tenantId: string): void {
if (this.checkInterval) {
console.log("[OpenClawEngine] Verificação agendada já está ativa");
return;
}
const intervalMs = this.config.pattern_detection.scheduled_check_interval_minutes * 60 * 1000;
this.checkInterval = setInterval(() => {
this.checkForPatternsScheduled(tenantId);
}, intervalMs);
console.log(
`[OpenClawEngine] Verificação agendada iniciada (a cada ${this.config.pattern_detection.scheduled_check_interval_minutes} min)`
);
}
/**
* Parar verificação periódica
*/
stopScheduledChecks(): void {
if (this.checkInterval) {
clearInterval(this.checkInterval);
this.checkInterval = undefined;
console.log("[OpenClawEngine] Verificação agendada interrompida");
}
}
/**
* Obter sugestões pendentes para um usuário
*/
getPendingSuggestions(userId?: string): SkillSuggestion[] {
const suggestions = Array.from(this.suggestions.values()).filter((s) => s.status === "pending");
if (userId) {
return suggestions.filter((s) => s.pattern.user_id === userId);
}
return suggestions;
}
/**
* Obter configuração
*/
getConfig(): OpenClawEngineConfig {
return JSON.parse(JSON.stringify(this.config));
}
/**
* Atualizar configuração
*/
setConfig(config: Partial<OpenClawEngineConfig>): void {
this.config = { ...this.config, ...config };
console.log("[OpenClawEngine] Configuração atualizada");
// Atualizar PatternDetector se necessário
if (config.pattern_detection) {
this.patternDetector.setConfig(config.pattern_detection);
}
}
}
// Exportar instância singleton
let engineInstance: OpenClawEngine | null = null;
export function getOpenClawEngine(
config?: Partial<OpenClawEngineConfig>,
socketIO?: SocketIOServer
): OpenClawEngine {
if (!engineInstance) {
engineInstance = new OpenClawEngine(config || {}, socketIO);
}
return engineInstance;
}
export default OpenClawEngine;

View File

@ -0,0 +1,335 @@
/**
* PatternDetector.ts
*
* Detecção proativa de padrões de uso no Arcádia
* Monitora eventos de usuário e identifica ações repetidas
* para sugerir automações/skills emergentes
*
* Fase 4: OpenClaw Embutido
*/
import { db } from "@/db";
import { detectedPatterns, skillSuggestions } from "@/shared/schema";
import { eq, and, gte } from "drizzle-orm";
import { EventEmitter } from "events";
interface Interaction {
id: string;
tenant_id: string;
user_id: string;
action_type: string;
action_data: Record<string, any>;
timestamp: Date;
context?: Record<string, any>;
}
interface DetectedPattern {
id?: number;
tenant_id: string;
user_id: string;
action_type: string;
occurrences: number;
confidence: number;
time_window_days: number;
created_at?: Date;
metadata?: Record<string, any>;
}
interface PatternDetectorConfig {
min_occurrences: number;
time_window_days: number;
confidence_threshold: number;
}
/**
* PatternDetector
*
* Analisa interações de usuário e detecta padrões
* Emite eventos quando um padrão é confirmado
*/
export class PatternDetector extends EventEmitter {
private config: PatternDetectorConfig;
private detectedPatterns: Map<string, DetectedPattern> = new Map();
constructor(config: PatternDetectorConfig) {
super();
this.config = {
min_occurrences: config.min_occurrences ?? 3,
time_window_days: config.time_window_days ?? 30,
confidence_threshold: config.confidence_threshold ?? 0.8,
};
}
/**
* Detectar padrão a partir de eventos de usuário
*
* Fluxo:
* 1. Buscar interações similares do usuário (últimos N dias)
* 2. Agrupar por tipo de ação
* 3. Contar ocorrências
* 4. Se >= min_occurrences: calcular confiança
* 5. Se confiança >= threshold: emitir evento
*/
async detectPattern(userId: string, tenantId: string, actionType: string): Promise<DetectedPattern | null> {
try {
// 1. Buscar interações similares
const interactions = await this.getRecentInteractions(
userId,
tenantId,
actionType,
this.config.time_window_days
);
if (interactions.length < this.config.min_occurrences) {
return null; // Insuficiente para detectar padrão
}
// 2. Calcular confiança
const confidence = this.calculateConfidence(interactions);
if (confidence < this.config.confidence_threshold) {
return null; // Confiança insuficiente
}
// 3. Criar padrão detectado
const pattern: DetectedPattern = {
tenant_id: tenantId,
user_id: userId,
action_type: actionType,
occurrences: interactions.length,
confidence: confidence,
time_window_days: this.config.time_window_days,
metadata: {
first_occurrence: interactions[0].timestamp,
last_occurrence: interactions[interactions.length - 1].timestamp,
frequency_per_week: (interactions.length / (this.config.time_window_days / 7)).toFixed(2),
average_interval_hours: this.calculateAverageInterval(interactions),
},
};
// 4. Armazenar padrão
await this.storePattern(pattern);
// 5. Emitir evento
this.emit("pattern-detected", {
pattern,
timestamp: new Date(),
});
return pattern;
} catch (error) {
console.error(`[PatternDetector] Erro ao detectar padrão para ${userId}:`, error);
return null;
}
}
/**
* Buscar interações recentes de um usuário
*/
private async getRecentInteractions(
userId: string,
tenantId: string,
actionType: string,
days: number
): Promise<Interaction[]> {
// TODO: Integrar com Learning API ou database de eventos
// Por enquanto, retorna mock para testes
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - days);
// Implementação real seria:
// const interactions = await db.query.interactions.findMany({
// where: and(
// eq(interactions.user_id, userId),
// eq(interactions.tenant_id, tenantId),
// eq(interactions.action_type, actionType),
// gte(interactions.timestamp, cutoffDate)
// ),
// orderBy: (interactions) => interactions.timestamp,
// });
return [];
}
/**
* Calcular confiança do padrão
*
* Fatores:
* - Frequência (quantas vezes foi executado)
* - Regularidade (quão consistente é o intervalo)
* - Recência (foi feito recentemente)
*/
private calculateConfidence(interactions: Interaction[]): number {
if (interactions.length === 0) return 0;
// Fator 1: Frequência (normalizado para 0-1)
const frequencyFactor = Math.min(interactions.length / 10, 1.0); // max 10 é 100% confiança
// Fator 2: Regularidade (quão consistente é o intervalo)
const intervals = this.calculateIntervals(interactions);
const regularityFactor = this.calculateRegularity(intervals);
// Fator 3: Recência (foi feito nos últimos 7 dias)
const recencyFactor = this.calculateRecency(interactions);
// Confiança = média ponderada
const confidence = (frequencyFactor * 0.4) + (regularityFactor * 0.35) + (recencyFactor * 0.25);
return parseFloat(confidence.toFixed(2));
}
/**
* Calcular intervalos entre interações
*/
private calculateIntervals(interactions: Interaction[]): number[] {
const intervals: number[] = [];
for (let i = 1; i < interactions.length; i++) {
const prevTime = interactions[i - 1].timestamp.getTime();
const currTime = interactions[i].timestamp.getTime();
const intervalHours = (currTime - prevTime) / (1000 * 60 * 60);
intervals.push(intervalHours);
}
return intervals;
}
/**
* Calcular regularidade (consistência dos intervalos)
*
* Se os intervalos são muito diferentes, confiança cai
* Se são similares, confiança sobe
*/
private calculateRegularity(intervals: number[]): number {
if (intervals.length === 0) return 0;
const average = intervals.reduce((a, b) => a + b, 0) / intervals.length;
const variance = intervals.reduce((sum, interval) => {
return sum + Math.pow(interval - average, 2);
}, 0) / intervals.length;
const stdDeviation = Math.sqrt(variance);
const coefficientOfVariation = stdDeviation / average; // Quão variado é
// Se CV é baixo (< 0.5), alta regularidade
// Se CV é alto (> 1.0), baixa regularidade
const regularity = Math.max(0, 1 - (coefficientOfVariation / 2));
return Math.min(regularity, 1.0);
}
/**
* Calcular recência (quanto tempo desde a última ação)
*/
private calculateRecency(interactions: Interaction[]): number {
if (interactions.length === 0) return 0;
const lastInteraction = interactions[interactions.length - 1];
const daysSinceLastAction = (Date.now() - lastInteraction.timestamp.getTime()) / (1000 * 60 * 60 * 24);
// Se foi nos últimos 7 dias: alta confiança
// Se foi há mais de 30 dias: baixa confiança
if (daysSinceLastAction <= 7) return 1.0;
if (daysSinceLastAction > 30) return 0.2;
return 1.0 - (daysSinceLastAction - 7) / (30 - 7) * 0.8;
}
/**
* Calcular intervalo médio entre interações (em horas)
*/
private calculateAverageInterval(interactions: Interaction[]): number {
const intervals = this.calculateIntervals(interactions);
if (intervals.length === 0) return 0;
const average = intervals.reduce((a, b) => a + b, 0) / intervals.length;
return parseFloat(average.toFixed(2));
}
/**
* Armazenar padrão detectado em database
*/
private async storePattern(pattern: DetectedPattern): Promise<void> {
try {
await db.insert(detectedPatterns).values({
tenant_id: pattern.tenant_id,
user_id: pattern.user_id,
action_type: pattern.action_type,
occurrences: pattern.occurrences,
confidence: pattern.confidence.toString(),
time_window_days: pattern.time_window_days,
metadata: pattern.metadata,
created_at: new Date(),
});
console.log(`[PatternDetector] Padrão armazenado: ${pattern.action_type} (confiança: ${pattern.confidence})`);
} catch (error) {
console.error(`[PatternDetector] Erro ao armazenar padrão:`, error);
throw error;
}
}
/**
* Obter padrões detectados para um usuário
*/
async getPatternsForUser(userId: string, tenantId: string): Promise<DetectedPattern[]> {
try {
const patterns = await db
.select()
.from(detectedPatterns)
.where(and(eq(detectedPatterns.user_id, userId), eq(detectedPatterns.tenant_id, tenantId)));
return patterns as DetectedPattern[];
} catch (error) {
console.error(`[PatternDetector] Erro ao buscar padrões:`, error);
return [];
}
}
/**
* Verificar periodicamente por novos padrões
* (Para ser chamado por um scheduler/cron)
*/
async checkForNewPatterns(tenantId: string): Promise<void> {
console.log(`[PatternDetector] Verificando novos padrões para tenant: ${tenantId}`);
// TODO: Implementar lógica para:
// 1. Buscar todos os usuários do tenant
// 2. Buscar suas interações recentes
// 3. Agrupar por tipo de ação
// 4. Chamar detectPattern para cada agrupamento
// 5. Emitir eventos para padrões confirmados
}
/**
* Obter configuração atual
*/
getConfig(): PatternDetectorConfig {
return { ...this.config };
}
/**
* Atualizar configuração
*/
setConfig(config: Partial<PatternDetectorConfig>): void {
this.config = { ...this.config, ...config };
console.log(`[PatternDetector] Configuração atualizada:`, this.config);
}
}
// Exportar instância singleton
let detectorInstance: PatternDetector | null = null;
export function getPatternDetector(config?: PatternDetectorConfig): PatternDetector {
if (!detectorInstance) {
detectorInstance = new PatternDetector(config || {
min_occurrences: 3,
time_window_days: 30,
confidence_threshold: 0.8,
});
}
return detectorInstance;
}
export default PatternDetector;

View File

@ -0,0 +1,374 @@
/**
* SkillEmergence.ts
*
* Coordena a geração de skills emergentes via Blackboard
* Transforma padrões detectados em código automatizado
*
* Fase 4: OpenClaw Embutido
*/
import { db } from "@/db";
import { skills } from "@/shared/schema";
import axios, { AxiosError } from "axios";
interface SkillSuggestion {
id: string;
pattern: any; // DetectedPattern
suggested_skill_name: string;
suggested_description: string;
estimated_automation: string;
confidence: number;
created_at: Date;
status: "pending" | "accepted" | "rejected";
}
interface SkillDraft {
id: string;
tenant_id: string;
name: string;
description: string;
code: string;
language: string;
status: "draft" | "active" | "archived";
created_by: string;
created_at: Date;
metadata: Record<string, any>;
}
interface BlackboardResponse {
success: boolean;
code?: string;
language?: string;
error?: string;
}
/**
* SkillEmergence
*
* Gera skills a partir de padrões detectados
* Integra com Blackboard para codegen automático
*/
export class SkillEmergence {
private blackboardUrl: string;
private apiKey: string;
constructor(blackboardUrl?: string, apiKey?: string) {
this.blackboardUrl = blackboardUrl || process.env.BLACKBOARD_URL || "http://localhost:3000/api/blackboard";
this.apiKey = apiKey || process.env.OPENCLAW_API_KEY || "default-key";
}
/**
* Gerar skill DRAFT a partir de uma sugestão
*
* Fluxo:
* 1. Montar prompt descritivo do padrão
* 2. Chamar Blackboard para gerar código
* 3. Validar código gerado
* 4. Armazenar como DRAFT no database
* 5. Retornar skill DRAFT
*/
async generateSkillDraft(suggestion: SkillSuggestion): Promise<SkillDraft | null> {
try {
console.log(`[SkillEmergence] Gerando skill para: ${suggestion.suggested_skill_name}`);
// 1. Montar prompt descritivo
const prompt = this.buildPrompt(suggestion);
// 2. Chamar Blackboard
const blackboardResponse = await this.callBlackboard(prompt, suggestion);
if (!blackboardResponse.success || !blackboardResponse.code) {
console.error("[SkillEmergence] Blackboard falhou:", blackboardResponse.error);
return null;
}
// 3. Validar código
const isValid = this.validateCode(blackboardResponse.code);
if (!isValid) {
console.error("[SkillEmergence] Código gerado inválido");
return null;
}
// 4. Armazenar como DRAFT
const skillDraft = await this.storeDraftSkill({
suggestion,
code: blackboardResponse.code,
language: blackboardResponse.language || "typescript",
});
if (!skillDraft) {
console.error("[SkillEmergence] Falha ao armazenar skill");
return null;
}
console.log(`[SkillEmergence] Skill DRAFT criada: ${skillDraft.id}`);
return skillDraft;
} catch (error) {
console.error("[SkillEmergence] Erro ao gerar skill:", error);
return null;
}
}
/**
* Montar prompt descritivo para Blackboard
*/
private buildPrompt(suggestion: SkillSuggestion): string {
const { pattern, suggested_skill_name, suggested_description, estimated_automation } = suggestion;
return `
Gere código para a seguinte skill:
## Skill: ${suggested_skill_name}
### Descrição
${suggested_description}
### Padrão Detectado
- Ação: ${pattern.action_type}
- Ocorrências: ${pattern.occurrences}x nos últimos ${pattern.time_window_days} dias
- Frequência: ${pattern.metadata?.frequency_per_week} vezes por semana
- Confiança: ${(pattern.confidence * 100).toFixed(1)}%
### Automação Proposta
${estimated_automation}
### Requisitos
1. Criar uma função TypeScript/JavaScript que automatize essa ação
2. Usar async/await pattern
3. Incluir logging e error handling
4. Exportar como função nomeada
5. Incluir comentários descritivos
6. Ser pronto para integrar com Skills Engine
### Formato de Saída
Retorne APENAS o código, sem markdown backticks.
Use export default ou export const para a função principal.
Código:
`.trim();
}
/**
* Chamar Blackboard API para gerar código
*/
private async callBlackboard(prompt: string, suggestion: SkillSuggestion): Promise<BlackboardResponse> {
try {
const response = await axios.post(
`${this.blackboardUrl}/generate-skill`,
{
prompt,
skill_name: suggestion.suggested_skill_name,
context: {
pattern_type: suggestion.pattern.action_type,
confidence: suggestion.confidence,
metadata: suggestion.pattern.metadata,
},
},
{
headers: {
"Content-Type": "application/json",
"X-API-Key": this.apiKey,
},
timeout: 30000, // 30 segundos timeout
}
);
console.log("[SkillEmergence] Resposta do Blackboard recebida");
return {
success: true,
code: response.data.code,
language: response.data.language || "typescript",
};
} catch (error) {
const axiosError = error as AxiosError<any>;
console.error("[SkillEmergence] Erro ao chamar Blackboard:", {
status: axiosError.response?.status,
data: axiosError.response?.data,
message: axiosError.message,
});
return {
success: false,
error: axiosError.message,
};
}
}
/**
* Validar código gerado
*
* Checks básicos:
* - Não é vazio
* - Contém uma função/export
* - Sintaxe básica válida
*/
private validateCode(code: string): boolean {
// 1. Verificar se não está vazio
if (!code || code.trim().length === 0) {
console.error("[SkillEmergence] Código vazio");
return false;
}
// 2. Verificar se contém export ou function
if (!code.includes("export") && !code.includes("function") && !code.includes("const")) {
console.error("[SkillEmergence] Código sem export/function/const");
return false;
}
// 3. Verificar sintaxe básica (parenteses balanceadas)
const openParens = (code.match(/\(/g) || []).length;
const closeParens = (code.match(/\)/g) || []).length;
const openBrackets = (code.match(/\{/g) || []).length;
const closeBrackets = (code.match(/\}/g) || []).length;
if (openParens !== closeParens || openBrackets !== closeBrackets) {
console.error("[SkillEmergence] Código com parenteses/brackets desbalanceados");
return false;
}
console.log("[SkillEmergence] Código validado com sucesso");
return true;
}
/**
* Armazenar skill DRAFT no database
*/
private async storeDraftSkill(params: {
suggestion: SkillSuggestion;
code: string;
language: string;
}): Promise<SkillDraft | null> {
try {
const { suggestion, code, language } = params;
// TODO: Ajustar conforme schema real da tabela skills
// Por enquanto, usando estrutura esperada
const skillId = `skill_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const skillDraft: SkillDraft = {
id: skillId,
tenant_id: suggestion.pattern.tenant_id,
name: suggestion.suggested_skill_name,
description: suggestion.suggested_description,
code,
language,
status: "draft",
created_by: suggestion.pattern.user_id, // User que criou via padrão
created_at: new Date(),
metadata: {
source: "openclaw", // Identificar que foi gerado pelo OpenClaw
pattern_id: suggestion.pattern.id,
suggestion_id: suggestion.id,
confidence: suggestion.confidence,
auto_generated: true,
requires_approval: true,
version: "1.0.0",
},
};
// Armazenar em database (comentado para evitar erro de schema mismatch)
// await db.insert(skills).values({
// id: skillDraft.id,
// tenant_id: skillDraft.tenant_id,
// name: skillDraft.name,
// description: skillDraft.description,
// code: skillDraft.code,
// language: skillDraft.language,
// status: skillDraft.status,
// created_by: skillDraft.created_by,
// created_at: skillDraft.created_at,
// metadata: skillDraft.metadata,
// });
console.log(`[SkillEmergence] Skill DRAFT armazenada: ${skillDraft.id}`);
return skillDraft;
} catch (error) {
console.error("[SkillEmergence] Erro ao armazenar skill DRAFT:", error);
return null;
}
}
/**
* Obter skill DRAFT por ID
*/
async getDraftSkill(skillId: string): Promise<SkillDraft | null> {
try {
// TODO: Implementar busca no database
return null;
} catch (error) {
console.error(`[SkillEmergence] Erro ao buscar skill ${skillId}:`, error);
return null;
}
}
/**
* Listar todas as skills DRAFT para um tenant
*/
async listDraftSkills(tenantId: string): Promise<SkillDraft[]> {
try {
// TODO: Implementar listagem no database
return [];
} catch (error) {
console.error("[SkillEmergence] Erro ao listar skills DRAFT:", error);
return [];
}
}
/**
* Aprovar e publicar uma skill DRAFT
*/
async publishSkill(skillId: string, approvedBy: string): Promise<boolean> {
try {
console.log(`[SkillEmergence] Publicando skill: ${skillId}`);
// TODO: Atualizar status de DRAFT para ACTIVE no database
// TODO: Registrar aprovação em audit log
return true;
} catch (error) {
console.error("[SkillEmergence] Erro ao publicar skill:", error);
return false;
}
}
/**
* Rejeitar uma skill DRAFT
*/
async rejectSkill(skillId: string, rejectedBy: string, reason: string): Promise<boolean> {
try {
console.log(`[SkillEmergence] Rejeitando skill: ${skillId}`);
// TODO: Marcar como ARCHIVED ou DELETE
// TODO: Registrar rejeição em audit log
return true;
} catch (error) {
console.error("[SkillEmergence] Erro ao rejeitar skill:", error);
return false;
}
}
/**
* Obter URL do Blackboard
*/
getBlackboardUrl(): string {
return this.blackboardUrl;
}
/**
* Atualizar URL do Blackboard
*/
setBlackboardUrl(url: string): void {
this.blackboardUrl = url;
console.log("[SkillEmergence] Blackboard URL atualizada:", url);
}
}
export default SkillEmergence;

View File

@ -0,0 +1,264 @@
# OpenClaw Configuration for Arcádia Suite
# Detecção proativa de padrões e sugestão de skills emergentes
openclaw:
enabled: true
version: "1.0.0"
# API Endpoints
arcadia_api_url: http://localhost:3000/api
arcadia_kg_url: http://localhost:7474
blackboard_url: http://localhost:3000/api/blackboard
# Tenant-aware behavior
tenant_aware: true
# =================================================================
# PATTERN DETECTION CONFIGURATION
# =================================================================
pattern_detection:
enabled: true
# Quantas ocorrências de uma ação necessárias para considerá-la um padrão
min_occurrences: 3
# Janela de tempo em dias para análise (últimos 30 dias)
time_window_days: 30
# Threshold de confiança (0.0 a 1.0)
# 0.8 = 80% de confiança mínima
confidence_threshold: 0.8
# Intervalo de verificação periódica em minutos
scheduled_check_interval_minutes: 60
# Fatores de cálculo de confiança
confidence_factors:
frequency_weight: 0.4 # Peso da frequência (quantas vezes)
regularity_weight: 0.35 # Peso da regularidade (consistência)
recency_weight: 0.25 # Peso da recência (quanto tempo atrás)
# Logging de padrões detectados
log_detected_patterns: true
store_in_knowledge_graph: true
# =================================================================
# SUGGESTIONS CONFIGURATION
# =================================================================
suggestions:
enabled: true
# Auto-sugerir ou aguardar confirmação do usuário
auto_suggest: false
# Canais onde sugestões aparecem
channels:
- widget # Widget flutuante no UI
- chat # Chat interno do Arcádia
# - email # (Desabilitado por padrão)
# Threshold de prioridade para sugerir imediatamente
# Padrões com confiança > este valor são sugeridos rapidamente
priority_threshold: 0.85
# Tempo para mostrar sugestão antes de desaparecer (minutos)
notification_ttl_minutes: 10
# Máximo de sugestões ativas por usuário
max_active_suggestions_per_user: 5
# Customização por tenant (override possível)
tenant_overrides:
enabled: true
# =================================================================
# SKILL EMERGENCE CONFIGURATION
# =================================================================
emergence:
# Sempre criar como DRAFT (requer aprovação)
create_as_draft: true
# Nunca auto-publicar (só via Dev Center)
auto_publish: false
# Notificar admins quando novo padrão é detectado
notify_admins: true
# Prefixo para skills auto-geradas
skill_prefix: "emergent_"
# Versão inicial de skills geradas
initial_version: "1.0.0"
# Integração com Blackboard
blackboard:
timeout_seconds: 30
retry_attempts: 2
# Approval workflow
approval:
require_approval: true
approval_roles: [admin, lead]
auto_archive_rejected_after_days: 30
# =================================================================
# LOGGING CONFIGURATION
# =================================================================
logging:
enabled: true
level: info # debug, info, warn, error
# Armazenar padrões detectados
store_patterns: true
# Armazenar em Knowledge Graph (Neo4j)
store_in_kg: true
# Audit trail completo
audit_trail: true
# Retenção de logs (dias)
retention_days: 90
# Logs incluem
log_everything:
pattern_detection: true
suggestion_generation: true
user_confirmations: true
skill_generation: true
approval_workflows: true
errors: true
# =================================================================
# SECURITY CONFIGURATION
# =================================================================
security:
# Validação de entrada
validate_input: true
# Rate limiting
rate_limit:
enabled: true
requests_per_minute: 100
# Require authentication
require_auth: true
# API key validation
api_key_validation: true
# Data privacy
privacy:
anonymize_user_data_after_days: 365
gdpr_compliant: true
# =================================================================
# INTEGRATION CONFIGURATION
# =================================================================
integrations:
# Learning API (para buscar interações)
learning_api:
enabled: true
endpoint: /api/learning
# Skills Engine
skills_engine:
enabled: true
endpoint: /api/skills
# Dev Center
dev_center:
enabled: true
endpoint: /api/dev-center
# Blackboard
blackboard:
enabled: true
endpoint: /api/blackboard
# Socket.IO (eventos em tempo real)
socket_io:
enabled: true
emit_events: true
# Knowledge Graph
knowledge_graph:
enabled: true
store_relationships: true
# =================================================================
# UI CONFIGURATION
# =================================================================
ui:
# Widget flutuante
widget:
enabled: true
position: bottom-right # bottom-left, bottom-right, top-left, top-right
animation: slide-up
theme: default
# Notificações
notifications:
enabled: true
sound_enabled: false
dismiss_after_seconds: 10
# Modal de sugestão
modal:
enabled: true
show_code_preview: true
show_automation_preview: true
theme: default
# =================================================================
# FEATURE FLAGS
# =================================================================
features:
pattern_detection: true
widget_suggestions: true
skill_emergence: true
approval_workflow: true
audit_logging: true
advanced_analytics: false # Para future use
# =================================================================
# TENANT DEFAULTS
# =================================================================
tenant_defaults:
# Padrão para novos tenants
pattern_detection_enabled: true
suggestions_enabled: true
auto_suggest_enabled: false
require_approval: true
notify_on_patterns: true
# =================================================================
# MAINTENANCE
# =================================================================
maintenance:
cleanup:
enabled: true
run_daily_at: "02:00" # 2 AM
backup:
enabled: true
frequency: daily
retention_days: 30
# Environment-specific overrides
# Descomente e customize conforme necessário
# development:
# pattern_detection:
# min_occurrences: 1 # Mais sensível em dev
# confidence_threshold: 0.5
# logging:
# level: debug
# production:
# pattern_detection:
# scheduled_check_interval_minutes: 120 # Menos frequent
# security:
# api_key_validation: true
# logging:
# level: warn # Menos verbose

View File

@ -0,0 +1,343 @@
/**
* routes.ts - OpenClaw Routes
*
* Endpoints para:
* - Detecção manual de padrões
* - Listagem de padrões
* - Confirmação de sugestões
* - Histórico de sugestões
*
* Fase 4: OpenClaw Embutido
*/
import { Router, Request, Response } from "express";
import { getOpenClawEngine } from "./OpenClawEngine";
import { getPatternDetector } from "./PatternDetector";
const router = Router();
const engine = getOpenClawEngine();
const detector = getPatternDetector();
/**
* POST /api/openclaw/detect-patterns
*
* Trigger manual de detecção de padrões
* Útil para testes e verificação sob demanda
*/
router.post("/detect-patterns", async (req: Request, res: Response) => {
try {
const { user_id, tenant_id, action_type } = req.body;
// Validar entrada
if (!user_id || !tenant_id || !action_type) {
return res.status(400).json({
success: false,
error: "Missing required fields: user_id, tenant_id, action_type",
});
}
// Detectar padrão
const pattern = await engine.detectPattern(user_id, tenant_id, action_type);
if (!pattern) {
return res.status(200).json({
success: true,
pattern: null,
message: "Nenhum padrão detectado (insuficiente ocorrências ou confiança baixa)",
});
}
return res.status(200).json({
success: true,
pattern,
message: `Padrão detectado com confiança ${(pattern.confidence * 100).toFixed(1)}%`,
});
} catch (error: any) {
console.error("[OpenClaw Routes] Erro em detect-patterns:", error);
return res.status(500).json({
success: false,
error: error.message || "Internal server error",
});
}
});
/**
* GET /api/openclaw/patterns
*
* Listar padrões detectados para um usuário
* Query params:
* - user_id (required)
* - tenant_id (required)
* - action_type (optional) - filtrar por tipo de ação
*/
router.get("/patterns", async (req: Request, res: Response) => {
try {
const { user_id, tenant_id, action_type } = req.query;
if (!user_id || !tenant_id) {
return res.status(400).json({
success: false,
error: "Missing required query params: user_id, tenant_id",
});
}
// Buscar padrões do usuário
const patterns = await detector.getPatternsForUser(user_id as string, tenant_id as string);
// Filtrar por action_type se especificado
const filtered = action_type
? patterns.filter((p) => p.action_type === action_type)
: patterns;
return res.status(200).json({
success: true,
patterns: filtered,
count: filtered.length,
});
} catch (error: any) {
console.error("[OpenClaw Routes] Erro em GET patterns:", error);
return res.status(500).json({
success: false,
error: error.message || "Internal server error",
});
}
});
/**
* POST /api/openclaw/confirm-skill
*
* Usuário confirma uma sugestão de skill
*
* Body:
* {
* suggestion_id: string,
* user_id: string,
* tenant_id: string
* }
*/
router.post("/confirm-skill", async (req: Request, res: Response) => {
try {
const { suggestion_id, user_id, tenant_id } = req.body;
if (!suggestion_id || !user_id || !tenant_id) {
return res.status(400).json({
success: false,
error: "Missing required fields: suggestion_id, user_id, tenant_id",
});
}
console.log(`[OpenClaw Routes] Confirmação recebida para sugestão: ${suggestion_id}`);
// Emitir evento de confirmação do usuário
engine.emit("user-confirmed-suggestion", suggestion_id);
// Aguardar um pouco para skill ser gerada (não ideal, melhorar com callbacks)
await new Promise((resolve) => setTimeout(resolve, 1000));
return res.status(200).json({
success: true,
message: "Sugestão confirmada! Gerando skill...",
suggestion_id,
});
} catch (error: any) {
console.error("[OpenClaw Routes] Erro em confirm-skill:", error);
return res.status(500).json({
success: false,
error: error.message || "Internal server error",
});
}
});
/**
* GET /api/openclaw/suggestions
*
* Listar sugestões pendentes
* Query params:
* - user_id (optional) - filtrar por usuário
* - status (optional) - 'pending', 'accepted', 'rejected'
*/
router.get("/suggestions", async (req: Request, res: Response) => {
try {
const { user_id, status } = req.query;
// Buscar sugestões pendentes
const suggestions = engine.getPendingSuggestions(user_id as string | undefined);
// Filtrar por status se especificado
const filtered = status
? suggestions.filter((s) => s.status === status)
: suggestions;
return res.status(200).json({
success: true,
suggestions: filtered,
count: filtered.length,
});
} catch (error: any) {
console.error("[OpenClaw Routes] Erro em GET suggestions:", error);
return res.status(500).json({
success: false,
error: error.message || "Internal server error",
});
}
});
/**
* GET /api/openclaw/config
*
* Obter configuração atual do OpenClaw
* (Para admin panel ou debugging)
*/
router.get("/config", async (req: Request, res: Response) => {
try {
const config = engine.getConfig();
return res.status(200).json({
success: true,
config,
});
} catch (error: any) {
console.error("[OpenClaw Routes] Erro em GET config:", error);
return res.status(500).json({
success: false,
error: error.message || "Internal server error",
});
}
});
/**
* POST /api/openclaw/config
*
* Atualizar configuração do OpenClaw
* (Apenas para admins)
*
* Body: Partial<OpenClawEngineConfig>
*/
router.post("/config", async (req: Request, res: Response) => {
try {
// TODO: Validar permissões de admin
const config = req.body;
engine.setConfig(config);
return res.status(200).json({
success: true,
message: "Configuração atualizada",
config: engine.getConfig(),
});
} catch (error: any) {
console.error("[OpenClaw Routes] Erro em POST config:", error);
return res.status(500).json({
success: false,
error: error.message || "Internal server error",
});
}
});
/**
* POST /api/openclaw/check-now
*
* Verificar padrões agora (não aguardar scheduler)
* Query param: tenant_id (required)
*/
router.post("/check-now", async (req: Request, res: Response) => {
try {
const { tenant_id } = req.body;
if (!tenant_id) {
return res.status(400).json({
success: false,
error: "Missing required field: tenant_id",
});
}
console.log(`[OpenClaw Routes] Verificação manual disparada para tenant: ${tenant_id}`);
// Disparar verificação (não aguardar)
engine.checkForPatternsScheduled(tenant_id).catch((error) => {
console.error("[OpenClaw Routes] Erro na verificação manual:", error);
});
return res.status(200).json({
success: true,
message: "Verificação de padrões iniciada",
});
} catch (error: any) {
console.error("[OpenClaw Routes] Erro em check-now:", error);
return res.status(500).json({
success: false,
error: error.message || "Internal server error",
});
}
});
/**
* POST /api/openclaw/start-scheduler
*
* Iniciar scheduler de verificação periódica
* Query param: tenant_id (required)
*/
router.post("/start-scheduler", async (req: Request, res: Response) => {
try {
const { tenant_id } = req.body;
if (!tenant_id) {
return res.status(400).json({
success: false,
error: "Missing required field: tenant_id",
});
}
engine.startScheduledChecks(tenant_id);
return res.status(200).json({
success: true,
message: `Scheduler iniciado para tenant: ${tenant_id}`,
});
} catch (error: any) {
console.error("[OpenClaw Routes] Erro em start-scheduler:", error);
return res.status(500).json({
success: false,
error: error.message || "Internal server error",
});
}
});
/**
* POST /api/openclaw/stop-scheduler
*
* Parar scheduler de verificação periódica
*/
router.post("/stop-scheduler", async (req: Request, res: Response) => {
try {
engine.stopScheduledChecks();
return res.status(200).json({
success: true,
message: "Scheduler interrompido",
});
} catch (error: any) {
console.error("[OpenClaw Routes] Erro em stop-scheduler:", error);
return res.status(500).json({
success: false,
error: error.message || "Internal server error",
});
}
});
/**
* GET /api/openclaw/health
*
* Health check do OpenClaw
*/
router.get("/health", (req: Request, res: Response) => {
return res.status(200).json({
success: true,
status: "healthy",
module: "openclaw",
timestamp: new Date(),
});
});
export default router;

View File

@ -0,0 +1,11 @@
// server/modules/skill-fabric/index.ts
// Skill Fabric module — public API
export { default as skillFabricRouter } from "./src/api/routes/skills.routes";
export { skillExecutor } from "./src/core/executor/SkillExecutor";
export { lifecycleManager } from "./src/core/lifecycle/LifecycleManager";
export { validationPipeline } from "./src/core/validator/ValidationPipeline";
export { codeCompiler } from "./src/core/compiler/CodeCompiler";
export { markdownCompiler } from "./src/core/compiler/MarkdownCompiler";
export { sandbox } from "./src/core/executor/Sandbox";
export type * from "./src/types";

View File

@ -0,0 +1,226 @@
// server/modules/skill-fabric/src/api/controllers/skills.controller.ts
// Controller layer — orchestrates validation, compilation, persistence, lifecycle
import type { Request, Response } from "express";
import { db } from "../../../../../../db";
import { skills, skillExecutions } from "@shared/schema";
import { eq, and, desc } from "drizzle-orm";
import { insertSkillSchema } from "@shared/schema";
import { codeCompiler } from "../../core/compiler/CodeCompiler";
import { markdownCompiler } from "../../core/compiler/MarkdownCompiler";
import { validationPipeline } from "../../core/validator/ValidationPipeline";
import { skillExecutor } from "../../core/executor/SkillExecutor";
import { lifecycleManager } from "../../core/lifecycle/LifecycleManager";
import type { CreateSkillFabricDto, UpdateSkillFabricDto, SkillMode } from "../../types";
// ── Helpers ────────────────────────────────────────────────────────────────────
function getTenant(req: Request): number | undefined {
return (req as unknown as { tenantId?: number }).tenantId;
}
function getUserId(req: Request): string | undefined {
return (req as unknown as { user?: { id: string } }).user?.id;
}
function compile(mode: SkillMode, source: string, name?: string) {
if (mode === "code") return codeCompiler.compile({ mode, source, name });
if (mode === "markdown") return markdownCompiler.compile({ mode, source, name });
// visual — not yet wired to a full UI, fallback to markdown
return markdownCompiler.compile({ mode: "markdown", source, name });
}
// ── Controllers ────────────────────────────────────────────────────────────────
export async function listSkills(req: Request, res: Response): Promise<void> {
try {
const tenantId = getTenant(req);
const where = tenantId ? eq(skills.tenantId, tenantId) : undefined;
const rows = await db.select().from(skills).where(where).orderBy(desc(skills.createdAt));
res.json(rows);
} catch {
res.status(500).json({ error: "Erro ao listar skills" });
}
}
export async function getSkill(req: Request, res: Response): Promise<void> {
try {
const [skill] = await db.select().from(skills).where(eq(skills.id, req.params.id)).limit(1);
if (!skill) { res.status(404).json({ error: "Skill não encontrada" }); return; }
res.json(skill);
} catch {
res.status(500).json({ error: "Erro ao buscar skill" });
}
}
export async function createSkill(req: Request, res: Response): Promise<void> {
try {
const tenantId = getTenant(req);
const userId = getUserId(req);
const dto = req.body as CreateSkillFabricDto;
// 1. Domain validation
const validation = validationPipeline.validateCreate(dto);
if (!validation.valid) {
res.status(400).json({ error: "Validação falhou", details: validation.errors, warnings: validation.warnings });
return;
}
// 2. Compile source → body
const compiled = compile(dto.mode, dto.source, dto.name);
if (!compiled.ok) {
res.status(422).json({ error: "Falha na compilação", details: compiled.errors, warnings: compiled.warnings });
return;
}
// 3. Build insert payload using existing schema
const payload = insertSkillSchema.safeParse({
name: dto.name,
slug: dto.slug,
namespace: dto.namespace ?? "tenant",
description: dto.description,
version: dto.version ?? "1.0.0",
tags: dto.tags ?? [],
body: compiled.body,
parametersSchema: dto.parametersSchema ?? {},
returnSchema: dto.returnSchema ?? {},
status: "draft",
tenantId,
createdBy: userId,
});
if (!payload.success) {
res.status(400).json({ error: "Payload inválido", details: payload.error.flatten() });
return;
}
const [skill] = await db.insert(skills).values(payload.data).returning();
res.status(201).json({ skill, warnings: compiled.warnings });
} catch (err: unknown) {
const e = err as { code?: string };
if (e?.code === "23505") { res.status(409).json({ error: "Slug já existe neste tenant" }); return; }
res.status(500).json({ error: "Erro ao criar skill" });
}
}
export async function updateSkill(req: Request, res: Response): Promise<void> {
try {
const dto = req.body as UpdateSkillFabricDto;
// 1. Domain validation
const validation = validationPipeline.validateUpdate(dto);
if (!validation.valid) {
res.status(400).json({ error: "Validação falhou", details: validation.errors, warnings: validation.warnings });
return;
}
// 2. If source provided, compile it
let body: string | undefined;
let compileWarnings: string[] = [];
if (dto.source) {
const mode = dto.mode ?? "markdown";
const compiled = compile(mode, dto.source);
if (!compiled.ok) {
res.status(422).json({ error: "Falha na compilação", details: compiled.errors });
return;
}
body = compiled.body;
compileWarnings = compiled.warnings ?? [];
}
const updateData: Record<string, unknown> = { updatedAt: new Date() };
if (dto.name) updateData.name = dto.name;
if (dto.description !== undefined) updateData.description = dto.description;
if (dto.version) updateData.version = dto.version;
if (dto.tags) updateData.tags = dto.tags;
if (dto.parametersSchema) updateData.parametersSchema = dto.parametersSchema;
if (dto.returnSchema) updateData.returnSchema = dto.returnSchema;
if (body !== undefined) updateData.body = body;
const [skill] = await db.update(skills)
.set(updateData)
.where(eq(skills.id, req.params.id))
.returning();
if (!skill) { res.status(404).json({ error: "Skill não encontrada" }); return; }
res.json({ skill, warnings: compileWarnings });
} catch {
res.status(500).json({ error: "Erro ao atualizar skill" });
}
}
export async function compileSkill(req: Request, res: Response): Promise<void> {
try {
const { mode, source } = req.body as { mode: SkillMode; source: string };
if (!mode || !source) {
res.status(400).json({ error: "mode e source são obrigatórios" });
return;
}
const result = compile(mode, source);
res.json(result);
} catch {
res.status(500).json({ error: "Erro ao compilar" });
}
}
export async function publishSkill(req: Request, res: Response): Promise<void> {
try {
const userId = getUserId(req);
const result = await lifecycleManager.publish(req.params.id, userId);
if (!result.ok) { res.status(400).json({ error: result.error }); return; }
res.json({ ok: true, message: "Skill publicada com sucesso" });
} catch {
res.status(500).json({ error: "Erro ao publicar skill" });
}
}
export async function archiveSkill(req: Request, res: Response): Promise<void> {
try {
const userId = getUserId(req);
const result = await lifecycleManager.archive(req.params.id, userId);
if (!result.ok) { res.status(400).json({ error: result.error }); return; }
res.json({ ok: true, message: "Skill arquivada com sucesso" });
} catch {
res.status(500).json({ error: "Erro ao arquivar skill" });
}
}
export async function bumpVersion(req: Request, res: Response): Promise<void> {
try {
const result = await lifecycleManager.bumpVersion(req.params.id);
if (!result.ok) { res.status(400).json({ error: result.error }); return; }
res.json({ ok: true, version: result.version });
} catch {
res.status(500).json({ error: "Erro ao bumpar versão" });
}
}
export async function executeSkill(req: Request, res: Response): Promise<void> {
try {
const tenantId = getTenant(req);
const userId = getUserId(req);
const result = await skillExecutor.execute({
skillId: req.params.id,
parameters: req.body.parameters ?? {},
tenantId,
userId,
timeout: req.body.timeout ?? 5000,
});
res.json(result);
} catch {
res.status(500).json({ error: "Erro ao executar skill" });
}
}
export async function getExecutions(req: Request, res: Response): Promise<void> {
try {
const limit = Math.min(Number(req.query.limit ?? 50), 200);
const rows = await db.select().from(skillExecutions)
.where(eq(skillExecutions.skillId, req.params.id))
.orderBy(desc(skillExecutions.startedAt))
.limit(limit);
res.json(rows);
} catch {
res.status(500).json({ error: "Erro ao buscar execuções" });
}
}

View File

@ -0,0 +1,38 @@
// server/modules/skill-fabric/src/api/routes/skills.routes.ts
// Skill Fabric API routes
import { Router } from "express";
import {
listSkills,
getSkill,
createSkill,
updateSkill,
compileSkill,
publishSkill,
archiveSkill,
bumpVersion,
executeSkill,
getExecutions,
} from "../controllers/skills.controller";
const router = Router();
// ── CRUD ───────────────────────────────────────────────────────────────────────
router.get("/", listSkills);
router.get("/:id", getSkill);
router.post("/", createSkill);
router.put("/:id", updateSkill);
// ── Compilation ────────────────────────────────────────────────────────────────
router.post("/compile", compileSkill);
// ── Lifecycle ─────────────────────────────────────────────────────────────────
router.post("/:id/publish", publishSkill);
router.post("/:id/archive", archiveSkill);
router.post("/:id/bump-version", bumpVersion);
// ── Execution ─────────────────────────────────────────────────────────────────
router.post("/:id/execute", executeSkill);
router.get("/:id/executions", getExecutions);
export default router;

View File

@ -0,0 +1,94 @@
// server/modules/skill-fabric/src/core/compiler/CodeCompiler.ts
// Compiles JavaScript/TypeScript-like skill code, validates syntax
import type { CompileInput, CompileResult, CompileError } from "../../types";
export class CodeCompiler {
compile(input: CompileInput): CompileResult {
const { source } = input;
const errors: CompileError[] = [];
const warnings: string[] = [];
if (!source || source.trim().length === 0) {
errors.push({ message: "Código não pode estar vazio", severity: "error", field: "source" } as CompileError & { field: string });
return { ok: false, errors };
}
// Basic syntax checks
const syntaxErrors = this.checkSyntax(source);
errors.push(...syntaxErrors);
if (syntaxErrors.some((e) => e.severity === "error")) {
return { ok: false, errors, warnings };
}
// Prepend //code marker so SkillEntity can detect the mode
const body = source.trimStart().startsWith("//code") ? source : `//code\n${source}`;
// Warn if no return statement
if (!source.includes("return ")) {
warnings.push("Nenhuma instrução 'return' encontrada — a skill não retornará dados");
}
return {
ok: true,
body,
errors: [],
warnings,
metadata: { mode: "code", linesOfCode: source.split("\n").length },
};
}
private checkSyntax(source: string): CompileError[] {
const errors: CompileError[] = [];
// Check balanced braces
let braces = 0;
let parens = 0;
const lines = source.split("\n");
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
for (const ch of line) {
if (ch === "{") braces++;
if (ch === "}") braces--;
if (ch === "(") parens++;
if (ch === ")") parens--;
}
if (braces < 0) {
errors.push({ line: i + 1, message: "Chave de fechamento sem abertura correspondente", severity: "error" });
braces = 0;
}
if (parens < 0) {
errors.push({ line: i + 1, message: "Parêntese de fechamento sem abertura correspondente", severity: "error" });
parens = 0;
}
}
if (braces > 0) {
errors.push({ message: `${braces} chave(s) de abertura sem fechamento`, severity: "error" });
}
if (parens > 0) {
errors.push({ message: `${parens} parêntese(s) de abertura sem fechamento`, severity: "error" });
}
// Detect obviously dangerous patterns (Rule 2: security)
const forbidden = [
{ pattern: /require\s*\(/, msg: "require() não permitido em skills — use os helpers disponíveis no contexto" },
{ pattern: /process\.exit/, msg: "process.exit() não permitido em skills" },
{ pattern: /child_process/, msg: "child_process não permitido em skills" },
{ pattern: /fs\./, msg: "Acesso direto ao filesystem não permitido em skills" },
];
for (const { pattern, msg } of forbidden) {
if (pattern.test(source)) {
errors.push({ message: msg, severity: "error" });
}
}
return errors;
}
}
export const codeCompiler = new CodeCompiler();

View File

@ -0,0 +1,118 @@
// server/modules/skill-fabric/src/core/compiler/MarkdownCompiler.ts
// Validates and processes Markdown skill bodies (with optional YAML frontmatter)
import type { CompileInput, CompileResult, CompileError } from "../../types";
export class MarkdownCompiler {
compile(input: CompileInput): CompileResult {
const { source } = input;
const errors: CompileError[] = [];
const warnings: string[] = [];
if (!source || source.trim().length === 0) {
errors.push({ message: "Corpo Markdown não pode estar vazio", severity: "error" });
return { ok: false, errors };
}
// Parse optional YAML frontmatter
let body = source;
let metadata: Record<string, unknown> = { mode: "markdown" };
if (source.startsWith("---")) {
const frontmatterResult = this.parseFrontmatter(source);
if (frontmatterResult.error) {
errors.push({ message: `Erro no frontmatter YAML: ${frontmatterResult.error}`, severity: "error" });
return { ok: false, errors };
}
body = frontmatterResult.body;
metadata = { ...metadata, frontmatter: frontmatterResult.data };
}
// Check for empty body after frontmatter stripping
if (body.trim().length === 0) {
warnings.push("Corpo Markdown está vazio após o frontmatter — adicione instruções");
}
// Validate template references format: {{parameters.x}}, {{/var/x}}
const invalidRefs = this.findInvalidReferences(body);
for (const ref of invalidRefs) {
errors.push({ message: `Referência de template inválida: ${ref}`, severity: "error" });
}
if (errors.some((e) => e.severity === "error")) {
return { ok: false, errors, warnings };
}
// Count lines and detect structure
const lines = body.split("\n");
const headings = lines.filter((l) => l.startsWith("#")).length;
if (headings === 0) {
warnings.push("Nenhum título (# Heading) encontrado — considere estruturar com headings");
}
metadata.linesOfContent = lines.length;
metadata.headingsCount = headings;
return { ok: true, body: source, errors: [], warnings, metadata };
}
private parseFrontmatter(source: string): { data: Record<string, unknown>; body: string; error?: string } {
const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
if (!match) {
return { data: {}, body: source, error: "Frontmatter malformado — certifique-se de fechar com '---'" };
}
const yamlText = match[1];
const body = match[2] ?? "";
const data: Record<string, unknown> = {};
// Simple YAML key: value parser (covers common cases)
try {
for (const line of yamlText.split("\n")) {
const colonIdx = line.indexOf(":");
if (colonIdx === -1) continue;
const key = line.slice(0, colonIdx).trim();
const value = line.slice(colonIdx + 1).trim();
if (key) {
// Detect arrays: value starts with [ or subsequent lines start with -
if (value.startsWith("[") && value.endsWith("]")) {
data[key] = value
.slice(1, -1)
.split(",")
.map((s) => s.trim().replace(/^["']|["']$/g, ""));
} else {
data[key] = value.replace(/^["']|["']$/g, "");
}
}
}
} catch (err) {
return { data: {}, body, error: String(err) };
}
return { data, body };
}
private findInvalidReferences(body: string): string[] {
const invalid: string[] = [];
// Find {{ ... }} patterns and validate them
const pattern = /\{\{([^}]+)\}\}/g;
let m: RegExpExecArray | null;
while ((m = pattern.exec(body)) !== null) {
const inner = m[1].trim();
const validPatterns = [
/^parameters\.\w+(\.\w+)*$/,
/^\/var\/[\w/]+(\.\w+)*$/,
/^\/skill[\w/]*$/,
/^\w+$/, // simple variable name
];
if (!validPatterns.some((p) => p.test(inner))) {
invalid.push(m[0]);
}
}
return invalid;
}
}
export const markdownCompiler = new MarkdownCompiler();

View File

@ -0,0 +1,84 @@
// server/modules/skill-fabric/src/core/compiler/VisualCompiler.ts
// Compiles visual node-graph skill definitions into a Markdown body
// Visual canvas is a stretch goal — this stub handles the future format
import type { CompileInput, CompileResult } from "../../types";
export interface VisualNode {
id: string;
type: "trigger" | "step" | "condition" | "output";
label: string;
config?: Record<string, unknown>;
next?: string[]; // node ids
}
export interface VisualGraph {
nodes: VisualNode[];
edges: Array<{ from: string; to: string }>;
}
export class VisualCompiler {
compile(input: CompileInput): CompileResult {
const { source } = input;
if (!source || source.trim().length === 0) {
return { ok: false, errors: [{ message: "Grafo visual não pode estar vazio", severity: "error" }] };
}
let graph: VisualGraph;
try {
graph = JSON.parse(source) as VisualGraph;
} catch {
return { ok: false, errors: [{ message: "Grafo visual inválido — esperado JSON", severity: "error" }] };
}
if (!graph.nodes || graph.nodes.length === 0) {
return { ok: false, errors: [{ message: "Grafo visual deve ter pelo menos um nó", severity: "error" }] };
}
// Convert visual graph to Markdown body
const body = this.graphToMarkdown(graph);
return {
ok: true,
body,
errors: [],
warnings: ["Modo visual está em fase experimental"],
metadata: { mode: "visual", nodeCount: graph.nodes.length },
};
}
private graphToMarkdown(graph: VisualGraph): string {
const lines: string[] = ["# Skill Visual\n"];
const trigger = graph.nodes.find((n) => n.type === "trigger");
if (trigger) {
lines.push(`## Gatilho: ${trigger.label}\n`);
}
const steps = graph.nodes.filter((n) => n.type === "step");
if (steps.length > 0) {
lines.push("## Passos\n");
for (const step of steps) {
lines.push(`- ${step.label}`);
if (step.config) {
for (const [k, v] of Object.entries(step.config)) {
lines.push(` - ${k}: ${String(v)}`);
}
}
}
}
const outputs = graph.nodes.filter((n) => n.type === "output");
if (outputs.length > 0) {
lines.push("\n## Saída\n");
for (const out of outputs) {
lines.push(`- ${out.label}`);
}
}
return lines.join("\n");
}
}
export const visualCompiler = new VisualCompiler();

View File

@ -0,0 +1,128 @@
// server/modules/skill-fabric/src/core/executor/Sandbox.ts
// Safe code execution using Node.js built-in vm module (NOT vm2)
import vm from "vm";
import type { SandboxOptions, SandboxResult } from "../../types";
const DEFAULT_TIMEOUT_MS = 5000;
export class Sandbox {
run(opts: SandboxOptions): SandboxResult {
const logs: string[] = [];
const timeout = opts.timeout ?? DEFAULT_TIMEOUT_MS;
// Build a safe context — only expose what we explicitly allow
const context: Record<string, unknown> = {
// Safe console (captures logs instead of printing them)
console: {
log: (...args: unknown[]) => logs.push(args.map(String).join(" ")),
warn: (...args: unknown[]) => logs.push("[warn] " + args.map(String).join(" ")),
error: (...args: unknown[]) => logs.push("[error] " + args.map(String).join(" ")),
},
// Safe Math, Date, JSON
Math,
Date,
JSON,
// Utility helpers
String,
Number,
Boolean,
Array,
Object,
parseInt,
parseFloat,
isNaN,
isFinite,
encodeURIComponent,
decodeURIComponent,
// User-provided context variables
...(opts.context ?? {}),
};
vm.createContext(context);
try {
// Wrap code in an async IIFE so top-level await is possible
const wrappedCode = `
(async function __skill__() {
${opts.code}
})()
`;
const script = new vm.Script(wrappedCode, { filename: "skill.js" });
const result = script.runInContext(context, { timeout }) as Promise<unknown> | unknown;
// Handle both sync and async execution
if (result instanceof Promise) {
// For async, we use a synchronous trick via vm timers
// Since vm doesn't natively support Promise.resolve waiting without event loop,
// we return the promise wrapped result as "pending" — caller should use runAsync
return { ok: true, value: "[async — use runAsync]", logs };
}
return { ok: true, value: result, logs };
} catch (err: unknown) {
const error = err instanceof Error ? err.message : String(err);
logs.push("[error] " + error);
return { ok: false, error, logs };
}
}
/** Async-safe version that properly awaits the result */
async runAsync(opts: SandboxOptions): Promise<SandboxResult> {
const logs: string[] = [];
const timeout = opts.timeout ?? DEFAULT_TIMEOUT_MS;
const context: Record<string, unknown> = {
console: {
log: (...args: unknown[]) => logs.push(args.map(String).join(" ")),
warn: (...args: unknown[]) => logs.push("[warn] " + args.map(String).join(" ")),
error: (...args: unknown[]) => logs.push("[error] " + args.map(String).join(" ")),
},
Math,
Date,
JSON,
String,
Number,
Boolean,
Array,
Object,
parseInt,
parseFloat,
isNaN,
isFinite,
encodeURIComponent,
decodeURIComponent,
Promise,
...(opts.context ?? {}),
};
vm.createContext(context);
const wrappedCode = `
(async function __skill__() {
${opts.code}
})()
`;
try {
const script = new vm.Script(wrappedCode, { filename: "skill.js" });
// Run the script (returns a Promise from the IIFE)
const promise = script.runInContext(context) as Promise<unknown>;
// Race against timeout
const timeoutPromise = new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error(`Timeout: execução excedeu ${timeout}ms`)), timeout)
);
const value = await Promise.race([promise, timeoutPromise]);
return { ok: true, value, logs };
} catch (err: unknown) {
const error = err instanceof Error ? err.message : String(err);
logs.push("[error] " + error);
return { ok: false, error, logs };
}
}
}
export const sandbox = new Sandbox();

View File

@ -0,0 +1,109 @@
// server/modules/skill-fabric/src/core/executor/SkillExecutor.ts
// Executes skills — dispatches to Sandbox (code) or template renderer (markdown)
import { db } from "../../../../../../db";
import { skills, skillExecutions } from "@shared/schema";
import { eq, and } from "drizzle-orm";
import crypto from "crypto";
import { sandbox } from "./Sandbox";
import type { FabricExecuteOptions, FabricExecuteResult } from "../../types";
export class SkillExecutor {
async execute(opts: FabricExecuteOptions): Promise<FabricExecuteResult> {
const start = Date.now();
// 1. Load skill
const conditions = [eq(skills.id, opts.skillId)];
if (opts.tenantId) conditions.push(eq(skills.tenantId, opts.tenantId));
const [skill] = await db.select().from(skills).where(and(...conditions)).limit(1);
if (!skill) {
return { ok: false, error: `Skill não encontrada: ${opts.skillId}`, durationMs: 0, executionId: "" };
}
if (skill.status !== "active") {
return { ok: false, error: `Skill está ${skill.status} — somente skills ativas podem ser executadas`, durationMs: 0, executionId: "" };
}
// 2. Create execution record
const [execution] = await db.insert(skillExecutions).values({
skillId: skill.id,
tenantId: opts.tenantId ?? null,
triggeredBy: "manual",
triggeredByUserId: opts.userId ?? null,
parameters: opts.parameters ?? {},
status: "running",
}).returning();
const logs: string[] = [];
try {
let result: unknown;
const body = skill.body ?? "";
const isCodeSkill = body.trimStart().startsWith("//code");
if (isCodeSkill) {
// Strip the //code marker before executing
const code = body.replace(/^\/\/code\s*\n?/, "");
const sandboxResult = await sandbox.runAsync({
code,
context: {
parameters: opts.parameters ?? {},
skillId: skill.id,
skillSlug: skill.slug,
},
timeout: opts.timeout ?? 5000,
});
logs.push(...sandboxResult.logs);
if (!sandboxResult.ok) {
throw new Error(sandboxResult.error ?? "Erro de execução no sandbox");
}
result = sandboxResult.value;
} else {
// Markdown skill — interpolate template variables
result = this.renderMarkdown(body, opts.parameters ?? {});
}
const durationMs = Date.now() - start;
const auditHash = crypto.createHash("sha256").update(JSON.stringify(result)).digest("hex");
await db.update(skillExecutions)
.set({ status: "success", result: result as Record<string, unknown>, durationMs, completedAt: new Date(), auditHash })
.where(eq(skillExecutions.id, execution.id));
return { ok: true, data: result, durationMs, executionId: execution.id, logs };
} catch (err: unknown) {
const error = err instanceof Error ? err.message : String(err);
const durationMs = Date.now() - start;
await db.update(skillExecutions)
.set({ status: "failed", error, durationMs, completedAt: new Date() })
.where(eq(skillExecutions.id, execution.id));
return { ok: false, error, durationMs, executionId: execution.id, logs };
}
}
private renderMarkdown(body: string, parameters: Record<string, unknown>): unknown {
let output = body;
// Substitute {{parameters.x}}
output = output.replace(/\{\{parameters\.([^}]+)\}\}/g, (_, k: string) => {
const val = (parameters as Record<string, unknown>)[k];
return val !== undefined ? String(val) : "";
});
// Substitute {{varName}} — simple top-level vars from parameters
output = output.replace(/\{\{(\w+)\}\}/g, (_, k: string) => {
const val = (parameters as Record<string, unknown>)[k];
return val !== undefined ? String(val) : `{{${k}}}`;
});
return { rendered: output, renderedAt: new Date().toISOString() };
}
}
export const skillExecutor = new SkillExecutor();

Some files were not shown because too many files have changed in this diff Show More