Compare commits

..

No commits in common. "main" and "PLUS_SUPERSET_NEXT" have entirely different histories.

5139 changed files with 1413 additions and 675647 deletions

View File

@ -1,24 +0,0 @@
# aider chat started at 2026-03-23 12:27:25
> You can skip this check with --no-gitignore
> Add .aider* to .gitignore (recommended)? (Y)es/(N)o [Yes]: y
> Added .aider* to .gitignore
> /home/ubuntu/.local/bin/aider --message liste os arquivos principais do projeto em uma linha cada
> Warning: ollama/deepseek-r1:14b expects these environment variables
> - OLLAMA_API_BASE: Not set
> Warning for ollama/deepseek-r1:14b: Unknown context window size and costs, using sane defaults.
> You can skip this check with --no-show-model-warnings
> https://aider.chat/docs/llms/warnings.html
> Open documentation url for more info? (Y)es/(N)o/(D)on't ask again [Yes]: y
> Aider v0.86.2
> Model: ollama/deepseek-r1:14b with diff edit format
> Git repo: .git with 5,113 files
> Warning: For large repos, consider using --subtree-only and .aiderignore
> See: https://aider.chat/docs/faq.html#can-i-use-aider-in-a-large-mono-repo
> Repo-map: using 1024 tokens, auto refresh
#### liste os arquivos principais do projeto em uma linha cada
> Initial repo scan can be slow in larger repos, but only happens once.
> litellm.APIConnectionError: OllamaException - [Errno 111] Connection refused
> Retrying in 0.2 seconds...

View File

@ -1,6 +0,0 @@
# 2026-03-23 12:27:36.139575
+liste os arquivos principais do projeto em uma linha cada
# 2026-03-23 12:27:36.139669
+liste os arquivos principais do projeto em uma linha cada

View File

@ -1,25 +0,0 @@
metabase/
attached_assets/
.git/
.cache/
.upm/
.config/
.local/
.pythonlibs/
uploads/
plus/
whatsapp-sessions/
migrations/
docs/
scripts/
server/python/
python-service/
*.tar.gz
*.zip
*.sql
*.log
*.docx
plus_backup_*/
RELATORIO_TECNICO_RETAIL.md
MAPA_GERAL_RETAIL.md
PLANO_EVOLUCAO_ARCADIA.docx

View File

@ -1,47 +0,0 @@
node_modules
dist
.git
*.zip
*.log
.env
.replit
replit.nix
.cache
.config
.upm
# MetaSet - Apache Superset source code (não necessário para build do app principal)
server/bi/metaset/superset-src/
server/bi/metaset/venv/
# Python cache
__pycache__/
*.pyc
*.pyo
*.pyd
.Python
*.so
# Logs e dados
data/
logs/
*.log
# IDE
.vscode/
.idea/
*.swp
*.swo
# Testes
.pytest_cache/
.playwright-mcp/
coverage/
# Docker
.docker/
# Outros - exceto attached_assets que são necessários
*.md
!attached_assets/
docs/

17
.env
View File

@ -1,17 +0,0 @@
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
# ── 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

@ -27,11 +27,7 @@ CONTABIL_PYTHON_URL=http://localhost:8003
BI_PYTHON_URL=http://localhost:8004
AUTOMATION_PYTHON_URL=http://localhost:8005
FISCO_PYTHON_URL=http://localhost:8002
# ── Embeddings / Learning Service ──────────────────────────────────────────────
# IMPORTANTE: Usar porta 8000 (não 8001) - o serviço escuta na porta 8000
# Em Docker: http://arcadia-prod-embeddings-1:8000
# Em dev local: http://localhost:8000
PYTHON_SERVICE_URL=http://localhost:8000
PYTHON_SERVICE_URL=http://localhost:8001
# ── IA — OpenAI ───────────────────────────────────────────────────────────────
# Deixe vazio se usar apenas Ollama (soberania total)
@ -53,8 +49,13 @@ AI_INTEGRATIONS_OPENAI_API_KEY=arcadia-internal
# 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)
# Deixe vazio para operação 100% soberana (apenas Ollama + LLMFit)
OPENAI_API_KEY=
ANTHROPIC_API_KEY=
GROQ_API_KEY=
@ -67,15 +68,9 @@ PLUS_URL=http://localhost:8080
PLUS_PORT=8080
PLUS_API_TOKEN=
# ── Superset (BI avançado — Arcádia Insights) ────────────────────────────────
SUPERSET_HOST=superset
# ── Superset (BI avançado) ────────────────────────────────────────────────────
SUPERSET_SECRET_KEY=troque-por-string-aleatoria-segura
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
@ -83,20 +78,8 @@ 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
# ── Integrações externas (opcional) ──────────────────────────────────────────
# ERPNext
ERPNEXT_URL=
ERPNEXT_API_KEY=
ERPNEXT_API_SECRET=

12
.gitignore vendored
View File

@ -1,6 +1,9 @@
node_modules/
dist/
.DS_Store
.env
.env.local
.env.production
# Replit specific
.cache/
@ -61,12 +64,3 @@ attached_assets/
# Package lock (regenerated on install)
package-lock.json
.claude/
# MetaSet - Apache Superset fork (não versionar código fonte completo)
server/bi/metaset/superset-src/
server/bi/metaset/venv/
server/bi/metaset/superset
# Backups gerados durante deploy
docker-compose.yml.backup.*
server/kernel/config/services.json.backup.*

3
.gitmodules vendored
View File

@ -1,3 +0,0 @@
[submodule "server/modules/openclaw"]
path = server/modules/openclaw
url = https://github.com/miaoxworld/OpenClawInstaller.git

View File

@ -1,26 +0,0 @@
- generic [ref=e2]:
- region "Notifications (F8)":
- list
- generic [ref=e5]:
- generic [ref=e6]:
- img "Arcadia" [ref=e7]
- heading "Arcádia Suite" [level=1] [ref=e8]
- generic [ref=e9]:
- tablist [ref=e10]:
- tab "Login" [selected] [ref=e11]
- tab "Criar Conta" [ref=e12]
- tabpanel "Login" [ref=e13]:
- generic [ref=e14]:
- generic [ref=e15]:
- generic [ref=e16]: Bem-vindo de volta
- generic [ref=e17]: Entre com suas credenciais para acessar o sistema
- generic [ref=e19]:
- generic [ref=e20]:
- text: Usuário
- textbox "Usuário" [ref=e21]:
- /placeholder: Digite seu usuário
- generic [ref=e22]:
- text: Senha
- textbox "Senha" [ref=e23]:
- /placeholder: Digite sua senha
- button "Entrar" [ref=e24]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

View File

@ -1,34 +0,0 @@
- generic [ref=e2]:
- region "Notifications (F8)":
- list [ref=e26]:
- status [ref=e27]:
- generic [ref=e28]:
- generic [ref=e29]: Login failed
- generic [ref=e30]: Invalid username or password
- button [ref=e31]:
- img [ref=e32]
- generic [ref=e5]:
- generic [ref=e6]:
- img "Arcadia" [ref=e7]
- heading "Arcádia Suite" [level=1] [ref=e8]
- generic [ref=e9]:
- tablist [ref=e10]:
- tab "Login" [selected] [ref=e11]
- tab "Criar Conta" [ref=e12]
- tabpanel "Login" [ref=e13]:
- generic [ref=e14]:
- generic [ref=e15]:
- generic [ref=e16]: Bem-vindo de volta
- generic [ref=e17]: Entre com suas credenciais para acessar o sistema
- generic [ref=e19]:
- generic [ref=e20]:
- text: Usuário
- textbox "Usuário" [ref=e21]:
- /placeholder: Digite seu usuário
- text: admin
- generic [ref=e22]:
- text: Senha
- textbox "Senha" [ref=e23]:
- /placeholder: Digite sua senha
- text: admin
- button "Entrar" [ref=e24]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

View File

@ -1,28 +0,0 @@
- generic [ref=e2]:
- region "Notifications (F8)":
- list
- generic [ref=e5]:
- generic [ref=e6]:
- img "Arcadia" [ref=e7]
- heading "Arcádia Suite" [level=1] [ref=e8]
- generic [ref=e9]:
- tablist [ref=e10]:
- tab "Login" [selected] [ref=e11]
- tab "Criar Conta" [ref=e12]
- tabpanel "Login" [ref=e13]:
- generic [ref=e14]:
- generic [ref=e15]:
- generic [ref=e16]: Bem-vindo de volta
- generic [ref=e17]: Entre com suas credenciais para acessar o sistema
- generic [ref=e19]:
- generic [ref=e20]:
- text: Usuário
- textbox "Usuário" [ref=e21]:
- /placeholder: Digite seu usuário
- text: admin
- generic [ref=e22]:
- text: Senha
- textbox "Senha" [ref=e23]:
- /placeholder: Digite sua senha
- text: admin
- button "Entrar" [ref=e24]

View File

@ -1,30 +0,0 @@
- generic [ref=e2]:
- region "Notifications (F8)":
- list
- generic [ref=e5]:
- generic [ref=e6]:
- img "Arcadia" [ref=e7]
- heading "Arcádia Suite" [level=1] [ref=e8]
- generic [ref=e9]:
- tablist [ref=e10]:
- tab "Login" [selected] [ref=e11]
- tab "Criar Conta" [ref=e12]
- tabpanel "Login" [ref=e13]:
- generic [ref=e14]:
- generic [ref=e15]:
- generic [ref=e16]: Bem-vindo de volta
- generic [ref=e17]: Entre com suas credenciais para acessar o sistema
- generic [ref=e19]:
- generic [ref=e20]:
- text: Usuário
- textbox "Usuário" [ref=e21]:
- /placeholder: Digite seu usuário
- text: admin
- generic [ref=e22]:
- text: Senha
- textbox "Senha" [ref=e23]:
- /placeholder: Digite sua senha
- text: admin
- button "Entrando..." [disabled]:
- img
- text: Entrando...

View File

@ -1,28 +0,0 @@
- generic [ref=e2]:
- region "Notifications (F8)":
- list
- generic [ref=e5]:
- generic [ref=e6]:
- img "Arcadia" [ref=e7]
- heading "Arcádia Suite" [level=1] [ref=e8]
- generic [ref=e9]:
- tablist [ref=e10]:
- tab "Login" [selected] [ref=e11]
- tab "Criar Conta" [ref=e12]
- tabpanel "Login" [ref=e13]:
- generic [ref=e14]:
- generic [ref=e15]:
- generic [ref=e16]: Bem-vindo de volta
- generic [ref=e17]: Entre com suas credenciais para acessar o sistema
- generic [ref=e19]:
- generic [ref=e20]:
- text: Usuário
- textbox "Usuário" [ref=e21]:
- /placeholder: Digite seu usuário
- text: admin
- generic [ref=e22]:
- text: Senha
- textbox "Senha" [ref=e23]:
- /placeholder: Digite sua senha
- text: admin
- button "Entrar" [ref=e24]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Binary file not shown.

View File

@ -20,7 +20,7 @@ server/
client/ # 66 páginas React
shared/schema.ts # Schema do banco (7317 linhas, Drizzle ORM)
docker/
litellm-config.yaml # Roteamento de LLMs (TIER 1: Ollama local, TIER 2: externos)
litellm-config.yaml # Roteamento de LLMs (TIER 1: LLMFit, TIER 2: Ollama, TIER 3: externos)
```
## Arquitetura de IA
@ -29,8 +29,9 @@ Manus / Agents / Embeddings
│ AI_INTEGRATIONS_OPENAI_BASE_URL
LiteLLM :4000 (gateway unificado, loga tudo no banco)
├──► Ollama :11434 (TIER 1 — local, padrão)
└──► OpenAI/Anthropic/Groq (TIER 2 — opt-in, só se API key configurada)
├──► LLMFit (TIER 1 — fine-tuned, soberano) [slot pronto, comentado]
├──► Ollama :11434 (TIER 2 — local, padrão)
└──► OpenAI/Anthropic/Groq (TIER 3 — opt-in, só se API key configurada)
```
**Variáveis chave do Manus:**
@ -44,46 +45,9 @@ AI_INTEGRATIONS_OPENAI_API_KEY=${LITELLM_API_KEY}
- `PLANO_EVOLUCAO_ARCADIA.md` — roadmap e evolução
- `MAPA_SISTEMA_ARCADIA.md` — mapa do sistema
## Git e Deploy
### Remote canônico
- **Remote principal:** `gitea``http://10.0.1.7:3000/arcadia-admin/arcadiasuite` (Gitea interno)
- **Branch de produção:** `main`
- **Push de deploy:** `git push gitea Servidor:main`
> O Coolify monitora o Gitea (`https://git.onboardbi.com.br/arcadia-admin/arcadiasuite`, branch `main`).
> Push para `gitea` → Coolify detecta → build automático → rolling deploy.
> O remote `origin` (GitHub) é backup secundário — push opcional para manter sincronizado.
> O desenvolvimento local ocorre na branch `Servidor` — fazer push para `main` no Gitea ao deployar.
### Deploy em produção
**Fluxo normal (CI/CD automático):**
```bash
cd /opt/arcadia_merged
git push gitea Servidor:main
```
O Coolify faz o build e deploy automaticamente (~3-5 min).
**Deploy manual (emergência):**
```bash
cd /opt/arcadia_merged
docker compose -f docker-compose.prod.yml build app
docker compose -f docker-compose.prod.yml up -d app
```
- Reconstrói só o container `app` (~3-5 min)
- Banco, Redis, MiroFlow, Python services **não são afetados**
- Downtime: ~30 segundos durante a troca do container
### Serviços em execução (não mexer sem necessidade)
```
arcadia-prod-app-1 porta 5000 → suite.onboardbi.com.br (Traefik)
arcadia-prod-db-1 PostgreSQL (dados em volume Docker)
arcadia-prod-redis-1 Redis
arcadia-prod-miroflow-1 MiroFlow agente
arcadia-prod-litellm-1 LiteLLM gateway
arcadia-prod-superset-1 Superset BI
arcadia-prod-{contabil,bi,automation,fisco,embeddings}-1 Python services
```
## Branch de desenvolvimento
Sempre commitar em: `claude/analyze-project-0mXjP`
Push: `git push -u origin claude/analyze-project-0mXjP`
## O que está implementado
- ✅ Manus (agente autônomo, 30+ ferramentas)
@ -95,11 +59,11 @@ arcadia-prod-{contabil,bi,automation,fisco,embeddings}-1 Python services
- ✅ Docker dev + prod, LiteLLM gateway
## O que ainda falta
- ❌ LLMFit: slot pronto em `litellm-config.yaml`, só habilitar quando disponível
- ❌ Testes automatizados / CI-CD
- ❌ Monitoramento (APM, Sentry, métricas)
- ❌ Multi-tenancy completo
- ❌ Rate limiting em todos os endpoints (parcial)
- ❌ Fine-tuning de modelos locais (dados do Arcádia)
## Comandos úteis
```bash
@ -121,6 +85,7 @@ npm run build
```
SESSION_SECRET, SSO_SECRET # gerar strings seguras em prod
AI_INTEGRATIONS_OPENAI_BASE_URL # aponta para LiteLLM
LLMFIT_BASE_URL # LLMFit quando disponível
OLLAMA_BASE_URL # Ollama host ou container
OPENAI_API_KEY # opcional (soberania: deixar vazio)
```

Binary file not shown.

View File

@ -1,33 +1,33 @@
# ─── Stage 1: Build ───────────────────────────────────────────────────────────
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json* ./
RUN NODE_ENV=development npm install --legacy-peer-deps
COPY package*.json ./
RUN npm ci --ignore-scripts
COPY . .
RUN npm run build
# ─── Stage 2: Production ──────────────────────────────────────────────────────
FROM node:20-alpine AS runner
RUN apk add --no-cache p7zip
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
COPY --from=builder /app/migrations ./migrations
COPY --from=builder /app/drizzle.config.ts ./drizzle.config.ts
COPY --from=builder /app/shared ./shared
COPY --from=builder /app/tsconfig.json ./tsconfig.json
ENV NODE_ENV=production
ENV PORT=5000
# Deps de produção apenas
COPY package*.json ./
RUN npm ci --omit=dev --ignore-scripts
# Artefatos do build
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/shared ./shared
COPY --from=builder /app/migrations ./migrations
EXPOSE 5000
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD wget -qO- http://127.0.0.1:5000/ || exit 1
CMD wget -qO- http://localhost:5000/api/health || exit 1
CMD ["node", "dist/index.cjs"]

View File

@ -1,24 +0,0 @@
FROM python:3.11-slim
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
&& rm -rf /var/lib/apt/lists/*
# Instalar MiroFlow e suas dependências direto do submodule
COPY server/modules/miroflow/ ./server/modules/miroflow/
RUN pip install --no-cache-dir -e ./server/modules/miroflow/
# Copiar o service script
COPY server/python/miroflow_service.py ./server/python/miroflow_service.py
ENV MIROFLOW_PORT=8006
ENV OLLAMA_BASE_URL=http://host.docker.internal:11434
EXPOSE 8006
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8006/health')" || exit 1
CMD ["python", "server/python/miroflow_service.py"]

View File

@ -1,6 +1,6 @@
# Integração de IA — Ollama no Servidor
# Integração de IA — Ollama + LLMFit no Servidor
Guia para conectar o Arcádia Suite à IA local em produção.
Guia para conectar o Arcádia Suite às IAs locais em produção.
---
@ -13,11 +13,11 @@ Arcádia Suite (Manus, Agents, Embeddings)
LiteLLM (porta 4000) — gateway único
├──► Ollama (modelos open source locais) [TIER 1 — padrão]
└──► OpenAI/Anthropic/Groq (opt-in) [TIER 2 — externo]
├──► LLMFit (seus modelos fine-tuned) [TIER 1 — prioridade]
└──► Ollama (modelos open source locais) [TIER 2 — padrão/fallback]
```
Nenhum serviço do Arcádia chama Ollama diretamente.
Nenhum serviço do Arcádia chama Ollama ou LLMFit diretamente.
Tudo passa pelo LiteLLM — que roteia, loga e faz fallback automaticamente.
---
@ -89,6 +89,66 @@ docker exec -it $(docker ps -qf "name=ollama") ollama pull deepseek-r1:7b
---
## Configuração do LLMFit
### 1. Pré-requisito
O LLMFit deve expor uma API compatível com OpenAI (formato `/v1/chat/completions`).
Verifique se está respondendo:
```bash
curl http://IP_DO_LLMFIT:PORTA/v1/models
```
### 2. Configurar variável no Coolify
```
LLMFIT_BASE_URL=http://IP_DO_LLMFIT:PORTA
```
### 3. Ativar no LiteLLM config
Edite `docker/litellm-config.yaml` e **descomente** o bloco TIER 1:
```yaml
model_list:
# TIER 1 — LLMFit (fine-tuned, prioridade máxima)
- model_name: arcadia-finetuned
litellm_params:
model: openai/NOME_DO_SEU_MODELO # substitua pelo nome real
api_base: os.environ/LLMFIT_BASE_URL
api_key: llmfit-internal
# Modelo de embeddings fine-tuned (se disponível)
- model_name: arcadia-embed
litellm_params:
model: openai/NOME_DO_MODELO_EMBED
api_base: os.environ/LLMFIT_BASE_URL
api_key: llmfit-internal
```
### 4. Definir LLMFit como modelo padrão do Arcádia
No mesmo arquivo, atualize o `arcadia-default`:
```yaml
- model_name: arcadia-default
litellm_params:
model: openai/NOME_DO_SEU_MODELO
api_base: os.environ/LLMFIT_BASE_URL
api_key: llmfit-internal
model_info:
fallbacks: ["llama3.3"] # cai para Ollama se LLMFit falhar
```
### 5. Reiniciar o LiteLLM para aplicar
```bash
docker compose -f docker-compose.prod.yml restart litellm
```
---
## Variáveis de ambiente — resumo completo
Configure todas no Coolify antes do deploy:
@ -114,6 +174,9 @@ AI_INTEGRATIONS_OPENAI_API_KEY=${LITELLM_API_KEY}
# Ollama em container: http://ollama:11434
OLLAMA_BASE_URL=http://host-gateway:11434
# ── LLMFit (deixar vazio até estar disponível) ────────────────────────────────
LLMFIT_BASE_URL=
# ── Providers externos (deixar vazio para soberania total) ───────────────────
OPENAI_API_KEY=
ANTHROPIC_API_KEY=
@ -139,7 +202,7 @@ curl http://localhost:4000/v1/chat/completions \
### 2. Testar Manus via interface
Acesse `https://seudominio.com.br` → abra o Manus → envie uma mensagem simples.
O Manus deve responder via Ollama.
O Manus deve responder via Ollama (ou LLMFit se configurado).
### 3. Ver logs em tempo real
@ -190,6 +253,10 @@ docker exec $(docker ps -qf "name=ollama") ollama pull nomic-embed-text
→ O modelo referenciado em `litellm-config.yaml` não foi baixado no Ollama.
→ Execute `ollama pull NOME_DO_MODELO`
**LLMFit não está sendo chamado**
→ Confirme que `LLMFIT_BASE_URL` está definido e o serviço está respondendo.
→ Reinicie o LiteLLM após alterar o config: `docker compose restart litellm`
**Ollama no host não é alcançado de dentro do Docker**
→ Tente `OLLAMA_BASE_URL=http://172.17.0.1:11434` (IP padrão do docker0)
→ Ou use o IP real da interface de rede: `ip addr show` para descobrir

View File

@ -46,27 +46,15 @@
## Mapa de Portas
| Porta | Serviço | Tecnologia | Observação |
|-------|---------|-----------|------------|
| 5000 | API Principal + Frontend | Express.js + React | Exposta publicamente |
| 5001 | **Arcadia Kernel** | Node.js/Express | Dashboard interno (mesmo container) |
| 8002 | Motor Fiscal (Fisco) | FastAPI (Python) | Via Docker network |
| 8003 | Motor Contábil | FastAPI (Python) | Via Docker network |
| 8004 | Motor BI (Insights) | FastAPI (Python) | Via Docker network |
| 8005 | Motor Automação | FastAPI (Python) | Via Docker network |
| 8006 | Motor Comunicação | Node.js/Express | Via Docker network |
| 8080 | Arcádia Plus (Laravel) | PHP/Laravel | Via Docker network |
### Sobre o Kernel (Porta 5001)
O **Arcadia Kernel** é o sistema operacional nativo que roda **dentro do mesmo container** da aplicação principal (porta 5000). Ele não é acessível externamente, mas fornece:
- **Service Registry**: Catálogo central de todos os serviços
- **Discovery Automático**: Detecta serviços via Coolify API, Docker, DNS
- **Health Monitoring**: Verifica saúde dos serviços periodicamente
- **Casa de Máquinas**: Acessa o Registry via memória (não HTTP)
A Casa de Máquinas (na porta 5000) consulta o Kernel via **acesso direto à memória** (`global.arcadiaKernelRegistry`), não via HTTP.
| 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 |
---

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

View File

@ -1,5 +1,5 @@
import { Switch, Route, useLocation } from "wouter";
import { lazy, Suspense, useEffect, useState } from "react";
import { Switch, Route } from "wouter";
import { lazy, Suspense } from "react";
import { queryClient } from "./lib/queryClient";
import { QueryClientProvider } from "@tanstack/react-query";
import { Toaster } from "@/components/ui/toaster";
@ -9,8 +9,6 @@ import { SoeMotorProvider } from "@/contexts/SoeMotorContext";
import { ProtectedRoute } from "@/lib/protected-route";
import { CommandPalette } from "@/components/CommandPalette";
import { KnowledgeCollectorInit } from "@/components/KnowledgeCollectorInit";
import { OpenClawWidget } from "@/components/openclaw/OpenClawWidget";
import { ErrorBoundary } from "@/components/ErrorBoundary";
import NotFound from "@/pages/not-found";
import AuthPage from "@/pages/auth-page";
@ -69,60 +67,14 @@ const XosAutomations = lazy(() => import("@/pages/XosAutomations"));
const XosSites = lazy(() => import("@/pages/XosSites"));
const XosGovernance = lazy(() => import("@/pages/XosGovernance"));
const XosPipeline = lazy(() => import("@/pages/XosPipeline"));
const XosSupervisor = lazy(() => import("@/pages/XosSupervisor"));
const XosReports = lazy(() => import("@/pages/XosReports"));
const XosProtocols = lazy(() => import("@/pages/XosProtocols"));
function LoadingFallback() {
const [showSlowLoading, setShowSlowLoading] = useState(false);
const [showError, setShowError] = useState(false);
useEffect(() => {
// Show "slow loading" message after 3 seconds
const slowTimer = setTimeout(() => setShowSlowLoading(true), 3000);
// Show error hint after 10 seconds
const errorTimer = setTimeout(() => setShowError(true), 10000);
return () => {
clearTimeout(slowTimer);
clearTimeout(errorTimer);
};
}, []);
return (
<div style={{ display: "flex", justifyContent: "center", alignItems: "center", height: "100vh", flexDirection: "column" }}>
<div style={{ display: "flex", justifyContent: "center", alignItems: "center", height: "100vh" }}>
<div style={{ textAlign: "center" }}>
<div style={{ width: 40, height: 40, border: "3px solid #e5e7eb", borderTopColor: "#3b82f6", borderRadius: "50%", animation: "spin 1s linear infinite", margin: "0 auto" }} />
<p style={{ marginTop: 16, color: "#6b7280" }}>Carregando...</p>
{showSlowLoading && (
<p style={{ marginTop: 8, color: "#9ca3af", fontSize: 14 }}>
Isso está demorando mais que o esperado...
</p>
)}
{showError && (
<div style={{ marginTop: 16, padding: 12, backgroundColor: "#fef3c7", borderRadius: 6, maxWidth: 300 }}>
<p style={{ color: "#92400e", fontSize: 13, margin: 0 }}>
A página pode ter um erro. Tente recarregar ou volte para a página anterior.
</p>
<button
onClick={() => window.location.reload()}
style={{
marginTop: 8,
padding: "6px 12px",
backgroundColor: "#f59e0b",
color: "white",
border: "none",
borderRadius: 4,
cursor: "pointer",
fontSize: 13
}}
>
Recarregar Página
</button>
</div>
)}
</div>
<style>{`@keyframes spin { to { transform: rotate(360deg) } }`}</style>
</div>
@ -131,74 +83,69 @@ function LoadingFallback() {
function Router() {
return (
<ErrorBoundary>
<Suspense fallback={<LoadingFallback />}>
<Switch>
<ProtectedRoute path="/" component={Cockpit} />
<ProtectedRoute path="/agent" component={Agent} />
<ProtectedRoute path="/admin" component={Admin} />
<ProtectedRoute path="/chat" component={Chat} />
<ProtectedRoute path="/whatsapp" component={WhatsApp} />
<ProtectedRoute path="/comunicacao" component={XosInbox} />
<ProtectedRoute path="/automations" component={Automations} />
<ProtectedRoute path="/insights" component={BiWorkspace} />
<ProtectedRoute path="/compass" component={ProcessCompass} />
<ProtectedRoute path="/crm" component={Crm} />
<ProtectedRoute path="/production" component={Production} />
<ProtectedRoute path="/support" component={Support} />
<ProtectedRoute path="/valuation" component={Valuation} />
<ProtectedRoute path="/canvas" component={Canvas} />
<ProtectedRoute path="/ide" component={IDE} />
<ProtectedRoute path="/scientist" component={Scientist} />
<ProtectedRoute path="/knowledge" component={Knowledge} />
<ProtectedRoute path="/central-apis" component={CentralApis} />
<ProtectedRoute path="/api-tester" component={ApiTesterPage} />
<ProtectedRoute path="/api-hub" component={ApiHub} />
<ProtectedRoute path="/fisco" component={Fisco} />
<ProtectedRoute path="/people" component={People} />
<ProtectedRoute path="/contabil" component={Contabil} />
<ProtectedRoute path="/soe" component={SOE} />
<ProtectedRoute path="/erp" component={SOE} />
<ProtectedRoute path="/financeiro" component={Financeiro} />
<ProtectedRoute path="/communities" component={Communities} />
<ProtectedRoute path="/quality" component={QualityModule} />
<ProtectedRoute path="/commercial-env" component={CommercialEnv} />
<ProtectedRoute path="/field-ops" component={FieldOperations} />
<ProtectedRoute path="/technical" component={TechnicalModule} />
<ProtectedRoute path="/suppliers" component={SuppliersPortal} />
<ProtectedRoute path="/nps" component={NPSSurvey} />
<ProtectedRoute path="/engineering" component={EngineeringHub} />
<ProtectedRoute path="/development" component={DevelopmentModule} />
<ProtectedRoute path="/retail" component={ArcadiaRetail} />
<ProtectedRoute path="/plus" component={() => { const [, nav] = useLocation(); useEffect(() => nav("/soe"), []); return null; }} />
<ProtectedRoute path="/super-admin" component={SuperAdmin} />
<ProtectedRoute path="/marketplace" component={Marketplace} />
<ProtectedRoute path="/lms" component={LMS} />
<ProtectedRoute path="/apps" component={AppCenter} />
<ProtectedRoute path="/xos" component={XosCentral} />
<ProtectedRoute path="/xos/crm" component={XosCrm} />
<ProtectedRoute path="/xos/inbox" component={XosInbox} />
<ProtectedRoute path="/xos/tickets" component={XosTickets} />
<ProtectedRoute path="/xos/campaigns" component={XosCampaigns} />
<ProtectedRoute path="/xos/automations" component={XosAutomations} />
<ProtectedRoute path="/xos/sites" component={XosSites} />
<ProtectedRoute path="/xos/governance" component={XosGovernance} />
<ProtectedRoute path="/xos/pipeline" component={XosPipeline} />
<ProtectedRoute path="/xos/supervisor" component={XosSupervisor} />
<ProtectedRoute path="/xos/reports" component={XosReports} />
<ProtectedRoute path="/xos/protocols" component={XosProtocols} />
<Suspense fallback={<LoadingFallback />}>
<Switch>
<ProtectedRoute path="/" component={Cockpit} />
<ProtectedRoute path="/agent" component={Agent} />
<ProtectedRoute path="/admin" component={Admin} />
<ProtectedRoute path="/chat" component={Chat} />
<ProtectedRoute path="/whatsapp" component={WhatsApp} />
<ProtectedRoute path="/comunicacao" component={XosInbox} />
<ProtectedRoute path="/automations" component={Automations} />
<ProtectedRoute path="/insights" component={BiWorkspace} />
<ProtectedRoute path="/compass" component={ProcessCompass} />
<ProtectedRoute path="/crm" component={Crm} />
<ProtectedRoute path="/production" component={Production} />
<ProtectedRoute path="/support" component={Support} />
<ProtectedRoute path="/valuation" component={Valuation} />
<ProtectedRoute path="/canvas" component={Canvas} />
<ProtectedRoute path="/ide" component={IDE} />
<ProtectedRoute path="/scientist" component={Scientist} />
<ProtectedRoute path="/knowledge" component={Knowledge} />
<ProtectedRoute path="/central-apis" component={CentralApis} />
<ProtectedRoute path="/api-tester" component={ApiTesterPage} />
<ProtectedRoute path="/api-hub" component={ApiHub} />
<ProtectedRoute path="/fisco" component={Fisco} />
<ProtectedRoute path="/people" component={People} />
<ProtectedRoute path="/contabil" component={Contabil} />
<ProtectedRoute path="/soe" component={SOE} />
<ProtectedRoute path="/erp" component={SOE} />
<ProtectedRoute path="/financeiro" component={Financeiro} />
<ProtectedRoute path="/communities" component={Communities} />
<ProtectedRoute path="/quality" component={QualityModule} />
<ProtectedRoute path="/commercial-env" component={CommercialEnv} />
<ProtectedRoute path="/field-ops" component={FieldOperations} />
<ProtectedRoute path="/technical" component={TechnicalModule} />
<ProtectedRoute path="/suppliers" component={SuppliersPortal} />
<ProtectedRoute path="/nps" component={NPSSurvey} />
<ProtectedRoute path="/engineering" component={EngineeringHub} />
<ProtectedRoute path="/development" component={DevelopmentModule} />
<ProtectedRoute path="/retail" component={ArcadiaRetail} />
<ProtectedRoute path="/plus" component={Plus} />
<ProtectedRoute path="/super-admin" component={SuperAdmin} />
<ProtectedRoute path="/marketplace" component={Marketplace} />
<ProtectedRoute path="/lms" component={LMS} />
<ProtectedRoute path="/apps" component={AppCenter} />
<ProtectedRoute path="/xos" component={XosCentral} />
<ProtectedRoute path="/xos/crm" component={XosCrm} />
<ProtectedRoute path="/xos/inbox" component={XosInbox} />
<ProtectedRoute path="/xos/tickets" component={XosTickets} />
<ProtectedRoute path="/xos/campaigns" component={XosCampaigns} />
<ProtectedRoute path="/xos/automations" component={XosAutomations} />
<ProtectedRoute path="/xos/sites" component={XosSites} />
<ProtectedRoute path="/xos/governance" component={XosGovernance} />
<ProtectedRoute path="/xos/pipeline" component={XosPipeline} />
<ProtectedRoute path="/doctype-builder" component={DocTypeBuilder} />
<ProtectedRoute path="/page-builder" component={PageBuilder} />
<ProtectedRoute path="/migration" component={Migration} />
<ProtectedRoute path="/dev-center" component={DevCenter} />
<ProtectedRoute path="/page/:id" component={WorkspacePage} />
<ProtectedRoute path="/app/:id" component={AppViewer} />
<Route path="/auth" component={AuthPage} />
<Route component={NotFound} />
</Switch>
</Suspense>
</ErrorBoundary>
<ProtectedRoute path="/doctype-builder" component={DocTypeBuilder} />
<ProtectedRoute path="/page-builder" component={PageBuilder} />
<ProtectedRoute path="/migration" component={Migration} />
<ProtectedRoute path="/dev-center" component={DevCenter} />
<ProtectedRoute path="/page/:id" component={WorkspacePage} />
<ProtectedRoute path="/app/:id" component={AppViewer} />
<Route path="/auth" component={AuthPage} />
<Route component={NotFound} />
</Switch>
</Suspense>
);
}
@ -211,7 +158,6 @@ function App() {
<KnowledgeCollectorInit />
<Toaster />
<CommandPalette />
<OpenClawWidget />
<Router />
</TooltipProvider>
</SoeMotorProvider>

View File

@ -91,7 +91,7 @@ function CompactNavigationBar() {
</div>
<span className="hidden md:inline">Insights</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"
onClick={() => navigateTo("/compass", "Compass")}
data-testid="bookmark-compass"

View File

@ -1,146 +0,0 @@
import { lazy, Suspense } from "react";
interface ChartData {
type: "bar" | "line" | "pie" | "area";
title: string;
data: Record<string, any>[];
xKey: string;
yKeys: string[];
colors: string[];
}
// Lazy load do recharts para não pesar o bundle inicial
const RechartsComponents = lazy(() =>
import("recharts").then((module) => ({
default: function RechartsWrapper({ chart }: { chart: ChartData }) {
const {
BarChart,
Bar,
LineChart,
Line,
PieChart,
Pie,
AreaChart,
Area,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
Cell,
} = module;
const renderChart = () => {
switch (chart.type) {
case "bar":
return (
<BarChart data={chart.data}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey={chart.xKey} />
<YAxis />
<Tooltip />
<Legend />
{chart.yKeys.map((key, index) => (
<Bar
key={key}
dataKey={key}
fill={chart.colors[index % chart.colors.length]}
/>
))}
</BarChart>
);
case "line":
return (
<LineChart data={chart.data}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey={chart.xKey} />
<YAxis />
<Tooltip />
<Legend />
{chart.yKeys.map((key, index) => (
<Line
key={key}
type="monotone"
dataKey={key}
stroke={chart.colors[index % chart.colors.length]}
/>
))}
</LineChart>
);
case "area":
return (
<AreaChart data={chart.data}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey={chart.xKey} />
<YAxis />
<Tooltip />
<Legend />
{chart.yKeys.map((key, index) => (
<Area
key={key}
type="monotone"
dataKey={key}
stroke={chart.colors[index % chart.colors.length]}
fill={chart.colors[index % chart.colors.length]}
fillOpacity={0.6}
/>
))}
</AreaChart>
);
case "pie":
return (
<PieChart>
<Tooltip />
<Legend />
<Pie
data={chart.data}
dataKey={chart.yKeys[0]}
nameKey={chart.xKey}
cx="50%"
cy="50%"
outerRadius={80}
label
>
{chart.data.map((_, index) => (
<Cell
key={`cell-${index}`}
fill={chart.colors[index % chart.colors.length]}
/>
))}
</Pie>
</PieChart>
);
default:
return null;
}
};
return (
<div className="w-full h-64">
<ResponsiveContainer width="100%" height="100%">
{renderChart()}
</ResponsiveContainer>
</div>
);
},
}))
);
interface ChartRendererProps {
chart: ChartData;
}
export function ChartRenderer({ chart }: ChartRendererProps) {
return (
<Suspense
fallback={
<div className="w-full h-64 flex items-center justify-center bg-muted/20 rounded">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
</div>
}
>
<RechartsComponents chart={chart} />
</Suspense>
);
}

View File

@ -1,90 +0,0 @@
import { Component, ReactNode } from "react";
import { AlertCircle, RefreshCw } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error?: Error;
}
export class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error("[ErrorBoundary] Erro capturado:", error);
console.error("[ErrorBoundary] Info:", errorInfo);
}
handleRetry = () => {
this.setState({ hasError: false, error: undefined });
window.location.reload();
};
handleGoHome = () => {
this.setState({ hasError: false, error: undefined });
window.location.href = "/";
};
render() {
if (this.state.hasError) {
// Fallback customizado ou padrão
if (this.props.fallback) {
return this.props.fallback;
}
return (
<div className="min-h-screen flex items-center justify-center bg-slate-50 p-4">
<Card className="max-w-lg w-full">
<CardHeader className="text-center">
<div className="mx-auto w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mb-4">
<AlertCircle className="w-8 h-8 text-red-600" />
</div>
<CardTitle className="text-xl">Ops! Algo deu errado</CardTitle>
<CardDescription>
Não foi possível carregar esta página. O módulo pode estar incompleto ou com erro.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{this.state.error && (
<div className="bg-slate-100 p-3 rounded text-sm font-mono text-slate-600 overflow-auto max-h-32">
{this.state.error.message}
</div>
)}
<div className="flex gap-3 justify-center">
<Button
variant="outline"
onClick={this.handleGoHome}
>
Voltar ao Início
</Button>
<Button
onClick={this.handleRetry}
className="gap-2"
>
<RefreshCw className="w-4 h-4" />
Tentar Novamente
</Button>
</div>
</CardContent>
</Card>
</div>
);
}
return this.props.children;
}
}

View File

@ -1,277 +0,0 @@
import { useState } from "react";
import { useMutation } from "@tanstack/react-query";
import { apiRequest } from "@/lib/queryClient";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Textarea } from "@/components/ui/textarea";
import { Badge } from "@/components/ui/badge";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Brain, Loader2 } from "lucide-react";
interface AnalyzeRequest {
agent: "statistician" | "fiscal_auditor" | "researcher";
task: string;
context?: Record<string, unknown>;
}
interface AnalyzeResponse {
agent: string;
model: string;
result: string;
execution_id: string;
duration_ms: number;
}
const AGENTS = [
{
value: "statistician",
label: "Statistician",
model: "llama3.1:8b",
placeholder:
"Ex: Analise a distribuição de vendas por região no último trimestre",
},
{
value: "fiscal_auditor",
label: "Fiscal Auditor",
model: "llama3.1:8b",
placeholder:
"Ex: Verifique inconsistências nos registros NFe do CNPJ 12.345.678/0001-90",
},
{
value: "researcher",
label: "Researcher",
model: "llama3.1:8b",
placeholder:
"Ex: Quais fornecedores têm maior correlação com atrasos de entrega?",
},
] as const;
interface MiroFlowControlProps {
currentDashboardData?: Record<string, unknown>;
}
export function MiroFlowControl({
currentDashboardData,
}: MiroFlowControlProps) {
const [enabled, setEnabled] = useState(false);
const [selectedAgent, setSelectedAgent] = useState<
"statistician" | "fiscal_auditor" | "researcher"
>("statistician");
const [task, setTask] = useState("");
const mutation = useMutation({
mutationFn: async (request: AnalyzeRequest) => {
const response = await apiRequest("POST", "/api/miroflow/analyze", request);
return response.json() as Promise<AnalyzeResponse>;
},
});
const createDashboardMutation = useMutation({
mutationFn: async (dashboardData: {
dashboardTitle: string;
sqlQuery: string;
}) => {
const response = await apiRequest(
"POST",
"/api/superset/miroflow/create-dashboard",
{
...dashboardData,
analysisId: mutation.data?.execution_id,
agent: selectedAgent,
task,
insights: mutation.data?.result,
}
);
return response.json();
},
});
const handleAnalyze = () => {
if (!task.trim()) {
return;
}
mutation.mutate({
agent: selectedAgent,
task,
context: currentDashboardData,
});
};
const currentAgentConfig = AGENTS.find((a) => a.value === selectedAgent);
return (
<div className="space-y-6">
<Card className="bg-[#1f334d] border border-[#c89b3c]/20">
<CardHeader className="border-b border-[#c89b3c]/20 pb-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Brain className="w-6 h-6 text-[#c89b3c]" />
<CardTitle className="text-white">Modo Científico</CardTitle>
</div>
<button
onClick={() => setEnabled(!enabled)}
className={`relative inline-flex h-8 w-14 items-center rounded-full transition-colors ${
enabled ? "bg-[#c89b3c]" : "bg-gray-600"
}`}
>
<span
className={`inline-block h-6 w-6 transform rounded-full bg-white transition-transform ${
enabled ? "translate-x-7" : "translate-x-1"
}`}
/>
</button>
</div>
</CardHeader>
{enabled && (
<CardContent className="space-y-4 pt-6">
{/* Agent Selection */}
<div>
<label className="block text-sm font-medium text-white mb-2">
Selecione o Agente
</label>
<Select value={selectedAgent} onValueChange={(value: any) => setSelectedAgent(value)}>
<SelectTrigger className="bg-[#2a4466] border border-[#c89b3c]/20 text-white">
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-[#2a4466] border border-[#c89b3c]/20">
{AGENTS.map((agent) => (
<SelectItem key={agent.value} value={agent.value} className="text-white">
<div>
<div>{agent.label}</div>
<div className="text-xs text-gray-400">{agent.model}</div>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Task Input */}
<div>
<label className="block text-sm font-medium text-white mb-2">
Digite a Tarefa
</label>
<Textarea
placeholder={currentAgentConfig?.placeholder || "Digite sua tarefa..."}
value={task}
onChange={(e) => setTask(e.target.value)}
disabled={mutation.isPending}
className="bg-[#2a4466] border border-[#c89b3c]/20 text-white placeholder:text-gray-500 min-h-24"
/>
</div>
{/* Analyze Button */}
<Button
onClick={handleAnalyze}
disabled={!task.trim() || mutation.isPending}
className="w-full bg-[#c89b3c] hover:bg-[#d4a94a] text-[#1f334d] font-semibold"
>
{mutation.isPending ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Analisando...
</>
) : (
"Analisar"
)}
</Button>
{/* Error Display */}
{mutation.isError && (
<div className="rounded-lg p-4 bg-red-900/20 border border-red-500/50">
<Badge className="mb-2 bg-red-600 text-white">Erro</Badge>
<p className="text-sm text-red-100">
{mutation.error instanceof Error
? mutation.error.message
: "Erro ao processar a análise"}
</p>
</div>
)}
{/* Result Display */}
{mutation.isSuccess && (
<div className="space-y-3">
<div className="flex items-center gap-2 mb-2">
<Badge className="bg-green-600 text-white">Sucesso</Badge>
</div>
<div className="rounded-lg p-4 bg-[#2a4466] border border-[#c89b3c]/20">
<ScrollArea className="h-64 w-full pr-4">
<p className="text-white text-sm whitespace-pre-wrap">
{mutation.data?.result}
</p>
</ScrollArea>
</div>
{/* Metadata */}
<div className="grid grid-cols-3 gap-2 text-xs text-gray-400">
<div>
<span className="font-semibold text-[#c89b3c]">Modelo:</span>{" "}
{mutation.data?.model}
</div>
<div>
<span className="font-semibold text-[#c89b3c]">Tempo:</span>{" "}
{mutation.data?.duration_ms}ms
</div>
<div>
<span className="font-semibold text-[#c89b3c]">ID:</span>{" "}
{mutation.data?.execution_id?.slice(0, 8)}...
</div>
</div>
{/* Create Dashboard Button */}
<div className="pt-4 border-t border-[#c89b3c]/20">
<Button
onClick={() => {
const title = `Análise ${selectedAgent} - ${new Date().toLocaleDateString('pt-BR')}`;
createDashboardMutation.mutate({
dashboardTitle: title,
sqlQuery: `SELECT * FROM arcadia LIMIT 100`, // Placeholder - idealmente vem do MiroFlow
});
}}
disabled={createDashboardMutation.isPending}
className="w-full bg-[#8b7355] hover:bg-[#9d8568] text-white font-semibold"
>
{createDashboardMutation.isPending ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Criando Dashboard...
</>
) : (
"📊 Criar Dashboard no Superset"
)}
</Button>
{createDashboardMutation.isSuccess && (
<div className="mt-2 rounded-lg p-3 bg-green-900/20 border border-green-500/50">
<p className="text-xs text-green-100">
Dashboard criado! Atualizando...
</p>
</div>
)}
{createDashboardMutation.isError && (
<div className="mt-2 rounded-lg p-3 bg-red-900/20 border border-red-500/50">
<p className="text-xs text-red-100">
Erro: {createDashboardMutation.error instanceof Error ? createDashboardMutation.error.message : "Erro ao criar dashboard"}
</p>
</div>
)}
</div>
</div>
)}
</CardContent>
)}
</Card>
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@ -1,245 +0,0 @@
/**
* OpenClawPanel Phase 4
*
* Painel de visualização de Skills Emergentes detectadas pelo PatternDetector.
* Exibido como aba na sidebar do /development.
*/
import { useState } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import {
Sparkles, CheckCircle, XCircle, ChevronDown, ChevronRight,
Loader2, Activity, Clock, BarChart2
} from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import { useToast } from "@/hooks/use-toast";
interface Suggestion {
id: string;
suggestedSkillName: string;
suggestedDescription: string | null;
estimatedAutomation: string | null;
confidence: string;
draftBody: string | null;
createdAt: string;
}
interface Pattern {
id: string;
actionType: string;
description: string | null;
frequency: number;
confidence: string;
firstSeenAt: string;
lastSeenAt: string;
status: string;
}
export default function OpenClawPanel() {
const [tab, setTab] = useState<"suggestions" | "patterns">("suggestions");
const [expanded, setExpanded] = useState<string | null>(null);
const { toast } = useToast();
const qc = useQueryClient();
const { data: sugData, isLoading: sugLoading } = useQuery<{ suggestions: Suggestion[]; total: number }>({
queryKey: ["/api/openclaw/suggestions"],
refetchInterval: 2 * 60 * 1000,
});
const { data: patData, isLoading: patLoading } = useQuery<{ patterns: Pattern[]; total: number }>({
queryKey: ["/api/openclaw/patterns"],
enabled: tab === "patterns",
});
const accept = useMutation({
mutationFn: async (id: string) => {
const res = await fetch(`/api/openclaw/suggestions/${id}/accept`, { method: "POST" });
if (!res.ok) throw new Error();
return res.json();
},
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["/api/openclaw/suggestions"] });
qc.invalidateQueries({ queryKey: ["/api/skills"] });
toast({ title: "Skill publicada!" });
},
onError: () => toast({ title: "Erro ao aceitar", variant: "destructive" }),
});
const reject = useMutation({
mutationFn: async (id: string) => {
const res = await fetch(`/api/openclaw/suggestions/${id}/reject`, { method: "POST" });
if (!res.ok) throw new Error();
return res.json();
},
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["/api/openclaw/suggestions"] });
toast({ title: "Sugestão rejeitada" });
},
onError: () => toast({ title: "Erro ao rejeitar", variant: "destructive" }),
});
const suggestions = sugData?.suggestions ?? [];
const patterns = patData?.patterns ?? [];
return (
<div className="h-full flex flex-col bg-gray-900 text-white">
{/* Header */}
<div className="px-5 py-4 border-b border-white/10 flex items-center gap-2">
<Sparkles className="w-4 h-4 text-yellow-400" />
<div>
<h2 className="text-sm font-semibold">Skills Emergentes</h2>
<p className="text-[11px] text-white/40">OpenClaw detecção automática de padrões</p>
</div>
</div>
{/* Tabs */}
<div className="flex border-b border-white/10 text-xs">
<button
onClick={() => setTab("suggestions")}
className={`px-4 py-2.5 flex items-center gap-1.5 transition-colors border-b-2 ${tab === "suggestions" ? "border-yellow-400 text-yellow-400" : "border-transparent text-white/50 hover:text-white"}`}
>
<Sparkles className="w-3 h-3" />
Sugestões
{suggestions.length > 0 && (
<span className="ml-1 rounded-full bg-yellow-400 text-black text-[9px] font-bold w-4 h-4 flex items-center justify-center">
{suggestions.length}
</span>
)}
</button>
<button
onClick={() => setTab("patterns")}
className={`px-4 py-2.5 flex items-center gap-1.5 transition-colors border-b-2 ${tab === "patterns" ? "border-yellow-400 text-yellow-400" : "border-transparent text-white/50 hover:text-white"}`}
>
<Activity className="w-3 h-3" />
Padrões
</button>
</div>
<ScrollArea className="flex-1">
{/* Sugestões */}
{tab === "suggestions" && (
<div>
{sugLoading && (
<div className="flex justify-center py-10">
<Loader2 className="w-5 h-5 animate-spin text-white/30" />
</div>
)}
{!sugLoading && suggestions.length === 0 && (
<div className="px-5 py-12 text-center">
<Sparkles className="w-8 h-8 text-white/10 mx-auto mb-3" />
<p className="text-sm text-white/30">Nenhuma sugestão pendente</p>
<p className="text-xs text-white/20 mt-1">O detector roda a cada hora</p>
</div>
)}
<div className="divide-y divide-white/5">
{suggestions.map((s) => {
const conf = Math.round(Number(s.confidence) * 100);
const isExp = expanded === s.id;
const isAccepting = accept.isPending && accept.variables === s.id;
const isRejecting = reject.isPending && reject.variables === s.id;
return (
<div key={s.id} className="px-4 py-4 space-y-2.5">
<div className="flex items-start gap-2">
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-white truncate">{s.suggestedSkillName}</p>
{s.suggestedDescription && (
<p className="text-xs text-white/50 mt-0.5 line-clamp-2">{s.suggestedDescription}</p>
)}
</div>
<Badge className="shrink-0 bg-yellow-400/10 text-yellow-400 border-yellow-400/20 text-[10px]">
{conf}%
</Badge>
</div>
{s.draftBody && (
<div>
<button
onClick={() => setExpanded(isExp ? null : s.id)}
className="flex items-center gap-1 text-[11px] text-white/40 hover:text-white/60 transition-colors"
>
{isExp ? <ChevronDown className="w-3 h-3" /> : <ChevronRight className="w-3 h-3" />}
{isExp ? "Ocultar body" : "Ver body gerado"}
</button>
{isExp && (
<pre className="mt-2 rounded bg-black/40 p-3 text-[11px] text-white/60 overflow-x-auto whitespace-pre-wrap font-mono max-h-40 border border-white/5">
{s.draftBody}
</pre>
)}
</div>
)}
<div className="flex gap-2">
<Button
size="sm"
className="h-7 text-xs bg-emerald-900/40 text-emerald-400 border border-emerald-700/40 hover:bg-emerald-900/60"
onClick={() => accept.mutate(s.id)}
disabled={isAccepting || isRejecting}
>
{isAccepting ? <Loader2 className="w-3 h-3 animate-spin" /> : <CheckCircle className="w-3 h-3" />}
<span className="ml-1">Aceitar</span>
</Button>
<Button
size="sm"
variant="ghost"
className="h-7 text-xs text-red-400 hover:text-red-300 hover:bg-red-900/20"
onClick={() => reject.mutate(s.id)}
disabled={isAccepting || isRejecting}
>
{isRejecting ? <Loader2 className="w-3 h-3 animate-spin" /> : <XCircle className="w-3 h-3" />}
<span className="ml-1">Rejeitar</span>
</Button>
</div>
</div>
);
})}
</div>
</div>
)}
{/* Padrões */}
{tab === "patterns" && (
<div>
{patLoading && (
<div className="flex justify-center py-10">
<Loader2 className="w-5 h-5 animate-spin text-white/30" />
</div>
)}
{!patLoading && patterns.length === 0 && (
<div className="px-5 py-12 text-center">
<BarChart2 className="w-8 h-8 text-white/10 mx-auto mb-3" />
<p className="text-sm text-white/30">Nenhum padrão detectado</p>
<p className="text-xs text-white/20 mt-1">Execute skills manualmente para gerar dados</p>
</div>
)}
<div className="divide-y divide-white/5">
{patterns.map((p) => {
const conf = Math.round(Number(p.confidence) * 100);
const last = new Date(p.lastSeenAt).toLocaleDateString("pt-BR");
return (
<div key={p.id} className="px-4 py-3 space-y-1">
<div className="flex items-center justify-between gap-2">
<p className="text-xs font-mono text-white/70 truncate">{p.actionType}</p>
<Badge className="shrink-0 bg-white/5 text-white/50 border-white/10 text-[10px]">
{conf}%
</Badge>
</div>
{p.description && (
<p className="text-[11px] text-white/40">{p.description}</p>
)}
<div className="flex items-center gap-3 text-[10px] text-white/30">
<span className="flex items-center gap-1"><Activity className="w-3 h-3" />{p.frequency}x</span>
<span className="flex items-center gap-1"><Clock className="w-3 h-3" />{last}</span>
</div>
</div>
);
})}
</div>
</div>
)}
</ScrollArea>
</div>
);
}

View File

@ -1,353 +0,0 @@
import { useState, useEffect, useCallback } from "react";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { useToast } from "@/hooks/use-toast";
import {
RefreshCw, CheckCircle, XCircle, Loader2, Users, Package,
ShoppingCart, FileText, ExternalLink, Building2, Link2
} from "lucide-react";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
interface Empresa {
id: number;
tenantId: number;
razaoSocial: string;
nomeFantasia: string | null;
cnpj: string;
tipo: string;
plusEmpresaId: number | null;
isActive: boolean;
}
interface SyncResult {
success: boolean;
created?: number;
updated?: number;
failed?: number;
errors?: string[];
message?: string;
}
interface PlusStatus {
connected: boolean;
url?: string;
message?: string;
}
export default function PlusSyncPanel({ tenantId }: { tenantId?: number }) {
const { toast } = useToast();
const [empresas, setEmpresas] = useState<Empresa[]>([]);
const [selectedEmpresa, setSelectedEmpresa] = useState<string>("");
const [plusStatus, setPlusStatus] = useState<PlusStatus | null>(null);
const [loading, setLoading] = useState(false);
const [syncingCustomers, setSyncingCustomers] = useState(false);
const [syncingProducts, setSyncingProducts] = useState(false);
const [syncingSales, setSyncingSales] = useState(false);
const [importingCustomers, setImportingCustomers] = useState(false);
const [importingProducts, setImportingProducts] = useState(false);
const [bindPlusId, setBindPlusId] = useState("");
const [lastResults, setLastResults] = useState<Record<string, SyncResult>>({});
const currentTenant = tenantId || 1;
const loadEmpresas = useCallback(async () => {
try {
const res = await fetch(`/api/retail/plus/empresas?tenantId=${currentTenant}`);
if (res.ok) {
const data = await res.json();
setEmpresas(data);
if (data.length > 0 && !selectedEmpresa) {
setSelectedEmpresa(data[0].id.toString());
}
}
} catch (error) {
console.error("Error loading empresas:", error);
}
}, [currentTenant, selectedEmpresa]);
const checkStatus = useCallback(async () => {
setLoading(true);
try {
const empresaId = selectedEmpresa ? `?empresaId=${selectedEmpresa}` : "";
const res = await fetch(`/api/retail/plus/status${empresaId}`);
if (res.ok) {
const data = await res.json();
setPlusStatus(data);
}
} catch (error) {
setPlusStatus({ connected: false, message: "Erro ao verificar conexão" });
} finally {
setLoading(false);
}
}, [selectedEmpresa]);
useEffect(() => {
loadEmpresas();
}, [loadEmpresas]);
useEffect(() => {
if (selectedEmpresa) checkStatus();
}, [selectedEmpresa, checkStatus]);
const handleSync = async (action: string, setLoading: (v: boolean) => void) => {
setLoading(true);
try {
const body: any = { tenantId: currentTenant };
if (selectedEmpresa) body.empresaId = parseInt(selectedEmpresa);
const res = await fetch(`/api/retail/plus/${action}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const data = await res.json();
setLastResults(prev => ({ ...prev, [action]: data }));
if (data.success) {
toast({ title: "Sincronização concluída", description: `Criados: ${data.created || 0}, Atualizados: ${data.updated || 0}` });
} else {
toast({ title: "Erro na sincronização", description: data.message || "Falha na operação", variant: "destructive" });
}
} catch (error) {
toast({ title: "Erro", description: "Falha na comunicação com o servidor", variant: "destructive" });
} finally {
setLoading(false);
}
};
const handleBindEmpresa = async () => {
if (!selectedEmpresa || !bindPlusId) return;
try {
const res = await fetch(`/api/retail/plus/empresas/${selectedEmpresa}/bind`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ plusEmpresaId: parseInt(bindPlusId) }),
});
if (res.ok) {
toast({ title: "Empresa vinculada", description: "Empresa vinculada ao Plus com sucesso" });
loadEmpresas();
setBindPlusId("");
}
} catch (error) {
toast({ title: "Erro", description: "Falha ao vincular empresa", variant: "destructive" });
}
};
const currentEmpresa = empresas.find(e => e.id.toString() === selectedEmpresa);
const SyncResultBadge = ({ result }: { result?: SyncResult }) => {
if (!result) return null;
return (
<div className="flex items-center gap-2 text-xs mt-1">
{result.success ? (
<Badge variant="outline" className="text-green-600 border-green-300">
<CheckCircle className="h-3 w-3 mr-1" />
OK: {result.created || 0} criados, {result.updated || 0} atualizados
</Badge>
) : (
<Badge variant="destructive" className="text-xs">
<XCircle className="h-3 w-3 mr-1" />
{result.message || "Falha"}
</Badge>
)}
</div>
);
};
return (
<div className="space-y-6" data-testid="plus-sync-panel">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold">Integração Plus ERP</h3>
<p className="text-sm text-muted-foreground">Sincronização bidirecional com o Arcádia Plus (Laravel ERP)</p>
</div>
<div className="flex items-center gap-2">
{plusStatus && (
<Badge variant={plusStatus.connected ? "default" : "destructive"} data-testid="plus-connection-status">
{plusStatus.connected ? (
<><CheckCircle className="h-3 w-3 mr-1" /> Conectado</>
) : (
<><XCircle className="h-3 w-3 mr-1" /> Desconectado</>
)}
</Badge>
)}
<Button variant="outline" size="sm" onClick={checkStatus} disabled={loading} data-testid="btn-check-plus-status">
<RefreshCw className={`h-4 w-4 mr-1 ${loading ? "animate-spin" : ""}`} />
Testar Conexão
</Button>
</div>
</div>
<Card>
<CardHeader>
<CardTitle className="text-base flex items-center gap-2">
<Building2 className="h-4 w-4" />
Empresa Ativa
</CardTitle>
<CardDescription>Selecione a empresa para operações de sincronização</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-end gap-3">
<div className="flex-1">
<Label>Empresa</Label>
<Select value={selectedEmpresa} onValueChange={setSelectedEmpresa}>
<SelectTrigger data-testid="select-empresa-plus">
<SelectValue placeholder="Selecione uma empresa" />
</SelectTrigger>
<SelectContent>
{empresas.map(emp => (
<SelectItem key={emp.id} value={emp.id.toString()}>
{emp.nomeFantasia || emp.razaoSocial} ({emp.cnpj}) {emp.tipo === "matriz" ? "📍" : "🔗"}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{currentEmpresa && (
<div className="rounded-lg border p-3 bg-muted/50 space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">{currentEmpresa.razaoSocial}</span>
<Badge variant="outline">{currentEmpresa.tipo === "matriz" ? "Matriz" : "Filial"}</Badge>
</div>
<div className="text-xs text-muted-foreground">CNPJ: {currentEmpresa.cnpj}</div>
<div className="flex items-center gap-2">
<Link2 className="h-3 w-3" />
<span className="text-xs">Plus ID: {currentEmpresa.plusEmpresaId || "Não vinculada"}</span>
{!currentEmpresa.plusEmpresaId && (
<div className="flex items-center gap-1 ml-2">
<Input
type="number"
placeholder="ID da empresa no Plus"
value={bindPlusId}
onChange={e => setBindPlusId(e.target.value)}
className="h-7 w-36 text-xs"
data-testid="input-bind-plus-id"
/>
<Button size="sm" variant="outline" className="h-7 text-xs" onClick={handleBindEmpresa} data-testid="btn-bind-empresa">
Vincular
</Button>
</div>
)}
</div>
</div>
)}
</CardContent>
</Card>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card>
<CardHeader>
<CardTitle className="text-base flex items-center gap-2">
<Users className="h-4 w-4" />
Clientes / Fornecedores
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
className="flex-1"
disabled={syncingCustomers}
onClick={() => handleSync("sync/customers", setSyncingCustomers)}
data-testid="btn-sync-customers-to-plus"
>
{syncingCustomers ? <Loader2 className="h-4 w-4 mr-1 animate-spin" /> : <ExternalLink className="h-4 w-4 mr-1" />}
Enviar Plus
</Button>
<Button
variant="outline"
size="sm"
className="flex-1"
disabled={importingCustomers}
onClick={() => handleSync("import/customers", setImportingCustomers)}
data-testid="btn-import-customers-from-plus"
>
{importingCustomers ? <Loader2 className="h-4 w-4 mr-1 animate-spin" /> : <ExternalLink className="h-4 w-4 mr-1 rotate-180" />}
Plus Importar
</Button>
</div>
<SyncResultBadge result={lastResults["sync/customers"] || lastResults["import/customers"]} />
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base flex items-center gap-2">
<Package className="h-4 w-4" />
Produtos / Estoque
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
className="flex-1"
disabled={importingProducts}
onClick={() => handleSync("import/products", setImportingProducts)}
data-testid="btn-import-products-from-plus"
>
{importingProducts ? <Loader2 className="h-4 w-4 mr-1 animate-spin" /> : <Package className="h-4 w-4 mr-1" />}
Plus Importar Produtos
</Button>
</div>
<SyncResultBadge result={lastResults["import/products"]} />
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base flex items-center gap-2">
<ShoppingCart className="h-4 w-4" />
Vendas PDV
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<p className="text-xs text-muted-foreground">
As vendas são sincronizadas automaticamente ao finalizar no PDV.
Use o botão abaixo para reenviar vendas pendentes.
</p>
<Button
variant="outline"
size="sm"
disabled={syncingSales}
onClick={() => {
toast({ title: "Em breve", description: "Sincronização em lote será implementada" });
}}
data-testid="btn-sync-pending-sales"
>
{syncingSales ? <Loader2 className="h-4 w-4 mr-1 animate-spin" /> : <RefreshCw className="h-4 w-4 mr-1" />}
Reenviar Pendentes
</Button>
<SyncResultBadge result={lastResults["sync/sales"]} />
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base flex items-center gap-2">
<FileText className="h-4 w-4" />
Documentos Fiscais
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<p className="text-xs text-muted-foreground">
Emissão de NF-e e NFC-e via Plus ERP. Selecione a venda na aba PDV
e use a opção "Emitir NF-e" para gerar o documento fiscal.
</p>
<Badge variant="outline" className="text-xs">
<FileText className="h-3 w-3 mr-1" />
Integrado via Plus API
</Badge>
</CardContent>
</Card>
</div>
</div>
);
}

View File

@ -1,160 +0,0 @@
import { useEffect, useRef, useState, useCallback } from "react";
import { Loader2, AlertTriangle, ExternalLink, RefreshCw } from "lucide-react";
interface SupersetDashboardProps {
dashboardId: string;
height?: string | number;
className?: string;
showOpenLink?: boolean;
}
/**
* Componente reutilizável para embedar dashboards do Apache Superset (Arcádia Insights)
* em QUALQUER tela do Arcádia Suite via Guest Token JWT.
*
* Uso:
* <SupersetDashboard dashboardId="financial-overview" />
* <SupersetDashboard dashboardId="dre-mensal" height={400} />
*/
export function SupersetDashboard({
dashboardId,
height = "calc(100vh - 320px)",
className = "",
showOpenLink = true,
}: SupersetDashboardProps) {
const containerRef = useRef<HTMLDivElement>(null);
const sdkRef = useRef<any>(null);
const [status, setStatus] = useState<"loading" | "ready" | "error" | "unavailable">("loading");
const [errorMsg, setErrorMsg] = useState<string>("");
const fetchGuestToken = useCallback(async (): Promise<string> => {
const resp = await fetch("/api/superset/guest-token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ dashboardId }),
credentials: "include",
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({ error: "Erro desconhecido" }));
throw new Error(err.error || `HTTP ${resp.status}`);
}
const { token } = await resp.json();
return token;
}, [dashboardId]);
const loadDashboard = useCallback(async () => {
if (!containerRef.current) return;
setStatus("loading");
setErrorMsg("");
try {
// Verifica se Superset está disponível
const health = await fetch("/api/superset/health", { credentials: "include" });
const healthData = await health.json();
if (!healthData.online) {
setStatus("unavailable");
return;
}
// Importa SDK dinamicamente (só quando necessário)
const { embedDashboard } = await import("@superset-ui/embedded-sdk");
// Limpa embedding anterior se houver
if (sdkRef.current) {
containerRef.current.innerHTML = "";
}
sdkRef.current = await embedDashboard({
id: dashboardId,
supersetDomain: window.location.origin + "/superset",
mountPoint: containerRef.current,
fetchGuestToken,
dashboardUiConfig: {
hideTitle: true,
hideChartControls: false,
filters: { visible: true, expanded: false },
},
});
setStatus("ready");
} catch (err: any) {
console.error("[SupersetDashboard] Erro:", err.message);
// Se SDK não estiver instalado, usa iframe como fallback
if (err.message?.includes("Cannot find module") || err.message?.includes("embedDashboard")) {
setStatus("unavailable");
} else {
setErrorMsg(err.message);
setStatus("error");
}
}
}, [dashboardId, fetchGuestToken]);
useEffect(() => {
loadDashboard();
return () => {
if (containerRef.current) containerRef.current.innerHTML = "";
};
}, [loadDashboard]);
const heightStyle = typeof height === "number" ? `${height}px` : height;
if (status === "unavailable") {
return (
<div className={`rounded-xl overflow-hidden border border-[#c89b3c]/20 bg-white shadow-sm ${className}`} style={{ height: heightStyle }}>
<iframe
src="/superset/login"
className="w-full h-full border-0"
title="Arcádia Insights — Apache Superset"
allow="fullscreen"
/>
</div>
);
}
if (status === "error") {
return (
<div className={`flex flex-col items-center justify-center gap-4 rounded-xl border border-red-100 bg-red-50 ${className}`} style={{ height: heightStyle }}>
<AlertTriangle className="w-8 h-8 text-red-400" />
<div className="text-center">
<p className="font-medium text-red-700 text-sm">Erro ao carregar dashboard</p>
<p className="text-red-500 text-xs mt-1">{errorMsg}</p>
</div>
<button
onClick={loadDashboard}
className="flex items-center gap-2 px-4 py-2 bg-red-600 text-white rounded-lg text-sm hover:bg-red-700 transition-colors"
>
<RefreshCw className="w-4 h-4" /> Tentar novamente
</button>
</div>
);
}
return (
<div className={`relative ${className}`} style={{ height: heightStyle }}>
{status === "loading" && (
<div className="absolute inset-0 flex items-center justify-center bg-white/90 z-10 rounded-xl">
<div className="text-center">
<Loader2 className="w-8 h-8 text-[#c89b3c] animate-spin mx-auto mb-3" />
<p className="text-sm text-[#1f334d] font-medium">Carregando Arcádia Insights...</p>
</div>
</div>
)}
{showOpenLink && status === "ready" && (
<a
href="/superset"
target="_blank"
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"
>
<ExternalLink className="w-3 h-3" /> Abrir completo
</a>
)}
<div
ref={containerRef}
className="w-full h-full rounded-xl overflow-hidden border border-[#c89b3c]/20"
/>
</div>
);
}

View File

@ -1,131 +0,0 @@
/**
* 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

@ -1,101 +0,0 @@
/**
* 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,25 +0,0 @@
import { createContext, useContext, useState, ReactNode } from "react";
interface SoeMotorContextValue {
activeCompany: string | null;
setActiveCompany: (id: string | null) => void;
}
const SoeMotorContext = createContext<SoeMotorContextValue>({
activeCompany: null,
setActiveCompany: () => {},
});
export function SoeMotorProvider({ children }: { children: ReactNode }) {
const [activeCompany, setActiveCompany] = useState<string | null>(null);
return (
<SoeMotorContext.Provider value={{ activeCompany, setActiveCompany }}>
{children}
</SoeMotorContext.Provider>
);
}
export function useSoeMotor() {
return useContext(SoeMotorContext);
}

View File

@ -1,59 +0,0 @@
import { useQuery } from "@tanstack/react-query";
export type TenantFeatures = {
ide: boolean;
ideMode: 'none' | 'no-code' | 'low-code' | 'pro-code';
whatsapp: boolean;
whatsappSessions: number;
crm: boolean;
erp: boolean;
bi: boolean;
manus: boolean;
manusTools: string[];
centralApis: boolean;
centralApisManage: boolean;
comunidades: boolean;
maxChannels: number;
biblioteca: boolean;
bibliotecaPublish: boolean;
suporteN3: boolean;
retail: boolean;
plus: boolean;
fisco: boolean;
cockpit: boolean;
compass: boolean;
production: boolean;
support: boolean;
xosCrm: boolean;
};
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
};
export function useTenantFeatures() {
const { data, isLoading } = useQuery({
queryKey: ["tenant-modules"],
queryFn: async () => {
const res = await fetch("/api/soe/tenant/modules", { credentials: "include" });
if (!res.ok) throw new Error("Failed to fetch tenant modules");
return res.json();
},
staleTime: 60000,
retry: 1
});
const features: TenantFeatures = { ...defaultFeatures, ...(data?.features || {}) };
return {
features,
isLoading,
plan: data?.plan as string | undefined,
isEnabled: (key: keyof TenantFeatures) => !!features[key],
};
}

View File

@ -1,103 +0,0 @@
/**
* useXosSocket subscribes to XOS real-time events from Socket.IO.
* Usage:
* const { supervisorStats, lastSlaBreachEvent } = useXosSocket();
*/
import { useEffect, useRef, useState, useCallback } from "react";
import { io, Socket } from "socket.io-client";
export interface SupervisorStats {
openConversations: number;
resolvedToday: number;
urgentTickets: number;
agentsOnline: number;
timestamp: string;
}
export interface SlaBreachEvent {
protocolId: number;
protocolNumber: string;
tenantId: number;
queueId: number | null;
assignedTo: number | null;
breachedAt: string;
}
interface XosSocketState {
connected: boolean;
supervisorStats: SupervisorStats | null;
lastSlaBreachEvent: SlaBreachEvent | null;
}
let _socket: Socket | null = null;
let _refCount = 0;
function getSocket(): Socket {
if (!_socket) {
_socket = io(window.location.origin, {
path: "/socket.io",
transports: ["websocket", "polling"],
reconnectionAttempts: 10,
reconnectionDelay: 2000,
});
}
return _socket;
}
export function useXosSocket() {
const [state, setState] = useState<XosSocketState>({
connected: false,
supervisorStats: null,
lastSlaBreachEvent: null,
});
const socketRef = useRef<Socket | null>(null);
const handleStats = useCallback((data: SupervisorStats) => {
setState((s) => ({ ...s, supervisorStats: data }));
}, []);
const handleSlaBreach = useCallback((data: SlaBreachEvent) => {
setState((s) => ({ ...s, lastSlaBreachEvent: data }));
}, []);
const handleConnect = useCallback(() => {
setState((s) => ({ ...s, connected: true }));
}, []);
const handleDisconnect = useCallback(() => {
setState((s) => ({ ...s, connected: false }));
}, []);
useEffect(() => {
const socket = getSocket();
socketRef.current = socket;
_refCount++;
socket.on("connect", handleConnect);
socket.on("disconnect", handleDisconnect);
socket.on("xos:supervisor.stats", handleStats);
socket.on("xos:sla.breach", handleSlaBreach);
// Sync initial connection state
if (socket.connected) {
setState((s) => ({ ...s, connected: true }));
}
return () => {
socket.off("connect", handleConnect);
socket.off("disconnect", handleDisconnect);
socket.off("xos:supervisor.stats", handleStats);
socket.off("xos:sla.breach", handleSlaBreach);
_refCount--;
// Keep socket alive as long as any component uses it
if (_refCount <= 0 && _socket) {
_socket.disconnect();
_socket = null;
_refCount = 0;
}
};
}, [handleConnect, handleDisconnect, handleStats, handleSlaBreach]);
return state;
}

View File

@ -1,143 +0,0 @@
/**
* 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

@ -1,142 +0,0 @@
// Utilitários de exportação com lazy loading dinâmico
// Reduz o bundle inicial do Agent.tsx
export async function exportToPDF(content: string, prompt?: string, chartContainer?: HTMLElement | null) {
const [{ jsPDF }, html2canvas] = await Promise.all([
import("jspdf"),
import("html2canvas").then(m => m.default)
]);
const doc = new jsPDF();
const pageWidth = doc.internal.pageSize.getWidth();
const margin = 20;
const maxWidth = pageWidth - margin * 2;
doc.setFontSize(16);
doc.setTextColor(31, 51, 77);
doc.text("Arcádia Agent - Resultado", margin, 20);
let currentY = 30;
if (prompt) {
doc.setFontSize(10);
doc.setTextColor(90, 108, 125);
doc.text("Tarefa:", margin, currentY);
currentY += 7;
const promptLines = doc.splitTextToSize(prompt, maxWidth);
doc.text(promptLines, margin, currentY);
currentY += promptLines.length * 5 + 10;
}
doc.setFontSize(12);
doc.setTextColor(31, 51, 77);
doc.text("Resposta:", margin, currentY);
currentY += 8;
const textContent = content.replace(/__CHART_DATA__[\s\S]*?__END_CHART_DATA__/g, "").trim();
doc.setFontSize(11);
const lines = doc.splitTextToSize(textContent, maxWidth);
doc.text(lines, margin, currentY);
currentY += lines.length * 5 + 10;
// Verifica se há dados de gráfico e captura
const chartDataMatch = content.match(/__CHART_DATA__([\s\S]*?)__END_CHART_DATA__/);
if (chartDataMatch && chartContainer) {
try {
const canvas = await html2canvas(chartContainer, {
backgroundColor: "#ffffff",
scale: 2,
});
const imgData = canvas.toDataURL("image/png");
const imgWidth = maxWidth;
const imgHeight = (canvas.height * imgWidth) / canvas.width;
if (currentY + imgHeight > doc.internal.pageSize.getHeight() - margin) {
doc.addPage();
currentY = margin;
}
doc.addImage(imgData, "PNG", margin, currentY, imgWidth, imgHeight);
} catch (e) {
console.error("Error capturing chart:", e);
}
}
doc.save("arcadia-resultado.pdf");
}
export async function exportToWord(content: string, prompt?: string) {
const { Document, Packer, Paragraph, TextRun } = await import("docx");
const { saveAs } = await import("file-saver");
const paragraphs = [];
paragraphs.push(
new Paragraph({
children: [new TextRun({ text: "Arcádia Agent - Resultado", bold: true, size: 32 })],
})
);
if (prompt) {
paragraphs.push(
new Paragraph({ children: [] }),
new Paragraph({
children: [new TextRun({ text: "Tarefa: ", bold: true }), new TextRun({ text: prompt })],
})
);
}
paragraphs.push(
new Paragraph({ children: [] }),
new Paragraph({
children: [new TextRun({ text: "Resposta:", bold: true })],
})
);
content.split("\n").forEach((line) => {
paragraphs.push(new Paragraph({ children: [new TextRun({ text: line })] }));
});
const doc = new Document({
sections: [{ properties: {}, children: paragraphs }],
});
const blob = await Packer.toBlob(doc);
saveAs(blob, "arcadia-resultado.docx");
}
export async function exportToText(content: string, prompt?: string) {
const { saveAs } = await import("file-saver");
const textContent = prompt ? `Tarefa: ${prompt}\n\nResposta:\n${content}` : content;
const blob = new Blob([textContent], { type: "text/plain;charset=utf-8" });
saveAs(blob, "arcadia-resultado.txt");
}
export async function exportToCSV(content: string) {
const { saveAs } = await import("file-saver");
const lines = content.split("\n");
let csvContent = "";
for (const line of lines) {
if (line.includes("|")) {
const cells = line
.split("|")
.map((cell) => cell.trim())
.filter((cell) => cell && !cell.match(/^[-:]+$/));
if (cells.length > 0) {
csvContent += cells.map((cell) => `"${cell.replace(/"/g, """)}"`).join(",") + "\n";
}
} else if (line.includes("\t")) {
csvContent +=
line
.split("\t")
.map((cell) => `"${cell.trim().replace(/"/g, """)}"`)
.join(",") + "\n";
} else if (line.trim()) {
csvContent += `"${line.replace(/"/g, """)}"\n`;
}
}
const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8" });
saveAs(blob, "arcadia-resultado.csv");
}

View File

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

View File

@ -54,10 +54,13 @@ import {
} from "lucide-react";
import { CardDescription } from "@/components/ui/card";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { jsPDF } from "jspdf";
import { Document, Packer, Paragraph, TextRun } from "docx";
import { saveAs } from "file-saver";
import html2canvas from "html2canvas";
import { BarChart, Bar, LineChart, Line, PieChart, Pie, AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, Cell } from "recharts";
import { ResultViewer } from "@/components/ResultViewer";
import DevHistory from "@/components/DevHistory";
import { ChartRenderer } from "@/components/ChartRenderer";
import { exportToPDF, exportToWord, exportToText, exportToCSV } from "@/lib/export-utils";
interface ChartData {
type: 'bar' | 'line' | 'pie' | 'area';

View File

@ -1,267 +0,0 @@
/**
* AutomationCenter Phase 5: Automation Fabric
*
* Visão unificada de todas as automações: Central + XOS.
* Rota: /automations-center
*/
import { useState } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { BrowserFrame } from "@/components/Browser/BrowserFrame";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Zap, Search, Play, Loader2, CheckCircle, XCircle,
Clock, Webhook, Bot, Calendar, Activity, Filter
} from "lucide-react";
import { useToast } from "@/hooks/use-toast";
interface UnifiedAutomation {
id: string;
sourceId: number;
source: "central" | "xos";
name: string;
description: string | null;
triggerType: string;
isActive: boolean;
executionCount: number;
lastExecutedAt: string | null;
createdAt: string;
}
const TRIGGER_ICONS: Record<string, React.ReactNode> = {
schedule: <Clock className="w-3.5 h-3.5" />,
webhook: <Webhook className="w-3.5 h-3.5" />,
agent: <Bot className="w-3.5 h-3.5" />,
event: <Activity className="w-3.5 h-3.5" />,
contact_created: <Zap className="w-3.5 h-3.5" />,
deal_stage_changed: <Zap className="w-3.5 h-3.5" />,
form_submitted: <Zap className="w-3.5 h-3.5" />,
};
const TRIGGER_LABEL: Record<string, string> = {
schedule: "Agendada",
webhook: "Webhook",
manual: "Manual",
event: "Evento",
agent: "Agente",
contact_created: "Novo Contato",
deal_stage_changed: "Mudança de Etapa",
form_submitted: "Formulário",
};
export default function AutomationCenter() {
const [search, setSearch] = useState("");
const [filterSource, setFilterSource] = useState<"all" | "central" | "xos">("all");
const [filterStatus, setFilterStatus] = useState<"all" | "active" | "inactive">("all");
const { toast } = useToast();
const qc = useQueryClient();
const { data, isLoading } = useQuery<{ automations: UnifiedAutomation[]; total: number }>({
queryKey: ["/api/automation-fabric/list"],
refetchInterval: 30_000,
});
const runMutation = useMutation({
mutationFn: async (id: string) => {
const res = await fetch(`/api/automation-fabric/${encodeURIComponent(id)}/run`, { method: "POST" });
if (!res.ok) throw new Error();
return res.json();
},
onSuccess: (_, id) => {
toast({ title: "Automação executada" });
qc.invalidateQueries({ queryKey: ["/api/automation-fabric/list"] });
},
onError: () => toast({ title: "Erro ao executar", variant: "destructive" }),
});
const toggleMutation = useMutation({
mutationFn: async ({ id, isActive }: { id: string; isActive: boolean }) => {
const res = await fetch(`/api/automation-fabric/${encodeURIComponent(id)}/toggle`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ isActive }),
});
if (!res.ok) throw new Error();
return res.json();
},
onSuccess: () => qc.invalidateQueries({ queryKey: ["/api/automation-fabric/list"] }),
onError: () => toast({ title: "Erro ao atualizar status", variant: "destructive" }),
});
const automations = data?.automations ?? [];
const filtered = automations.filter((a) => {
if (filterSource !== "all" && a.source !== filterSource) return false;
if (filterStatus === "active" && !a.isActive) return false;
if (filterStatus === "inactive" && a.isActive) return false;
if (search) {
const q = search.toLowerCase();
return (
a.name.toLowerCase().includes(q) ||
(a.description ?? "").toLowerCase().includes(q) ||
a.triggerType.toLowerCase().includes(q)
);
}
return true;
});
const activeCount = automations.filter((a) => a.isActive).length;
const centralCount = automations.filter((a) => a.source === "central").length;
const xosCount = automations.filter((a) => a.source === "xos").length;
return (
<BrowserFrame>
<div className="flex flex-col h-full bg-[#0a0f1a] text-white">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-white/10">
<div>
<h1 className="text-lg font-semibold flex items-center gap-2">
<Zap className="w-5 h-5 text-[#c89b3c]" />
Automation Center
</h1>
<p className="text-xs text-muted-foreground mt-0.5">
{automations.length} automações · {activeCount} ativas · Central ({centralCount}) + XOS ({xosCount})
</p>
</div>
</div>
{/* Filters */}
<div className="flex gap-2 p-3 border-b border-white/10 flex-wrap">
<div className="relative flex-1 min-w-48">
<Search className="absolute left-2.5 top-2.5 w-3.5 h-3.5 text-muted-foreground" />
<Input
placeholder="Buscar automações..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="pl-8 h-8 text-sm bg-white/5 border-white/10"
/>
</div>
{/* Source filter */}
<div className="flex rounded-md border border-white/10 overflow-hidden">
{(["all", "central", "xos"] as const).map((s) => (
<button
key={s}
onClick={() => setFilterSource(s)}
className={`px-2.5 py-1.5 text-xs transition-colors ${
filterSource === s ? "bg-[#c89b3c] text-black font-medium" : "text-white/50 hover:text-white hover:bg-white/5"
}`}
>
{s === "all" ? "Todas" : s === "central" ? "Central" : "XOS"}
</button>
))}
</div>
{/* Status filter */}
<div className="flex rounded-md border border-white/10 overflow-hidden">
{(["all", "active", "inactive"] as const).map((s) => (
<button
key={s}
onClick={() => setFilterStatus(s)}
className={`px-2.5 py-1.5 text-xs transition-colors ${
filterStatus === s ? "bg-[#c89b3c] text-black font-medium" : "text-white/50 hover:text-white hover:bg-white/5"
}`}
>
{s === "all" ? "Todas" : s === "active" ? "Ativas" : "Inativas"}
</button>
))}
</div>
</div>
{/* List */}
<ScrollArea className="flex-1">
{isLoading && (
<div className="flex justify-center py-16">
<Loader2 className="w-6 h-6 animate-spin text-white/30" />
</div>
)}
{!isLoading && filtered.length === 0 && (
<div className="flex flex-col items-center justify-center py-20 text-white/30 gap-3">
<Filter className="w-10 h-10 opacity-30" />
<p className="text-sm">Nenhuma automação encontrada</p>
{search && <p className="text-xs">Tente outro filtro ou busca</p>}
</div>
)}
<div className="divide-y divide-white/5">
{filtered.map((a) => {
const isRunning = runMutation.isPending && runMutation.variables === a.id;
const isToggling = toggleMutation.isPending && toggleMutation.variables?.id === a.id;
const triggerIcon = TRIGGER_ICONS[a.triggerType] ?? <Zap className="w-3.5 h-3.5" />;
const triggerLabel = TRIGGER_LABEL[a.triggerType] ?? a.triggerType;
const lastRun = a.lastExecutedAt
? new Date(a.lastExecutedAt).toLocaleDateString("pt-BR")
: null;
return (
<div key={a.id} className="px-4 py-3 flex items-center gap-3 hover:bg-white/3 transition-colors">
{/* Icon */}
<div className={`w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0 ${a.isActive ? "bg-[#c89b3c]/15 text-[#c89b3c]" : "bg-white/5 text-white/30"}`}>
{triggerIcon}
</div>
{/* Info */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-sm font-medium truncate">{a.name}</span>
<Badge
className={`text-[9px] px-1.5 py-0 shrink-0 ${
a.source === "central"
? "bg-blue-500/15 text-blue-400 border-blue-500/20"
: "bg-purple-500/15 text-purple-400 border-purple-500/20"
}`}
>
{a.source === "central" ? "Central" : "XOS"}
</Badge>
</div>
<div className="flex items-center gap-3 mt-0.5">
<span className="text-[11px] text-white/40 flex items-center gap-1">
{triggerIcon} {triggerLabel}
</span>
{a.executionCount > 0 && (
<span className="text-[11px] text-white/30">{a.executionCount}x</span>
)}
{lastRun && (
<span className="text-[11px] text-white/30 flex items-center gap-1">
<Calendar className="w-3 h-3" /> {lastRun}
</span>
)}
</div>
</div>
{/* Actions */}
<div className="flex items-center gap-2 shrink-0">
<Button
size="sm"
variant="ghost"
className="h-7 w-7 p-0 text-white/40 hover:text-white hover:bg-white/10"
onClick={() => runMutation.mutate(a.id)}
disabled={isRunning || !a.isActive}
title="Executar agora"
>
{isRunning ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Play className="w-3.5 h-3.5" />}
</Button>
<Switch
checked={a.isActive}
disabled={isToggling}
onCheckedChange={(v) => toggleMutation.mutate({ id: a.id, isActive: v })}
className="scale-75"
/>
</div>
</div>
);
})}
</div>
</ScrollArea>
</div>
</BrowserFrame>
);
}

View File

@ -1,5 +1,5 @@
import { BrowserFrame } from "@/components/Browser/BrowserFrame";
import { useState, useEffect } from "react";
import { useState } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
@ -49,8 +49,6 @@ import {
MessageSquare,
} from "lucide-react";
import { Textarea } from "@/components/ui/textarea";
import { SupersetDashboard } from "@/components/SupersetDashboard";
import { MiroFlowControl } from "@/components/MiroFlowControl";
import {
Dialog,
DialogContent,
@ -83,73 +81,6 @@ import {
Cell,
} from "recharts";
// ── 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 {
dataSources: number;
datasets: number;
@ -2958,11 +2889,8 @@ export default function BiWorkspace() {
<TabsTrigger value="staging" className="data-[state=active]:bg-[#c89b3c] data-[state=active]:text-[#1f334d] text-white/70">
<Layers className="w-4 h-4 mr-2" /> Staging
</TabsTrigger>
<TabsTrigger value="cientifico" className="data-[state=active]:bg-[#c89b3c] data-[state=active]:text-[#1f334d] text-white/70">
Científico
</TabsTrigger>
<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" /> MetaSet
</TabsTrigger>
</TabsList>
@ -2987,12 +2915,33 @@ export default function BiWorkspace() {
<TabsContent value="staging" className="mt-0">
<StagingTab />
</TabsContent>
<TabsContent value="cientifico" className="mt-0">
<MiroFlowControl />
</TabsContent>
<TabsContent value="advanced" className="mt-0">
<SupersetAdvancedTab />
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<h2 className="text-lg font-semibold text-[#1f334d]">MetaSet - Motor de BI</h2>
<p className="text-sm text-gray-500">Acesso completo para criação manual de consultas SQL, gráficos e dashboards</p>
</div>
<a
href="/api/bi/metaset/autologin"
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-metaset-external"
>
<ArrowRight className="w-4 h-4" /> Abrir em Nova Aba
</a>
</div>
<div className="rounded-xl overflow-hidden border border-[#c89b3c]/20 bg-white shadow-sm" style={{ height: 'calc(100vh - 320px)' }}>
<iframe
src="/api/bi/metaset/autologin"
className="w-full h-full border-0"
title="MetaSet - Arcádia Insights"
data-testid="iframe-metaset-advanced"
allow="fullscreen"
/>
</div>
</div>
</TabsContent>
</Tabs>
</div>

View File

@ -1,29 +0,0 @@
# Design Studio — Contexto para próximo agente
## O que é
Design Studio é a entrada na sidebar de `/development` (DevelopmentModule.tsx) que abre o `DevCenter.tsx` como painel interno.
## Estado atual (27/03/2026)
O ícone e a navegação estão implementados. O conteúdo renderizado é o `DevCenter.tsx` existente, que já possui 4 abas funcionais:
| Aba | O que faz |
|-----|-----------|
| **Design** | Editor de definições de agente (modos: Markdown, TypeScript, Visual-JSON) |
| **Assemble** | Agentes draft → Blackboard gera o código → polling de status |
| **Deploy** | Agentes prontos → deploy, re-montar |
| **Galeria** | Grid de agentes implantados, busca, executar, fork |
## O que FALTA (João tem ideias — perguntar antes de implementar)
- **Visual canvas drag-and-drop** para o modo "Visual" do editor (hoje é um Textarea com JSON manual)
- **Suporte a UML** no editor de definições
- O usuário João mencionou ter ideias específicas para o Design Studio — **confirmar com ele antes de implementar qualquer coisa nova aqui**
## Arquivos relacionados
- `client/src/pages/DevCenter.tsx` — componente principal renderizado pelo Design Studio
- `client/src/pages/DevelopmentModule.tsx` — sidebar onde o item foi adicionado (id: "designstudio", ícone: PenTool)
- `server/routes/agent-defs.ts` (ou similar) — API de definições de agentes usada pelo DevCenter
- `migrations/phase6_agent_defs.sql` — schema das tabelas de agent definitions

File diff suppressed because it is too large Load Diff

View File

@ -11,12 +11,12 @@ import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { useToast } from "@/hooks/use-toast";
import {
import {
Code2, Database, Layout, GitBranch, BarChart3, FileText,
Plus, Settings, Play, Zap, Layers, Puzzle, Palette,
Terminal, Box, Workflow, PanelLeft, Table2, FormInput,
ListChecks, PieChart, LineChart, Gauge, Bot, Code, Trash2, Save,
Sparkles, Rocket, PenTool, Brain
Sparkles, Rocket
} from "lucide-react";
import { useLocation } from "wouter";
@ -25,12 +25,8 @@ import PageBuilder from "./PageBuilder";
import WorkflowBuilder from "./WorkflowBuilder";
import IDE from "./IDE";
import DevAgent from "@/components/lowcode/DevAgent";
import OpenClawPanel from "@/components/OpenClawPanel";
import AutomationCenter from "./AutomationCenter";
import DevCenter from "./DevCenter";
import { MiroFlowControl } from "@/components/MiroFlowControl";
type ActiveTool = "home" | "doctypes" | "pages" | "workflows" | "dashboards" | "reports" | "scripts" | "ide" | "agent" | "openclaw" | "automationcenter" | "designstudio" | "miroflow";
type ActiveTool = "home" | "doctypes" | "pages" | "workflows" | "dashboards" | "reports" | "scripts" | "ide" | "agent";
interface Dashboard {
id: number;
@ -254,51 +250,15 @@ export default function DevelopmentModule() {
count: 0,
category: "dev"
},
{
id: "agent" as ActiveTool,
name: "Manus AI",
{
id: "agent" as ActiveTool,
name: "Manus AI",
description: "Agente autônomo de IA",
icon: Bot,
icon: Bot,
color: "bg-violet-600",
count: 0,
category: "dev"
},
{
id: "openclaw" as ActiveTool,
name: "Skills Emergentes",
description: "Padrões detectados automaticamente",
icon: Sparkles,
color: "bg-yellow-500",
count: 0,
category: "dev"
},
{
id: "automationcenter" as ActiveTool,
name: "Automation Center",
description: "Automações unificadas Central + XOS",
icon: Workflow,
color: "bg-blue-600",
count: 0,
category: "dev"
},
{
id: "designstudio" as ActiveTool,
name: "Design Studio",
description: "Projete e monte agentes autônomos",
icon: PenTool,
color: "bg-purple-600",
count: 0,
category: "dev"
},
{
id: "miroflow" as ActiveTool,
name: "MiroFlow",
description: "Análises científicas e estatísticas",
icon: Brain,
color: "bg-cyan-500",
count: 0,
category: "scientific"
},
];
const [, navigate] = useLocation();
@ -711,8 +671,8 @@ export default function DevelopmentModule() {
);
return (
<div className="h-screen flex bg-gray-50">
<div className="w-16 bg-gray-900 flex flex-col items-center py-4 gap-2 overflow-y-auto h-full">
<div className="h-full flex bg-gray-50">
<div className="w-16 bg-gray-900 flex flex-col items-center py-4 gap-2">
<button
onClick={() => setActiveTool("home")}
className={`p-3 rounded-lg transition-colors ${activeTool === "home" ? "bg-blue-600" : "hover:bg-gray-700"}`}
@ -735,8 +695,8 @@ export default function DevelopmentModule() {
))}
</div>
<div className="flex-1 h-full overflow-hidden">
{activeTool === "home" && <div className="h-full overflow-y-auto">{renderHome()}</div>}
<div className="flex-1 overflow-hidden">
{activeTool === "home" && renderHome()}
{activeTool === "doctypes" && <DocTypeBuilder />}
{activeTool === "pages" && <PageBuilder />}
{activeTool === "workflows" && <WorkflowBuilder />}
@ -745,10 +705,6 @@ export default function DevelopmentModule() {
{activeTool === "scripts" && renderScriptEditor()}
{activeTool === "ide" && <IDE />}
{activeTool === "agent" && <DevAgent />}
{activeTool === "openclaw" && <OpenClawPanel />}
{activeTool === "automationcenter" && <AutomationCenter />}
{activeTool === "designstudio" && <DevCenter />}
{activeTool === "miroflow" && <MiroFlowControl />}
</div>
<Dialog open={showNewDashboardDialog} onOpenChange={setShowNewDashboardDialog}>

View File

@ -1,78 +0,0 @@
import React, { useState } from "react";
import { TradeInFiltersWidget } from "../components/retail-reports/TradeInFiltersWidget";
import { TradeInDetailsModal } from "../components/retail-reports/TradeInDetailsModal";
import { TradeInPrintButton } from "../components/retail-reports/TradeInPrintButton";
import { useQuery } from "@tanstack/react-query";
const fetchTradeIns = async (filters) => {
const query = new URLSearchParams(filters).toString();
const response = await fetch(`/api/modules/retail-reports/trade-ins?${query}`);
if (!response.ok) throw new Error("Failed to fetch trade-ins.");
return response.json();
};
export const RetailReportsPage: React.FC = () => {
const [filters, setFilters] = useState({});
const [selectedTradeIn, setSelectedTradeIn] = useState(null);
const { data: tradeIns, isLoading, error } = useQuery(["tradeIns", filters], () => fetchTradeIns(filters));
return (
<div className="p-4">
<h1 className="text-2xl font-bold mb-4">Retail Trade-In Reports</h1>
<TradeInFiltersWidget onApplyFilters={setFilters} data-testid="filters-widget" />
{isLoading && <p>Loading trade-ins...</p>}
{error && <p className="text-red-500">Failed to load trade-ins.</p>}
<table className="table-auto w-full border-collapse border border-gray-300 mt-4">
<thead>
<tr>
<th className="border border-gray-300 px-4 py-2">ID</th>
<th className="border border-gray-300 px-4 py-2">Seller</th>
<th className="border border-gray-300 px-4 py-2">Company</th>
<th className="border border-gray-300 px-4 py-2">Client</th>
<th className="border border-gray-300 px-4 py-2">Date</th>
<th className="border border-gray-300 px-4 py-2">Value</th>
<th className="border border-gray-300 px-4 py-2">Status</th>
<th className="border border-gray-300 px-4 py-2">Actions</th>
</tr>
</thead>
<tbody>
{tradeIns?.map((tradeIn) => (
<tr key={tradeIn.id}>
<td className="border border-gray-300 px-4 py-2">{tradeIn.id}</td>
<td className="border border-gray-300 px-4 py-2">{tradeIn.sellerId}</td>
<td className="border border-gray-300 px-4 py-2">{tradeIn.companyId}</td>
<td className="border border-gray-300 px-4 py-2">{tradeIn.clientId}</td>
<td className="border border-gray-300 px-4 py-2">{new Date(tradeIn.tradeInDate).toLocaleDateString()}</td>
<td className="border border-gray-300 px-4 py-2">{tradeIn.tradeInValue}</td>
<td className="border border-gray-300 px-4 py-2">{tradeIn.status}</td>
<td className="border border-gray-300 px-4 py-2">
<button
className="text-blue-500 hover:underline"
onClick={() => setSelectedTradeIn(tradeIn)}
data-testid="view-details-button"
>
View Details
</button>
</td>
</tr>
))}
</tbody>
</table>
<TradeInPrintButton tradeIns={tradeIns} data-testid="print-button" />
{selectedTradeIn && (
<TradeInDetailsModal
tradeIn={selectedTradeIn}
onClose={() => setSelectedTradeIn(null)}
data-testid="details-modal"
/>
)}
</div>
);
};
export default RetailReportsPage;

View File

@ -1,2 +0,0 @@
// SOE — Sistema de Operações Empresariais
export { default } from "./ERP";

View File

@ -1,5 +1,4 @@
import { BrowserFrame } from "@/components/Browser/BrowserFrame";
import { MiroFlowControl } from "@/components/MiroFlowControl";
import { useState } from "react";
import { useMutation, useQuery } from "@tanstack/react-query";
import { Button } from "@/components/ui/button";
@ -201,10 +200,6 @@ export default function Scientist() {
<Lightbulb className="w-4 h-4 mr-2" />
Sugestões
</TabsTrigger>
<TabsTrigger value="scientific" className="data-[state=active]:bg-cyan-500">
<Brain className="w-4 h-4 mr-2" />
Científico
</TabsTrigger>
</TabsList>
<TabsContent value="knowledge" className="space-y-4">
@ -832,10 +827,6 @@ export default function Scientist() {
</Card>
)}
</TabsContent>
<TabsContent value="scientific" className="space-y-4">
<MiroFlowControl />
</TabsContent>
</Tabs>
</div>
</div>

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +1,11 @@
import { useState } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { useQuery } from "@tanstack/react-query";
import { Link } from "wouter";
import { BrowserFrame } from "@/components/Browser/BrowserFrame";
import {
Users, Building2, TrendingUp, MessageSquare, Ticket, Zap, LayoutGrid,
import {
Users, Building2, TrendingUp, MessageSquare, Ticket, Zap, LayoutGrid,
ChevronRight, PlusCircle, Filter, Search, Bell, Calendar, Target,
BarChart3, DollarSign, Clock, AlertCircle, Phone, Mail, ArrowUpRight,
Activity, Hash, Shield
BarChart3, DollarSign, Clock, AlertCircle, Phone, Mail, ArrowUpRight
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
@ -14,9 +13,6 @@ import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
interface XosStats {
total_contacts: number;
@ -70,48 +66,6 @@ interface Activity {
export default function XosCentral() {
const [activeTab, setActiveTab] = useState("dashboard");
const [isNewContactOpen, setIsNewContactOpen] = useState(false);
const [isNewActivityOpen, setIsNewActivityOpen] = useState(false);
const [newContact, setNewContact] = useState({ name: "", email: "", phone: "", company: "", position: "" });
const [newActivity, setNewActivity] = useState({ type: "task", title: "", due_at: "", priority: "medium" });
const queryClient = useQueryClient();
const createContactMutation = useMutation({
mutationFn: async (data: typeof newContact) => {
const res = await fetch("/api/xos/contacts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
if (!res.ok) throw new Error("Erro ao criar contato");
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/xos/contacts"] });
queryClient.invalidateQueries({ queryKey: ["/api/xos/stats"] });
setIsNewContactOpen(false);
setNewContact({ name: "", email: "", phone: "", company: "", position: "" });
},
});
const createActivityMutation = useMutation({
mutationFn: async (data: typeof newActivity) => {
const res = await fetch("/api/xos/activities", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
if (!res.ok) throw new Error("Erro ao criar atividade");
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/xos/activities"] });
queryClient.invalidateQueries({ queryKey: ["/api/xos/stats"] });
setIsNewActivityOpen(false);
setNewActivity({ type: "task", title: "", due_at: "", priority: "medium" });
},
});
const { data: stats } = useQuery<XosStats>({
queryKey: ["/api/xos/stats"],
@ -143,9 +97,6 @@ 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: "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: "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) => {
@ -226,9 +177,9 @@ export default function XosCentral() {
</span>
)}
</Button>
<Button data-testid="button-new-contact" onClick={() => setIsNewContactOpen(true)}>
<Button data-testid="button-new-contact">
<PlusCircle className="h-4 w-4 mr-2" />
Novo Contato
Novo
</Button>
</div>
</div>
@ -313,7 +264,7 @@ export default function XosCentral() {
{/* Modules Grid */}
<div>
<h2 className="text-lg font-semibold text-slate-800 mb-4">Módulos XOS</h2>
<div className="grid grid-cols-3 md:grid-cols-4 lg:grid-cols-9 gap-4">
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
{modules.map((mod) => (
<Link key={mod.id} href={mod.href}>
<Card className="hover:shadow-lg transition-all cursor-pointer group" data-testid={`card-module-${mod.id}`}>
@ -455,7 +406,7 @@ export default function XosCentral() {
<Filter className="h-4 w-4 mr-2" />
Filtros
</Button>
<Button size="sm" onClick={() => setIsNewContactOpen(true)}>
<Button size="sm">
<PlusCircle className="h-4 w-4 mr-2" />
Novo Contato
</Button>
@ -527,7 +478,7 @@ export default function XosCentral() {
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>Atividades</CardTitle>
<Button size="sm" onClick={() => setIsNewActivityOpen(true)}>
<Button size="sm">
<PlusCircle className="h-4 w-4 mr-2" />
Nova Atividade
</Button>
@ -569,129 +520,6 @@ export default function XosCentral() {
</Tabs>
</main>
</div>
{/* Modal: Novo Contato */}
<Dialog open={isNewContactOpen} onOpenChange={setIsNewContactOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Novo Contato</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label>Nome *</Label>
<Input
value={newContact.name}
onChange={(e) => setNewContact({ ...newContact, name: e.target.value })}
placeholder="Nome do contato"
/>
</div>
<div className="space-y-2">
<Label>Email</Label>
<Input
type="email"
value={newContact.email}
onChange={(e) => setNewContact({ ...newContact, email: e.target.value })}
placeholder="email@exemplo.com"
/>
</div>
<div className="space-y-2">
<Label>Telefone</Label>
<Input
value={newContact.phone}
onChange={(e) => setNewContact({ ...newContact, phone: e.target.value })}
placeholder="(00) 00000-0000"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Empresa</Label>
<Input
value={newContact.company}
onChange={(e) => setNewContact({ ...newContact, company: e.target.value })}
placeholder="Empresa"
/>
</div>
<div className="space-y-2">
<Label>Cargo</Label>
<Input
value={newContact.position}
onChange={(e) => setNewContact({ ...newContact, position: e.target.value })}
placeholder="Cargo"
/>
</div>
</div>
<Button
className="w-full"
onClick={() => createContactMutation.mutate(newContact)}
disabled={!newContact.name || createContactMutation.isPending}
>
{createContactMutation.isPending ? "Salvando..." : "Criar Contato"}
</Button>
</div>
</DialogContent>
</Dialog>
{/* Modal: Nova Atividade */}
<Dialog open={isNewActivityOpen} onOpenChange={setIsNewActivityOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Nova Atividade</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label>Título *</Label>
<Input
value={newActivity.title}
onChange={(e) => setNewActivity({ ...newActivity, title: e.target.value })}
placeholder="Descrição da atividade"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Tipo</Label>
<Select value={newActivity.type} onValueChange={(v) => setNewActivity({ ...newActivity, type: v })}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="task">Tarefa</SelectItem>
<SelectItem value="call">Ligação</SelectItem>
<SelectItem value="email">E-mail</SelectItem>
<SelectItem value="meeting">Reunião</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Prioridade</Label>
<Select value={newActivity.priority} onValueChange={(v) => setNewActivity({ ...newActivity, priority: v })}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="low">Baixa</SelectItem>
<SelectItem value="medium">Média</SelectItem>
<SelectItem value="high">Alta</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<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
className="w-full"
onClick={() => createActivityMutation.mutate(newActivity)}
disabled={!newActivity.title || createActivityMutation.isPending}
>
{createActivityMutation.isPending ? "Salvando..." : "Criar Atividade"}
</Button>
</div>
</DialogContent>
</Dialog>
</BrowserFrame>
);
}

View File

@ -1,506 +0,0 @@
import { useState } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { Link } from "wouter";
import { BrowserFrame } from "@/components/Browser/BrowserFrame";
import {
Hash, Search, Filter, ArrowLeft, CheckCircle2, Clock, AlertTriangle,
Plus, Star, Shield, Copy, ChevronRight, User, Inbox
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { useToast } from "@/hooks/use-toast";
interface Protocol {
id: number;
protocol_number: string;
status: "open" | "resolved" | "cancelled";
subject: string | null;
contact_name: string | null;
queue_name: string | null;
assigned_to: string | null;
sla_deadline: string | null;
sla_breach: boolean;
satisfaction_score: number | null;
opened_at: string;
resolved_at: string | null;
conversation_id: number | null;
ticket_id: number | null;
}
export default function XosProtocols() {
const [search, setSearch] = useState("");
const [statusFilter, setStatusFilter] = useState("all");
const [selectedProtocol, setSelectedProtocol] = useState<Protocol | null>(null);
const [showNewDialog, setShowNewDialog] = useState(false);
const [newForm, setNewForm] = useState({ subject: "", contactId: "", queueId: "", slaMinutes: "" });
const [csatDialog, setCsatDialog] = useState<{ open: boolean; protocol: Protocol | null }>({ open: false, protocol: null });
const [csatScore, setCsatScore] = useState(5);
const [csatComment, setCsatComment] = useState("");
const queryClient = useQueryClient();
const { toast } = useToast();
const { data: protocols = [], isLoading } = useQuery<Protocol[]>({
queryKey: ["/api/xos/protocols", search, statusFilter],
queryFn: async () => {
const params = new URLSearchParams();
if (search) params.set("search", search);
if (statusFilter !== "all") params.set("status", statusFilter);
const res = await fetch(`/api/xos/protocols?${params}`);
return res.json();
},
});
const createProtocol = useMutation({
mutationFn: async (data: typeof newForm) => {
const res = await fetch("/api/xos/protocols", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
subject: data.subject || null,
contactId: data.contactId ? parseInt(data.contactId) : null,
queueId: data.queueId ? parseInt(data.queueId) : null,
slaMinutes: data.slaMinutes ? parseInt(data.slaMinutes) : null,
}),
});
if (!res.ok) throw new Error("Falha ao criar protocolo");
return res.json();
},
onSuccess: (prot) => {
toast({ title: "Protocolo criado", description: `Número: ${prot.protocol_number}` });
setShowNewDialog(false);
setNewForm({ subject: "", contactId: "", queueId: "", slaMinutes: "" });
queryClient.invalidateQueries({ queryKey: ["/api/xos/protocols"] });
},
onError: () => toast({ title: "Erro ao criar protocolo", variant: "destructive" }),
});
const updateProtocol = useMutation({
mutationFn: async ({ id, status }: { id: number; status: string }) => {
const res = await fetch(`/api/xos/protocols/${id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ status }),
});
return res.json();
},
onSuccess: () => {
toast({ title: "Protocolo atualizado" });
queryClient.invalidateQueries({ queryKey: ["/api/xos/protocols"] });
setSelectedProtocol(null);
},
});
const submitCsat = useMutation({
mutationFn: async ({ protocol, score, comment }: { protocol: Protocol; score: number; comment: string }) => {
const endpoint = protocol.conversation_id
? `/api/xos/conversations/${protocol.conversation_id}/csat`
: protocol.ticket_id
? `/api/xos/tickets/${protocol.ticket_id}/csat`
: null;
if (!endpoint) throw new Error("Sem conversa ou ticket vinculado");
const res = await fetch(endpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ score, comment }),
});
if (!res.ok) throw new Error("Falha ao registrar CSAT");
return res.json();
},
onSuccess: () => {
toast({ title: "Avaliação registrada!", description: "CSAT salvo com sucesso." });
setCsatDialog({ open: false, protocol: null });
setCsatScore(5);
setCsatComment("");
queryClient.invalidateQueries({ queryKey: ["/api/xos/protocols"] });
},
onError: (e: any) => toast({ title: "Erro", description: e.message, variant: "destructive" }),
});
const getStatusConfig = (status: string) => ({
open: { label: "Aberto", color: "bg-blue-100 text-blue-700", icon: Clock },
resolved: { label: "Resolvido", color: "bg-emerald-100 text-emerald-700", icon: CheckCircle2 },
cancelled: { label: "Cancelado", color: "bg-slate-100 text-slate-600", icon: AlertTriangle },
}[status] || { label: status, color: "bg-slate-100 text-slate-600", icon: Clock });
const isSlaBreached = (p: Protocol) => {
if (p.sla_breach) return true;
if (p.status !== "open" || !p.sla_deadline) return false;
return new Date(p.sla_deadline) < new Date();
};
const getSlaTimeLeft = (deadline: string) => {
const diff = new Date(deadline).getTime() - Date.now();
if (diff < 0) return null;
const h = Math.floor(diff / 3600000);
const m = Math.floor((diff % 3600000) / 60000);
return h > 0 ? `${h}h ${m}m` : `${m}m`;
};
const copyProtocol = (number: string) => {
navigator.clipboard.writeText(number);
toast({ title: "Copiado!", description: number });
};
const open = protocols.filter(p => p.status === "open").length;
const resolved = protocols.filter(p => p.status === "resolved").length;
const breached = protocols.filter(p => isSlaBreached(p)).length;
return (
<BrowserFrame>
<div className="min-h-screen bg-slate-50">
{/* Header */}
<header className="sticky top-0 z-50 bg-white border-b shadow-sm">
<div className="max-w-7xl mx-auto px-4 py-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Link href="/xos">
<Button variant="ghost" size="icon">
<ArrowLeft className="h-4 w-4" />
</Button>
</Link>
<div className="bg-gradient-to-br from-teal-600 to-cyan-700 p-2 rounded-xl">
<Hash className="h-5 w-5 text-white" />
</div>
<div>
<h1 className="text-xl font-bold text-slate-800">Protocolos</h1>
<p className="text-xs text-slate-500">Rastreamento de atendimentos</p>
</div>
</div>
<Button onClick={() => setShowNewDialog(true)}>
<Plus className="h-4 w-4 mr-2" />
Novo Protocolo
</Button>
</div>
</div>
</header>
<main className="max-w-7xl mx-auto px-4 py-6 space-y-6">
{/* Stats */}
<div className="grid grid-cols-3 gap-4">
<Card className="bg-gradient-to-br from-blue-500 to-blue-600 text-white">
<CardContent className="p-4 flex items-center gap-3">
<Clock className="h-8 w-8 text-blue-200" />
<div>
<p className="text-blue-100 text-sm">Abertos</p>
<p className="text-2xl font-bold">{open}</p>
</div>
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-emerald-500 to-emerald-600 text-white">
<CardContent className="p-4 flex items-center gap-3">
<CheckCircle2 className="h-8 w-8 text-emerald-200" />
<div>
<p className="text-emerald-100 text-sm">Resolvidos</p>
<p className="text-2xl font-bold">{resolved}</p>
</div>
</CardContent>
</Card>
<Card className={`text-white ${breached > 0 ? "bg-gradient-to-br from-red-500 to-red-600" : "bg-gradient-to-br from-slate-500 to-slate-600"}`}>
<CardContent className="p-4 flex items-center gap-3">
<AlertTriangle className="h-8 w-8 opacity-70" />
<div>
<p className="opacity-80 text-sm">SLA Violado</p>
<p className="text-2xl font-bold">{breached}</p>
</div>
</CardContent>
</Card>
</div>
{/* Filters */}
<div className="flex gap-3">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-400" />
<Input
placeholder="Buscar por número ou assunto..."
className="pl-10"
value={search}
onChange={e => setSearch(e.target.value)}
/>
</div>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-44">
<Filter className="h-4 w-4 mr-2" />
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Todos</SelectItem>
<SelectItem value="open">Abertos</SelectItem>
<SelectItem value="resolved">Resolvidos</SelectItem>
<SelectItem value="cancelled">Cancelados</SelectItem>
</SelectContent>
</Select>
</div>
{/* List */}
{isLoading ? (
<div className="flex justify-center py-16">
<div className="w-8 h-8 border-4 border-blue-600 border-t-transparent rounded-full animate-spin" />
</div>
) : protocols.length === 0 ? (
<Card>
<CardContent className="py-16 text-center">
<Inbox className="h-12 w-12 mx-auto mb-3 text-slate-300" />
<p className="text-slate-500">Nenhum protocolo encontrado</p>
<Button className="mt-4" onClick={() => setShowNewDialog(true)}>
<Plus className="h-4 w-4 mr-2" /> Criar Protocolo
</Button>
</CardContent>
</Card>
) : (
<div className="space-y-2">
{protocols.map(protocol => {
const statusCfg = getStatusConfig(protocol.status);
const StatusIcon = statusCfg.icon;
const breached = isSlaBreached(protocol);
const slaLeft = protocol.sla_deadline && protocol.status === "open" ? getSlaTimeLeft(protocol.sla_deadline) : null;
return (
<Card
key={protocol.id}
className={`hover:shadow-md transition-all cursor-pointer ${breached ? "border-red-300 bg-red-50" : ""}`}
onClick={() => setSelectedProtocol(protocol)}
>
<CardContent className="p-4">
<div className="flex items-center gap-4">
<div className="flex-shrink-0">
<StatusIcon className={`h-5 w-5 ${protocol.status === "resolved" ? "text-emerald-500" : protocol.status === "cancelled" ? "text-slate-400" : "text-blue-500"}`} />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<button
className="font-mono font-semibold text-slate-800 hover:text-blue-600 flex items-center gap-1"
onClick={e => { e.stopPropagation(); copyProtocol(protocol.protocol_number); }}
>
#{protocol.protocol_number}
<Copy className="h-3 w-3 opacity-50" />
</button>
<Badge className={statusCfg.color}>{statusCfg.label}</Badge>
{breached && <Badge className="bg-red-100 text-red-700">SLA Violado</Badge>}
{protocol.satisfaction_score && (
<Badge className="bg-yellow-100 text-yellow-700">
<Star className="h-3 w-3 mr-1 fill-current" />
{protocol.satisfaction_score}/5
</Badge>
)}
</div>
<p className="text-sm text-slate-600 mt-1 truncate">{protocol.subject || "Sem assunto"}</p>
<div className="flex items-center gap-3 mt-1 text-xs text-slate-500">
{protocol.contact_name && (
<span className="flex items-center gap-1">
<User className="h-3 w-3" /> {protocol.contact_name}
</span>
)}
{protocol.queue_name && (
<span>{protocol.queue_name}</span>
)}
<span>{new Date(protocol.opened_at).toLocaleDateString("pt-BR")}</span>
</div>
</div>
<div className="flex-shrink-0 text-right">
{slaLeft && (
<p className="text-xs text-emerald-600 font-medium flex items-center gap-1 justify-end">
<Shield className="h-3 w-3" />
{slaLeft} restante
</p>
)}
{breached && protocol.status === "open" && (
<p className="text-xs text-red-600 font-medium">SLA vencido</p>
)}
<ChevronRight className="h-4 w-4 text-slate-400 mt-1 ml-auto" />
</div>
</div>
</CardContent>
</Card>
);
})}
</div>
)}
</main>
{/* Protocol detail dialog */}
{selectedProtocol && (
<Dialog open onOpenChange={() => setSelectedProtocol(null)}>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Hash className="h-5 w-5 text-teal-600" />
Protocolo #{selectedProtocol.protocol_number}
</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-3">
<div>
<p className="text-xs text-slate-500">Status</p>
<Badge className={getStatusConfig(selectedProtocol.status).color}>
{getStatusConfig(selectedProtocol.status).label}
</Badge>
</div>
<div>
<p className="text-xs text-slate-500">Abertura</p>
<p className="text-sm font-medium">{new Date(selectedProtocol.opened_at).toLocaleString("pt-BR")}</p>
</div>
{selectedProtocol.contact_name && (
<div>
<p className="text-xs text-slate-500">Contato</p>
<p className="text-sm font-medium">{selectedProtocol.contact_name}</p>
</div>
)}
{selectedProtocol.queue_name && (
<div>
<p className="text-xs text-slate-500">Fila</p>
<p className="text-sm font-medium">{selectedProtocol.queue_name}</p>
</div>
)}
{selectedProtocol.sla_deadline && (
<div>
<p className="text-xs text-slate-500">SLA Deadline</p>
<p className={`text-sm font-medium ${isSlaBreached(selectedProtocol) ? "text-red-600" : "text-emerald-600"}`}>
{new Date(selectedProtocol.sla_deadline).toLocaleString("pt-BR")}
{isSlaBreached(selectedProtocol) && " (violado)"}
</p>
</div>
)}
{selectedProtocol.satisfaction_score && (
<div>
<p className="text-xs text-slate-500">CSAT</p>
<div className="flex items-center gap-1 text-yellow-600">
{"★".repeat(selectedProtocol.satisfaction_score)}
<span className="text-sm ml-1">{selectedProtocol.satisfaction_score}/5</span>
</div>
</div>
)}
</div>
{selectedProtocol.subject && (
<div>
<p className="text-xs text-slate-500 mb-1">Assunto</p>
<p className="text-sm bg-slate-50 p-2 rounded">{selectedProtocol.subject}</p>
</div>
)}
</div>
<DialogFooter className="flex flex-wrap gap-2">
{selectedProtocol.status === "open" && (
<>
<Button
variant="outline"
size="sm"
onClick={() => setCsatDialog({ open: true, protocol: selectedProtocol })}
>
<Star className="h-4 w-4 mr-2" /> Registrar CSAT
</Button>
<Button
size="sm"
onClick={() => updateProtocol.mutate({ id: selectedProtocol.id, status: "resolved" })}
>
<CheckCircle2 className="h-4 w-4 mr-2" /> Resolver
</Button>
</>
)}
<Button variant="ghost" size="sm" onClick={() => setSelectedProtocol(null)}>Fechar</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)}
{/* New Protocol Dialog */}
<Dialog open={showNewDialog} onOpenChange={setShowNewDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>Novo Protocolo</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div>
<Label htmlFor="subject">Assunto</Label>
<Input
id="subject"
placeholder="Descreva o motivo do atendimento"
value={newForm.subject}
onChange={e => setNewForm(f => ({ ...f, subject: e.target.value }))}
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<Label htmlFor="contactId">ID do Contato</Label>
<Input
id="contactId"
type="number"
placeholder="Ex: 42"
value={newForm.contactId}
onChange={e => setNewForm(f => ({ ...f, contactId: e.target.value }))}
/>
</div>
<div>
<Label htmlFor="slaMinutes">SLA (minutos)</Label>
<Input
id="slaMinutes"
type="number"
placeholder="Ex: 480"
value={newForm.slaMinutes}
onChange={e => setNewForm(f => ({ ...f, slaMinutes: e.target.value }))}
/>
</div>
</div>
</div>
<DialogFooter>
<Button variant="ghost" onClick={() => setShowNewDialog(false)}>Cancelar</Button>
<Button onClick={() => createProtocol.mutate(newForm)} disabled={createProtocol.isPending}>
{createProtocol.isPending ? "Criando..." : "Criar Protocolo"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* CSAT Dialog */}
<Dialog open={csatDialog.open} onOpenChange={o => !o && setCsatDialog({ open: false, protocol: null })}>
<DialogContent className="max-w-sm">
<DialogHeader>
<DialogTitle>Registrar Avaliação (CSAT)</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div>
<Label>Nota (1 a 5)</Label>
<div className="flex gap-2 mt-2">
{[1, 2, 3, 4, 5].map(n => (
<button
key={n}
onClick={() => setCsatScore(n)}
className={`text-2xl transition-transform hover:scale-110 ${n <= csatScore ? "text-yellow-400" : "text-slate-300"}`}
>
</button>
))}
</div>
<p className="text-xs text-slate-500 mt-1">{["", "Muito insatisfeito", "Insatisfeito", "Neutro", "Satisfeito", "Muito satisfeito"][csatScore]}</p>
</div>
<div>
<Label htmlFor="csatComment">Comentário (opcional)</Label>
<Textarea
id="csatComment"
placeholder="O que poderia melhorar?"
value={csatComment}
onChange={e => setCsatComment(e.target.value)}
rows={3}
/>
</div>
</div>
<DialogFooter>
<Button variant="ghost" onClick={() => setCsatDialog({ open: false, protocol: null })}>Cancelar</Button>
<Button
onClick={() => csatDialog.protocol && submitCsat.mutate({ protocol: csatDialog.protocol, score: csatScore, comment: csatComment })}
disabled={submitCsat.isPending}
>
{submitCsat.isPending ? "Salvando..." : "Salvar Avaliação"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</BrowserFrame>
);
}

View File

@ -1,514 +0,0 @@
import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { Link } from "wouter";
import { BrowserFrame } from "@/components/Browser/BrowserFrame";
import {
BarChart3, Star, Shield, Users, MessageSquare, Ticket, TrendingUp,
ArrowLeft, DollarSign, CheckCircle2, AlertTriangle, Clock, Download
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Progress } from "@/components/ui/progress";
interface OverviewReport {
period: string;
conversations: { total: number; resolved: number; open: number; avg_csat: number; avg_handle_minutes: number; channels_used: number };
tickets: { total: number; resolved: number; open: number; sla_breaches: number; avg_csat: number };
contacts: { total: number; leads: number; customers: number; new_in_period: number };
deals: { total: number; won: number; lost: number; won_value: number; win_rate_pct: number };
generated_at: string;
}
interface AgentReport {
agent_id: string;
agent_name: string;
conversations_handled: number;
resolved: number;
avg_csat: number;
avg_handle_minutes: number;
tickets_handled: number;
tickets_resolved: number;
}
interface SlaReport {
period: string;
protocols: { total: number; breached: number; compliant: number; compliance_rate_pct: number };
tickets_by_priority: Array<{ priority: string; total: number; breached: number; avg_resolution_minutes: number }>;
}
interface CsatSummary {
period: string;
total_responses: number;
avg_score: number;
satisfaction_rate_pct: number;
distribution: { 5: number; 4: number; 3: number; "1-2": number };
}
export default function XosReports() {
const [period, setPeriod] = useState("30d");
const [activeTab, setActiveTab] = useState("overview");
const { data: overview, isLoading } = useQuery<OverviewReport>({
queryKey: ["/api/xos/reports/overview", period],
queryFn: async () => {
const res = await fetch(`/api/xos/reports/overview?period=${period}`);
return res.json();
},
});
const { data: agentReport = [] } = useQuery<AgentReport[]>({
queryKey: ["/api/xos/reports/agents", period],
queryFn: async () => {
const res = await fetch(`/api/xos/reports/agents?period=${period}`);
return res.json();
},
});
const { data: slaReport } = useQuery<SlaReport>({
queryKey: ["/api/xos/reports/sla", period],
queryFn: async () => {
const res = await fetch(`/api/xos/reports/sla?period=${period}`);
return res.json();
},
});
const { data: csatSummary } = useQuery<CsatSummary>({
queryKey: ["/api/xos/csat/summary", period],
queryFn: async () => {
const res = await fetch(`/api/xos/csat/summary?period=${period}`);
return res.json();
},
});
const formatCurrency = (v: number) =>
new Intl.NumberFormat("pt-BR", { style: "currency", currency: "BRL" }).format(v || 0);
const periodLabel: Record<string, string> = { "7d": "7 dias", "30d": "30 dias", "90d": "90 dias", "1y": "1 ano" };
const getPriorityColor = (p: string) => ({
low: "text-slate-500", normal: "text-blue-600", high: "text-orange-600", urgent: "text-red-600"
}[p] || "text-slate-600");
const getPriorityBadge = (p: string) => ({
low: "bg-slate-100 text-slate-700", normal: "bg-blue-100 text-blue-700",
high: "bg-orange-100 text-orange-700", urgent: "bg-red-100 text-red-700"
}[p] || "bg-gray-100 text-gray-700");
const csatBar = (label: string, count: number, total: number, color: string) => (
<div className="flex items-center gap-3">
<span className="text-sm text-slate-600 w-8">{label}</span>
<Progress value={total > 0 ? (count / total) * 100 : 0} className={`flex-1 h-3 ${color}`} />
<span className="text-sm font-medium text-slate-700 w-8 text-right">{count}</span>
</div>
);
if (isLoading) {
return (
<BrowserFrame>
<div className="flex items-center justify-center h-screen">
<div className="w-10 h-10 border-4 border-blue-600 border-t-transparent rounded-full animate-spin" />
</div>
</BrowserFrame>
);
}
const conv = overview?.conversations;
const tick = overview?.tickets;
const contacts = overview?.contacts;
const deals = overview?.deals;
return (
<BrowserFrame>
<div className="min-h-screen bg-slate-50">
{/* Header */}
<header className="sticky top-0 z-50 bg-white border-b shadow-sm">
<div className="max-w-7xl mx-auto px-4 py-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Link href="/xos">
<Button variant="ghost" size="icon">
<ArrowLeft className="h-4 w-4" />
</Button>
</Link>
<div className="bg-gradient-to-br from-emerald-600 to-teal-700 p-2 rounded-xl">
<BarChart3 className="h-5 w-5 text-white" />
</div>
<div>
<h1 className="text-xl font-bold text-slate-800">Relatórios XOS</h1>
<p className="text-xs text-slate-500">Análise de atendimento e CRM</p>
</div>
</div>
<div className="flex items-center gap-2">
<Select value={period} onValueChange={setPeriod}>
<SelectTrigger className="w-36">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="7d">Últimos 7 dias</SelectItem>
<SelectItem value="30d">Últimos 30 dias</SelectItem>
<SelectItem value="90d">Últimos 90 dias</SelectItem>
<SelectItem value="1y">Último ano</SelectItem>
</SelectContent>
</Select>
<Button variant="outline" size="sm">
<Download className="h-4 w-4 mr-2" />
Exportar
</Button>
</div>
</div>
</div>
</header>
<main className="max-w-7xl mx-auto px-4 py-6 space-y-6">
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList>
<TabsTrigger value="overview">Visão Geral</TabsTrigger>
<TabsTrigger value="csat">CSAT</TabsTrigger>
<TabsTrigger value="sla">SLA</TabsTrigger>
<TabsTrigger value="agents">Agentes</TabsTrigger>
</TabsList>
{/* Overview */}
<TabsContent value="overview" className="space-y-6">
<p className="text-sm text-slate-500">Período: {periodLabel[period]}</p>
{/* Top KPI row */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<Card className="bg-gradient-to-br from-blue-500 to-blue-600 text-white">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-blue-100 text-sm">Conversas</p>
<p className="text-3xl font-bold">{conv?.total || 0}</p>
</div>
<MessageSquare className="h-10 w-10 text-blue-200" />
</div>
<div className="mt-2 text-sm text-blue-100">
{conv?.resolved || 0} resolvidas ({conv?.total ? Math.round((conv.resolved / conv.total) * 100) : 0}%)
</div>
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-orange-500 to-orange-600 text-white">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-orange-100 text-sm">Tickets</p>
<p className="text-3xl font-bold">{tick?.total || 0}</p>
</div>
<Ticket className="h-10 w-10 text-orange-200" />
</div>
<div className="mt-2 text-sm text-orange-100">
{tick?.sla_breaches || 0} SLA violados
</div>
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-emerald-500 to-emerald-600 text-white">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-emerald-100 text-sm">Receita Ganha</p>
<p className="text-2xl font-bold">{formatCurrency(deals?.won_value || 0)}</p>
</div>
<DollarSign className="h-10 w-10 text-emerald-200" />
</div>
<div className="mt-2 text-sm text-emerald-100">
{deals?.won || 0} deals {deals?.win_rate_pct || 0}% taxa
</div>
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-violet-500 to-violet-600 text-white">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-violet-100 text-sm">Novos Contatos</p>
<p className="text-3xl font-bold">{contacts?.new_in_period || 0}</p>
</div>
<Users className="h-10 w-10 text-violet-200" />
</div>
<div className="mt-2 text-sm text-violet-100">
{contacts?.leads || 0} leads {contacts?.customers || 0} clientes
</div>
</CardContent>
</Card>
</div>
{/* Detail cards */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle className="text-base flex items-center gap-2">
<MessageSquare className="h-4 w-4 text-blue-500" />
Atendimento
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="bg-slate-50 rounded-lg p-3 text-center">
<p className="text-2xl font-bold text-slate-800">{conv?.avg_handle_minutes || 0}<span className="text-sm font-normal text-slate-500">min</span></p>
<p className="text-xs text-slate-500 mt-1">Tempo Médio de Atendimento</p>
</div>
<div className="bg-slate-50 rounded-lg p-3 text-center">
<p className="text-2xl font-bold text-slate-800">{conv?.channels_used || 0}</p>
<p className="text-xs text-slate-500 mt-1">Canais Utilizados</p>
</div>
<div className="bg-emerald-50 rounded-lg p-3 text-center">
<p className="text-2xl font-bold text-emerald-700">{conv?.resolved || 0}</p>
<p className="text-xs text-slate-500 mt-1">Resolvidas</p>
</div>
<div className="bg-blue-50 rounded-lg p-3 text-center">
<p className="text-2xl font-bold text-blue-700">{conv?.open || 0}</p>
<p className="text-xs text-slate-500 mt-1">Em Aberto</p>
</div>
</div>
{conv?.avg_csat ? (
<div className="flex items-center gap-3 p-3 bg-yellow-50 rounded-lg">
<Star className="h-5 w-5 text-yellow-500 fill-yellow-500" />
<div>
<p className="font-semibold text-slate-800">{Number(conv.avg_csat).toFixed(1)} / 5</p>
<p className="text-xs text-slate-500">CSAT Médio das Conversas</p>
</div>
</div>
) : null}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base flex items-center gap-2">
<TrendingUp className="h-4 w-4 text-emerald-500" />
Pipeline de Vendas
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="bg-emerald-50 rounded-lg p-3 text-center">
<p className="text-2xl font-bold text-emerald-700">{deals?.won || 0}</p>
<p className="text-xs text-slate-500 mt-1">Ganhos</p>
</div>
<div className="bg-red-50 rounded-lg p-3 text-center">
<p className="text-2xl font-bold text-red-700">{deals?.lost || 0}</p>
<p className="text-xs text-slate-500 mt-1">Perdidos</p>
</div>
</div>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-slate-600">Taxa de conversão</span>
<span className="font-semibold">{deals?.win_rate_pct || 0}%</span>
</div>
<Progress value={deals?.win_rate_pct || 0} className="h-3" />
</div>
<div className="p-3 bg-slate-50 rounded-lg">
<p className="text-xs text-slate-500">Valor Total Ganho</p>
<p className="text-xl font-bold text-emerald-600">{formatCurrency(deals?.won_value || 0)}</p>
</div>
</CardContent>
</Card>
</div>
</TabsContent>
{/* CSAT */}
<TabsContent value="csat" className="space-y-6">
{csatSummary ? (
<>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Card className="bg-gradient-to-br from-yellow-400 to-orange-500 text-white">
<CardContent className="p-5 text-center">
<Star className="h-10 w-10 mx-auto mb-2 fill-white" />
<p className="text-4xl font-bold">{Number(csatSummary.avg_score).toFixed(1)}</p>
<p className="text-yellow-100 text-sm mt-1">Nota Média / 5</p>
</CardContent>
</Card>
<Card>
<CardContent className="p-5 text-center">
<CheckCircle2 className="h-10 w-10 mx-auto mb-2 text-emerald-500" />
<p className="text-4xl font-bold text-emerald-600">{csatSummary.satisfaction_rate_pct}%</p>
<p className="text-slate-500 text-sm mt-1">Taxa de Satisfação (4-5 )</p>
</CardContent>
</Card>
<Card>
<CardContent className="p-5 text-center">
<Users className="h-10 w-10 mx-auto mb-2 text-blue-500" />
<p className="text-4xl font-bold text-blue-600">{csatSummary.total_responses}</p>
<p className="text-slate-500 text-sm mt-1">Total de Avaliações</p>
</CardContent>
</Card>
</div>
<Card>
<CardHeader>
<CardTitle>Distribuição de Notas</CardTitle>
<CardDescription>Período: {periodLabel[period]}</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
{csatBar("⭐⭐⭐⭐⭐", csatSummary.distribution[5], csatSummary.total_responses, "[&>div]:bg-emerald-500")}
{csatBar("⭐⭐⭐⭐", csatSummary.distribution[4], csatSummary.total_responses, "[&>div]:bg-green-400")}
{csatBar("⭐⭐⭐", csatSummary.distribution[3], csatSummary.total_responses, "[&>div]:bg-yellow-400")}
{csatBar("⭐⭐", csatSummary.distribution["1-2"], csatSummary.total_responses, "[&>div]:bg-red-400")}
</CardContent>
</Card>
</>
) : (
<Card>
<CardContent className="py-16 text-center">
<Star className="h-12 w-12 mx-auto mb-3 text-slate-300" />
<p className="text-slate-500">Nenhuma avaliação no período</p>
</CardContent>
</Card>
)}
</TabsContent>
{/* SLA */}
<TabsContent value="sla" className="space-y-6">
{slaReport && (
<>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Card className={`${slaReport.protocols.compliance_rate_pct >= 90 ? "bg-gradient-to-br from-emerald-500 to-emerald-600" : slaReport.protocols.compliance_rate_pct >= 70 ? "bg-gradient-to-br from-yellow-500 to-yellow-600" : "bg-gradient-to-br from-red-500 to-red-600"} text-white`}>
<CardContent className="p-5 text-center">
<Shield className="h-10 w-10 mx-auto mb-2 opacity-80" />
<p className="text-4xl font-bold">{slaReport.protocols.compliance_rate_pct}%</p>
<p className="text-sm opacity-80 mt-1">Compliance de SLA</p>
</CardContent>
</Card>
<Card>
<CardContent className="p-5 text-center">
<CheckCircle2 className="h-10 w-10 mx-auto mb-2 text-emerald-500" />
<p className="text-4xl font-bold text-emerald-600">{slaReport.protocols.compliant}</p>
<p className="text-slate-500 text-sm mt-1">Protocolos dentro do SLA</p>
</CardContent>
</Card>
<Card>
<CardContent className="p-5 text-center">
<AlertTriangle className="h-10 w-10 mx-auto mb-2 text-red-500" />
<p className="text-4xl font-bold text-red-600">{slaReport.protocols.breached}</p>
<p className="text-slate-500 text-sm mt-1">Violações de SLA</p>
</CardContent>
</Card>
</div>
<Card>
<CardHeader>
<CardTitle>Tickets por Prioridade</CardTitle>
</CardHeader>
<CardContent>
{slaReport.tickets_by_priority.length === 0 ? (
<p className="text-center text-slate-500 py-8">Nenhum ticket no período</p>
) : (
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-slate-50 border-b">
<tr>
<th className="text-left p-3 text-sm font-medium text-slate-600">Prioridade</th>
<th className="text-center p-3 text-sm font-medium text-slate-600">Total</th>
<th className="text-center p-3 text-sm font-medium text-slate-600">Violações</th>
<th className="text-center p-3 text-sm font-medium text-slate-600">Tempo Médio Res.</th>
</tr>
</thead>
<tbody className="divide-y">
{slaReport.tickets_by_priority.map(row => (
<tr key={row.priority} className="hover:bg-slate-50">
<td className="p-3">
<Badge className={getPriorityBadge(row.priority)}>{row.priority}</Badge>
</td>
<td className="p-3 text-center font-medium">{row.total}</td>
<td className="p-3 text-center">
<span className={row.breached > 0 ? "text-red-600 font-semibold" : "text-emerald-600"}>
{row.breached}
</span>
</td>
<td className="p-3 text-center text-slate-600">
{row.avg_resolution_minutes ? (
<div className="flex items-center justify-center gap-1">
<Clock className="h-3 w-3" />
{Number(row.avg_resolution_minutes).toFixed(0)}min
</div>
) : "—"}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</CardContent>
</Card>
</>
)}
</TabsContent>
{/* Agents */}
<TabsContent value="agents">
<Card>
<CardHeader>
<CardTitle>Performance por Agente</CardTitle>
<CardDescription>Período: {periodLabel[period]}</CardDescription>
</CardHeader>
<CardContent className="p-0">
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-slate-50 border-b">
<tr>
<th className="text-left p-4 text-sm font-medium text-slate-600">Agente</th>
<th className="text-center p-4 text-sm font-medium text-slate-600">Conversas</th>
<th className="text-center p-4 text-sm font-medium text-slate-600">Resolvidas</th>
<th className="text-center p-4 text-sm font-medium text-slate-600">CSAT</th>
<th className="text-center p-4 text-sm font-medium text-slate-600">TMA</th>
<th className="text-center p-4 text-sm font-medium text-slate-600">Tickets</th>
</tr>
</thead>
<tbody className="divide-y">
{agentReport.map(agent => {
const resolutionRate = agent.conversations_handled > 0
? Math.round((agent.resolved / agent.conversations_handled) * 100)
: 0;
return (
<tr key={agent.agent_id} className="hover:bg-slate-50 transition-colors">
<td className="p-4 font-medium text-slate-800">{agent.agent_name}</td>
<td className="p-4 text-center">{agent.conversations_handled}</td>
<td className="p-4 text-center">
<div className="flex flex-col items-center">
<span className="font-medium">{agent.resolved}</span>
<span className="text-xs text-slate-500">{resolutionRate}%</span>
</div>
</td>
<td className="p-4 text-center">
{agent.avg_csat ? (
<div className="flex items-center justify-center gap-1 text-yellow-600 font-semibold">
<Star className="h-3 w-3 fill-current" />
{Number(agent.avg_csat).toFixed(1)}
</div>
) : <span className="text-slate-400"></span>}
</td>
<td className="p-4 text-center text-slate-600">
{agent.avg_handle_minutes ? `${Number(agent.avg_handle_minutes).toFixed(0)}min` : "—"}
</td>
<td className="p-4 text-center">
<span className="text-sm">{agent.tickets_handled} / {agent.tickets_resolved}</span>
</td>
</tr>
);
})}
{agentReport.length === 0 && (
<tr>
<td colSpan={6} className="text-center py-12 text-slate-500">
Nenhum dado de agente no período
</td>
</tr>
)}
</tbody>
</table>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</main>
</div>
</BrowserFrame>
);
}

View File

@ -1,478 +0,0 @@
import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { useXosSocket } from "@/hooks/use-xos-socket";
import { Link } from "wouter";
import { BrowserFrame } from "@/components/Browser/BrowserFrame";
import {
Users, MessageSquare, Ticket, Clock, AlertTriangle, CheckCircle2,
TrendingUp, ChevronRight, RefreshCw, Activity, UserCheck, PhoneOff,
BarChart3, Inbox, Star, ArrowLeft, Circle
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Progress } from "@/components/ui/progress";
interface SupervisorOverview {
conversations: {
open: number;
pending: number;
resolved_today: number;
unassigned: number;
avg_handle_time_minutes: number;
};
tickets: {
open: number;
resolved_today: number;
urgent_open: number;
sla_breached: number;
};
agents_active: Array<{ agent_id: string; active_conversations: number }>;
generated_at: string;
}
interface QueueStat {
id: number;
name: string;
color: string;
active_conversations: number;
waiting: number;
total_agents: number;
avg_first_response_minutes: number;
}
interface AgentStat {
agent_id: string;
agent_name: string;
avatar: string | null;
active_conversations: number;
resolved_today: number;
avg_csat: number;
avg_handle_time_minutes: number;
queues: string[];
}
interface LiveConversation {
id: number;
contact_name: string;
contact_phone: string;
queue_name: string;
queue_color: string;
channel: string;
status: string;
assigned_to: string | null;
age_minutes: number;
last_message: string;
}
export default function XosSupervisor() {
const [activeTab, setActiveTab] = useState("overview");
const [queueFilter, setQueueFilter] = useState("all");
const [autoRefresh] = useState(true);
const { connected: socketConnected, supervisorStats: liveStats } = useXosSocket();
const refetchInterval = autoRefresh ? 30000 : false;
const { data: overview, isLoading: loadingOverview, refetch: refetchOverview } = useQuery<SupervisorOverview>({
queryKey: ["/api/xos/supervisor/overview"],
refetchInterval,
});
const { data: queues = [] } = useQuery<QueueStat[]>({
queryKey: ["/api/xos/supervisor/queues"],
refetchInterval,
});
const { data: agents = [] } = useQuery<AgentStat[]>({
queryKey: ["/api/xos/supervisor/agents", queueFilter],
queryFn: async () => {
const url = queueFilter === "all"
? "/api/xos/supervisor/agents"
: `/api/xos/supervisor/agents?queueId=${queueFilter}`;
const res = await fetch(url);
return res.json();
},
refetchInterval,
});
const { data: liveConvs = [] } = useQuery<LiveConversation[]>({
queryKey: ["/api/xos/supervisor/conversations/live", queueFilter],
queryFn: async () => {
const url = queueFilter === "all"
? "/api/xos/supervisor/conversations/live"
: `/api/xos/supervisor/conversations/live?queueId=${queueFilter}`;
const res = await fetch(url);
return res.json();
},
refetchInterval,
});
const getQueueColor = (color: string) => {
const map: Record<string, string> = {
blue: "bg-blue-500", green: "bg-green-500", red: "bg-red-500",
yellow: "bg-yellow-500", purple: "bg-purple-500", orange: "bg-orange-500",
cyan: "bg-cyan-500", pink: "bg-pink-500",
};
return map[color] || "bg-slate-500";
};
const getAgentCsatColor = (score: number) => {
if (!score) return "text-slate-400";
if (score >= 4.5) return "text-emerald-600";
if (score >= 3.5) return "text-yellow-600";
return "text-red-600";
};
const getAgeColor = (minutes: number) => {
if (minutes < 10) return "text-emerald-600";
if (minutes < 30) return "text-yellow-600";
return "text-red-600";
};
if (loadingOverview) {
return (
<BrowserFrame>
<div className="flex items-center justify-center h-screen">
<div className="text-center">
<div className="w-10 h-10 border-4 border-blue-600 border-t-transparent rounded-full animate-spin mx-auto" />
<p className="mt-4 text-slate-500">Carregando dados do supervisor...</p>
</div>
</div>
</BrowserFrame>
);
}
// Prefer real-time socket stats over polling data when available
const conv = overview?.conversations;
const tick = overview?.tickets;
const totalActive = liveStats?.openConversations ?? ((conv?.open || 0) + (conv?.pending || 0));
const resolvedToday = liveStats?.resolvedToday ?? conv?.resolved_today ?? 0;
const urgentTickets = liveStats?.urgentTickets ?? tick?.urgent_open ?? 0;
const agentsOnline = liveStats?.agentsOnline ?? overview?.agents_active?.length ?? 0;
return (
<BrowserFrame>
<div className="min-h-screen bg-slate-50">
{/* Header */}
<header className="sticky top-0 z-50 bg-white border-b shadow-sm">
<div className="max-w-7xl mx-auto px-4 py-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Link href="/xos">
<Button variant="ghost" size="icon">
<ArrowLeft className="h-4 w-4" />
</Button>
</Link>
<div className="bg-gradient-to-br from-indigo-600 to-purple-700 p-2 rounded-xl">
<Activity className="h-5 w-5 text-white" />
</div>
<div>
<h1 className="text-xl font-bold text-slate-800">Monitor de Supervisor</h1>
<p className="text-xs text-slate-500">
Atualizado às {overview ? new Date(overview.generated_at).toLocaleTimeString("pt-BR") : "—"}
{socketConnected
? <span className="ml-2 text-emerald-600"> tempo real</span>
: autoRefresh && <span className="ml-2 text-yellow-600"> polling 30s</span>
}
</p>
</div>
</div>
<div className="flex items-center gap-2">
<Select value={queueFilter} onValueChange={setQueueFilter}>
<SelectTrigger className="w-44">
<SelectValue placeholder="Todas as filas" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Todas as filas</SelectItem>
{queues.map(q => (
<SelectItem key={q.id} value={String(q.id)}>{q.name}</SelectItem>
))}
</SelectContent>
</Select>
<Button variant="outline" size="icon" onClick={() => refetchOverview()}>
<RefreshCw className="h-4 w-4" />
</Button>
</div>
</div>
</div>
</header>
<main className="max-w-7xl mx-auto px-4 py-6 space-y-6">
{/* KPI Cards */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<Card className="bg-gradient-to-br from-blue-500 to-blue-600 text-white">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-blue-100 text-sm">Em Atendimento</p>
<p className="text-3xl font-bold">{totalActive}</p>
</div>
<MessageSquare className="h-10 w-10 text-blue-200" />
</div>
<div className="mt-2 flex items-center gap-2 text-sm text-blue-100">
<span>{conv?.unassigned || 0} sem agente</span>
<span></span>
<span>{conv?.pending || 0} pendentes</span>
</div>
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-emerald-500 to-emerald-600 text-white">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-emerald-100 text-sm">Resolvidos Hoje</p>
<p className="text-3xl font-bold">{resolvedToday}</p>
</div>
<CheckCircle2 className="h-10 w-10 text-emerald-200" />
</div>
<div className="mt-2 text-sm text-emerald-100">
TMA: {conv?.avg_handle_time_minutes || 0}min
</div>
</CardContent>
</Card>
<Card className={`text-white ${(tick?.sla_breached || 0) > 0 ? "bg-gradient-to-br from-red-500 to-red-600" : "bg-gradient-to-br from-orange-500 to-orange-600"}`}>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-orange-100 text-sm">Tickets Urgentes</p>
<p className="text-3xl font-bold">{urgentTickets}</p>
</div>
<Ticket className="h-10 w-10 text-orange-200" />
</div>
<div className="mt-2 flex items-center gap-2 text-sm text-orange-100">
{(tick?.sla_breached || 0) > 0 && (
<span className="flex items-center gap-1 text-red-200">
<AlertTriangle className="h-3 w-3" />
{tick?.sla_breached} SLA violado
</span>
)}
{(tick?.sla_breached || 0) === 0 && <span>SLA OK</span>}
</div>
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-violet-500 to-violet-600 text-white">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-violet-100 text-sm">Agentes Ativos</p>
<p className="text-3xl font-bold">{agentsOnline}</p>
</div>
<Users className="h-10 w-10 text-violet-200" />
</div>
<div className="mt-2 text-sm text-violet-100">
{agents.length} no total
</div>
</CardContent>
</Card>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList>
<TabsTrigger value="overview">Filas</TabsTrigger>
<TabsTrigger value="agents">Agentes</TabsTrigger>
<TabsTrigger value="live">Ao Vivo ({liveConvs.length})</TabsTrigger>
</TabsList>
{/* Queues Tab */}
<TabsContent value="overview" className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{queues.map(queue => {
const occupancy = queue.total_agents > 0
? Math.min(100, Math.round((queue.active_conversations / queue.total_agents) * 100))
: 0;
return (
<Card key={queue.id} className="hover:shadow-md transition-shadow">
<CardContent className="p-4">
<div className="flex items-center gap-3 mb-3">
<div className={`w-3 h-3 rounded-full ${getQueueColor(queue.color)}`} />
<h3 className="font-semibold text-slate-800">{queue.name}</h3>
<Badge variant="outline" className="ml-auto text-xs">
{queue.total_agents} agentes
</Badge>
</div>
<div className="grid grid-cols-3 gap-3 text-center mb-3">
<div>
<p className="text-2xl font-bold text-blue-600">{queue.active_conversations}</p>
<p className="text-xs text-slate-500">ativos</p>
</div>
<div>
<p className={`text-2xl font-bold ${queue.waiting > 0 ? "text-orange-600" : "text-slate-400"}`}>
{queue.waiting}
</p>
<p className="text-xs text-slate-500">aguardando</p>
</div>
<div>
<p className="text-2xl font-bold text-slate-600">
{queue.avg_first_response_minutes ? `${queue.avg_first_response_minutes}m` : "—"}
</p>
<p className="text-xs text-slate-500">TMP</p>
</div>
</div>
<div className="space-y-1">
<div className="flex justify-between text-xs text-slate-500">
<span>Ocupação</span>
<span>{occupancy}%</span>
</div>
<Progress value={occupancy} className="h-2" />
</div>
</CardContent>
</Card>
);
})}
{queues.length === 0 && (
<div className="col-span-3 text-center py-12 text-slate-500">
<Inbox className="h-12 w-12 mx-auto mb-3 text-slate-300" />
<p>Nenhuma fila configurada</p>
</div>
)}
</div>
</TabsContent>
{/* Agents Tab */}
<TabsContent value="agents">
<Card>
<CardContent className="p-0">
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-slate-50 border-b">
<tr>
<th className="text-left p-4 text-sm font-medium text-slate-600">Agente</th>
<th className="text-center p-4 text-sm font-medium text-slate-600">Em Atend.</th>
<th className="text-center p-4 text-sm font-medium text-slate-600">Resolvidos Hoje</th>
<th className="text-center p-4 text-sm font-medium text-slate-600">CSAT Médio</th>
<th className="text-center p-4 text-sm font-medium text-slate-600">TMA (min)</th>
<th className="text-left p-4 text-sm font-medium text-slate-600">Filas</th>
</tr>
</thead>
<tbody className="divide-y">
{agents.map(agent => (
<tr key={agent.agent_id} className="hover:bg-slate-50 transition-colors">
<td className="p-4">
<div className="flex items-center gap-3">
<Avatar className="h-9 w-9">
<AvatarFallback className="bg-gradient-to-br from-blue-500 to-indigo-500 text-white text-sm">
{(agent.agent_name || "?").slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<div>
<p className="font-medium text-slate-800">{agent.agent_name}</p>
<div className="flex items-center gap-1">
<Circle className={`h-2 w-2 fill-current ${agent.active_conversations > 0 ? "text-emerald-500" : "text-slate-300"}`} />
<span className="text-xs text-slate-500">
{agent.active_conversations > 0 ? "online" : "disponível"}
</span>
</div>
</div>
</div>
</td>
<td className="p-4 text-center">
<span className={`text-xl font-bold ${agent.active_conversations > 5 ? "text-red-600" : agent.active_conversations > 2 ? "text-yellow-600" : "text-slate-700"}`}>
{agent.active_conversations}
</span>
</td>
<td className="p-4 text-center">
<span className="text-lg font-semibold text-emerald-600">{agent.resolved_today || 0}</span>
</td>
<td className="p-4 text-center">
<div className={`flex items-center justify-center gap-1 font-semibold ${getAgentCsatColor(agent.avg_csat)}`}>
{agent.avg_csat ? (
<>
<Star className="h-3 w-3 fill-current" />
{Number(agent.avg_csat).toFixed(1)}
</>
) : "—"}
</div>
</td>
<td className="p-4 text-center text-slate-600">
{agent.avg_handle_time_minutes ? `${agent.avg_handle_time_minutes}m` : "—"}
</td>
<td className="p-4">
<div className="flex flex-wrap gap-1">
{(agent.queues || []).filter(Boolean).map((q, i) => (
<Badge key={i} variant="outline" className="text-xs">{q}</Badge>
))}
</div>
</td>
</tr>
))}
{agents.length === 0 && (
<tr>
<td colSpan={6} className="text-center py-12 text-slate-500">
<UserCheck className="h-12 w-12 mx-auto mb-3 text-slate-300" />
Nenhum agente encontrado
</td>
</tr>
)}
</tbody>
</table>
</div>
</CardContent>
</Card>
</TabsContent>
{/* Live Conversations Tab */}
<TabsContent value="live">
<div className="space-y-2">
{liveConvs.length === 0 && (
<Card>
<CardContent className="py-16 text-center">
<MessageSquare className="h-12 w-12 mx-auto mb-3 text-slate-300" />
<p className="text-slate-500">Nenhuma conversa em aberto</p>
</CardContent>
</Card>
)}
{liveConvs.map(conv => (
<Card key={conv.id} className={`hover:shadow-md transition-shadow ${Number(conv.age_minutes) > 30 ? "border-red-200 bg-red-50" : ""}`}>
<CardContent className="p-4">
<div className="flex items-center gap-4">
<Avatar className="h-10 w-10 flex-shrink-0">
<AvatarFallback className="bg-gradient-to-br from-slate-400 to-slate-600 text-white">
{(conv.contact_name || "?").slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<p className="font-medium text-slate-800">{conv.contact_name || "Desconhecido"}</p>
<Badge variant="outline" className="text-xs">{conv.channel}</Badge>
</div>
<p className="text-sm text-slate-500 truncate">{conv.last_message || "—"}</p>
</div>
<div className="flex items-center gap-4 flex-shrink-0">
{conv.queue_name && (
<Badge variant="secondary" className="text-xs">{conv.queue_name}</Badge>
)}
{conv.assigned_to ? (
<div className="flex items-center gap-1 text-xs text-slate-500">
<UserCheck className="h-3 w-3" />
<span className="hidden sm:block">{conv.assigned_to}</span>
</div>
) : (
<div className="flex items-center gap-1 text-xs text-orange-500">
<PhoneOff className="h-3 w-3" />
<span>Sem agente</span>
</div>
)}
<div className={`flex items-center gap-1 text-sm font-medium ${getAgeColor(Number(conv.age_minutes))}`}>
<Clock className="h-4 w-4" />
{Number(conv.age_minutes).toFixed(0)}m
</div>
</div>
</div>
</CardContent>
</Card>
))}
</div>
</TabsContent>
</Tabs>
</main>
</div>
</BrowserFrame>
);
}

View File

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

View File

@ -8,22 +8,10 @@ if (!process.env.DATABASE_URL) {
);
}
// CORREÇÃO: Usar Pool em vez de Client único para melhor performance
// e evitar queries travadas quando conexão falha
const pool = new pg.Pool({
const client = new pg.Client({
connectionString: process.env.DATABASE_URL,
max: 20, // máximo de conexões no pool
idleTimeoutMillis: 30000, // tempo máximo de inatividade
connectionTimeoutMillis: 5000, // timeout de conexão: 5s
statement_timeout: 10000, // timeout de query: 10s
});
// Log de erros do pool
pool.on('error', (err) => {
console.error('[DB Pool] Erro inesperado:', err.message);
});
client.connect();
// Exportar pool para uso no session store
export { pool };
export const db = drizzle({ client: pool, schema });
export const db = drizzle({ client, schema });

View File

@ -1,72 +0,0 @@
#!/bin/bash
# ==============================================
# Arcádia Suite - Script de Deploy para VPS
# ==============================================
set -e
echo "=========================================="
echo " Arcádia Suite - Deploy VPS"
echo "=========================================="
if ! command -v docker &> /dev/null; then
echo "Docker não encontrado. Instalando..."
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
echo "Docker instalado! Faça logout e login novamente para aplicar as permissões."
exit 1
fi
if ! command -v docker-compose &> /dev/null && ! docker compose version &> /dev/null; then
echo "Docker Compose não encontrado. Instalando..."
sudo apt-get update && sudo apt-get install -y docker-compose-plugin
fi
if [ ! -f .env ]; then
echo "Arquivo .env não encontrado. Criando a partir do .env.example..."
cp .env.example .env
echo ""
echo "IMPORTANTE: Edite o arquivo .env com suas configurações antes de continuar!"
echo " nano .env"
echo ""
echo "Depois execute este script novamente."
exit 1
fi
echo ""
echo "1/4 - Parando containers existentes..."
docker compose down 2>/dev/null || docker-compose down 2>/dev/null || true
echo ""
echo "2/5 - Construindo a aplicação..."
docker compose build --no-cache 2>/dev/null || docker-compose build --no-cache
echo ""
echo "3/5 - Iniciando o banco de dados..."
docker compose up -d db 2>/dev/null || docker-compose up -d db
echo "Aguardando banco ficar pronto..."
sleep 5
echo ""
echo "4/5 - Aplicando migrations do banco de dados..."
docker compose run --rm app npx drizzle-kit push 2>/dev/null || docker-compose run --rm app npx drizzle-kit push
echo ""
echo "5/5 - Iniciando a aplicação..."
docker compose up -d 2>/dev/null || docker-compose up -d
sleep 10
echo ""
echo "=========================================="
echo " Deploy concluído!"
echo "=========================================="
echo ""
echo " App: http://$(hostname -I | awk '{print $1}'):5000"
echo " DB: PostgreSQL na porta 5432"
echo ""
echo " Comandos úteis:"
echo " Ver logs: docker compose logs -f app"
echo " Parar tudo: docker compose down"
echo " Reiniciar: docker compose restart app"
echo " Status: docker compose ps"
echo ""

View File

@ -1,42 +0,0 @@
# ─── MiroFlow — Aplicação Coolify ─────────────────────────────────────────────
# Conecta aos serviços compartilhados (ollama) via network arcadia-internal
# Requer: docker-compose.shared.yml rodando
name: arcadia-miroflow
services:
# ── MiroFlow (Cientistas via Ollama) ───────────────────────────────────────────
miroflow:
build:
context: .
dockerfile: Dockerfile.miroflow
restart: always
environment:
OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:-http://ollama:11434}
MIROFLOW_PORT: 8006
MIROFLOW_RESEARCHER_MODEL: ${MIROFLOW_RESEARCHER_MODEL:-llama3.1:8b}
networks:
- arcadia-internal
- coolify
labels:
- "traefik.enable=true"
- "traefik.docker.network=coolify"
- "traefik.http.routers.miroflow.entrypoints=https"
- "traefik.http.routers.miroflow.rule=Host(`${MIROFLOW_DOMAIN:-miroflow.onboardbi.com.br}`)"
- "traefik.http.routers.miroflow.tls=true"
- "traefik.http.routers.miroflow.tls.certresolver=letsencrypt"
- "traefik.http.services.miroflow.loadbalancer.server.port=8006"
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8006/health')"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
networks:
arcadia-internal:
external: true
name: arcadia-internal
coolify:
external: true

View File

@ -25,7 +25,6 @@ services:
retries: 10
networks:
- arcadia-internal
- coolify
# ── Redis ────────────────────────────────────────────────────────────────────
redis:
@ -36,7 +35,6 @@ services:
- redis_data:/data
networks:
- arcadia-internal
- coolify
# ── App principal ─────────────────────────────────────────────────────────
app:
@ -62,26 +60,9 @@ services:
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}
# ── Arcádia Plus (Laravel) ─────────────────────────────────────────────
PLUS_HOST: plus
PLUS_PORT: "8080"
PLUS_AUTO_START: "false" # container separado em prod
SSO_PLUS_BASE_URL: http://plus:8080
# ── MiroFlow ──────────────────────────────────────────────────────────
MIROFLOW_HOST: miroflow
MIROFLOW_PORT: "8006"
# ── Apache Superset ───────────────────────────────────────────────────
SUPERSET_HOST: superset
SUPERSET_ADMIN_USER: ${SUPERSET_ADMIN_USER:-admin}
SUPERSET_ADMIN_PASSWORD: ${SUPERSET_ADMIN_PASSWORD}
# ── ERPNext ───────────────────────────────────────────────────────────
ERPNEXT_PROXY_ENABLED: ${ERPNEXT_PROXY_ENABLED:-false}
ERPNEXT_CONTAINER_HOST: erpnext
ERPNEXT_URL: ${ERPNEXT_URL:-http://erpnext:8080}
ERPNEXT_API_KEY: ${ERPNEXT_API_KEY:-}
ERPNEXT_API_SECRET: ${ERPNEXT_API_SECRET:-}
ports:
- "5000:5000"
depends_on:
@ -91,113 +72,12 @@ services:
condition: service_started
networks:
- arcadia-internal
- coolify
- arcadia-public
labels:
- "traefik.enable=true"
- "traefik.docker.network=coolify"
- "traefik.http.routers.arcadia.entrypoints=https"
- "traefik.http.routers.arcadia.rule=Host(`${DOMAIN}`)"
- "traefik.http.routers.arcadia.tls=true"
- "traefik.http.routers.arcadia.tls.certresolver=letsencrypt"
- "traefik.http.services.arcadia.loadbalancer.server.port=5000"
# ── Arcádia Plus (Laravel + MySQL) ──────────────────────────────────────────
# Perfil `plus` — suba com: docker compose --profile plus up
plus-db:
image: mysql:8.0
restart: always
profiles: [plus]
environment:
MYSQL_ROOT_PASSWORD: ${PLUS_DB_ROOT_PASSWORD}
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
# ── 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:
- arcadia-internal
# ── Microserviços Python ─────────────────────────────────────────────────────
contabil:
@ -273,9 +153,6 @@ services:
SERVICE_NAME: embeddings
SERVICE_PORT: 8001
DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/${PGDATABASE:-arcadia}
LITELLM_BASE_URL: http://litellm:4000
LITELLM_API_KEY: ${LITELLM_API_KEY}
OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:-http://ollama:11434}
depends_on:
db:
condition: service_healthy
@ -283,7 +160,7 @@ services:
- arcadia-internal
# ── LiteLLM (gateway unificado de LLM — soberania dos dados) ─────────────────
# Roteia: Ollama (local) → externo (opt-in)
# Roteia: LLMFit (fine-tuned) → Ollama (local) → externo (opt-in)
litellm:
image: ghcr.io/berriai/litellm:main-latest
restart: always
@ -293,10 +170,12 @@ services:
environment:
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
LITELLM_MASTER_KEY: ${LITELLM_API_KEY}
DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/litellm
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:-}
@ -305,7 +184,6 @@ services:
condition: service_healthy
networks:
- arcadia-internal
- coolify
# ── Ollama (LLMs locais — soberania total) ────────────────────────────────────
# OPÇÃO A (padrão): Ollama como container Docker
@ -318,8 +196,10 @@ services:
- ollama_models:/root/.ollama
networks:
- arcadia-internal
# Remova 'profiles: [ai]' para ativar por padrão no deploy
profiles: [ai]
# ── Open WebUI (interface para Ollama) ───────────────────────────────
# ── Open WebUI (interface para Ollama + LLMFit) ───────────────────────────────
open-webui:
image: ghcr.io/open-webui/open-webui:main
restart: always
@ -335,7 +215,7 @@ services:
- litellm
networks:
- arcadia-internal
- coolify
- arcadia-public
labels:
- "traefik.enable=true"
- "traefik.http.routers.webui.rule=Host(`ai.${DOMAIN}`)"
@ -344,88 +224,14 @@ services:
- "traefik.http.services.webui.loadbalancer.server.port=8080"
profiles: [ai]
# ── Superset BI ───────────────────────────────────────────────────────────────
superset:
build:
context: ./docker/superset
dockerfile: Dockerfile
restart: always
environment:
SUPERSET_SECRET_KEY: ${SUPERSET_SECRET_KEY}
SQLALCHEMY_DATABASE_URI: postgresql://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/superset
PYTHONPATH: /app/pythonpath
SUPERSET_ADMIN_USERNAME: ${SUPERSET_ADMIN_USERNAME:-admin}
SUPERSET_ADMIN_PASSWORD: ${SUPERSET_ADMIN_PASSWORD}
SUPERSET_ADMIN_EMAIL: ${SUPERSET_ADMIN_EMAIL:-admin@onboardbi.com.br}
ARCADIA_DATABASE_URL: postgresql+psycopg2://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/${PGDATABASE:-arcadia}
command: ["/bin/bash", "/app/docker/init.sh"]
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
networks:
- arcadia-internal
- coolify
labels:
- "traefik.enable=true"
- "traefik.docker.network=coolify"
- "traefik.http.routers.superset.entrypoints=https"
- "traefik.http.routers.superset.rule=Host(`${SUPERSET_DOMAIN:-bi.onboardbi.com.br}`)"
- "traefik.http.routers.superset.tls=true"
- "traefik.http.routers.superset.tls.certresolver=letsencrypt"
- "traefik.http.services.superset.loadbalancer.server.port=8088"
# ── MiroFlow (Cientistas via Ollama) ───────────────────────────────────────────
miroflow:
build:
context: .
dockerfile: Dockerfile.miroflow
restart: always
environment:
OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:-http://ollama:11434}
MIROFLOW_PORT: 8006
MIROFLOW_RESEARCHER_MODEL: ${MIROFLOW_RESEARCHER_MODEL:-llama3.1:8b}
depends_on:
ollama:
condition: service_started
networks:
- arcadia-internal
- coolify
labels:
- "traefik.enable=true"
- "traefik.docker.network=coolify"
- "traefik.http.routers.miroflow.entrypoints=https"
- "traefik.http.routers.miroflow.rule=Host(`${MIROFLOW_DOMAIN:-miroflow.onboardbi.com.br}`)"
- "traefik.http.routers.miroflow.tls=true"
- "traefik.http.routers.miroflow.tls.certresolver=letsencrypt"
- "traefik.http.services.miroflow.loadbalancer.server.port=8006"
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8006/health')"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
networks:
arcadia-internal:
driver: bridge
coolify:
external: true
arcadia-public:
driver: bridge
volumes:
pgdata:
redis_data:
ollama_models:
open_webui_data:
plus_db:
plus_storage:
superset_home:
erpnext_db:
erpnext_sites:
erpnext_logs:

View File

@ -1,62 +0,0 @@
# ─── Arcádia — Infraestrutura Compartilhada ─────────────────────────────────
# Serviços base: db, redis, ollama
# Usado por: superset, miroflow, app, etc.
name: arcadia-shared
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
# ── Ollama (LLMs locais — soberania total) ────────────────────────────────────
ollama:
image: ollama/ollama:latest
restart: always
volumes:
- ollama_models:/root/.ollama
networks:
- arcadia-internal
- coolify
networks:
arcadia-internal:
driver: bridge
name: arcadia-internal
coolify:
external: true
volumes:
pgdata:
external: true
name: arcadia-prod_pgdata
redis_data:
external: true
name: arcadia-prod_redis_data
ollama_models:

View File

@ -1,49 +0,0 @@
# ─── Superset BI — Aplicação Coolify ───────────────────────────────────────
# Conecta aos serviços compartilhados (db, redis) via network arcadia-internal
# Requer: docker-compose.shared.yml rodando
name: arcadia-superset
services:
# ── Superset BI ───────────────────────────────────────────────────────────────
superset:
image: arcadia-prod-superset:latest
restart: always
environment:
DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/superset
SUPERSET_SECRET_KEY: ${SUPERSET_SECRET_KEY}
SQLALCHEMY_DATABASE_URI: postgresql://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/superset
PYTHONPATH: /app/pythonpath
SUPERSET_ADMIN_USERNAME: ${SUPERSET_ADMIN_USERNAME:-admin}
SUPERSET_ADMIN_PASSWORD: ${SUPERSET_ADMIN_PASSWORD}
SUPERSET_ADMIN_EMAIL: ${SUPERSET_ADMIN_EMAIL:-admin@onboardbi.com.br}
ARCADIA_DATABASE_URL: postgresql+psycopg2://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/${PGDATABASE:-arcadia}
command: ["/bin/bash", "/app/docker/init.sh"]
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
networks:
- arcadia-internal
- coolify
labels:
- "traefik.enable=true"
- "traefik.docker.network=coolify"
- "traefik.http.routers.superset.entrypoints=https"
- "traefik.http.routers.superset.rule=Host(`${SUPERSET_DOMAIN:-bi.onboardbi.com.br}`)"
- "traefik.http.routers.superset.tls=true"
- "traefik.http.routers.superset.tls.certresolver=letsencrypt"
- "traefik.http.services.superset.loadbalancer.server.port=8088"
networks:
arcadia-internal:
external: true
name: arcadia-internal
coolify:
external: true
volumes:
superset_home:
external: true
name: arcadia-prod_superset_home

View File

@ -29,6 +29,8 @@ services:
redis:
image: redis:7-alpine
restart: unless-stopped
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
@ -59,23 +61,12 @@ services:
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
LITELLM_BASE_URL: http://litellm:4000
LITELLM_API_KEY: ${LITELLM_API_KEY:-arcadia-internal}
# MetaSet BI
METASET_HOST: metaset
METASET_PORT: 8100
# Kernel Arcadia (Service Discovery)
KERNEL_ENABLED: "true"
KERNEL_AUTOSTART: "true"
COOLIFY_API_TOKEN: "${COOLIFY_API_TOKEN:-}"
ports:
- "5000:5000"
- "5001:5001" # Kernel Arcadia (Service Discovery)
volumes:
- .:/app
- /app/node_modules
- /app/dist
networks:
- arcadia
- coolify
depends_on:
db:
condition: service_healthy
@ -92,16 +83,6 @@ services:
context: .
dockerfile: Dockerfile.python
restart: unless-stopped
labels:
- "arcadia.discovery.enabled=true"
- "arcadia.name=Motor Contábil"
- "arcadia.type=python"
- "arcadia.category=contabil"
- "arcadia.port=8003"
- "arcadia.capabilities=lancamentos,dre,balancete,razao"
- "arcadia.requires_ia=false"
- "arcadia.health_path=/health"
- "arcadia.description=Contabilidade - Lançamentos, DRE, Balancete, Razão"
environment:
SERVICE_NAME: contabil
SERVICE_PORT: 8003
@ -119,17 +100,6 @@ services:
context: .
dockerfile: Dockerfile.python
restart: unless-stopped
labels:
- "arcadia.discovery.enabled=true"
- "arcadia.name=Motor BI"
- "arcadia.type=python"
- "arcadia.category=bi"
- "arcadia.port=8004"
- "arcadia.capabilities=sql,charts,microbi,cache"
- "arcadia.requires_ia=true"
- "arcadia.ia_policies=bi,lgpd"
- "arcadia.health_path=/health"
- "arcadia.description=Business Intelligence - SQL, Charts, Micro-BI, Cache"
environment:
SERVICE_NAME: bi
SERVICE_PORT: 8004
@ -147,17 +117,6 @@ services:
context: .
dockerfile: Dockerfile.python
restart: unless-stopped
labels:
- "arcadia.discovery.enabled=true"
- "arcadia.name=Motor Automação"
- "arcadia.type=python"
- "arcadia.category=automation"
- "arcadia.port=8005"
- "arcadia.capabilities=scheduler,eventbus,workflow"
- "arcadia.requires_ia=true"
- "arcadia.ia_policies=automation"
- "arcadia.health_path=/health"
- "arcadia.description=Scheduler, Event Bus, Workflow Executor"
environment:
SERVICE_NAME: automation
SERVICE_PORT: 8005
@ -175,17 +134,6 @@ services:
context: .
dockerfile: Dockerfile.python
restart: unless-stopped
labels:
- "arcadia.discovery.enabled=true"
- "arcadia.name=Motor Fiscal"
- "arcadia.type=python"
- "arcadia.category=fiscal"
- "arcadia.port=8002"
- "arcadia.capabilities=nfe,nfce,ncm,cfop,cest,sefaz"
- "arcadia.requires_ia=true"
- "arcadia.ia_policies=fiscal"
- "arcadia.health_path=/health"
- "arcadia.description=NF-e/NFC-e - NCMs, CFOPs, CESTs, SEFAZ"
environment:
SERVICE_NAME: fisco
SERVICE_PORT: 8002
@ -203,17 +151,6 @@ services:
context: .
dockerfile: Dockerfile.python
restart: unless-stopped
labels:
- "arcadia.discovery.enabled=true"
- "arcadia.name=Serviço Embeddings"
- "arcadia.type=python"
- "arcadia.category=ai"
- "arcadia.port=8001"
- "arcadia.capabilities=embeddings,vectorsearch"
- "arcadia.requires_ia=true"
- "arcadia.ia_policies=ai,lgpd"
- "arcadia.health_path=/health"
- "arcadia.description=Embeddings e Vector Search com pgvector"
environment:
SERVICE_NAME: embeddings
SERVICE_PORT: 8001
@ -224,145 +161,46 @@ services:
db:
condition: service_healthy
# ── MetaSet BI (Apache Superset Fork) ────────────────────────────────────────
metaset:
build:
context: ./server/bi/metaset
dockerfile: Dockerfile
restart: unless-stopped
labels:
- "arcadia.discovery.enabled=true"
- "arcadia.name=MetaSet BI"
- "arcadia.type=python"
- "arcadia.category=bi"
- "arcadia.port=8100"
- "arcadia.capabilities=dashboards,charts,sql_lab,datasets,embedding,rls"
- "arcadia.requires_ia=false"
- "arcadia.health_path=/health"
- "arcadia.description=MetaSet BI - Business Intelligence by ArcadiaSuite"
- "arcadia.version=4.1.0-arcadia"
- "traefik.enable=true"
- "traefik.docker.network=coolify"
- "traefik.http.routers.metaset.entrypoints=https"
- "traefik.http.routers.metaset.rule=Host(`bi.onboardbi.com.br`)"
- "traefik.http.routers.metaset.tls=true"
- "traefik.http.routers.metaset.tls.certresolver=letsencrypt"
- "traefik.http.services.metaset.loadbalancer.server.port=8100"
environment:
DATABASE_URL: postgresql://arcadia:arcadia123@db:5432/metaset_db
ARCADIA_DATABASE_URL: postgresql://arcadia:arcadia123@db:5432/arcadia
SUPERSET_SECRET_KEY: ${SUPERSET_SECRET_KEY:-arcadia-superset-secret-change-in-prod}
METASET_ADMIN_USER: ${SUPERSET_ADMIN_USER:-admin}
METASET_ADMIN_EMAIL: ${SUPERSET_ADMIN_EMAIL:-admin@arcadia.app}
METASET_ADMIN_PASSWORD: ${SUPERSET_ADMIN_PASSWORD:-arcadia2026}
REDIS_URL: redis://redis:6379/1
PYTHONPATH: /app
FLASK_ENV: production
ports:
- "8100:8100"
volumes:
- metaset_home:/app/superset_home
- ./server/bi/metaset/branding:/app/superset/static/assets/images/branding:ro
- ./server/bi/metaset/superset_config.py:/app/superset_config.py:ro
extra_hosts:
- "db:10.0.10.2"
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8100/health"]
interval: 30s
timeout: 10s
retries: 5
start_period: 60s
# ── BI-API Gateway (Node.js) ─────────────────────────────────────────────────
bi-api:
build:
context: .
dockerfile: server/bi/Dockerfile
restart: unless-stopped
labels:
- "arcadia.discovery.enabled=true"
- "arcadia.name=BI API Gateway"
- "arcadia.type=node"
- "arcadia.category=bi"
- "arcadia.port=8004"
- "arcadia.capabilities=proxy,embed,api"
- "arcadia.requires_ia=false"
- "arcadia.health_path=/health"
- "arcadia.description=BI Gateway - Proxy para MetaSet e FDB-Bridge"
- "arcadia.version=1.0.0"
environment:
NODE_ENV: production
PORT: 8004
METASET_HOST: metaset
METASET_PORT: 8100
FDB_BRIDGE_HOST: fdb-bridge
FDB_BRIDGE_PORT: 8200
ports:
- "8004:8004"
depends_on:
metaset:
condition: service_healthy
# ── Apache Superset (BI avançado - LEGADO, será removido) ─────────────────────
# ── Apache Superset (BI avançado) ────────────────────────────────────────────
superset:
image: apache/superset:4.1.0
restart: unless-stopped
profiles: [bi]
labels:
- "arcadia.discovery.enabled=true"
- "arcadia.name=Apache Superset"
- "arcadia.type=java"
- "arcadia.category=data"
- "arcadia.port=8088"
- "arcadia.capabilities=dashboards,charts,sql_lab,datasets"
- "arcadia.requires_ia=false"
- "arcadia.health_path=/health"
- "arcadia.description=Apache Superset original (legado)"
- "arcadia.version=4.1.0"
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_EMAIL: ${SUPERSET_ADMIN_EMAIL:-admin@arcadia.app}
SUPERSET_ADMIN_PASSWORD: ${SUPERSET_ADMIN_PASSWORD:-arcadia2026}
SUPERSET_WEBSERVER_PORT: 8088
PYTHONPATH: /app/pythonpath
FLASK_ENV: production
DATABASE_URL: postgresql://arcadia:arcadia123@db:5432/arcadia_superset
ports:
- "8088:8088"
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
extra_hosts:
- "db:10.0.10.2"
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
profiles: [bi]
# ─────────────── PERFIL: ai (Soberania de IA) ────────────────────────────────
# ── Ollama (LLMs locais) ─────────────────────────────────────────────────────
ollama:
image: ollama/ollama:latest
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-ia1upsekrad96at5hq97e4qa:11434
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"
@ -370,6 +208,8 @@ services:
- "3001:8080"
volumes:
- open_webui_data:/app/backend/data
depends_on:
- ollama
profiles: [ai]
# ── LiteLLM (proxy unificado de LLMs) ───────────────────────────────────────
@ -381,103 +221,13 @@ services:
command: ["--config", "/app/config.yaml", "--port", "4000", "--detailed_debug"]
environment:
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
OLLAMA_BASE_URL: http://ollama-ia1upsekrad96at5hq97e4qa:11434
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:
NEO4J_AUTH: neo4j/${NEO4J_PASSWORD:-arcadia123}
NEO4J_PLUGINS: '["apoc"]'
NEO4J_dbms_security_procedures_unrestricted: apoc.*
volumes:
- neo4j_data:/data
- neo4j_logs:/logs
ports:
- "7474:7474"
- "7687:7687"
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:
pgdata:
ollama_models:
open_webui_data:
superset_home:
metaset_home:
erpnext_db:
erpnext_sites:
erpnext_logs:
neo4j_data:
neo4j_logs:
networks:
arcadia:
driver: bridge
coolify:
external: true
name: coolify

View File

@ -1,56 +0,0 @@
#!/bin/bash
# ─── ERPNext init — cria site e instala app se necessário ─────────────────────
# Executado como entrypoint do container frappe/erpnext:version-15
set -e
SITE_NAME="${FRAPPE_SITE_NAME:-erpnext.local}"
DB_HOST="${DB_HOST:-erpnext-db}"
DB_ROOT_PASSWORD="${ERPNEXT_DB_ROOT_PASSWORD:-erpnext-root-2026}"
ADMIN_PASSWORD="${ERPNEXT_ADMIN_PASSWORD:-admin2026}"
BENCH_DIR="/home/frappe/frappe-bench"
echo "[ERPNext Init] Aguardando MariaDB em ${DB_HOST}..."
until mysqladmin ping -h "${DB_HOST}" -u root --password="${DB_ROOT_PASSWORD}" --silent 2>/dev/null; do
echo "[ERPNext Init] MariaDB ainda não disponível — aguardando 3s..."
sleep 3
done
echo "[ERPNext Init] MariaDB disponível."
cd "${BENCH_DIR}"
SITES_DIR="${BENCH_DIR}/sites"
SITE_DIR="${SITES_DIR}/${SITE_NAME}"
if [ ! -f "${SITE_DIR}/site_config.json" ]; then
echo "[ERPNext Init] Criando site ${SITE_NAME}..."
bench new-site "${SITE_NAME}" \
--mariadb-root-password "${DB_ROOT_PASSWORD}" \
--admin-password "${ADMIN_PASSWORD}" \
--db-host "${DB_HOST}" \
--no-mariadb-socket \
--verbose || true
echo "[ERPNext Init] Instalando app erpnext..."
bench --site "${SITE_NAME}" install-app erpnext || true
echo "[ERPNext Init] Ativando modo developer..."
bench --site "${SITE_NAME}" set-config developer_mode 1 || true
echo "[ERPNext Init] Desativando SSL para env local..."
bench --site "${SITE_NAME}" set-config ssl_certificate "" || true
echo "[ERPNext Init] Site criado com sucesso."
else
echo "[ERPNext Init] Site ${SITE_NAME} já existe — pulando criação."
fi
# Configura common_site_config.json para servir na porta 8080
if [ ! -f "${SITES_DIR}/common_site_config.json" ]; then
echo '{"serve_default_site": true, "socketio_port": 9000, "background_workers": 1}' \
> "${SITES_DIR}/common_site_config.json"
fi
echo "[ERPNext Init] Iniciando servidor Frappe na porta 8080..."
exec bench serve --port 8080 --site "${SITE_NAME}"

View File

@ -3,18 +3,45 @@
#
# ESTRATÉGIA DE SOBERANIA DOS DADOS:
# ┌─────────────────────────────────────────────────────────────────────────┐
# │ TIER 1 (soberania total): Ollama — modelos open source no servidor │
# │ TIER 2 (opt-in): Providers externos — só com configuração explícita │
# │ TIER 1 (soberania total): LLMFit — modelos fine-tuned locais │
# │ TIER 2 (soberania total): Ollama — modelos open source no servidor │
# │ TIER 3 (opt-in): Providers externos — só com configuração explícita │
# └─────────────────────────────────────────────────────────────────────────┘
# O Manus, Autonomous Agents e todos os serviços chamam APENAS este proxy.
# Nunca chamam APIs externas diretamente.
model_list:
# ── TIER 1: Ollama (LLMs locais — soberania total) ───────────────────────────
- model_name: llama3.2
# ── TIER 1: LLMFit (modelos fine-tuned locais — máxima soberania) ────────────
# Descomente quando o LLMFit estiver rodando no servidor
# O LLMFit expõe API compatível com OpenAI — basta apontar a URL
#
# - model_name: arcadia-finetuned
# litellm_params:
# model: openai/arcadia-v1 # nome do modelo no LLMFit
# api_base: os.environ/LLMFIT_BASE_URL
# api_key: llmfit-internal
#
# - model_name: arcadia-embed
# litellm_params:
# model: openai/arcadia-embed-v1 # modelo de embeddings fine-tuned
# api_base: os.environ/LLMFIT_BASE_URL
# api_key: llmfit-internal
# ── TIER 2: Ollama (LLMs locais — soberania total) ───────────────────────────
- model_name: llama3.3
litellm_params:
model: ollama/llama3.2:3b
model: ollama/llama3.3
api_base: os.environ/OLLAMA_BASE_URL
- model_name: qwen2.5-coder
litellm_params:
model: ollama/qwen2.5-coder:7b
api_base: os.environ/OLLAMA_BASE_URL
- model_name: deepseek-r1
litellm_params:
model: ollama/deepseek-r1:7b
api_base: os.environ/OLLAMA_BASE_URL
- model_name: nomic-embed-text
@ -22,13 +49,7 @@ model_list:
model: ollama/nomic-embed-text
api_base: os.environ/OLLAMA_BASE_URL
# Alias OpenAI-compatible para embedding (Manus usa este nome por padrão)
- model_name: text-embedding-3-small
litellm_params:
model: ollama/nomic-embed-text
api_base: os.environ/OLLAMA_BASE_URL
# ── TIER 2: OpenAI (opt-in — só ativo se OPENAI_API_KEY configurado) ─────────
# ── TIER 3: OpenAI (opt-in — só ativo se OPENAI_API_KEY configurado) ─────────
- model_name: gpt-4o
litellm_params:
model: openai/gpt-4o
@ -39,35 +60,42 @@ model_list:
model: openai/gpt-4o-mini
api_key: os.environ/OPENAI_API_KEY
# ── TIER 2: Anthropic (opt-in — descomente para habilitar) ───────────────────
# ── TIER 3: Anthropic (opt-in — descomente para habilitar) ───────────────────
# - model_name: claude-sonnet
# litellm_params:
# model: anthropic/claude-sonnet-4-6
# api_key: os.environ/ANTHROPIC_API_KEY
# ── TIER 2: 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
# litellm_params:
# model: groq/llama-3.3-70b-versatile
# api_key: os.environ/GROQ_API_KEY
# ── Modelo padrão do Arcádia (Manus usa este) ─────────────────────────────────
# Prioridade: LLMFit → OpenAI (se configurado) → Ollama (sempre disponível)
# Para soberania total: remova o fallback para gpt-4o-mini
- model_name: arcadia-default
litellm_params:
model: ollama/llama3.2:3b
model: ollama/llama3.3
api_base: os.environ/OLLAMA_BASE_URL
model_info:
# fallbacks: ["gpt-4o-mini"] # descomente para habilitar fallback externo
router_settings:
routing_strategy: least-busy
fallbacks:
- {"gpt-4o": ["llama3.2"]}
- {"gpt-4o-mini": ["llama3.2"]}
- {"arcadia-default": ["llama3.2"]}
fallbacks:
- {"gpt-4o": ["llama3.3"]}
- {"gpt-4o-mini": ["llama3.3"]}
- {"arcadia-default": ["llama3.3"]}
litellm_settings:
drop_params: true
request_timeout: 120
set_verbose: false
# Loga todas as chamadas no banco — essencial para auditoria e soberania
success_callback: ["langfuse"]
failure_callback: ["langfuse"]
general_settings:
master_key: os.environ/LITELLM_MASTER_KEY

View File

@ -1,5 +0,0 @@
FROM apache/superset:latest
USER root
RUN pip install psycopg2-binary && \
sed -i 's/include-system-site-packages = false/include-system-site-packages = true/' /app/.venv/pyvenv.cfg
USER superset

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

View File

@ -1,83 +0,0 @@
#!/bin/bash
# Arcádia Suite — Apache Superset Init Script
# Executado ao iniciar o container
set -e
SUPERSET_ADMIN_USER="${SUPERSET_ADMIN_USERNAME:-${SUPERSET_ADMIN_USER:-admin}}"
SUPERSET_ADMIN_EMAIL="${SUPERSET_ADMIN_EMAIL:-admin@arcadia.app}"
SUPERSET_ADMIN_PASSWORD="${SUPERSET_ADMIN_PASSWORD:-arcadia2026}"
ARCADIA_DB_URL="${ARCADIA_DATABASE_URL:-postgresql://arcadia:arcadia123@db:5432/arcadia}"
echo "[Superset Init] Aguardando PostgreSQL..."
cat > /tmp/pgcheck.py << 'EOF'
import psycopg2, os, sys
url = os.environ.get('SQLALCHEMY_DATABASE_URI', 'postgresql://arcadia:arcadia123@db:5432/superset')
try:
psycopg2.connect(url)
except Exception:
sys.exit(1)
EOF
until python /tmp/pgcheck.py 2>/dev/null; do
sleep 2
done
echo "[Superset Init] PostgreSQL disponível!"
echo "[Superset Init] Rodando migrações do banco..."
superset db upgrade
echo "[Superset Init] Criando admin..."
superset fab create-admin \
--username "${SUPERSET_ADMIN_USER}" \
--firstname "Arcádia" \
--lastname "Admin" \
--email "${SUPERSET_ADMIN_EMAIL}" \
--password "${SUPERSET_ADMIN_PASSWORD}" 2>/dev/null || echo "[Superset Init] Admin já existe."
echo "[Superset Init] Inicializando roles e permissões..."
superset init
echo "[Superset Init] Registrando banco Arcádia Suite..."
python - <<PYEOF
import sys
try:
from superset import create_app
from superset.extensions import db as sdb
from superset.models.core import Database as SupersetDB
app = create_app()
with app.app_context():
existing = sdb.session.query(SupersetDB).filter_by(database_name="Arcádia Suite").first()
if not existing:
new_db = SupersetDB(
database_name="Arcádia Suite",
sqlalchemy_uri="${ARCADIA_DB_URL}",
expose_in_sqllab=True,
allow_run_async=True,
allow_csv_upload=False,
allow_ctas=False,
allow_cvas=False,
allow_dml=False,
extra='{"metadata_params":{},"engine_params":{},"metadata_cache_timeout":{},"schemas_allowed_for_file_upload":[]}',
)
sdb.session.add(new_db)
sdb.session.commit()
print("[Superset Init] Banco 'Arcádia Suite' registrado com sucesso!")
else:
print("[Superset Init] Banco 'Arcádia Suite' já está registrado.")
except Exception as e:
print(f"[Superset Init] Aviso ao registrar banco: {e}")
sys.exit(0)
PYEOF
echo "[Superset Init] Iniciando servidor Superset..."
exec gunicorn \
--bind "0.0.0.0:${SUPERSET_WEBSERVER_PORT:-8088}" \
--access-logfile - \
--error-logfile - \
--workers "${SUPERSET_WORKERS:-4}" \
--worker-class gthread \
--threads 2 \
--timeout 120 \
--limit-request-line 0 \
--limit-request-field_size 0 \
"superset.app:create_app()"

View File

@ -1,62 +0,0 @@
"""
Arcádia Suite Apache Superset Configuration
Habilita embedding nativo via Guest Token JWT
"""
import os
# ── Segurança ─────────────────────────────────────────────────────────────────
SECRET_KEY = os.environ.get("SUPERSET_SECRET_KEY", "change-in-production-use-openssl-rand-hex-32")
# ── Banco de metadados do Superset ────────────────────────────────────────────
SQLALCHEMY_DATABASE_URI = os.environ.get(
"SQLALCHEMY_DATABASE_URI",
os.environ.get("DATABASE_URL", "postgresql://arcadia:arcadia123@db:5432/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": "*"}},
}
# ── Feature Flags ─────────────────────────────────────────────────────────────
FEATURE_FLAGS = {
"EMBEDDED_SUPERSET": True, # Embedding via Guest Token
"ENABLE_TEMPLATE_PROCESSING": True, # Jinja templates em queries
"ALERT_REPORTS": True, # Alertas automáticos
"DRILL_TO_DETAIL": True, # Drill-down em charts
"DRILL_BY": True, # Drill-by em charts
"DASHBOARD_NATIVE_FILTERS": True, # Filtros nativos em dashboards
"DASHBOARD_CROSS_FILTERS": True, # Cross-filter entre charts
"ENABLE_JAVASCRIPT_CONTROLS": False, # Desabilitado por segurança
}
# ── Cache ─────────────────────────────────────────────────────────────────────
CACHE_CONFIG = {
"CACHE_TYPE": "SimpleCache",
"CACHE_DEFAULT_TIMEOUT": 300, # 5 minutos
}
# ── Timeout de queries ────────────────────────────────────────────────────────
SUPERSET_WEBSERVER_TIMEOUT = 300
SQLLAB_TIMEOUT = 300
SQLLAB_ASYNC_TIME_LIMIT_SEC = 300
# ── Branding Arcádia ──────────────────────────────────────────────────────────
APP_NAME = "Arcádia Insights"
LOGO_TARGET_PATH = "/"
FAVICONS = [{"href": "/static/assets/images/favicon.png"}]
# ── Segurança de sessão ───────────────────────────────────────────────────────
SESSION_COOKIE_SAMESITE = "Lax"
SESSION_COOKIE_SECURE = False # True em prod com HTTPS
WTF_CSRF_ENABLED = True
WTF_CSRF_EXEMPT_LIST = ["superset.views.core.log"]
# ── Guest Token (para embedding) ──────────────────────────────────────────────
GUEST_TOKEN_JWT_EXP_SECONDS = 300 # 5 minutos — frontend renova automaticamente
GUEST_ROLE_NAME = "Public"
GUEST_TOKEN_JWT_ALGO = "HS256"
GUEST_TOKEN_HEADER_NAME = "X-GuestToken"

View File

@ -1,58 +0,0 @@
# ===========================================
# Arcádia Suite - Variáveis de Ambiente
# ===========================================
# Copie este arquivo para .env na raiz do projeto
# e preencha com seus valores
# -----------------------------
# Banco de Dados PostgreSQL
# -----------------------------
DATABASE_URL=postgresql://arcadia:SUA_SENHA_AQUI@localhost:5432/arcadia_suite
PGHOST=localhost
PGPORT=5432
PGUSER=arcadia
PGPASSWORD=SUA_SENHA_AQUI
PGDATABASE=arcadia_suite
# -----------------------------
# Ambiente
# -----------------------------
NODE_ENV=production
PORT=5000
# -----------------------------
# Sessão e Segurança
# -----------------------------
# Gere uma chave aleatória: openssl rand -base64 32
SESSION_SECRET=GERE_UMA_CHAVE_ALEATORIA_LONGA_AQUI
# -----------------------------
# ERPNext (opcional)
# -----------------------------
ERPNEXT_URL=https://seu-erpnext.com
ERPNEXT_API_KEY=sua_api_key
ERPNEXT_API_SECRET=seu_api_secret
# -----------------------------
# OpenAI (opcional - para IA)
# -----------------------------
OPENAI_API_KEY=sk-sua-chave-openai
# -----------------------------
# WhatsApp Baileys (opcional)
# -----------------------------
# As sessões são armazenadas automaticamente em ./baileys_sessions
# -----------------------------
# Fiscal - SEFAZ (opcional)
# -----------------------------
# Caminho para o certificado digital A1 (.pfx)
CERT_PATH=/path/to/certificado.pfx
CERT_PASSWORD=senha_do_certificado
SEFAZ_AMBIENTE=2 # 1=Produção, 2=Homologação
# -----------------------------
# ChromaDB (opcional)
# -----------------------------
CHROMA_HOST=localhost
CHROMA_PORT=8000

View File

@ -1,54 +0,0 @@
---
⚠️ **AVISO: DOCUMENTAÇÃO HISTÓRICA**
Este documento foi criado em **Março/2026** como planejamento estratégico.
Pode conter informações desatualizadas em relação ao sistema atual.
Para documentação técnica atualizada, consulte:
- \_SPEC_NAVBAR.md\_ (componentes UI)
- \_KERNEL_SPEC.md\_ (arquitetura do Kernel)
- Código-fonte em \_server/modules/\_
---
# Agente: Monitor de Skills
## Objetivo
Verificar as skills mais executadas nos últimos 7 dias e gerar um resumo com recomendações.
## Passos
1. **Consultar execuções recentes**
- Buscar em `/api/skills` todas as skills ativas do tenant
- Para cada skill, buscar `/api/skills/:id/executions?limit=50`
- Filtrar execuções dos últimos 7 dias
2. **Analisar padrões**
- Contar total de execuções por skill
- Identificar taxa de sucesso (status = "success" / total)
- Destacar skills com taxa de falha > 20%
3. **Gerar relatório**
- Listar top 5 skills mais executadas
- Alertar skills com problemas
- Sugerir skills candidatas a automação (executadas 3+ vezes por dia)
## Saída esperada
```
## Relatório de Skills — últimos 7 dias
### Top 5 mais usadas
1. [nome] — X execuções (Y% sucesso)
...
### Atenção
- [skill] com taxa de falha de Z%
### Candidatas ao OpenClaw
- [skill] — média de N exec/dia
```
## Parâmetros
- `tenant_id` — filtro de tenant (opcional)
- `dias` — janela de análise (padrão: 7)

View File

@ -1,203 +0,0 @@
# 🤖 Referência Rápida para Agentes - Arcádia Suite
> Guia de referência para desenvolvimento autônomo no projeto Arcádia Suite
---
## 📁 Documentação Técnica por Componente
### Navegação (Navbar)
- **Especificação Completa:** [`docs/SPEC_NAVBAR.md`](docs/SPEC_NAVBAR.md)
- **Arquivo Fonte:** `client/src/components/Browser/BrowserFrame.tsx`
- **Hook de Tracking:** `client/src/hooks/use-navigation-tracking.ts`
- **Error Boundary:** `client/src/components/ErrorBoundary.tsx` (tratamento de erros lazy loading)
### Banco de Dados (Schema)
- **Arquivo Principal:** `shared/schema.ts` (7.000+ linhas)
- **Tabelas Principais:**
- `users`, `profiles`, `roles` - Autenticação e RBAC
- `tenants`, `tenantUsers` - Multi-tenancy
- `workspacePages`, `pageBlocks` - Produtividade (estilo Notion)
- `conversations`, `messages` - Chat/Agente
- `whatsappSessions`, `whatsappMessages` - WhatsApp
- `crmPartners`, `crmContracts` - CRM
- `fiscalNcms`, `fiscalGruposTributacao` - Fiscal
- `retailStores`, `posSales`, `mobileDevices` - Retail
- `manusRuns`, `manusSteps` - Agente Manus
- `biDatasets`, `biCharts`, `biDashboards` - BI
### Backend (API Routes)
- **Registro de Rotas:** `server/routes.ts`
- **Autenticação:** `server/auth.ts`
- **Agente Manus:** `server/manus/service.ts` + `server/manus/tools.ts`
- **WhatsApp:** `server/whatsapp/routes.ts` + `server/whatsapp/service.ts`
- **CRM:** `server/crm/routes.ts`
- **Fiscal:** `server/fisco/routes.ts`
- **Retail:** `server/retail/routes.ts`
- **Blackboard (Agentes):** `server/blackboard/routes.ts`
### Frontend (Páginas)
- **Router:** `client/src/App.tsx` (inclui ErrorBoundary + Suspense)
- **Cockpit (Home):** `client/src/pages/Cockpit.tsx`
- **Agent:** `client/src/pages/Agent.tsx`
- **DevCenter:** `client/src/pages/DevCenter.tsx`
- **Crm:** `client/src/pages/Crm.tsx`
- **Fisco:** `client/src/pages/Fisco.tsx`
- **Retail:** `client/src/pages/ArcadiaRetail.tsx`
- **WhatsApp:** `client/src/pages/WhatsApp.tsx`
- **ErrorBoundary:** `client/src/components/ErrorBoundary.tsx`
---
## 🏗️ Arquitetura do Sistema
```
┌─────────────────────────────────────────────────────────────────┐
│ CAMADA DE APRESENTAÇÃO │
│ React 18 + TypeScript + Tailwind CSS + shadcn/ui │
│ 66 páginas / Interface tipo browser com abas │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ CAMADA DE ORQUESTRAÇÃO │
│ Express.js + Socket.IO + Manus Agent │
│ API REST + WebSocket em tempo real │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ CAMADA DE INTELIGÊNCIA │
│ FastAPI (Contábil 8003, BI 8004, Automação 8005) │
│ OpenAI GPT-4o + LiteLLM Gateway + Ollama │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ CAMADA DE DADOS │
│ PostgreSQL 16 + Drizzle ORM + pgvector │
│ Session Store + Multi-tenant │
└─────────────────────────────────────────────────────────────────┘
```
---
## 🛠️ Stack Tecnológico
| Camada | Tecnologia |
|--------|------------|
| **Frontend** | React 18, TypeScript, Tailwind CSS, shadcn/ui |
| **Backend** | Express.js, Socket.IO, Passport.js |
| **Banco** | PostgreSQL 16, Drizzle ORM, pgvector |
| **Python** | FastAPI (microserviços nas portas 8001-8005) |
| **IA** | OpenAI GPT-4o, LiteLLM, Ollama |
| **Deploy** | Docker Compose, Coolify, PM2 |
---
## 🔧 Comandos Úteis
```bash
# Desenvolvimento
npm run dev # Inicia servidor de desenvolvimento
npm run dev:client # Apenas frontend (porta 5000)
# Banco de dados
npm run db:push # Aplica migrations Drizzle
npm run db:apply-rls # Aplica políticas RLS
# Build
npm run build # Build para produção
npm run start # Inicia em produção
# Docker
docker compose up -d # Sobe todos os serviços
docker compose --profile ai up # Sobe com IA (Ollama + LiteLLM)
```
---
## 📋 Convenções de Código
### Nomenclatura
- **Componentes:** PascalCase (ex: `BrowserFrame.tsx`)
- **Hooks:** camelCase com prefixo `use` (ex: `useAuth.tsx`)
- **Utilitários:** camelCase (ex: `navigation-tracking.ts`)
- **Rotas API:** kebab-case (ex: `/api/whatsapp/connect`)
### Estrutura de Pastas
```
client/src/
components/ # Componentes reutilizáveis
pages/ # Páginas da aplicação
hooks/ # Custom hooks
lib/ # Utilitários e configurações
server/
[modulo]/ # Um diretório por módulo
routes.ts # Rotas da API
service.ts # Lógica de negócio
storage.ts # Acesso a dados
```
---
## 🧪 Test IDs
Sempre adicione `data-testid` em elementos interativos:
```tsx
<button
data-testid="button-submit"
onClick={handleSubmit}
>
Enviar
</button>
```
---
## 📚 Documentação Adicional
- **Documentação Técnica Completa:** [`DOCUMENTATION.md`](DOCUMENTATION.md)
- **Mapa do Sistema:** [`MAPA_SISTEMA_ARCADIA.md`](MAPA_SISTEMA_ARCADIA.md)
- **Contexto para Claude:** [`CLAUDE.md`](CLAUDE.md)
- **README:** [`README.md`](README.md)
---
## 🆘 Precisa de Ajuda?
1. Consulte a especificação técnica específica em `docs/`
2. Verifique exemplos similares no código existente
3. Siga os padrões já estabelecidos no projeto
---
*Última atualização: 2026-04-08*
---
## 🐛 Tratamento de Erros - Lazy Loading
### Problema Resolvido: Loading Infinito na Navegação
**Sintoma:** Ao clicar em itens da navbar, se a página tivesse erro (import quebrado, sintaxe inválida), o loading ficava infinito.
**Causa:** `React.lazy()` falha silenciosamente e `Suspense` não tem como capturar o erro.
**Solução:**
1. **ErrorBoundary** (`client/src/components/ErrorBoundary.tsx`):
- Captura erros de renderização, incluindo lazy load
- Mostra UI de fallback com botão de retry
2. **LoadingFallback com Timeout** (`client/src/App.tsx`):
- 3s: mensagem "Isso está demorando..."
- 10s: alerta amarelo com botão recarregar
3. **Integração:**
```tsx
<ErrorBoundary>
<Suspense fallback={<LoadingFallback />}>
<Switch>{/* rotas */}</Switch>
</Suspense>
</ErrorBoundary>
```

View File

@ -1,374 +0,0 @@
# AUDITORIA DO PLANO XOS - Arcádia Suite v1.0
**Data:** 06 de Fevereiro de 2026
**Projeto:** Arcádia Suite - Office Estratégico Empresarial
**Escopo:** Sistema Operacional de Desenvolvimento Autônomo (XOS) com Blackboard Architecture
---
## RESUMO EXECUTIVO
O plano XOS v1.0 foi executado em **4 fases** progressivas, transformando o Arcádia Suite de um sistema de automação empresarial em um **sistema operacional de desenvolvimento autônomo** com 6 agentes de IA especializados, governança de segurança, e pipeline autônomo de ponta a ponta.
| Indicador | Valor |
|---|---|
| Fases planejadas | 4 |
| Fases executadas | 4 (100%) |
| Agentes de IA criados | 6 |
| Tabelas de banco criadas | 31 (XOS) + 3 (Blackboard) = 34 novas tabelas |
| Linhas de código (XOS backend) | ~5.491 linhas |
| Linhas de código (XOS frontend) | ~988 linhas |
| APIs criadas | 22+ endpoints |
| Páginas UI criadas | 2 (Governance Dashboard + Pipeline UI) |
---
## FASE 1 - FUNDAÇÃO (Blackboard + Agentes Base)
**Status: CONCLUIDO**
### O que foi planejado:
- Criar arquitetura Blackboard centralizada para coordenação de agentes
- Implementar agentes base de IA para desenvolvimento autônomo
- Sistema de ferramentas (ToolManager) para interação com código
### O que foi executado:
#### 1.1 Blackboard Service (`server/blackboard/service.ts` - 476 linhas)
- Quadro negro centralizado para coordenação de tarefas entre agentes
- Criação, atribuição e monitoramento de tarefas
- Gerenciamento de artefatos (código gerado, especificações, relatórios)
- Logging estruturado por agente
#### 1.2 Base Agent (`server/blackboard/BaseBlackboardAgent.ts` - 189 linhas)
- Classe abstrata para todos os agentes de IA
- Loop thought-action-observation com OpenAI
- Polling automático para busca de tarefas pendentes
- Integração com governança para verificação de políticas
- Event emitter para comunicação entre agentes
#### 1.3 Context Indexer (`server/blackboard/ContextIndexer.ts` - 277 linhas)
- Indexação inteligente da estrutura do projeto
- Mapeamento de arquivos, imports e dependências
- Contexto técnico para agentes tomarem decisões informadas
#### 1.4 ToolManager (`server/autonomous/tools/ToolManager.ts` - 171 linhas)
- Registro e execução de ferramentas para agentes
- 10 ferramentas implementadas:
| Ferramenta | Arquivo | Linhas | Função |
|---|---|---|---|
| ReadFileTool | filesystem/ReadFileTool.ts | 66 | Leitura de arquivos do projeto |
| WriteFileTool | filesystem/WriteFileTool.ts | 88 | Escrita segura de arquivos |
| ListDirectoryTool | filesystem/ListDirectoryTool.ts | 85 | Listagem de diretórios |
| SearchCodeTool | filesystem/SearchCodeTool.ts | 92 | Busca por padrões no código |
| RunCommandTool | command/RunCommandTool.ts | 76 | Execução de comandos shell |
| TypeCheckTool | command/TypeCheckTool.ts | 70 | Validação TypeScript real |
| GitStatusTool | git/GitStatusTool.ts | 47 | Status do repositório |
| GitCommitTool | git/GitCommitTool.ts | 67 | Commits automáticos |
| AnalyzeRepoTool | github/AnalyzeRepoTool.ts | 41 | Análise de repos externos |
| ReadExternalFileTool | github/ReadExternalFileTool.ts | 41 | Leitura de repos GitHub |
| GitHubCommitTool | github/GitHubCommitTool.ts | 55 | Commits via GitHub API |
#### 1.5 Tabelas de Banco (Fase 1)
| Tabela | Função |
|---|---|
| `blackboard_tasks` | Tarefas do quadro negro (status, agente, prioridade, parent) |
| `blackboard_artifacts` | Artefatos gerados (código, specs, relatórios) |
| `blackboard_agent_logs` | Logs estruturados por agente |
---
## FASE 2 - AGENTES ESPECIALIZADOS + GOVERNANÇA
**Status: CONCLUIDO**
### O que foi planejado:
- Criar 5 agentes especializados com papéis distintos
- Implementar camada de governança com políticas de segurança
- Sistema de auditoria imutável
### O que foi executado:
#### 2.1 Agentes Especializados (5 agentes)
| Agente | Arquivo | Linhas | Papel |
|---|---|---|---|
| Architect | agents/ArchitectAgent.ts | 156 | Interpreta requisitos, cria specs técnicas, analisa estrutura existente |
| Generator | agents/GeneratorAgent.ts | 187 | Gera código TypeScript/React de alta qualidade seguindo padrões do projeto |
| Validator | agents/ValidatorAgent.ts | 232 | Valida código com TypeScript real (tsc --noEmit), análise de qualidade, score >= 60 para aprovar |
| Executor | agents/ExecutorAgent.ts | 178 | Prepara código para staging, verifica arquivos protegidos, NÃO aplica direto |
| Evolution | agents/EvolutionAgent.ts | 263 | Aprende com execuções passadas, documenta padrões, sugere melhorias |
#### 2.2 Governança de Segurança (`server/governance/service.ts` - 281 linhas)
**Motor de Políticas (Fail-Closed):**
- Avaliação de políticas antes de qualquer ação
- Se não há política explícita permitindo, a ação é BLOQUEADA
- Integrado automaticamente em BaseBlackboardAgent e ToolManager
**5 Políticas de Segurança:**
| # | Política | Regra |
|---|---|---|
| 1 | Proteção de arquivos críticos | Bloqueia escrita em server/routes.ts, server/index.ts, shared/schema.ts, etc. |
| 2 | Permissões de leitura | Permite leitura de qualquer arquivo do projeto |
| 3 | Bloqueio de comandos destrutivos | Bloqueia rm -rf, DROP TABLE, DELETE FROM, etc. |
| 4 | Aprovação humana para produção | Requer aprovação do usuário antes de deploy |
| 5 | Staging automático com threshold | Score de validação mínimo para ir para staging |
#### 2.3 Registros de Governança
| Componente | Tabela | Função |
|---|---|---|
| Contract Registry | `xos_contract_registry` | Contratos entre agentes (SLAs, dependências) |
| Tool Registry | `xos_tool_registry` | Registro de ferramentas disponíveis (auto-sync do ToolManager) |
| Skill Registry | `xos_skill_registry` | Habilidades aprendidas com tracking de uso |
| Policy Rules | `xos_policy_rules` | Regras de política de segurança |
| Audit Trail | `xos_audit_trail` | Trilha de auditoria imutável de todas as ações |
#### 2.4 API de Governança (`server/governance/routes.ts` - 266 linhas)
| Endpoint | Método | Função |
|---|---|---|
| `/api/governance/evaluate` | POST | Avaliar política para ação |
| `/api/governance/contracts` | GET/POST | Listar/criar contratos |
| `/api/governance/tools` | GET | Listar ferramentas registradas |
| `/api/governance/skills` | GET/POST | Listar/registrar habilidades |
| `/api/governance/policies` | GET/POST | Listar/criar políticas |
| `/api/governance/audit` | GET | Consultar trilha de auditoria |
| `/api/governance/dashboard` | GET | Dashboard com estatísticas |
---
## FASE 3 - JOB QUEUE + METRICS + RESEARCHER + DASHBOARD
**Status: CONCLUIDO**
### O que foi planejado:
- Job Queue PostgreSQL para processamento assíncrono
- Métricas de agentes para monitoramento
- 6o agente (Researcher) para análise de repositórios externos
- Dashboard visual de governança
### O que foi executado:
#### 3.1 Job Queue PostgreSQL (`server/governance/jobQueue.ts` - 252 linhas)
| Feature | Implementação |
|---|---|
| Fila de jobs | Tabela `xos_job_queue` com prioridade (0-10) |
| Claiming atômico | `UPDATE ... FOR UPDATE SKIP LOCKED` (previne race conditions) |
| Retry automático | Máximo de tentativas configurável por job |
| Dead-letter | Jobs que falharam todas tentativas vão para dead-letter |
| Handler registry | Registro de handlers por tipo de job |
| Processamento | Loop de polling com intervalo configurável |
#### 3.2 Métricas de Agentes
| Tabela | Campos | Função |
|---|---|---|
| `xos_agent_metrics` | agentName, tasksCompleted, tasksFailed, avgDuration, lastActive | Monitoramento de performance |
#### 3.3 Researcher Agent (`server/blackboard/agents/ResearcherAgent.ts` - 145 linhas)
- 6o agente especializado em pesquisa
- Analisa repositórios GitHub externos
- Pesquisa padrões de implementação e boas práticas
- Compara soluções e frameworks
- Gera relatórios de viabilidade técnica
#### 3.4 Governance Dashboard (`client/src/pages/XosGovernance.tsx` - 517 linhas)
**Rota:** `/xos/governance`
| Seção | Conteúdo |
|---|---|
| Stats Cards | Contratos, ferramentas, habilidades, políticas, auditorias, jobs |
| Audit Trail | Tabela com ações, decisões, agentes, timestamps |
| Policies | Lista de políticas de segurança ativas |
| Tools | Ferramentas registradas com descrição |
| Skills | Habilidades aprendidas com contagem de uso |
| Jobs | Fila de jobs com status e prioridade |
| Agent Monitoring | Métricas por agente (tarefas, falhas, duração média) |
#### 3.5 APIs Adicionais (Fase 3)
| Endpoint | Método | Função |
|---|---|---|
| `/api/governance/jobs` | GET | Listar jobs da fila |
| `/api/governance/jobs` | POST | Enfileirar novo job |
| `/api/governance/jobs/:id/cancel` | POST | Cancelar job |
| `/api/governance/jobs/:id/retry` | POST | Retentar job |
| `/api/governance/metrics` | GET | Métricas dos agentes |
---
## FASE 4 - PIPELINE AUTÔNOMO (Prompt-to-Production)
**Status: CONCLUIDO**
### O que foi planejado:
- Pipeline Orchestrator que encadeia todos os 6 agentes
- Fluxo: Prompt em Português -> Design -> Codegen -> Validação -> Staging -> Evolução
- Sistema de staging com revisão e aprovação do usuário
- Streaming em tempo real via SSE
- UI para submissão, acompanhamento e aprovação
### O que foi executado:
#### 4.1 Pipeline Orchestrator (`server/blackboard/PipelineOrchestrator.ts` - 350 linhas)
| Feature | Implementação |
|---|---|
| Criação de pipeline | Registra em `xos_dev_pipelines` + cria tarefa principal |
| Fases encadeadas | design -> codegen -> validation -> staging -> evolution |
| Monitoramento | Polling a cada 3s para verificar progresso de cada fase |
| Job handlers | Registra handlers para cada tipo de job (pipeline_*) |
| SSE Broadcasting | Emite eventos em tempo real para clientes conectados |
| Staging review | Pausa no staging para revisão humana antes de aplicar |
| Aplicação de código | Aplica mudanças aprovadas ao filesystem com backup |
| Proteção de arquivos | Verifica lista PROTECTED_FILES antes de aplicar |
**Fluxo completo do pipeline:**
```
1. Usuário envia prompt em português
2. DESIGN: ArchitectAgent analisa e cria especificação técnica
3. CODEGEN: GeneratorAgent gera código TypeScript/React
4. VALIDATION: ValidatorAgent executa tsc --noEmit real
5. STAGING: ExecutorAgent prepara para staging (NÃO aplica direto)
6. REVIEW: Usuário revisa código staged e aprova/rejeita
7. EVOLUTION: EvolutionAgent documenta aprendizados
```
#### 4.2 Tabelas de Banco (Fase 4)
| Tabela | Campos Principais | Função |
|---|---|---|
| `xos_dev_pipelines` | prompt, status, currentPhase, mainTaskId, phases (JSON), error | Rastreamento do pipeline |
| `xos_staging_changes` | pipelineId, filePath, content, action, status, reviewedBy | Código staged para revisão |
#### 4.3 Pipeline API (`server/blackboard/pipelineRoutes.ts` - 134 linhas)
| Endpoint | Método | Função |
|---|---|---|
| `/api/xos/pipeline` | POST | Criar e iniciar pipeline com prompt |
| `/api/xos/pipeline` | GET | Listar pipelines recentes |
| `/api/xos/pipeline/:id` | GET | Detalhes do pipeline (staging, tarefas, artefatos, logs) |
| `/api/xos/pipeline/:id/staging` | GET | Listar mudanças staged |
| `/api/xos/pipeline/:id/approve` | POST | Aprovar e aplicar código staged |
| `/api/xos/pipeline/:id/reject` | POST | Rejeitar código staged |
| `/api/xos/pipeline/:id/stream` | GET | SSE para eventos em tempo real |
#### 4.4 Pipeline UI (`client/src/pages/XosPipeline.tsx` - 471 linhas)
**Rota:** `/xos/pipeline`
| Componente | Funcionalidade |
|---|---|
| Formulário de prompt | Textarea com instruções em português + botão de envio |
| Timeline ao vivo | 5 fases com ícones, cores e status em tempo real |
| Staging review | Preview do código gerado com path dos arquivos |
| Approve/Reject | Botões para aprovar ou rejeitar código staged |
| Pipeline history | Lista de pipelines passados com status badges |
| SSE EventSource | Conexão em tempo real para atualizações |
---
## INVENTÁRIO COMPLETO DE ARTEFATOS
### Arquivos Backend (server/)
| Caminho | Linhas | Fase |
|---|---|---|
| `server/blackboard/BaseBlackboardAgent.ts` | 189 | 1 |
| `server/blackboard/service.ts` | 476 | 1 |
| `server/blackboard/ContextIndexer.ts` | 277 | 1 |
| `server/blackboard/routes.ts` | - | 1 |
| `server/blackboard/agents/index.ts` | 58 | 1/2 |
| `server/blackboard/agents/ArchitectAgent.ts` | 156 | 2 |
| `server/blackboard/agents/GeneratorAgent.ts` | 187 | 2 |
| `server/blackboard/agents/ValidatorAgent.ts` | 232 | 2 |
| `server/blackboard/agents/ExecutorAgent.ts` | 178 | 2 |
| `server/blackboard/agents/EvolutionAgent.ts` | 263 | 2 |
| `server/blackboard/agents/ResearcherAgent.ts` | 145 | 3 |
| `server/blackboard/PipelineOrchestrator.ts` | 350 | 4 |
| `server/blackboard/pipelineRoutes.ts` | 134 | 4 |
| `server/governance/service.ts` | 281 | 2 |
| `server/governance/routes.ts` | 266 | 2/3 |
| `server/governance/jobQueue.ts` | 252 | 3 |
| `server/autonomous/tools/ToolManager.ts` | 171 | 1 |
| `server/autonomous/tools/BaseTool.ts` | 82 | 1 |
| `server/autonomous/tools/**/*.ts` | ~750 | 1 |
| `server/autonomous/agents/**/*.ts` | ~1.003 | 1 |
| **TOTAL BACKEND XOS** | **~5.491** | |
### Arquivos Frontend (client/src/)
| Caminho | Linhas | Fase |
|---|---|---|
| `client/src/pages/XosGovernance.tsx` | 517 | 3 |
| `client/src/pages/XosPipeline.tsx` | 471 | 4 |
| **TOTAL FRONTEND XOS** | **988** | |
### Tabelas de Banco de Dados (34 novas tabelas)
| Tabela | Fase | Propósito |
|---|---|---|
| `blackboard_tasks` | 1 | Tarefas do quadro negro |
| `blackboard_artifacts` | 1 | Artefatos gerados |
| `blackboard_agent_logs` | 1 | Logs de agentes |
| `xos_contract_registry` | 2 | Contratos entre agentes |
| `xos_tool_registry` | 2 | Ferramentas registradas |
| `xos_skill_registry` | 2 | Habilidades aprendidas |
| `xos_policy_rules` | 2 | Políticas de segurança |
| `xos_audit_trail` | 2 | Trilha de auditoria |
| `xos_job_queue` | 3 | Fila de jobs assíncronos |
| `xos_agent_metrics` | 3 | Métricas de agentes |
| `xos_dev_pipelines` | 4 | Pipelines de desenvolvimento |
| `xos_staging_changes` | 4 | Código staged para revisão |
---
## DECISÕES ARQUITETURAIS REGISTRADAS
| # | Decisão | Justificativa |
|---|---|---|
| 1 | SSE em vez de Socket.IO para pipeline | Evita conflito com sockets existentes (WhatsApp, Chat) |
| 2 | FOR UPDATE SKIP LOCKED no job claiming | Previne race conditions com múltiplos workers |
| 3 | Fail-closed na governança | Segurança: ação sem política explícita = BLOQUEADA |
| 4 | Staging obrigatório antes de aplicar | Nunca auto-aplica código em produção |
| 5 | Arquivos protegidos (PROTECTED_FILES) | Lista de arquivos críticos que agentes não podem sobrescrever |
| 6 | Polling de 3s no pipeline monitor | Equilíbrio entre responsividade e carga no servidor |
| 7 | Score >= 60 para validação | Threshold mínimo para código passar pela validação |
| 8 | Prompts em português nos agentes | Sistema projetado para prompts de negócio em português brasileiro |
---
## VERIFICAÇÃO DE FUNCIONAMENTO
| Teste | Status | Evidência |
|---|---|---|
| API Pipeline retorna lista | OK | `GET /api/xos/pipeline` -> `{"success": true, "pipelines": [...]}` |
| Criação de pipeline funciona | OK | `POST /api/xos/pipeline` -> Pipeline #1 criado e rodando |
| Fases iniciam corretamente | OK | Design phase `status: "running"` após criação |
| SSE streaming configurado | OK | Endpoint `/api/xos/pipeline/:id/stream` implementado |
| Staging review configurado | OK | Endpoints approve/reject implementados |
| Governance dashboard funciona | OK | Página `/xos/governance` com 517 linhas de UI |
| Pipeline UI funciona | OK | Página `/xos/pipeline` com 471 linhas de UI |
| Todas as rotas registradas | OK | Verificado em server/routes.ts e client/src/App.tsx |
| 6 agentes rodando | OK | Architect, Generator, Validator, Executor, Evolution, Researcher |
| Job Queue operacional | OK | Atomic claiming com FOR UPDATE SKIP LOCKED |
---
## CONCLUSÃO
O plano XOS v1.0 foi executado com **100% de completude** em suas 4 fases:
- **Fase 1:** Fundação sólida com Blackboard architecture, 10 ferramentas e sistema de coordenação
- **Fase 2:** 5 agentes especializados + governança com 5 políticas de segurança fail-closed
- **Fase 3:** Job Queue PostgreSQL + Métricas + 6o agente Researcher + Dashboard visual
- **Fase 4:** Pipeline autônomo prompt-to-production com staging review e SSE streaming
O sistema está operacional e pronto para uso, com todas as salvaguardas de segurança ativas (staging obrigatório, arquivos protegidos, governança fail-closed, aprovação humana para produção).
---
*Documento gerado em 06/02/2026 como registro de auditoria do plano XOS v1.0 do Arcádia Suite.*

View File

@ -1,235 +0,0 @@
# Avaliação de Aderência - Impacto Geologia
## Arcádia Suite vs Requisitos do Book de Requisitos
### Data: Fevereiro 2026
-## 1. RESUMO EXECUTIVO
Este documento apresenta a análise de aderência entre os requisitos levantados no Book de Requisitos da Impacto Geologia e as funcionalidades atualmente implementadas na plataforma Arcádia Suite. A avaliação considera o estado atual do sistema, identificando o que já está pronto, o que está parcialmente implementado e o que precisa ser desenvolvido.
**Resultado Geral:**
| Status | Quantidade | % |
|--------|-----------|---|
| Implementado | 14 | 38% |
| Parcialmente Implementado | 10 | 27% |
| Não Implementado | 13 | 35% |
| **Total de Requisitos** | **37** | **100%** |
---
## 2. ANÁLISE POR MÓDULO
### 2.1 Módulo Administrativo Financeiro
| ID | Requisito | Status | Observação |
|----|-----------|--------|------------|
| RF-AF01 | Conciliação Bancária Automatizada | Parcial | Existe `finBankAccounts` e `finTransactions` para contas bancárias e transações, mas não há rotina de conciliação automática com extratos OFX/CSV |
| RF-AF02 | Fluxo de Pagamentos Controlado | Parcial | `finAccountsPayable` existe para contas a pagar, mas falta workflow de aprovação (programação → aprovação → execução) |
| RF-AF03 | Controle de Fluxo de Caixa Projetado | Parcial | `finCashFlowCategories` e transações existem, mas falta projeção automática de 90 dias com cenários |
| RF-AF04 | Gestão de Impostos e Obrigações | Implementado | Módulo fiscal completo: `fiscalNcms`, `fiscalCfops`, `fiscalCests`, `fiscalGruposTributacao`, `fiscalConfiguracoes`, `fiscalNotas` com integração Arcádia Plus |
| RF-AF05 | Prestação de Contas Digital | Parcial | `fieldExpenses` existe para despesas de campo, mas falta fluxo de aprovação digital e comprovantes fotográficos via mobile |
| - | Gestão de Ativos e Endividamento | Não Implementado | Não existe módulo de controle de ativos fixos nem dashboard de endividamento |
| - | Gestão de Cartões Corporativos | Não Implementado | Não há funcionalidade para tratar cartões como "contas correntes individuais" com controle de cargas/depósitos |
**Resumo Financeiro: 1 implementado, 4 parciais, 2 não implementados**
---
### 2.2 Módulo Comercial (Vendas e CRM)
| ID | Requisito | Status | Observação |
|----|-----------|--------|------------|
| RF-COM01 | Conversão Proposta → Projeto | Parcial | `crmProposals`, `crmProposalItems`, `crmOpportunities` existem, mas falta a automação de conversão proposta aprovada → criação automática de Projeto + Ordem de Venda |
| RF-COM02 | Gestão de Incentivos/Comissões | Implementado | `crmCommissionRules`, `crmCommissions` no CRM + `retailCommissionPlans`, `retailCommissionClosures` no Retail - sistema completo de comissões |
| RF-COM03 | Pesquisa de Satisfação (NPS) | Não Implementado | Campos de `satisfactionScore` existem em algumas tabelas, mas não há módulo dedicado de envio automático de pesquisa NPS pós-projeto |
**Resumo Comercial: 1 implementado, 1 parcial, 1 não implementado**
---
### 2.3 Módulo Operações e Projetos
| ID | Requisito | Status | Observação |
|----|-----------|--------|------------|
| RF-OP01 | Rastreabilidade por ID de Projeto | Implementado | `pcProjects`, `pcProjectMembers`, `pcProjectActivities`, `pcProjectTasks`, `pcProjectFiles`, `pcProjectHistory` - sistema completo de gestão de projetos com ID único e rastreabilidade |
| RF-OP02 | Gestão de Insumos de Campo | Parcial | Existe estoque (`retailWarehouseStock`, `retailStockMovements`) mas não há vinculação automática de consumo de materiais/equipamentos por projeto |
| RF-OP03 | Digitalização de Fichas de Campo | Parcial | `qualityFieldForms` existe para formulários digitais, mas falta os formulários específicos de geologia (Plano de Amostragem, PT, Monitoramento de Poços) e app mobile dedicado |
| RF-OP04 | Gestão de Pendências de Faturamento | Não Implementado | Não há sistema de monitoramento de eventos de campo que causem quebra de faturamento com alertas automáticos |
| - | Acompanhamento do Ciclo de Vida | Parcial | `pcProjectActivities` e `pcProjectHistory` registram atividades, mas falta pipeline visual desde pré-programação até entrega do relatório final |
| - | Custódia de Equipamentos | Não Implementado | Não existe módulo para controle de custódia de equipamentos por equipe de campo (DOC-010) |
| - | Programação de Equipes de Campo | Não Implementado | Não há funcionalidade de programação de equipes para mobilização de campo (DOC-008) |
| - | Programação de Relatórios | Não Implementado | Não existe módulo de programação e acompanhamento de relatórios técnicos (DOC-009) |
**Resumo Operações: 1 implementado, 3 parciais, 4 não implementados**
---
### 2.4 Módulo Qualidade e Serviços de Terceiros
| ID | Requisito | Status | Observação |
|----|-----------|--------|------------|
| RF-QC01 | Controle de Amostras e Laudos | Implementado | `qualitySamples`, `qualityLabReports` - sistema para rastreamento de amostras enviadas a laboratórios e laudos recebidos |
| RF-QC02 | Gestão de Documentos (QMS) | Implementado | `qualityDocuments`, `qualityDocumentRevisions` - controle de versão e lista mestra de documentos internos (FT-xx) |
| RF-QC03 | Controle de Não Conformidades (RNC) | Implementado | `qualityNonConformities` - registro e gestão de RNC, Ações Corretivas e Oportunidades de Melhoria |
| RF-QC04 | Homologação de Fornecedores (ISO 17025) | Parcial | `suppliers` e `purchaseOrders` existem, mas falta controle de certificações ISO 17025, bloqueio de compras de não-homologados e portal de fornecedores |
| - | Matriz de Treinamentos | Implementado | `qualityTrainingMatrix` - controle de treinamentos, participação e validade |
| - | Controle de Brancos | Não Implementado | Não existe funcionalidade específica para controle de brancos de laboratório (FT-58, FT-68) |
**Resumo Qualidade: 4 implementados, 1 parcial, 1 não implementado**
---
### 2.5 Módulo Fiscal e Tributário
| ID | Requisito | Status | Observação |
|----|-----------|--------|------------|
| RF-FT01 | Módulo Fiscal e Tributário | Implementado | Motor Fiscal completo (Arcádia Fisco): NCMs, CFOPs, CESTs, grupos tributação, certificados digitais, NF-e/NFC-e com integração SEFAZ via nfelib |
| - | Emissão de NF-e de Serviço | Parcial | Infraestrutura fiscal existe, mas NFS-e (Nota Fiscal de Serviço Eletrônica) requer integração com prefeituras, que varia por município - não implementado |
| - | Integração com Arcádia Plus | Implementado | `FiscalAdapter` com conexão via API para transmissão fiscal e emissão de documentos fiscais via Arcádia Plus |
**Resumo Fiscal: 2 implementados, 1 parcial, 0 não implementados**
---
### 2.6 Módulo Estoque e Logística
| ID | Requisito | Status | Observação |
|----|-----------|--------|------------|
| - | Gestão de Estoque | Implementado | `retailWarehouseStock`, `retailStockMovements`, `retailInventories`, `retailInventoryItems` - estoque completo com movimentações e inventário |
| - | Transferências entre Almoxarifados | Implementado | `retailStockTransfers`, `retailStockTransferItems`, `retailTransferSerials` - transferências com rastreio de seriais |
| - | Estoque de Segurança | Não Implementado | Não há funcionalidade de definição de estoque mínimo com alertas automáticos para itens com lead time > 90 dias |
| - | Controle de Km Rodado | Não Implementado | Não existe módulo para controle de custos de km rodado por veículo/equipe |
**Resumo Estoque: 2 implementados, 0 parciais, 2 não implementados**
---
### 2.7 Módulo Gestão de Pessoas (RH)
| ID | Requisito | Status | Observação |
|----|-----------|--------|------------|
| - | Cadastro de Colaboradores | Implementado | `peopleFuncionarios`, `peopleDependentes`, `peopleCargos`, `peopleDepartamentos` |
| - | Folha de Pagamento | Implementado | `peopleFolhaPagamento`, `peopleFolhaItens`, `peopleFolhaEventos`, `peopleEventosFolha` |
| - | Controle de Férias | Implementado | `peopleFerias` |
| - | Controle de Ponto | Implementado | `peoplePonto` |
| - | Benefícios | Implementado | `peopleBeneficios`, `peopleFuncionarioBeneficios` |
**Resumo RH: 5 implementados - módulo completo**
---
### 2.8 Módulo Contabilidade
| ID | Requisito | Status | Observação |
|----|-----------|--------|------------|
| - | Plano de Contas | Implementado | `contabilPlanoContas` |
| - | Centros de Custo | Implementado | `contabilCentrosCusto` - permite vincular custos por projeto |
| - | Lançamentos Contábeis | Implementado | `contabilLancamentos`, `contabilPartidas` |
| - | Períodos e Saldos | Implementado | `contabilPeriodos`, `contabilSaldos` |
**Resumo Contabilidade: 4 implementados - módulo completo**
---
## 3. REQUISITOS NÃO-FUNCIONAIS
| Requisito | Status | Observação |
|-----------|--------|------------|
| Usabilidade/Interface intuitiva | Implementado | Interface moderna React + Tailwind + shadcn/ui, estilo WhatsApp Web |
| Integração via APIs | Implementado | APIs REST completas, protocolos MCP/A2A implementados |
| Segurança e Governança | Implementado | Multi-tenant com tenant scoping, XOS Governance Layer, audit trail |
| Mobile | Não Implementado | Não há aplicativo móvel dedicado; interface web é responsiva mas não tem funcionalidades offline para campo |
| Framework FA (DocTypes customizados) | Implementado | Sistema ArcDocTypes com fields, layouts, pages, scripts e widgets |
---
## 4. PRIORIZAÇÃO - O QUE FALTA PARA FECHAR O PROJETO
### Prioridade ALTA (Essencial para entrega)
| # | Item | Esforço Estimado | Descrição |
|---|------|-----------------|-----------|
| 1 | Conversão Proposta → Projeto automática | 2-3 dias | Workflow que ao aprovar proposta cria automaticamente o Projeto e Ordem de Venda |
| 2 | Workflow de aprovação de pagamentos | 2-3 dias | Fluxo: programação → aprovação (Fran → Sidney) → execução de pagamentos |
| 3 | Conciliação bancária | 3-5 dias | Import de extratos bancários (OFX/CSV) e conciliação automática com transações |
| 4 | Vinculação Insumos ↔ Projetos | 2-3 dias | Associar consumo de materiais e equipamentos a cada projeto |
| 5 | Homologação de Fornecedores ISO 17025 | 3-4 dias | Status de homologação, certificações, bloqueio de compras para não-homologados |
| 6 | Gestão de Pendências de Faturamento | 2-3 dias | Alertas para eventos de campo que impedem faturamento |
### Prioridade MÉDIA (Importante para operação completa)
| # | Item | Esforço Estimado | Descrição |
|---|------|-----------------|-----------|
| 7 | Prestação de contas digital com aprovação | 2-3 dias | Upload de comprovantes, fluxo aprovação de despesas de campo |
| 8 | Projeção de fluxo de caixa 90 dias | 2-3 dias | Dashboard com projeção baseada em recebíveis e pagáveis programados |
| 9 | Formulários de campo específicos | 3-5 dias | Plano de Amostragem, PT, Monitoramento de Poços como DocTypes |
| 10 | Portal de Fornecedores | 3-5 dias | Área externa para fornecedores atualizarem documentos e certificações |
| 11 | NPS automático pós-projeto | 1-2 dias | Envio automático de pesquisa de satisfação ao concluir projeto |
| 12 | Programação de Equipes de Campo | 2-3 dias | Calendário de mobilização de equipes para projetos |
| 13 | Programação de Relatórios | 2 dias | Controle de prazos e status de relatórios técnicos |
| 14 | NFS-e (Nota Fiscal de Serviço) | 5-10 dias | Integração com webservices municipais (varia por prefeitura) |
### Prioridade BAIXA (Desejável / Fase 2)
| # | Item | Esforço Estimado | Descrição |
|---|------|-----------------|-----------|
| 15 | Gestão de Ativos Fixos | 3-5 dias | Cadastro, depreciação e controle de ativos da empresa |
| 16 | Dashboard de Endividamento | 1-2 dias | BI com visão consolidada de endividamento |
| 17 | Gestão de Cartões Corporativos | 2-3 dias | Tratar cartões como contas individuais com cargas |
| 18 | Estoque de Segurança com alertas | 1-2 dias | Definição de estoque mínimo e alertas automáticos |
| 19 | Controle de Km Rodado | 1-2 dias | Registro de km por veículo/equipe com cálculo de custos |
| 20 | Custódia de Equipamentos | 2-3 dias | Controle de equipamentos alocados por equipe de campo |
| 21 | Controle de Brancos de Laboratório | 1-2 dias | Registro e rastreamento de amostras branco |
| 22 | App Mobile para Campo | 15-20 dias | PWA ou app nativo para coleta de dados offline em campo |
| 23 | Contratos Guarda-Chuva | 2-3 dias | Gestão de contratos de fornecimento de longo prazo |
---
## 5. ESTIMATIVA TOTAL DE ESFORÇO
| Prioridade | Itens | Dias Estimados |
|------------|-------|---------------|
| Alta | 6 itens | 14-21 dias |
| Média | 8 itens | 19-33 dias |
| Baixa | 9 itens | 28-42 dias |
| **Total** | **23 itens** | **61-96 dias** |
### Recomendação de Fases:
**Fase 1 (MVP Impacto):** Prioridade Alta → ~3-4 semanas
- Entrega o mínimo necessário para operação integrada
**Fase 2 (Operação Completa):** Prioridade Média → ~4-5 semanas
- Completa todas as funcionalidades operacionais do dia a dia
**Fase 3 (Evolução):** Prioridade Baixa → ~5-7 semanas
- Funcionalidades avançadas e app mobile
---
## 6. O QUE JÁ ESTÁ PRONTO E FUNCIONANDO
A Arcádia Suite já possui uma base sólida implementada:
- **Contabilidade completa** (plano de contas, centros de custo, lançamentos, períodos)
- **Gestão de Pessoas completa** (funcionários, folha, férias, ponto, benefícios)
- **Motor Fiscal completo** (NCMs, CFOPs, CESTs, NF-e/NFC-e, certificados digitais)
- **CRM e Comercial** (leads, oportunidades, propostas, comissões)
- **Gestão de Projetos** (projetos, membros, tarefas, atividades, histórico)
- **Qualidade** (amostras, laudos, RNC, documentos, treinamentos)
- **Estoque** (warehouses, movimentações, transferências, inventário)
- **Financeiro básico** (contas bancárias, a pagar, a receber, transações)
- **Framework FA** (DocTypes customizados para fichas de campo)
- **Multi-tenant com governança** (segurança, audit trail, políticas)
- **Inteligência Artificial** (Manus agent, Dev Center, auto-programação)
- **Comunicação unificada** (WhatsApp, chat interno, email)
- **BI e Dashboards** (motor BI com análise de dados)
---
## 7. CONCLUSÃO
O sistema Arcádia Suite apresenta **aderência de ~65%** aos requisitos da Impacto Geologia, com os módulos estruturais (Contabilidade, RH, Fiscal, Qualidade) praticamente completos. Os principais gaps estão nos **fluxos de negócio específicos** (workflows de aprovação, conciliação bancária, conversão proposta→projeto) e nas **funcionalidades de campo** (formulários específicos de geologia, app mobile, programação de equipes).--
A recomendação é priorizar os 6 itens de alta prioridade para uma entrega funcional em **3-4 semanas**, seguida de evolução gradual nos itens médios e baixos.

View File

@ -1,386 +0,0 @@
# Deploy no Coolify — Arcádia Suite
> Guia completo para subir o Arcádia Suite no Coolify com Docker Compose.
---
## Índice
1. [Pré-requisitos](#pré-requisitos)
2. [Arquitetura de Serviços](#arquitetura-de-serviços)
3. [Configuração no Coolify](#configuração-no-coolify)
4. [Variáveis de Ambiente](#variáveis-de-ambiente)
5. [Serviços Opcionais (Profiles)](#serviços-opcionais-profiles)
6. [Sequência de Inicialização](#sequência-de-inicialização)
7. [Traefik e HTTPS](#traefik-e-https)
8. [Volumes e Persistência](#volumes-e-persistência)
9. [Stack de IA (LiteLLM + Ollama)](#stack-de-ia-litellm--ollama)
10. [Pós-deploy](#pós-deploy)
11. [Checklist Final](#checklist-final)
---
## Pré-requisitos
- Coolify instalado e acessível (v4+)
- Docker Engine 24+ e Docker Compose v2+ no servidor
- Domínio apontando para o IP do servidor (`A record`)
- Mínimo recomendado: **4 vCPU, 8 GB RAM, 50 GB SSD**
- Para IA local (Ollama): **16 GB RAM, GPU opcional**
---
## Arquitetura de Serviços
```
Internet → Traefik (Coolify) → app:5000 → arcadia-internal
→ open-webui:8080 (opcional)
arcadia-internal:
db (PostgreSQL 16 + pgvector) :5432
redis :6379
app (Node.js/Express) :5000
embeddings (Python FastAPI) :8001
fisco (Python FastAPI) :8002
contabil (Python FastAPI) :8003
bi (Python FastAPI) :8004
automation (Python FastAPI) :8005
litellm (gateway LLM) :4000 ← profile: ai
ollama (modelos locais) :11434 ← profile: ai
superset (BI) :8088 ← profile: bi
plus (Laravel/ERP) :8080 ← profile: plus
erpnext :8090 ← profile: erpnext
```
---
## Configuração no Coolify
### 1. Criar novo projeto
No painel Coolify: **New Project → Docker Compose**.
### 2. Apontar para o repositório
- **Source:** Git (GitHub/GitLab/Gitea)
- **Branch:** `main` (ou sua branch de produção)
- **Docker Compose file:** `docker-compose.prod.yml`
### 3. Definir o domínio
Em **Domains**, adicione `seu-dominio.com`. O Coolify irá automaticamente:
- Criar as rotas no Traefik
- Emitir certificado Let's Encrypt
---
## Variáveis de Ambiente
Configure todas no painel **Environment Variables** do Coolify (nunca no `.env` do repositório).
### Obrigatórias
```env
# Aplicação
NODE_ENV=production
PORT=5000
DOMAIN=seu-dominio.com
DOCKER_MODE=true
# Segredos — gerar com: openssl rand -hex 32
SESSION_SECRET=<gerado>
SSO_SECRET=<gerado>
# Banco de dados
PGHOST=db
PGPORT=5432
PGUSER=arcadia
PGPASSWORD=<senha-forte>
PGDATABASE=arcadia
DATABASE_URL=postgresql://arcadia:<senha>@db:5432/arcadia
# Redis
REDIS_URL=redis://redis:6379
# IA — LiteLLM gateway
LITELLM_API_KEY=<gerado>
AI_INTEGRATIONS_OPENAI_BASE_URL=http://litellm:4000/v1
AI_INTEGRATIONS_OPENAI_API_KEY=<mesmo-que-LITELLM_API_KEY>
OLLAMA_BASE_URL=http://ollama:11434
# Microserviços Python
PYTHON_SERVICE_URL=http://embeddings:8001
FISCO_PYTHON_URL=http://fisco:8002
CONTABIL_PYTHON_URL=http://contabil:8003
BI_PYTHON_URL=http://bi:8004
AUTOMATION_PYTHON_URL=http://automation:8005
```
### LLMs externos (opcionais — soberania: deixar em branco)
```env
OPENAI_API_KEY=
ANTHROPIC_API_KEY=
GROQ_API_KEY=
```
### Superset — profile `bi`
```env
SUPERSET_SECRET_KEY=<openssl rand -hex 32>
SUPERSET_ADMIN_USER=admin
SUPERSET_ADMIN_EMAIL=admin@seu-dominio.com
SUPERSET_ADMIN_PASSWORD=<senha-forte>
SUPERSET_HOST=superset
SUPERSET_PORT=8088
```
### Arcádia Plus — profile `plus`
```env
SSO_PLUS_BASE_URL=http://plus:8080
PLUS_APP_KEY=<laravel-key>
PLUS_DB_ROOT_PASSWORD=<senha>
PLUS_DB_PASSWORD=<senha>
PLUS_DB_DATABASE=arcadia_plus
PLUS_DB_USER=plus
```
### ERPNext — profile `erpnext`
```env
ERPNEXT_PROXY_ENABLED=true
ERPNEXT_CONTAINER_HOST=erpnext
ERPNEXT_DB_ROOT_PASSWORD=<senha>
ERPNEXT_DB_PASSWORD=<senha>
ERPNEXT_ADMIN_PASSWORD=<senha>
ERPNEXT_URL=http://erpnext:8090
```
---
## Serviços Opcionais (Profiles)
O `docker-compose.prod.yml` usa profiles para habilitar serviços sob demanda.
No Coolify, adicione a variável de ambiente `COMPOSE_PROFILES`:
| O que habilitar | `COMPOSE_PROFILES` |
|---|---|
| Somente core | *(vazio)* |
| IA local (Ollama + LiteLLM + WebUI) | `ai` |
| Superset BI | `bi` |
| Arcádia Plus (Laravel ERP) | `plus` |
| ERPNext | `erpnext` |
| Tudo | `ai,bi,plus,erpnext` |
> **Atenção:** Ollama com modelos grandes (llama3.3, qwen2.5) pode consumir 1020 GB de disco. Certifique-se de ter espaço no volume `ollama_models`.
---
## Sequência de Inicialização
Os `depends_on` garantem a ordem correta automaticamente:
```
1. db → healthcheck: pg_isready (até 10 tentativas)
2. redis → healthcheck: redis-cli ping
3. embeddings, fisco, contabil, bi, automation → aguardam db healthy
4. litellm, ollama → aguardam redis (se profile ai)
5. app → aguarda db healthy + redis started
6. open-webui, superset, plus, erpnext → aguardam serviços base
```
Se um serviço falhar ao iniciar, cheque os logs:
```bash
# No servidor
docker compose -f docker-compose.prod.yml logs app --tail 50
docker compose -f docker-compose.prod.yml logs db --tail 50
```
---
## Traefik e HTTPS
O Coolify gerencia o Traefik. As labels já estão no `docker-compose.prod.yml`:
```yaml
# Serviço principal
labels:
- "traefik.enable=true"
- "traefik.http.routers.arcadia.rule=Host(`${DOMAIN}`)"
- "traefik.http.routers.arcadia.tls=true"
- "traefik.http.routers.arcadia.tls.certresolver=letsencrypt"
- "traefik.http.services.arcadia.loadbalancer.server.port=5000"
# Open WebUI (profile ai) — subdomínio ai.seu-dominio.com
labels:
- "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"
```
Certifique-se de que o DNS do subdomínio `ai.seu-dominio.com` também aponta para o servidor antes de habilitar o profile `ai`.
---
## Volumes e Persistência
| Volume | Conteúdo | Criticidade |
|---|---|---|
| `pgdata` | Banco de dados PostgreSQL | **CRÍTICO — fazer backup** |
| `redis_data` | Sessões e cache | Alta |
| `ollama_models` | Modelos LLM baixados (~1020 GB) | Média (redownload possível) |
| `open_webui_data` | Histórico do WebUI | Baixa |
| `superset_home` | Dashboards Superset | Média |
| `plus_db` | Banco MySQL do Plus | Alta |
| `plus_storage` | Arquivos do Plus | Alta |
| `erpnext_sites` | Sites ERPNext | **CRÍTICO** |
| `erpnext_logs` | Logs ERPNext | Baixa |
### Backup do PostgreSQL
```bash
# Backup
docker exec arcadia-db pg_dump -U arcadia arcadia | gzip > backup-$(date +%Y%m%d).sql.gz
# Restore
gunzip < backup-20240101.sql.gz | docker exec -i arcadia-db psql -U arcadia arcadia
```
Configure backups automáticos no Coolify em **Project → Backups**.
---
## Stack de IA (LiteLLM + Ollama)
### Dois tiers configurados em `docker/litellm-config.yaml`
```
TIER 1 — Ollama (local, padrão) → llama3.3, qwen2.5-coder, nomic-embed-text
TIER 2 — Externos (opt-in) → OpenAI, Anthropic, Groq (apenas se API key definida)
```
### Baixar modelos Ollama após o primeiro deploy
```bash
docker exec arcadia-ollama ollama pull llama3.3
docker exec arcadia-ollama ollama pull qwen2.5-coder:7b
docker exec arcadia-ollama ollama pull nomic-embed-text
```
---
## Pós-deploy
### Rodar migrations do banco
```bash
# Via Coolify → Run Command, ou no servidor:
docker exec arcadia-app npm run db:push
```
### Verificar saúde dos serviços
```bash
docker compose -f docker-compose.prod.yml ps
```
Todos devem estar `healthy` ou `running`. Se algum ficar em `restarting`, verifique os logs.
### Verificar conectividade IA
```bash
curl -H "Authorization: Bearer $LITELLM_API_KEY" \
http://localhost:4000/v1/models
```
---
## Otimização de Build (.dockerignore)
O arquivo `.dockerignore` é crítico para builds rápidos. Ele evita que arquivos desnecessários sejam enviados para o Docker daemon.
### Estrutura atual otimizada
```
# Excluídos (reduzem contexto de build)
node_modules/ ← Instalados no container
dist/ ← Gerado no build
git/ ← Não necessário
server/bi/metaset/superset-src/ ← 3.7GB de código fonte Superset
server/bi/metaset/venv/ ← Python venv local
__pycache__/ ← Cache Python
.pytest_cache/ ← Cache de testes
.playwright-mcp/ ← Screenshots de teste
docs/ ← Documentação Markdown
*.md ← Arquivos Markdown
# INCLUÍDOS (necessários para build)
attached_assets/ ← Assets de branding (ícones, logos)
```
### Resultado da otimização
| Métrica | Antes | Depois | Melhoria |
|---------|-------|--------|----------|
| Contexto de build | ~5GB | ~93MB | **98% menor** |
| Tempo de transferência | ~2-3 min | ~5-10s | **~90% mais rápido** |
> ⚠️ **Atenção**: Ao adicionar novos diretórios ao `.dockerignore`, verifique se não há assets necessários para o build (como imagens importadas no frontend).
---
## Checklist Final
### Antes do deploy
- [ ] Domínio DNS apontando para o servidor (`A record`)
- [ ] Todas as variáveis obrigatórias definidas no Coolify
- [ ] `SESSION_SECRET` e `SSO_SECRET` gerados com `openssl rand -hex 32`
- [ ] Senhas do banco definidas (nunca reutilizar senhas de dev)
- [ ] `DOCKER_MODE=true` definido
- [ ] `.dockerignore` revisado (especialmente após adicionar novos diretórios grandes)
### No Coolify
- [ ] Projeto criado apontando para `docker-compose.prod.yml`
- [ ] Domínio configurado no Coolify
- [ ] HTTPS ativo (Let's Encrypt)
- [ ] Volumes persistentes configurados (especialmente `pgdata`)
- [ ] `COMPOSE_PROFILES` definido conforme os serviços desejados
### Após o deploy
- [ ] `npm run db:push` executado (migrations)
- [ ] Login funcional no `https://seu-dominio.com`
- [ ] Healthchecks verdes no painel Coolify
- [ ] Modelos Ollama baixados (se profile `ai` ativo)
- [ ] Backup automático configurado para `pgdata`
- [ ] Monitoramento/alertas configurados (Coolify suporta webhooks)
---
## Comandos de Referência Rápida
```bash
# Subir apenas o core
docker compose -f docker-compose.prod.yml up -d
# Subir com IA
docker compose -f docker-compose.prod.yml --profile ai up -d
# Subir tudo
docker compose -f docker-compose.prod.yml --profile ai --profile bi --profile plus up -d
# Ver logs em tempo real
docker compose -f docker-compose.prod.yml logs -f app
# Reiniciar um serviço específico
docker compose -f docker-compose.prod.yml restart app
# Atualizar para nova versão (após git pull)
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d --no-deps app
```

View File

@ -1,116 +0,0 @@
# Dev Center — Fábrica de Agentes
**Rota:** `/development` → ícone "Design Studio"
**Componente:** `client/src/pages/DevCenter.tsx` (função `AgentFactoryTabs`)
**Backend:** `server/agent-defs/routes.ts` + `server/blackboard/`
---
## Visão Geral
O Dev Center é a fábrica de agentes do Arcádia Suite. Permite criar, montar, implantar e monitorar agentes de IA de forma visual, seguindo o pipeline:
```
Design → Assemble → Deploy → Galeria
```
Cada agente tem um ciclo de vida com 4 status:
| Status | Descrição |
|--------|-----------|
| `draft` | Rascunho criado, aguardando montagem |
| `assembling` | Sendo montado pelo Blackboard |
| `ready` | Montagem concluída, pronto para deploy |
| `deployed` | Implantado e disponível para execução |
---
## Abas
### Design
Cria e edita definições de agentes (`agent_defs`).
- **Editor de spec** — suporta 3 modos: `markdown`, `typescript`, `visual`
- Salva via `PATCH /api/agent-defs/:id`
- Após salvar, agente fica com status `draft` e aparece na aba Assemble
### Assemble (Linha de Montagem)
Lista agentes em `draft` ou `assembling` e aciona a montagem via Blackboard.
- Botão **Montar**`POST /api/agent-defs/:id/assemble`
- Cria uma `MainTask` no Blackboard com a spec do agente
- Cria subtask `architect` para projetar a estrutura
- **Painel de progresso** (expansível por Task ID):
- Polling a cada 3s em `/api/blackboard/task/:id`
- Mostra subtasks com ícones de status (✓ ✗ ⏳ ⏰) e `agentRole`
- Quando a task completa → status muda para `ready` (ou `draft` se falhou)
### Deploy (Orchestrate Center)
Gerencia agentes `ready` e `deployed`.
- **Stats** no topo: total de agentes / prontos p/ deploy / implantados
- **Deploy**`POST /api/agent-defs/:id/deploy` (só para `ready`)
- Incrementa `version`, muda status para `deployed`
- **Executar**`POST /api/agent-defs/:id/run` (só para `deployed`)
- Cria nova task no Blackboard para execução
- **Re-montar**`POST /api/agent-defs/:id/redraft`
- Volta para `draft` para corrigir e remontar
### Galeria
Grid de agentes `deployed` com busca por nome/descrição.
- Cada card mostra: nome, descrição, versão
- Botão **Executar** — dispara execução imediata
- Botão **Fork**`POST /api/agent-defs/:id/fork` — duplica como novo `draft`
- Botão **Info** (ícone) — dialog com spec completo, modo do editor e data de criação
---
## API Reference
| Método | Endpoint | Descrição |
|--------|----------|-----------|
| `GET` | `/api/agent-defs` | Lista todos os agentes do tenant |
| `POST` | `/api/agent-defs` | Cria novo agente (status: draft) |
| `PATCH` | `/api/agent-defs/:id` | Atualiza nome, descrição ou spec |
| `DELETE` | `/api/agent-defs/:id` | Remove agente |
| `POST` | `/api/agent-defs/:id/assemble` | Inicia montagem via Blackboard |
| `POST` | `/api/agent-defs/:id/deploy` | Implanta agente (ready → deployed) |
| `POST` | `/api/agent-defs/:id/run` | Executa agente implantado |
| `POST` | `/api/agent-defs/:id/fork` | Duplica como draft |
| `POST` | `/api/agent-defs/:id/redraft` | Volta para draft |
---
## Schema
Tabela: `arcadia_agent_defs`
```sql
id serial PRIMARY KEY
tenant_id varchar
user_id varchar REFERENCES users(id)
name varchar(200) NOT NULL
description text
spec jsonb NOT NULL DEFAULT '{}' -- { mode, content }
status varchar(30) DEFAULT 'draft'
version integer DEFAULT 1
last_task_id integer -- FK blackboard_tasks
created_at timestamp
updated_at timestamp
```
Migration: `migrations/phase6_agent_defs.sql`
---
## Integração com Blackboard
O Assemble usa `blackboardService.createMainTask()` + `createSubtask()`.
A spec do agente é passada como `content` da task, com metadata `{ agentDefId, source: "agent-factory" }`.
Quando a task do Blackboard completa ou falha, o polling no frontend (`useEffect` com `setInterval(3000)`) atualiza o status do agente via `PATCH /api/agent-defs/:id`.

View File

@ -1,620 +0,0 @@
# Arcadia Suite - Guia Completo de Hospedagem e Deploy
## Indice
1. [Requisitos do Servidor](#1-requisitos-do-servidor)
2. [Variaveis de Ambiente](#2-variaveis-de-ambiente)
3. [Instalacao](#3-instalacao)
4. [Banco de Dados](#4-banco-de-dados)
5. [Servicos Python](#5-servicos-python)
6. [Metabase (Arcadia Insights)](#6-metabase-arcadia-insights)
7. [Build e Producao](#7-build-e-producao)
8. [Configuracao do Servidor Web (Nginx)](#8-configuracao-do-servidor-web-nginx)
9. [Process Manager (PM2)](#9-process-manager-pm2)
10. [SSL/HTTPS](#10-sslhttps)
11. [Estrutura de Portas](#11-estrutura-de-portas)
12. [Arcadia Plus (Laravel)](#12-arcadia-plus-laravel)
13. [Manutencao e Backups](#13-manutencao-e-backups)
14. [Troubleshooting](#14-troubleshooting)
---
## 1. Requisitos do Servidor
### Minimo Recomendado
- **SO:** Ubuntu 22.04 LTS ou Debian 12
- **CPU:** 4 vCPUs
- **RAM:** 8 GB (16 GB recomendado)
- **Disco:** 50 GB SSD
- **Node.js:** v20.x ou superior
- **Python:** 3.11 ou superior
- **PostgreSQL:** 15 ou superior
- **Java:** OpenJDK 11+ (para Metabase)
- **PHP:** 8.1+ (para Arcadia Plus, opcional)
- **Composer:** 2.x (para Arcadia Plus, opcional)
### Instalacao de Dependencias (Ubuntu/Debian)
```bash
# Atualizar sistema
sudo apt update && sudo apt upgrade -y
# Node.js 20
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs
# Python 3.11
sudo apt install -y python3.11 python3.11-venv python3-pip
# PostgreSQL 15
sudo apt install -y postgresql postgresql-contrib
# Java (para Metabase)
sudo apt install -y openjdk-11-jre-headless
# Ferramentas auxiliares
sudo apt install -y git nginx certbot python3-certbot-nginx build-essential
```
---
## 2. Variaveis de Ambiente
Crie um arquivo `.env` na raiz do projeto:
```env
# === BANCO DE DADOS ===
DATABASE_URL=postgresql://arcadia_user:SUA_SENHA_AQUI@localhost:5432/arcadia_suite
PGHOST=localhost
PGPORT=5432
PGUSER=arcadia_user
PGPASSWORD=SUA_SENHA_AQUI
PGDATABASE=arcadia_suite
# === OPENAI (IA) ===
OPENAI_API_KEY=sk-sua-chave-openai-aqui
# === ERPNext (opcional) ===
ERPNEXT_URL=https://seu-erpnext.com
ERPNEXT_API_KEY=sua-api-key
ERPNEXT_API_SECRET=seu-api-secret
# === GITHUB (opcional - para XOS Pipeline) ===
GITHUB_TOKEN=ghp_seu-token-github
# === PRODUCAO ===
NODE_ENV=production
PORT=5000
SESSION_SECRET=gere-uma-string-secreta-longa-e-aleatoria
# === METABASE ===
MB_DB_TYPE=postgres
MB_DB_DBNAME=arcadia_metabase
MB_DB_PORT=5432
MB_DB_USER=arcadia_user
MB_DB_PASS=SUA_SENHA_AQUI
MB_DB_HOST=localhost
MB_JETTY_PORT=8088
METABASE_PORT=8088
METASET_ADMIN_EMAIL=admin@arcadia.app
METASET_ADMIN_PASSWORD=SuaSenhaMetaSet
```
---
## 3. Instalacao
```bash
# Clonar o repositorio
git clone https://github.com/SEU_USUARIO/arcadia-suite.git
cd arcadia-suite
# Instalar dependencias Node.js
npm install
# Instalar dependencias Python
python3.11 -m venv .venv
source .venv/bin/activate
pip install -r python-service/requirements.txt
pip install fastapi uvicorn pandas numpy httpx pydantic nfelib lxml signxml \
cryptography zeep beautifulsoup4 psycopg2-binary matplotlib pymongo \
python-multipart python-docx pyopenssl
# Copiar arquivo de ambiente
cp .env.example .env
# Edite o .env com suas credenciais
nano .env
```
---
## 4. Banco de Dados
### Criar banco e usuario
```bash
sudo -u postgres psql
```
```sql
-- Criar usuario
CREATE USER arcadia_user WITH PASSWORD 'SUA_SENHA_SEGURA';
-- Criar banco principal
CREATE DATABASE arcadia_suite OWNER arcadia_user;
-- Criar banco do Metabase (separado)
CREATE DATABASE arcadia_metabase OWNER arcadia_user;
-- Permissoes
GRANT ALL PRIVILEGES ON DATABASE arcadia_suite TO arcadia_user;
GRANT ALL PRIVILEGES ON DATABASE arcadia_metabase TO arcadia_user;
\q
```
### Executar migracoes (Drizzle ORM)
```bash
# Sincronizar schema com o banco
npx drizzle-kit push
```
---
## 5. Servicos Python
O projeto utiliza servicos Python para funcionalidades especificas. Eles sao iniciados automaticamente pelo servidor Node.js, mas podem ser executados manualmente para testes:
| Servico | Porta | Funcao | Inicio Automatico |
|---------|-------|--------|--------------------|
| Contabil | 8003 | Motor contabil | Sim |
| BI Engine | 8004 | Business Intelligence | Sim |
| Automations | 8005 | Motor de automacoes | Sim |
| Scientist | 8001 | IA, embeddings, analise de dados | Opcional* |
| Fisco | 8002 | Motor fiscal NF-e/NFC-e | Opcional* |
| Communication | 8006 | Motor de comunicacao | Opcional* |
*Servicos opcionais podem ser ativados adicionando suas chamadas em `server/index.ts`.
Os servicos Contabil, BI e Automations sao iniciados automaticamente pelo `server/index.ts`.
---
## 6. Metabase (Arcadia Insights)
O Metabase e usado como motor de BI invisivel. Para instala-lo:
```bash
# Criar diretorio
mkdir -p metabase/plugins
# Baixar Metabase (versao 0.50.x recomendada)
wget https://downloads.metabase.com/v0.50.31/metabase.jar -O metabase/metabase.jar
# Testar execucao
java -jar metabase/metabase.jar &
```
O Metabase sera executado na porta 8088 e proxied pelo Node.js em `/api/bi/metaset/*` e `/metabase/`.
O script de inicio esta em `metabase/start-metabase.sh` e e gerenciado automaticamente pelo servidor Node.js.
---
## 7. Build e Producao
```bash
# Build do frontend (Vite) + backend
npm run build
# Iniciar em producao
npm run start
```
O comando `npm run start` executa `NODE_ENV=production node dist/index.cjs` na porta 5000.
---
## 8. Configuracao do Servidor Web (Nginx)
Crie o arquivo de configuracao:
```bash
sudo nano /etc/nginx/sites-available/arcadia-suite
```
```nginx
server {
listen 80;
server_name seu-dominio.com.br;
# Redirecionar HTTP para HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name seu-dominio.com.br;
# SSL (sera configurado pelo Certbot)
ssl_certificate /etc/letsencrypt/live/seu-dominio.com.br/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/seu-dominio.com.br/privkey.pem;
# Limites
client_max_body_size 50M;
# Proxy principal (Node.js na porta 5000)
location / {
proxy_pass http://127.0.0.1:5000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 86400;
}
# WebSocket (Socket.IO)
location /socket.io/ {
proxy_pass http://127.0.0.1:5000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 86400;
}
# Arcadia Plus (Laravel, opcional)
location /plus/ {
proxy_pass http://127.0.0.1:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
```bash
# Ativar o site
sudo ln -s /etc/nginx/sites-available/arcadia-suite /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
```
---
## 9. Process Manager (PM2)
Use PM2 para manter o servidor rodando:
```bash
# Instalar PM2 globalmente
sudo npm install -g pm2
# Iniciar a aplicacao
pm2 start dist/index.cjs --name arcadia-suite --env production
# Salvar a lista de processos
pm2 save
# Configurar para iniciar no boot
pm2 startup
```
### Arquivo ecosystem (opcional)
Crie `ecosystem.config.cjs`:
```javascript
module.exports = {
apps: [
{
name: "arcadia-suite",
script: "dist/index.cjs",
env: {
NODE_ENV: "production",
PORT: 5000,
},
instances: 1,
max_memory_restart: "1G",
log_date_format: "YYYY-MM-DD HH:mm:ss",
error_file: "logs/error.log",
out_file: "logs/output.log",
},
{
name: "arcadia-metabase",
script: "java",
args: "-jar metabase/metabase.jar",
interpreter: "none",
env: {
MB_DB_TYPE: "postgres",
MB_DB_DBNAME: "arcadia_metabase",
MB_DB_PORT: "5432",
MB_DB_USER: "arcadia_user",
MB_DB_PASS: "SUA_SENHA_AQUI",
MB_DB_HOST: "localhost",
MB_JETTY_PORT: "8088",
},
max_memory_restart: "2G",
},
],
};
```
```bash
# Iniciar com ecosystem
pm2 start ecosystem.config.cjs
pm2 save
```
---
## 10. SSL/HTTPS
```bash
# Certificado gratuito com Let's Encrypt
sudo certbot --nginx -d seu-dominio.com.br
# Renovacao automatica (ja configurada pelo Certbot)
sudo certbot renew --dry-run
```
---
## 11. Estrutura de Portas
| Porta | Servico | Acesso |
|-------|---------|--------|
| 5000 | Node.js (Express + Vite) | Principal - proxied pelo Nginx |
| 8088 | Metabase (Arcadia Insights) | Interno - proxied via /api/bi/metaset e /metabase |
| 8003 | Python Contabil | Interno - inicio automatico |
| 8004 | Python BI Engine | Interno - inicio automatico |
| 8005 | Python Automations | Interno - inicio automatico |
| 8001 | Python Scientist | Interno - opcional |
| 8002 | Python Fisco | Interno - opcional |
| 8006 | Node Communication | Interno - opcional |
| 8080 | Arcadia Plus (Laravel) | Interno - proxied via /plus |
Apenas a porta 80 (HTTP) e 443 (HTTPS) devem estar expostas publicamente via Nginx.
---
## 12. Arcadia Plus (Laravel)
Se voce utiliza o modulo Plus (ERP Laravel):
```bash
cd plus
# Instalar dependencias PHP
composer install --no-dev --optimize-autoloader
# Configurar ambiente
cp .env.example .env
php artisan key:generate
# Configurar banco (usar schema separado no mesmo PostgreSQL)
# Edite plus/.env com as credenciais do banco
# Migracoes
php artisan migrate
# Otimizar para producao
php artisan config:cache
php artisan route:cache
php artisan view:cache
# Servir (porta 8080)
php artisan serve --host=0.0.0.0 --port=8080
```
---
## 13. Manutencao e Backups
### Backup do Banco de Dados
```bash
# Criar backup
pg_dump -U arcadia_user -h localhost arcadia_suite > backup_$(date +%Y%m%d_%H%M%S).sql
# Restaurar backup
psql -U arcadia_user -h localhost arcadia_suite < backup_20260209.sql
```
### Atualizar a Aplicacao
```bash
cd /caminho/para/arcadia-suite
# Parar a aplicacao
pm2 stop arcadia-suite
# Atualizar codigo
git pull origin main
# Reinstalar dependencias
npm install
# Rebuild
npm run build
# Migrar banco (se necessario)
npx drizzle-kit push
# Reiniciar
pm2 restart arcadia-suite
```
### Logs
```bash
# Ver logs em tempo real
pm2 logs arcadia-suite
# Ver logs do Nginx
sudo tail -f /var/log/nginx/error.log
```
---
## 14. Troubleshooting
### Erro: "Cannot find module"
```bash
npm install
npm run build
```
### Erro: "ECONNREFUSED" no banco
```bash
# Verificar se PostgreSQL esta rodando
sudo systemctl status postgresql
sudo systemctl start postgresql
```
### Erro: "Port already in use"
```bash
# Encontrar processo na porta
lsof -i :5000
kill -9 PID_DO_PROCESSO
```
### Servicos Python nao iniciam
```bash
# Verificar Python
python3.11 --version
# Verificar ambiente virtual
source .venv/bin/activate
pip list | grep fastapi
```
### Metabase nao inicia
```bash
# Verificar Java
java -version
# Verificar se metabase.jar existe
ls -la metabase/metabase.jar
# Executar manualmente para ver erros
java -jar metabase/metabase.jar
```
---
## Hospedagem em Cloud (Alternativas)
### VPS Recomendados
- **Hetzner:** A partir de EUR 4/mes (melhor custo-beneficio)
- **DigitalOcean:** A partir de USD 12/mes (4GB RAM)
- **Vultr:** A partir de USD 12/mes
- **AWS EC2:** t3.medium (sob demanda)
- **Contabo:** A partir de EUR 5/mes (alto recurso por preco)
### Deploy com Docker (Alternativa)
Se preferir usar Docker, crie um `Dockerfile`:
```dockerfile
FROM node:20-slim
WORKDIR /app
# Instalar Python e Java
RUN apt-get update && apt-get install -y \
python3.11 python3.11-venv python3-pip \
openjdk-11-jre-headless \
&& rm -rf /var/lib/apt/lists/*
# Copiar e instalar dependencias
COPY package.json ./
RUN npm install --production
# Instalar dependencias Python
COPY python-service/requirements.txt ./python-requirements.txt
RUN python3.11 -m venv /app/.venv && \
/app/.venv/bin/pip install -r python-requirements.txt
# Copiar codigo
COPY . .
# Build
RUN npm run build
EXPOSE 5000
CMD ["npm", "run", "start"]
```
```yaml
# docker-compose.yml
version: "3.8"
services:
app:
build: .
ports:
- "5000:5000"
environment:
- DATABASE_URL=postgresql://arcadia_user:senha@db:5432/arcadia_suite
- NODE_ENV=production
depends_on:
- db
db:
image: postgres:15
environment:
POSTGRES_USER: arcadia_user
POSTGRES_PASSWORD: senha
POSTGRES_DB: arcadia_suite
volumes:
- pgdata:/var/lib/postgresql/data
metabase:
image: metabase/metabase:v0.50.31
ports:
- "8088:8088"
environment:
MB_DB_TYPE: postgres
MB_DB_DBNAME: arcadia_metabase
MB_DB_PORT: 5432
MB_DB_USER: arcadia_user
MB_DB_PASS: senha
MB_DB_HOST: db
MB_JETTY_PORT: 8088
volumes:
pgdata:
```
---
## Checklist de Deploy
- [ ] Servidor provisionado com requisitos minimos
- [ ] PostgreSQL instalado e configurado
- [ ] Node.js 20 instalado
- [ ] Python 3.11 instalado
- [ ] Repositorio clonado
- [ ] Dependencias instaladas (npm install)
- [ ] Dependencias Python instaladas
- [ ] Arquivo .env configurado com todas as variaveis
- [ ] Banco de dados criado e migracoes executadas
- [ ] Build de producao executado (npm run build)
- [ ] Metabase baixado e configurado
- [ ] PM2 configurado e aplicacao rodando
- [ ] Nginx configurado como reverse proxy
- [ ] SSL/HTTPS configurado com Certbot
- [ ] Firewall configurado (portas 80 e 443 abertas)
- [ ] Backup automatico do banco configurado
- [ ] Dominio apontando para o servidor
---
*Documento gerado em 09/02/2026 - Arcadia Suite v1.0*

View File

@ -1,252 +0,0 @@
---
⚠️ **AVISO: DOCUMENTAÇÃO HISTÓRICA**
Este documento foi criado em **Março/2026** como planejamento estratégico.
Pode conter informações desatualizadas em relação ao sistema atual.
Para documentação técnica atualizada, consulte:
- \_SPEC_NAVBAR.md\_ (componentes UI)
- \_KERNEL_SPEC.md\_ (arquitetura do Kernel)
- Código-fonte em \_server/modules/\_
---
# Índice: PLANEJAMENTO ESTRATÉGICO ARCÁDIA AGENTIC SUITE
**Documento:** `/home/ubuntu/# PLANEJAMENTO ESTRATÉGICO ARCÁDIA.txt`
**Versão:** 1.0 — Março 2026
**Estado:** Pronto para Implementação
---
## 📑 ESTRUTURA DO DOCUMENTO
### 1. **Executive Summary** (Seção 1)
- Visão: ERP tradicional → **Sistema Agêntico Orientado a Objetos**
- 5 decisões arquiteturais críticas (MiroFlow, OpenClaw, Superset, Skills POO, Automações)
### 2. **Arquitetura de Alto Nível** (Seção 2)
```
Interface Unificada (Chat + Dev Center + OpenClaw + BI)
Orquestradores (Manus + Blackboard + Automation Fabric)
Motores Especializados Embutidos (MiroFlow + OpenClaw + Skill Engine)
Infraestrutura (Knowledge Graph + PostgreSQL + Ollama/DeepSeek)
```
### 3. **Componentes Embutidos** (Seção 3+)
| Componente | Status | Local | Propósito |
|-----------|--------|-------|----------|
| **MiroFlow** | ✅ | `server/modules/miroflow/` | Motor científico, benchmarks, análise estatística |
| **OpenClaw** | 🔄 | `server/modules/openclaw/` + `client/src/components/openclaw/` | Detecção padrões, sugestão proativa, skills emergentes |
| **Skills Engine** | ✅ | `server/skills/` | POO: herança, composição, polimorfismo, versionamento |
| **Automation Fabric** | ⏳ | `server/modules/automation/` | Runtime unificado: Workflow, Rule, Agent, Schedule, Event |
| **Dev Center** | ⏳ | `client/src/pages/DevCenter.tsx` | Design → Assemble → Deploy de agentes |
| **Superset Integration** | ⏳ | `server/modules/superset/` | BI visual + MiroFlow bridge |
### 4. **Skills Engine POO** (Seção 4)
- ✅ Herança: `extends`, `isA`
- ✅ Composição: `dependencies`, `requires`
- ✅ Polimorfismo: `useMiroFlow`, `autoApprove`
- ✅ Versionamento Git-like (createVersion, listVersions, rollback, fork, diff)
- ✅ Marketplace / Biblioteca (import de skills públicas)
### 5. **Automation Fabric Unificada** (Seção 6)
- Runtime com suporte: Workflow, Rule, Agent, Schedule, Event
- Trigger-based (webhook, database, schedule, event)
- Integração com Blackboard para codegen
### 6. **Dev Center Completo** (Seção 8)
- **Design Studio**: UML, Visual, Markdown
- **Assemble Line**: Integração com Blackboard
- **Orchestrate Center**: Deploy, monitoramento
- **Galeria de Agentes**: Marketplace interno
### 7. **Infraestrutura** (Seção 9)
- PostgreSQL 16 + pgvector
- Neo4j (Knowledge Graph)
- Ollama (LLMs locais: deepseek-r1:32b/70b, llama3.1:8b/70b)
- Apache Superset (BI visual)
---
## 🗺️ ROADMAP E STATUS
### ✅ Fase 1: Fundação (Semanas 1-2)
**Status: CONCLUÍDA (2026-03-25)**
- Submodules MiroFlow + OpenClaw
- Tabelas skills, skill_executions
- Neo4j no Docker
- ReferenceParser/Resolver
- Superset + Ollama
**Entregável:** Infraestrutura base ✅
---
### ✅ Fase 2: Skills Engine (Semanas 3-4)
**Status: CONCLUÍDA (25/03/2026 - 15:04)**
- SkillEngine com herança/composição
- API REST CRUD + execução + histórico
- Editor Monaco (tabs Geral/Body/Params)
- Versionamento Git-like (VersionManager)
- Skill Marketplace / Biblioteca
**Entregável:** Skills criáveis, versionáveis, executáveis ✅
---
### ✅ Fase 3: MiroFlow Embutido (Semanas 5-6)
**Status: CONCLUÍDA PARCIAL (25/03/2026 - 15:04)**
**Implementado:**
- Adapt MiroFlow para Ollama local (llama3.2:3b / deepseek-r1:14b)
- Containerização em docker-compose.prod.yml
- Agentes especializados (Statistician, Auditor, Researcher)
- Proxy Node.js: `/api/miroflow/analyze`
- UI MiroFlowControl (tab Científico em /insights)
**Pendente:**
- [ ] MiroFlowBridge para Superset
- [ ] Integração com Arcádia Audit (imutabilidade)
- [ ] Dashboard de benchmarks no Dev Center
- [ ] **Tool SQL via Skills** (skill `db_query` como MCP tool)
**Entregável:** Análises científicas (ETA completo: 08/04/2026)
---
### 🔄 Fase 4: OpenClaw Embutido (Semanas 7-8)
**Status: SPRINT 1 COMPLETO (26/03/2026) | Sprints 2-3 Em Progresso**
#### ✅ Sprint 1: Backend (COMPLETO - 26/03)
- [x] PatternDetector.ts — detecção (3+ em 30d), confiança, Neo4j
- [x] OpenClawEngine.ts — orquestração, sugestões, scheduler, Socket.IO
- [x] SkillEmergence.ts — Blackboard codegen, validação, DRAFT storage
- [x] routes.ts — 10 endpoints REST
- [x] arcadia.config.yaml — pattern detection, suggestions, emergence
#### ⏳ Sprint 2: Frontend (PRÓXIMO - 28-29/03)
- [ ] OpenClawWidget.tsx — widget flutuante de sugestão
- [ ] SkillSuggestion.tsx — modal de sugestão
- [ ] useAgentEmergence.ts — hook de lógica
- [ ] Integração em App.tsx (layout principal)
#### ⏳ Sprint 3: Config + Testes (30-31/03)
- [ ] Schema database (detectedPatterns, skillSuggestions)
- [ ] Migration SQL (migrations/0004_openclaw_tables.sql)
- [ ] Testes unitários
- [ ] Documentação
**Fluxo:** Padrão → Detector → Engine → Sugestão → Widget → User → Blackboard → SkillEmergence → DRAFT → Dev Center → Publicada
**Entregável:** Skills emergentes (ETA: 08/04/2026)
---
### ⏳ Fase 5: Automation Fabric (Semanas 9-10)
**Status: PENDENTE**
- [ ] Unificar XOS/Automations + Central /automations
- [ ] 4 runtimes: Workflow, Rule, Agent, Schedule, Event
- [ ] UI única no Dev Center
- [ ] Migração dados existentes
- [ ] Testes carga/segurança
**Entregável:** Automações unificadas
---
### ⏳ Fase 6: Dev Center Completo (Semanas 11-12)
**Status: PENDENTE**
- [ ] Design Studio (UML, Visual, Markdown)
- [ ] Assemble Line (Blackboard integration)
- [ ] Orchestrate Center (deploy, monitoramento)
- [ ] Galeria de Agentes (marketplace interno)
- [ ] Documentação e treinamento
**Entregável:** Sistema completo operacional
---
## 🎯 DECISÕES CRÍTICAS (RESUMO)
| Decisão | Escolha | Justificativa |
|---------|---------|---------------|
| **MiroFlow** | **EMBUTIR** | Motor científico nativo, benchmarks, verificação multi-camada |
| **OpenClaw** | **EMBUTIR** | Widget interno, detecção contextual no Arcádia |
| **Superset** | **INTEGRAR** | BI visual mantido, bridge para MiroFlow |
| **Skills** | **POO** | Herança, composição, polimorfismo para reuso |
| **Automações** | **UNIFICAR** | Runtime único, múltiplos orquestradores |
| **Dev Center** | **FÁBRICA** | Design → Assemble → Deploy de agentes |
| **Multi-tenant** | **HIERARQUIA** | System → Tenant → Company → User skills |
---
## 📋 PRÓXIMO PASSO
### **Fase 4 Sprint 2: OpenClaw Frontend**
**ETA: 28-29/03/2026**
A Fase 4 Sprint 1 (backend) foi concluída em 26/03. Agora entrar na **Sprint 2 do frontend**, que consiste em:
#### Tarefas Sprint 2:
1. **OpenClawWidget.tsx** — Widget flutuante que:
- Escuta eventos de sugestões via Socket.IO
- Mostra notificação "Detectei padrão!"
- Abre SkillSuggestion modal ao ser clicado
2. **SkillSuggestion.tsx** — Modal que:
- Exibe padrão detectado (frequência, confiança)
- Preview do código da skill sugerida (gerado por Blackboard)
- Botões: Aprovar (→ DRAFT em Dev Center) ou Rejeitar
3. **useAgentEmergence.ts** — Hook que:
- Conecta a Socket.IO `/api/openclaw` events
- Gerencia estado de sugestões
- Sincroniza com OpenClawEngine backend
4. **Integração em App.tsx**:
- Importar OpenClawWidget
- Renderizar no layout principal (canto inferior direito)
- Garantir que não bloqueia interação com página
#### Localização dos arquivos:
```
client/src/
├── components/
│ └── openclaw/
│ ├── OpenClawWidget.tsx ← NOVO
│ ├── SkillSuggestion.tsx ← NOVO
│ └── ...
├── hooks/
│ └── useAgentEmergence.ts ← NOVO
└── App.tsx ← MODIFICAR (integração)
```
#### Integração com backend já pronto:
- PatternDetector.ts emite eventos via Socket.IO
- OpenClawEngine.ts orquestra sugestões
- SkillEmergence.ts gera código via Blackboard
- Todas as rotas REST já existem
**Próximo passo após Sprint 2:** Sprint 3 (Config + Testes), depois Fase 5 (Automation Fabric).
---
## 🔗 LINKS ESSENCIAIS
| Recurso | URL |
|---------|-----|
| **MiroFlow Docs** | https://github.com/MiroMindAI/MiroFlow/tree/main/docs |
| **MiroFlow Paper** | https://arxiv.org/abs/2602.22808 |
| **OpenClaw README** | https://github.com/miaoxworld/OpenClawInstaller/blob/main/README.md |
| **Superset Docs** | https://superset.apache.org/docs/intro |
| **Neo4j Docs** | https://neo4j.com/docs/ |
| **Ollama Docs** | https://github.com/ollama/ollama/blob/main/README.md |
---
**Pronto para começar Fase 4 Sprint 2?**

View File

@ -1,255 +0,0 @@
# Análise de Gap - Arcadia Kernel
**Data:** 09/04/2026
**Objetivo:** Comparar funcionalidades implementadas vs. sugeridas
---
## 🔴 Root Cause Analysis: Learning Service Timeouts (RESOLVIDO)
**Problema:** `PYTHON_SERVICE_URL` sendo resetado para porta 8001 em vez de 8000
**Causa Raiz Identificada:** O arquivo `.env.example` na linha 30 continha:
```bash
PYTHON_SERVICE_URL=http://localhost:8001 # ← ERRADO
```
Quando o Coolify recriava o container, ele usava o valor do `.env.example` como fallback, causando:
- Timeouts de 10s a cada 60s (ciclo do Learning Service)
- Degradação de performance do sistema
- Erros silenciosos no log
**Solução Aplicada (08/04/2026):** Corrigido `.env.example` para usar porta 8000 com comentário explicativo.
**Lição Aprendida:** O `.env.example` deve sempre estar sincronizado com a configuração de produção correta.
---
## 📊 Resumo das Funcionalidades
| Funcionalidade | Status | Implementação | Notas |
|----------------|--------|---------------|-------|
| **Service Discovery** | ✅ **Parcial** | `ServiceRegistry` + 4 providers | Docker, Coolify, XOS, Static |
| **Load Balancer** | ❌ **Não tem** | - | Não implementado |
| **Circuit Breaker** | ❌ **Não tem** | - | Não implementado |
| **Config Hot Reload** | ❌ **Não tem** | - | Requer restart do Kernel |
| **Resource Limits** | ⚠️ **Parcial** | Via `ProcessManager` | Limites básicos de processo |
| **Secrets Management** | ❌ **Não tem** | - | Variáveis em `.env` |
---
## ✅ O que Temos Implementado
### 1. Service Discovery (Parcial ✅)
**Arquivos:**
- `server/kernel/registry/ServiceRegistry.ts`
- `server/kernel/discovery/*`
**Providers Implementados:**
- `DockerDiscovery` - Descobre containers Docker
- `CoolifyDiscovery` - Integração com Coolify
- `XOSDiscovery` - Descobre serviços XOS
- `StaticDiscovery` - Configuração estática
**Funcionalidades:**
- ✅ Discovery automático (30s intervalo)
- ✅ Health checks (15s intervalo)
- ✅ Cleanup de serviços stale (5min)
- ✅ EventEmitter para mudanças
- ❌ **Falta:** Service mesh, load balancing entre instâncias
### 2. Health Monitor ✅
**Arquivo:** `server/kernel/core/HealthMonitor.ts`
**Funcionalidades:**
- ✅ Health checks HTTP periódicos
- ✅ Configurável (interval, timeout, retries)
- ✅ Eventos onHealthChange
- ✅ Tracking de falhas/sucessos consecutivos
### 3. Process Manager ✅
**Arquivo:** `server/kernel/core/ProcessManager.ts`
**Funcionalidades:**
- ✅ Gerenciamento de processos filhos
- ✅ Logs aggregator
- ✅ State tracking
- ⚠️ **Básico:** Resource limits ( apenas via SO)
### 4. Log Aggregator ✅
**Arquivo:** `server/kernel/core/LogAggregator.ts`
**Funcionalidades:**
- ✅ Centralização de logs
- ✅ Limite de linhas (configurável)
- ✅ Query/filter básico
### 5. Dashboard Web ✅
**Arquivo:** `server/kernel/dashboard/DashboardServer.ts`
**Funcionalidades:**
- ✅ Interface web (porta 5001)
- ✅ WebSocket para tempo real
- ✅ Visualização de serviços
---
## ❌ O que Falta Implementar
### 1. Load Balancer ❌
**Prioridade:** Alta
**Descrição:** Distribuir carga entre instâncias de serviços
**Casos de Uso:**
- Múltiplas instâncias do mesmo serviço
- Failover automático
- Distribuição por região
**Implementação Sugerida:**
```typescript
// server/kernel/balancer/LoadBalancer.ts
interface LoadBalancer {
selectInstance(serviceId: string): RegisteredService;
registerStrategy(strategy: BalanceStrategy): void;
}
```
### 2. Circuit Breaker ❌
**Prioridade:** Alta
**Descrição:** Parar de chamar serviço falho após N falhas
**Estados:**
- CLOSED: Funcionando normal
- OPEN: Circuito aberto (rejeita chamadas)
- HALF_OPEN: Testando se voltou
**Implementação Sugerida:**
```typescript
// server/kernel/circuit/CircuitBreaker.ts
interface CircuitBreaker {
call<T>(fn: () => Promise<T>): Promise<T>;
getState(): 'CLOSED' | 'OPEN' | 'HALF_OPEN';
}
```
### 3. Config Hot Reload ❌
**Prioridade:** Média
**Descrição:** Mudar configuração sem restartar Kernel
**Funcionalidades:**
- Watch de arquivo `services.json`
- Aplicar mudanças em runtime
- Notificar serviços afetados
**Implementação Sugerida:**
```typescript
// server/kernel/config/ConfigWatcher.ts
interface ConfigWatcher {
watch(path: string, onChange: (config: Config) => void): void;
}
```
### 4. Resource Limits (Melhorar) ⚠️
**Prioridade:** Média
**Status:** Básico via SO (ulimit)
**O que falta:**
- Limite de CPU por serviço (cgroups)
- Limite de memória por serviço
- Throttling de requisições
- Quotas por tenant
### 5. Secrets Management ❌
**Prioridade:** Alta
**Status:** Variáveis em `.env` (inseguro)
**Funcionalidades Necessárias:**
- Vault integrado (HashiCorp Vault style)
- Encryption at rest
- Rotacionamento automático
- Acesso por política (RBAC)
- Audit logging
**Implementação Sugerida:**
```typescript
// server/kernel/secrets/SecretVault.ts
interface SecretVault {
get(key: string): Promise<string>;
set(key: string, value: string): Promise<void>;
rotate(key: string): Promise<void>;
}
```
---
## 📋 Recomendações de Prioridade
### Fase 1 - Crítico (Segurança + Estabilidade)
1. **Secrets Management** - Dados sensíveis expostos em `.env`
2. **Circuit Breaker** - Evitar cascata de falhas
### Fase 2 - Performance
3. **Load Balancer** - Necessário para escalar
4. **Resource Limits** - Prevenir resource exhaustion
### Fase 3 - DX (Developer Experience)
5. **Config Hot Reload** - Evitar restart em produção
---
## 🔧 Notas Técnicas
### Service Discovery Atual
```
ServiceRegistry
├── DockerDiscovery (containers)
├── CoolifyDiscovery (apps Coolify)
├── XOSDiscovery (serviços XOS)
└── StaticDiscovery (config manual)
```
**Limitação:** Não faz load balancing entre múltiplas instâncias do mesmo serviço.
### Health Check Atual
- Interval: 15s
- Timeout: 5s
- Retries: 3
- **Falta:** Circuit breaker baseado em health
### Process Manager Atual
- Gerencia processos via Node.js spawn
- **Falta:** Isolamento real (containers), resource limits
---
## 📁 Arquivos Relacionados
| Componente | Arquivo |
|------------|---------|
| Entry Point | `server/kernel/index.ts` |
| Service Registry | `server/kernel/registry/ServiceRegistry.ts` |
| Health Monitor | `server/kernel/core/HealthMonitor.ts` |
| Process Manager | `server/kernel/core/ProcessManager.ts` |
| Discovery | `server/kernel/discovery/*.ts` |
| Dashboard | `server/kernel/dashboard/DashboardServer.ts` |
---
## 🎯 Próximos Passos Sugeridos
1. **Criar especificação técnica** para cada funcionalidade faltante
2. **Priorizar** baseado em necessidades de produção
3. **Implementar** gradualmente, começando por Secrets e Circuit Breaker
**Responsável:** Equipe de Platform/Infra
**Timeline:** 2-4 semanas por funcionalidade

View File

@ -1,365 +0,0 @@
# Especificação Técnica: Arcadia Kernel
> **Versão:** 3.3
> **Status:** ✅ Produção
> **Última atualização:** 2026-04-08
---
## 1. Visão Geral
O **Arcadia Kernel** é o sistema operacional nativo da ArcadiaSuite - um orquestrador de serviços em Node.js que unifica discovery, health monitoring e gerenciamento de processos.
### 1.1 Objetivos
- **Discovery**: Descobrir automaticamente todos os serviços do ecossistema
- **Orquestração**: Iniciar, parar e reiniciar serviços (processos e containers)
- **Observabilidade**: Health checks, logs e métricas centralizados
- **Integração**: Unificar visão da Casa de Máquinas com dados reais
### 1.2 Arquitetura
```
┌─────────────────────────────────────────────────────────────────────────┐
│ ARCADIA APPLICATION (5000) │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ ARCADIA KERNEL (5001) │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │ │
│ │ │ Dashboard │ │ Process │ │ LogAggregator │ │ │
│ │ │ (Web UI) │ │ Manager │ │ │ │ │
│ │ └──────┬──────┘ └──────┬──────┘ └─────────────────────────┘ │ │
│ │ │ │ │ │
│ │ └────────────────┴────────────────┐ │ │
│ │ │ │ │ │
│ │ ┌───────────┴───────────┐ │ │ │
│ │ │ Service Registry │◄──┼──┐ │ │
│ │ │ (Unified Discovery) │ │ │ │ │
│ │ └───────────┬───────────┘ │ │ │ │
│ │ │ │ │ │ │
│ │ ┌─────────────────────┼───────────────┘ │ │ │
│ │ │ │ │ │ │
│ │ ┌──┴──┐ ┌──────────┐ ┌┴─────────┐ ┌────┴─────┐ ┌────────┐ │ │
│ │ │Static│ │ Coolify │ │ XOS │ │ Docker │ │ ... │ │ │
│ │ │Disc. │ │ Disc. │ │ Disc. │ │ Disc. │ │ │ │ │
│ │ └──┬──┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ └───┬────┘ │ │
│ │ │ │ │ │ │ │ │
│ │ └──────────┴─────────────┴─────────────┴────────────┘ │ │
│ │ │ │ │
│ │ ┌──────────┴──────────┐ │ │
│ │ │ UnifiedRegistry │ │ │
│ │ │ (Agregação + Dedu) │ │ │
│ │ └──────────┬──────────┘ │ │
│ └─────────────────────────┼─────────────────────────────────────┘ │
│ │ │
│ ┌──────────────────┴──────────────────┐ │
│ │ global.arcadiaKernelRegistry │ │
│ │ (Acesso direto - mesmo processo) │ │
│ └──────────────────┬──────────────────┘ │
│ │ │
│ ┌───────────────────────┴───────────────────────┐ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────┐ ┌─────────────────────┐ │
│ │ Casa de │ │ /api/engine-room/* │ │
│ │ Máquinas │ │ (API Express) │ │
│ │ (React UI) │ │ │ │
│ └─────────────────┘ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
Legenda:
───► HTTP/HTTPS ─ ─► Acesso direto (memória)
Porta 5000: App principal Porta 5001: Kernel Dashboard (interno)
```
---
## 2. Componentes
### 2.1 Core (ProcessManager, HealthMonitor, LogAggregator)
**Status:** ✅ Estável
- **ProcessManager**: Gerencia processos Node.js/Python locais
- **HealthMonitor**: Health checks periódicos
- **LogAggregator**: Coleta e armazena logs
### 2.2 Service Registry
**Status:** ✅ Implementado
Catálogo central de serviços com:
- Registro manual ou automático
- Health status tracking
- Capabilities e metadados
- Eventos (service:registered, service:health_changed, etc)
### 2.3 Discovery Providers
| Provider | Status | Descrição |
|----------|--------|-----------|
| `StaticDiscovery` | ✅ | DNS interno (nomes fixos) |
| `CoolifyDiscovery` | ✅ | API Coolify (produção) |
| `DockerDiscovery` | ✅ | Docker labels (fallback) |
| `XOSDiscovery` | ✅ | Banco XOS (filas, integrações) |
| `UnifiedRegistry` | ⏳ | Agregador (Semana 5) |
---
## 3. Discovery Providers
### 3.1 StaticDiscovery
Descobre serviços via nomes DNS fixos na rede Docker.
```typescript
// Ex: http://contabil:8003, http://bi:8004
```
### 3.2 CoolifyDiscovery
Consulta API Coolify para descobrir serviços em produção.
**Endpoints:**
- `GET /api/v1/services` - Docker Compose services
- `GET /api/v1/applications` - Aplicações
**Filtro:** `arcadia.discovery.enabled=true`
### 3.3 DockerDiscovery
Lê labels de containers Docker na rede local.
**Labels:**
- `arcadia.discovery.enabled=true`
- `arcadia.name=MetaSet BI`
- `arcadia.type=python`
- `arcadia.port=8100`
- `arcadia.capabilities=dashboards,charts,sql_lab`
### 3.4 XOSDiscovery
Consulta banco de dados para descobrir serviços XOS.
**Descobre:**
- Filas de atendimento (`xos_queues`)
- Integrações WhatsApp (`whatsapp_sessions`)
- Automações ativas (`xos_automations`)
---
## 4. API do Kernel
### 4.1 Endpoints Core
| Endpoint | Método | Descrição |
|----------|--------|-----------|
| `/health` | GET | Health check do Kernel |
| `/api/kernel/services` | GET | Lista serviços do ProcessManager |
| `/api/kernel/services/:id/start` | POST | Inicia serviço |
| `/api/kernel/services/:id/stop` | POST | Para serviço |
| `/api/kernel/services/:id/restart` | POST | Reinicia serviço |
| `/api/kernel/services/:id/logs` | GET | Logs do serviço |
### 4.2 Endpoints Registry
| Endpoint | Método | Descrição |
|----------|--------|-----------|
| `/api/registry/services` | GET | Lista serviços descobertos |
| `/api/registry/services/:id` | GET | Detalhes de um serviço |
| `/api/registry/by-category/:cat` | GET | Serviços por categoria |
| `/api/registry/by-capability/:cap` | GET | Serviços por capability |
| `/api/registry/stats` | GET | Estatísticas do registry |
| `/api/registry/discover` | POST | Força discovery manual |
### 4.3 Endpoints Casa de Máquinas
| Endpoint | Método | Descrição |
|----------|--------|-----------|
| `/api/engine-room/status` | GET | Status unificado (Registry + Presets) |
| `/api/engine-room/engine/:name/health` | GET | Health de um motor |
| `/api/engine-room/engine/:name/start` | POST | Inicia motor |
| `/api/engine-room/engine/:name/stop` | POST | Para motor |
| `/api/engine-room/engine/:name/restart` | POST | Reinicia motor |
| `/api/engine-room/engine/:name/logs` | GET | Logs do motor |
---
## 5. Integração Casa de Máquinas
### 5.1 Fluxo de Dados
```
Casa de Máquinas (React)
/api/engine-room/status
┌─────────────────────────────┐
│ fetchRegistryServices() │
│ │
│ PRIORIDADE 1: Registry │
│ Global (mesmo processo) │
│ ───────────────────────── │
│ (global.arcadiaKernel │
│ .serviceRegistry) │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ Acesso │ │
│ │ direto │ │
│ │ (memória) │ │
│ └──────┬──────┘ │
│ │ │
│ Se falhar: │
│ ───────────────────────── │
│ PRIORIDADE 2: HTTP │
│ Fallback para porta 5001 │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ http.get │ │
│ │ 127.0.0.1 │ │
│ │ localhost │ │
│ └──────┬──────┘ │
└─────────┼───────────────────┘
Resposta JSON
Frontend Render
```
### 5.2 Implementação Atual
```typescript
// server/engine-room/routes.ts
async function fetchRegistryServicesWithRetry(): Promise<any[] | null> {
// PRIORIDADE 1: Registry global (mesmo processo Node.js)
const globalRegistry = (global as any).arcadiaKernelRegistry;
if (globalRegistry) {
const services = globalRegistry.getServices();
console.log(`[Engine Room] Usando Registry global: ${services.length} serviços`);
return services;
}
// PRIORIDADE 2: HTTP request para porta 5001 (fallback)
console.log('[Engine Room] Registry global não disponível, tentando HTTP...');
// ... retry com 127.0.0.1 e localhost
}
```
#### Vantagens do Registry Global
| Aspecto | HTTP (antigo) | Registry Global (novo) |
|---------|---------------|------------------------|
| Latência | ~10-50ms | ~0.1ms |
| Confiabilidade | Problemas com loopback Docker | 100% (mesmo processo) |
| Complexidade | Retry, timeout, hostname | Acesso direto |
| Failover | Necessário | Não aplicável |
#### Inicialização no server/index.ts
```typescript
// Inicia Kernel ANTES do servidor HTTP
const kernel = new ArcadiaKernel({
dashboard: { enabled: true, port: 5001, host: '0.0.0.0' },
autoStart: process.env.KERNEL_AUTOSTART === 'true',
});
await kernel.start();
// Exporta Registry globalmente
if (kernel.serviceRegistry) {
(global as any).arcadiaKernelRegistry = kernel.serviceRegistry;
}
// DEPOIS inicia servidor HTTP (porta 5000)
httpServer.listen({ port: 5000, host: "0.0.0.0" }, ...);
```
---
## 6. Configuração
### 6.1 Variáveis de Ambiente
```bash
# Kernel
KERNEL_ENABLED=true
KERNEL_AUTOSTART=true
# Coolify Discovery
COOLIFY_API_TOKEN=seu_token_aqui
# MetaSet
METASET_HOST=metaset
METASET_PORT=8100
# Modo Docker (desativa spawn de processos)
DOCKER_MODE=true
```
### 6.2 Docker Compose Labels
```yaml
services:
metaset:
labels:
- "arcadia.discovery.enabled=true"
- "arcadia.name=MetaSet BI"
- "arcadia.type=python"
- "arcadia.category=bi"
- "arcadia.port=8100"
- "arcadia.capabilities=dashboards,charts,sql_lab,rls,embedding"
```
---
## 7. Roadmap
### Semana 4 ✅ CONCLUÍDO
- [x] Service Registry
- [x] StaticDiscovery
- [x] CoolifyDiscovery
- [x] DockerDiscovery
- [x] Integração com Casa de Máquinas
### Semana 5 ✅ CONCLUÍDO
- [x] XOSDiscovery
- [x] Registry Global (acesso direto)
- [x] Integração Casa de Máquinas via memória
- [ ] UnifiedRegistry (agregador) - Opcional
### Semana 6 PLANEJADO
- [ ] Maestro IA (Guardião de IAs)
- [ ] Policy Engine
- [ ] Audit Trail
### Semana 7 PLANEJADO
- [ ] Integração completa Maestro
- [ ] Documentação P2P
---
## 8. Histórico de Mudanças
| Data | Versão | Mudança |
|------|--------|---------|
| 2026-04-06 | 3.0 | Service Registry + Discovery |
| 2026-04-07 | 3.1 | Integração Casa de Máquinas |
| 2026-04-08 | 3.2 | Coolify Discovery, XOS Discovery, correção MetaSet |
| 2026-04-08 | 3.3 | **Registry Global** - Casa de Máquinas usa acesso direto (memória) em vez de HTTP |
| 2026-04-08 | 3.4 | **MetaSet Consolidado** - Migração Metabase → Apache Superset 4.1.0 |
---
## 9. Referências
- `/server/kernel/` - Código fonte do Kernel
- `/server/kernel/registry/` - Service Registry
- `/server/kernel/discovery/` - Discovery Providers
- `/server/engine-room/` - Casa de Máquinas (backend)
- `/client/src/pages/Admin.tsx` - Casa de Máquinas (frontend)
- `/home/ubuntu/.kimi/kernel e tenants.md` - Planejamento executivo

View File

@ -1,885 +0,0 @@
# Arcádia Automações — Mapa Completo do Motor de Automação
> Mapa da arquitetura de automações da Arcádia Suite: Automation Engine
> (FastAPI :8005), Scheduler (Cron), Event Bus (Pub/Sub), Workflow
> Executor (multi-step), Service Node.js (fallback), XOS Automations
> (CRM), BPMN Viewer, e integração com Manus IA.
> Atualizado em: Março 2026
---
## 1. Visão Geral
O sistema de automações opera em **duas camadas** coordenadas: um motor Python (FastAPI) que executa workflows complexos com scheduler e event bus, e um serviço Node.js que gerencia o CRUD das automações e funciona como fallback de agendamento.
```
┌─────────────────────────────────────────────────────────────────────┐
│ ARCÁDIA AUTOMAÇÕES — STACK COMPLETO │
│ │
│ ┌─────────── Frontend ──────────────────────────────────────┐ │
│ │ │ │
│ │ Automations.tsx XosAutomations.tsx WorkflowBuilder │ │
│ │ (559 linhas) (350 linhas) (493 linhas) │ │
│ │ CRUD + Trigger CRM workflows Visual editor │ │
│ │ │ │
│ │ BpmnPage.tsx BpmnDiagram.tsx ProcessDiagram │ │
│ │ (12 linhas) (645 linhas) (184 linhas) │ │
│ │ BPMN viewer Diagrama visual Fluxos visuais │ │
│ └────────────────────────┬───────────────────────────────────┘ │
│ │ │
│ ┌─────────── Gateway (Node.js :5000) ───────────────────────┐ │
│ │ │ │ │
│ │ /api/automations/* ←───┤ CRUD + Execução (Drizzle) │ │
│ │ (routes.ts — 343L) │ │ │
│ │ │ │ │
│ │ /api/automation-engine/* ←┤ Proxy → FastAPI :8005 │ │
│ │ (engine-proxy.ts — 267L) │ │ │
│ │ │ │ │
│ │ AutomationService ←┤ Executor + Scheduler Node │ │
│ │ (service.ts — 209L) │ (polling 60s fallback) │ │
│ └────────────────────────┬───────────────────────────────────┘ │
│ │ │
│ ┌─────────── Motor Python (FastAPI :8005) ──────────────────┐ │
│ │ │ │ │
│ │ automation_engine.py (668 linhas) │ │
│ │ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │ │
│ │ │ Scheduler│ │Event Bus │ │Workflow Executor │ │ │
│ │ │ (Cron) │ │(Pub/Sub) │ │(Multi-step) │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ Entries │ │ Subscribe│ │ Condition │ │ │
│ │ │ Start/ │ │ Emit │ │ Action │ │ │
│ │ │ Stop │ │ History │ │ Delay │ │ │
│ │ │ 30s loop │ │ 500 max │ │ SQL Query │ │ │
│ │ │ │ │ │ │ HTTP Request │ │ │
│ │ │ │ │ │ │ Transform │ │ │
│ │ │ │ │ │ │ Notify │ │ │
│ │ └──────────┘ └──────────┘ └──────────────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────── PostgreSQL ────────────────────────────────────┐ │
│ │ automations, automation_actions, automation_logs, │ │
│ │ scheduled_tasks, xos_automations │ │
│ └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
```
---
## 2. Automation Engine (Python FastAPI — :8005)
O motor Python é o coração do sistema de automações. Roda como processo separado e expõe 3 subsistemas via REST:
### 2.1 — Event Bus (Barramento de Eventos)
```
┌──────────────────────────────────────────────────────────────────┐
│ EventBus — Pub/Sub em memória │
│ server/python/automation_engine.py:117 │
│ │
│ ┌─── Tipos de Eventos (EventType) ──────────────────────┐ │
│ │ │ │
│ │ record.created → Registro criado no banco │ │
│ │ record.updated → Registro atualizado │ │
│ │ record.deleted → Registro deletado │ │
│ │ schedule.fired → Scheduler disparou cron │ │
│ │ webhook.received → Webhook externo recebido │ │
│ │ threshold.reached → Limiar atingido │ │
│ │ agent.completed → Manus IA finalizou tarefa │ │
│ │ manual.trigger → Disparo manual do usuário │ │
│ │ system.event → Evento interno do sistema │ │
│ │ │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ ┌─── Operações ─────────────────────────────────────────┐ │
│ │ │ │
│ │ subscribe(event_type, handler_id, config) │ │
│ │ → Registra um handler para um tipo de evento │ │
│ │ │ │
│ │ unsubscribe(event_type, handler_id) │ │
│ │ → Remove assinatura │ │
│ │ │ │
│ │ emit(event_type, payload) │ │
│ │ → Emite evento, retorna handlers triggerados │ │
│ │ → Suporta wildcard "*" (ouve tudo) │ │
│ │ │ │
│ │ get_history(limit, event_type) │ │
│ │ → Últimos eventos (máx. 500 em memória) │ │
│ │ │ │
│ │ stats() │ │
│ │ → total_event_types, total_subscribers, │ │
│ │ history_size, event_types list │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ Fluxo: │
│ │
│ Emissor ──emit("record.created", {entity: "lead"})──▶ EventBus │
│ │ │
│ ┌────────────────────────────────┤ │
│ ▼ ▼ │
│ Handler A Handler B │
│ (workflow X) (notificação) │
└──────────────────────────────────────────────────────────────────┘
```
### 2.2 — Scheduler (Agendador Cron)
```
┌──────────────────────────────────────────────────────────────────┐
│ Scheduler — Cron-based scheduler │
│ server/python/automation_engine.py:186 │
│ │
│ ┌─── CronExpression Parser ─────────────────────────────┐ │
│ │ │ │
│ │ Formato: "minuto hora dia mês dia_semana" │ │
│ │ Suporta: *, ranges (1-5), steps (*/5), lists (1,3,5) │ │
│ │ │ │
│ │ Exemplos: │ │
│ │ "0 9 * * 1-5" → Seg-Sex às 9h │ │
│ │ "*/15 * * * *" → A cada 15 minutos │ │
│ │ "0 0 1 * *" → 1º dia de cada mês à meia-noite │ │
│ │ │ │
│ │ matches(datetime) → bool │ │
│ │ next_run(from_dt) → datetime │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ ┌─── SchedulerEntry ────────────────────────────────────┐ │
│ │ │ │
│ │ id: Identificador único │ │
│ │ name: Nome descritivo │ │
│ │ cron: Expressão cron (5 partes) │ │
│ │ automation_id: Link para automação (opcional) │ │
│ │ action: "trigger" (padrão) │ │
│ │ config: Configurações extras (JSON) │ │
│ │ is_active: Ativo/Inativo │ │
│ │ last_run: Última execução (ISO) │ │
│ │ next_run: Próxima execução (ISO) │ │
│ │ run_count: Contador de execuções │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ ┌─── Loop de Execução ──────────────────────────────────┐ │
│ │ │ │
│ │ Thread daemon, check_interval = 30 segundos │ │
│ │ │ │
│ │ A cada 30s: │ │
│ │ 1. Varre todas as entries ativas │ │
│ │ 2. Para cada entry: cron.matches(agora)? │ │
│ │ 3. Se match: │ │
│ │ a. Atualiza last_run, run_count │ │
│ │ b. Calcula next_run │ │
│ │ c. event_bus.emit("schedule.fired", { │ │
│ │ scheduler_id, automation_id, name │ │
│ │ }) │ │
│ │ 4. Sleep(30s) │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ Inicia automaticamente no startup do FastAPI │
└──────────────────────────────────────────────────────────────────┘
```
### 2.3 — Workflow Executor (Executor Multi-Step)
```
┌──────────────────────────────────────────────────────────────────┐
│ WorkflowExecutor │
│ server/python/automation_engine.py:273 │
│ │
│ ┌─── WorkflowDefinition ────────────────────────────────┐ │
│ │ │ │
│ │ id: Identificador do workflow │ │
│ │ name: Nome descritivo │ │
│ │ steps: Lista de WorkflowStep │ │
│ │ trigger: Tipo de trigger (opcional) │ │
│ │ variables: Variáveis iniciais (Dict) │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ ┌─── WorkflowStep ─────────────────────────────────────┐ │
│ │ │ │
│ │ id: Identificador do step │ │
│ │ type: Tipo (ver abaixo) │ │
│ │ config: Configuração do step (Dict) │ │
│ │ on_success: Próximo step se OK (opcional) │ │
│ │ on_failure: Próximo step se falha (opcional) │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ ┌─── Tipos de Steps (WorkflowStepType) ─────────────────┐ │
│ │ │ │
│ │ condition → Avalia condição (field, operator, value)│ │
│ │ Operadores: ==, !=, >, <, >=, <=, │ │
│ │ contains, exists │ │
│ │ │ │
│ │ action → Executa ação: │ │
│ │ • log (registra mensagem) │ │
│ │ • set_variable (define variável) │ │
│ │ • emit_event (emite evento no bus) │ │
│ │ │ │
│ │ delay → Pausa execução (max 30s) │ │
│ │ │ │
│ │ query → Executa SELECT no PostgreSQL │ │
│ │ Conexão read-only, timeout 10s, │ │
│ │ limite 100 rows │ │
│ │ │ │
│ │ http → Requisição HTTP (GET/POST/PUT/DELETE) │ │
│ │ Timeout 10s, resposta truncada em 5KB │ │
│ │ │ │
│ │ transform → Transforma dados: │ │
│ │ • count (conta itens) │ │
│ │ • sum (soma campo numérico) │ │
│ │ • filter (filtra por campo/valor) │ │
│ │ │ │
│ │ notify → Envia notificação (message + channel) │ │
│ │ │ │
│ │ loop → Iteração (planejado) │ │
│ │ parallel → Execução paralela (planejado) │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ ┌─── Fluxo de Execução ────────────────────────────────┐ │
│ │ │ │
│ │ 1. Recebe workflow_id + trigger_data + variables │ │
│ │ 2. Cria execution com status "running" │ │
│ │ 3. Merge: workflow.variables + variables + trigger │ │
│ │ 4. Para cada step (sequencial): │ │
│ │ a. Executa _execute_step(step, variables) │ │
│ │ b. Se resultado tem "output" → merge em variables │ │
│ │ c. Registra resultado do step │ │
│ │ 5. Status final: "completed" ou "error" │ │
│ │ 6. Guarda em memória (máx. 200 execuções) │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ Segurança: │
│ • SQL: somente SELECT, read-only connection, timeout 10s │
│ • HTTP: timeout 10s, resposta truncada │
│ • Delay: máximo 30 segundos │
└──────────────────────────────────────────────────────────────────┘
```
---
## 3. Automation Service (Node.js — Fallback)
O serviço Node.js complementa o motor Python, fornecendo CRUD das automações e um scheduler de fallback:
```
┌──────────────────────────────────────────────────────────────────┐
│ AutomationService │
│ server/automations/service.ts (209 linhas) │
│ │
│ ┌─── Executor de Ações (Node.js) ──────────────────────┐ │
│ │ │ │
│ │ runAutomation(automationId, userId, triggerData) │ │
│ │ │ │
│ │ 1. Cria log com status "running" │ │
│ │ 2. Busca automation + actions (orderBy orderIndex) │ │
│ │ 3. Executa cada action sequencialmente: │ │
│ │ │ │
│ │ Tipos de ação suportados: │ │
│ │ │ │
│ │ ┌────────────────────────────────────────────┐ │ │
│ │ │ agent_task → Dispara Manus IA │ │ │
│ │ │ manusService.run(prompt) │ │ │
│ │ │ Aguarda até 120s │ │ │
│ │ │ Polls a cada 2s │ │ │
│ │ ├────────────────────────────────────────────┤ │ │
│ │ │ send_notification→ Registra notificação │ │ │
│ │ ├────────────────────────────────────────────┤ │ │
│ │ │ erp_sync → Inicia sync com ERP │ │ │
│ │ ├────────────────────────────────────────────┤ │ │
│ │ │ generate_report → Gera relatório │ │ │
│ │ ├────────────────────────────────────────────┤ │ │
│ │ │ webhook → Chama URL externa │ │ │
│ │ │ POST + JSON payload │ │ │
│ │ ├────────────────────────────────────────────┤ │ │
│ │ │ send_email → Envia email │ │ │
│ │ ├────────────────────────────────────────────┤ │ │
│ │ │ send_whatsapp → Envia msg WhatsApp │ │ │
│ │ └────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ 4. Atualiza log: "completed" ou "error" │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ ┌─── Scheduler (Fallback) ──────────────────────────────┐ │
│ │ │ │
│ │ startScheduler() │ │
│ │ → setInterval(60000) — verifica a cada 60s │ │
│ │ │ │
│ │ checkScheduledTasks() │ │
│ │ → JOIN scheduled_tasks + automations │ │
│ │ → WHERE isActive = true AND nextRunAt <= agora │ │
│ │ → Executa runAutomation() para cada task due │ │
│ │ → Calcula nextRunAt baseado em intervalMinutes │ │
│ │ → Atualiza lastRunAt │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ Integração com Manus IA: │
│ • action "agent_task" → manusService.run(userId, prompt) │
│ • Aguarda execução com polling (até 120s) │
│ • Verifica status: completed | stopped | error │
└──────────────────────────────────────────────────────────────────┘
```
---
## 4. Engine Proxy (Node.js → Python)
O proxy traduz chamadas `/api/automation-engine/*` para o FastAPI em `:8005`:
```
┌──────────────────────────────────────────────────────────────────┐
│ Engine Proxy │
│ server/automations/engine-proxy.ts (267 linhas) │
│ │
│ Config: │
│ AUTOMATION_ENGINE_HOST = localhost (ou env var) │
│ AUTOMATION_ENGINE_PORT = 8005 │
│ TIMEOUT = 30 segundos │
│ │
│ Tradução: │
│ │
│ /api/automation-engine/health → GET :8005/health │
│ /api/automation-engine/version → GET :8005/version │
│ /api/automation-engine/metrics → GET :8005/metrics │
│ │
│ /api/automation-engine/scheduler/ → :8005/scheduler/ │
│ entries (GET/POST/DELETE) │
│ start (POST) │
│ stop (POST) │
│ │
│ /api/automation-engine/events/ → :8005/events/ │
│ emit (POST) │
│ subscribe (POST) │
│ subscribers (GET) │
│ history (GET) │
│ types (GET) │
│ │
│ /api/automation-engine/workflows/ → :8005/workflows/ │
│ register (POST) │
│ list (GET) │
│ get/:id (GET) │
│ execute/:id (POST) │
│ executions/:id (GET) │
│ │
│ /api/automation-engine/executions → :8005/executions │
│ /api/automation-engine/cron/validate → :8005/cron/validate │
│ │
│ Se engine offline → retorna { status: "offline" } no /health │
│ Demais rotas → 502 Bad Gateway se timeout │
└──────────────────────────────────────────────────────────────────┘
```
---
## 5. XOS Automations (CRM Workflows)
Automações específicas do CRM com triggers baseados em eventos de negócio:
```
┌──────────────────────────────────────────────────────────────────┐
│ XOS Automations │
│ Tabela: xos_automations — shared/schema.ts:6593 │
│ UI: client/src/pages/XosAutomations.tsx (350 linhas) │
│ │
│ ┌─── Schema ────────────────────────────────────────────┐ │
│ │ │ │
│ │ id serial PK │ │
│ │ tenantId FK → tenants.id │ │
│ │ name varchar(200) NOT NULL │ │
│ │ description text │ │
│ │ triggerType varchar(50) NOT NULL │ │
│ │ triggerConfig JSONB { condições do trigger } │ │
│ │ actions JSONB [ { type, config } ] │ │
│ │ conditions JSONB [ { field, operator, value } ] │ │
│ │ isActive boolean (default true) │ │
│ │ executionCount integer (default 0) │ │
│ │ lastExecutedAt timestamp │ │
│ │ createdBy FK → users.id │ │
│ │ createdAt, updatedAt timestamp │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ ┌─── Triggers CRM ─────────────────────────────────────┐ │
│ │ │ │
│ │ contact_created → Novo contato cadastrado │ │
│ │ deal_stage_changed → Deal mudou de estágio │ │
│ │ form_submitted → Formulário preenchido │ │
│ │ ticket_created → Ticket de suporte aberto │ │
│ │ deal_won → Negócio fechado (ganho) │ │
│ │ deal_lost → Negócio perdido │ │
│ │ message_received → Mensagem WhatsApp recebida │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ ┌─── Ações CRM ────────────────────────────────────────┐ │
│ │ │ │
│ │ send_email → Email automático │ │
│ │ send_whatsapp → Mensagem WhatsApp │ │
│ │ create_task → Cria tarefa/atividade │ │
│ │ assign_agent → Atribui agente ao ticket │ │
│ │ update_field → Atualiza campo do registro │ │
│ │ move_deal_stage → Move deal no pipeline │ │
│ │ webhook → Chama URL externa │ │
│ │ notify_team → Notifica equipe │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ Diferença vs automations genérica: │
│ • xos_automations → multi-tenant (tenantId) │
│ • xos_automations → triggers de negócio (CRM) │
│ • xos_automations → conditions inline (JSONB array) │
│ • automations → por usuário (userId) │
│ • automations → triggers genéricos (schedule/webhook/manual)│
└──────────────────────────────────────────────────────────────────┘
```
---
## 6. Banco de Dados — Tabelas
### 6.1 — automations (Automações Genéricas)
```
┌──────────────────────────────────────────────────────────────────┐
│ automations — shared/schema.ts:582 │
│ │
│ id serial PK │
│ userId FK → users.id (CASCADE) │
│ name text NOT NULL │
│ description text │
│ triggerType text NOT NULL │
│ "schedule" | "webhook" | "manual" | "event" │
│ triggerConfig text (JSON stringified) │
│ isActive "true" | "false" │
│ createdAt timestamp │
│ updatedAt timestamp │
└──────────────────────────────────────────────────────────────────┘
```
### 6.2 — automation_actions (Ações da Automação)
```
┌──────────────────────────────────────────────────────────────────┐
│ automation_actions — shared/schema.ts:594 │
│ │
│ id serial PK │
│ automationId FK → automations.id (CASCADE) │
│ orderIndex integer (ordem de execução) │
│ actionType text NOT NULL │
│ "agent_task" | "send_notification" | │
│ "erp_sync" | "generate_report" | │
│ "webhook" | "send_email" | "send_whatsapp" │
│ actionConfig text (JSON: url, method, to, prompt, etc.) │
│ conditionConfig text (JSON: condição para executar) │
└──────────────────────────────────────────────────────────────────┘
```
### 6.3 — automation_logs (Histórico de Execuções)
```
┌──────────────────────────────────────────────────────────────────┐
│ automation_logs — shared/schema.ts:603 │
│ │
│ id serial PK │
│ automationId FK → automations.id (CASCADE) │
│ status "running" | "completed" | "error" │
│ triggerData text (JSON: dados que dispararam) │
│ result text (resultado da execução) │
│ error text (mensagem de erro, se falhou) │
│ startedAt timestamp │
│ completedAt timestamp │
└──────────────────────────────────────────────────────────────────┘
```
### 6.4 — scheduled_tasks (Tarefas Agendadas)
```
┌──────────────────────────────────────────────────────────────────┐
│ scheduled_tasks — shared/schema.ts:614 │
│ │
│ id serial PK │
│ automationId FK → automations.id (CASCADE) │
│ cronExpression text ("0 9 * * 1-5") │
│ intervalMinutes integer (alternativa ao cron) │
│ nextRunAt timestamp (próxima execução) │
│ lastRunAt timestamp (última execução) │
│ isActive "true" | "false" │
│ │
│ Dois modos: │
│ 1. cronExpression → Parser Python (5 partes) │
│ 2. intervalMinutes → Simples: nextRunAt += interval │
└──────────────────────────────────────────────────────────────────┘
```
### 6.5 — xos_automations (CRM Automations)
```
┌──────────────────────────────────────────────────────────────────┐
│ xos_automations — shared/schema.ts:6593 │
│ │
│ id serial PK │
│ tenantId FK → tenants.id (CASCADE) │
│ name varchar(200) NOT NULL │
│ description text │
│ triggerType varchar(50) NOT NULL │
│ triggerConfig JSONB │
│ actions JSONB [ { type, config } ] │
│ conditions JSONB [ { field, operator, value } ] │
│ isActive boolean (default true) │
│ executionCount integer (default 0) │
│ lastExecutedAt timestamp │
│ createdBy FK → users.id │
│ createdAt, updatedAt timestamp │
└──────────────────────────────────────────────────────────────────┘
```
---
## 7. API REST Completa
### 7.1 — CRUD Automações (/api/automations/*)
| Método | Endpoint | Função |
|--------|----------|--------|
| GET | `/api/automations` | Lista automações do usuário |
| GET | `/api/automations/:id` | Detalhe (com actions + schedule) |
| POST | `/api/automations` | Cria automação (+ actions + schedule) |
| PUT | `/api/automations/:id` | Atualiza automação completa |
| DELETE | `/api/automations/:id` | Exclui automação (cascade) |
| POST | `/api/automations/:id/run` | Executa manualmente |
| GET | `/api/automations/:id/logs` | Histórico de execuções (últimas 50) |
### 7.2 — Automation Engine (/api/automation-engine/*)
#### Saúde e Métricas
| Método | Endpoint | Função |
|--------|----------|--------|
| GET | `/health` | Status (online/offline + stats) |
| GET | `/version` | Versão + capabilities |
| GET | `/metrics` | Métricas: scheduler, event_bus, workflows |
#### Scheduler
| Método | Endpoint | Função |
|--------|----------|--------|
| GET | `/scheduler/entries` | Lista entradas cron |
| POST | `/scheduler/entries` | Adiciona entrada cron |
| DELETE | `/scheduler/entries/:id` | Remove entrada |
| POST | `/scheduler/start` | Inicia loop do scheduler |
| POST | `/scheduler/stop` | Para loop do scheduler |
#### Event Bus
| Método | Endpoint | Função |
|--------|----------|--------|
| POST | `/events/emit` | Emite evento |
| POST | `/events/subscribe` | Registra handler |
| GET | `/events/subscribers` | Lista assinantes |
| GET | `/events/history` | Histórico de eventos |
| GET | `/events/types` | Tipos de eventos disponíveis |
#### Workflows
| Método | Endpoint | Função |
|--------|----------|--------|
| POST | `/workflows/register` | Registra workflow (steps) |
| GET | `/workflows` | Lista workflows |
| GET | `/workflows/:id` | Detalhe do workflow |
| POST | `/workflows/:id/execute` | Executa workflow |
| GET | `/workflows/:id/executions` | Execuções do workflow |
| GET | `/executions` | Todas as execuções |
#### Cron
| Método | Endpoint | Função |
|--------|----------|--------|
| POST | `/cron/validate` | Valida expressão cron + próximas 5 execuções |
**Total: 27+ endpoints de automação**
---
## 8. Frontend — Interfaces
### 8.1 — Automations.tsx (Gestão Geral)
```
┌──────────────────────────────────────────────────────────────────┐
│ Automations.tsx — 559 linhas │
│ Rota: /automations │
│ │
│ Componentes: │
│ │
│ ┌── CreateAutomationDialog ─────────────────────────────┐ │
│ │ • Nome, descrição │ │
│ │ • Tipo de trigger: schedule | webhook | manual | event│ │
│ │ • Config do trigger (JSON) │ │
│ │ • Actions: lista de { actionType, actionConfig } │ │
│ │ • Schedule: cronExpression ou intervalMinutes │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ ┌── AutomationCard ────────────────────────────────────┐ │
│ │ • Nome, tipo de trigger, status (ativo/inativo) │ │
│ │ • Botão: Executar agora │ │
│ │ • Botão: Ativar/Desativar │ │
│ │ • Botão: Excluir │ │
│ │ • Logs de execução (últimas 50) │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ Lista todas as automações do usuário logado │
└──────────────────────────────────────────────────────────────────┘
```
### 8.2 — XosAutomations.tsx (CRM Workflows)
```
┌──────────────────────────────────────────────────────────────────┐
│ XosAutomations.tsx — 350 linhas │
│ Rota: /xos/automations │
│ │
│ Foco em automações de CRM: │
│ • Triggers de negócio (contact_created, deal_stage_changed) │
│ • Ações comerciais (send_email, assign_agent, move_deal) │
│ • Condições inline (campo, operador, valor) │
│ • Contagem de execuções e última execução │
│ • Vinculado ao tenant (multi-tenant) │
└──────────────────────────────────────────────────────────────────┘
```
### 8.3 — WorkflowBuilder.tsx (Editor Visual)
```
┌──────────────────────────────────────────────────────────────────┐
│ WorkflowBuilder.tsx — 493 linhas │
│ Rota: /workflow-builder │
│ │
│ Editor visual de workflows: │
│ • Drag & drop de steps │
│ • Configuração de cada step (type + config) │
│ • Visualização do fluxo │
│ • Registro no Workflow Executor do engine │
│ • Execução e monitoramento │
└──────────────────────────────────────────────────────────────────┘
```
### 8.4 — BPMN (Diagramas de Processo)
```
┌──────────────────────────────────────────────────────────────────┐
│ BpmnDiagram.tsx — 645 linhas │
│ ProcessDiagram.tsx — 184 linhas │
│ BpmnPage.tsx — 12 linhas (wrapper) │
│ │
│ Visualização BPMN de processos: │
│ • Render de diagramas BPMN 2.0 │
│ • Nós: start, end, task, gateway, event │
│ • Conexões direcionais │
│ • Zoom e pan │
│ • Usado no Process Compass (Módulo Consultivo) │
└──────────────────────────────────────────────────────────────────┘
```
---
## 9. Fluxo de Execução Completo
### 9.1 — Automação Manual (Trigger: manual)
```
Usuário clica "Executar" na UI
POST /api/automations/:id/run
AutomationService.runAutomation()
├──▶ INSERT automation_logs (status: "running")
├──▶ SELECT automation_actions ORDER BY orderIndex
├──▶ Para cada action:
│ ├── agent_task → manusService.run() → GPT-4o
│ ├── webhook → fetch(url, POST, body)
│ ├── send_email → (placeholder)
│ ├── send_whatsapp → (placeholder)
│ ├── erp_sync → (placeholder)
│ └── generate_report → (placeholder)
└──▶ UPDATE automation_logs (status: "completed")
```
### 9.2 — Automação Agendada (Trigger: schedule)
```
┌───────────────────── Scheduler Python ──────────────────────┐
│ │
│ Thread daemon (30s loop) │
│ ↓ │
│ CronExpression.matches(agora)? → SIM │
│ ↓ │
│ event_bus.emit("schedule.fired", {automation_id: 42}) │
│ │
└──────────────────────────────────────────────────────────────┘
▼ (subscriber triggerado)
Handler executa ação configurada
┌───────────── OU ── Scheduler Node.js (Fallback) ────────────┐
│ │
│ setInterval(60000) → checkScheduledTasks() │
│ ↓ │
│ SELECT scheduled_tasks │
│ JOIN automations │
│ WHERE isActive AND nextRunAt <= NOW() │
│ ↓ │
│ AutomationService.runAutomation(id, userId) │
│ ↓ │
│ UPDATE nextRunAt = NOW() + intervalMinutes │
│ │
└──────────────────────────────────────────────────────────────┘
```
### 9.3 — Workflow Engine (Multi-Step)
```
POST /api/automation-engine/workflows/register
▼ Registra WorkflowDefinition com N steps
POST /api/automation-engine/workflows/:id/execute
WorkflowExecutor.execute(workflow_id, trigger_data, variables)
┌────────────── EXECUÇÃO SEQUENCIAL ──────────────────────────┐
│ │
│ Step 1: condition │
│ ├── field: "revenue" operator: ">" value: 10000 │
│ └── result: true → continua │
│ │
│ Step 2: query │
│ ├── sql: "SELECT COUNT(*) FROM crm_leads WHERE ..." │
│ └── output: { query_result: [{count: 42}] } │
│ │
│ Step 3: http │
│ ├── url: "https://api.slack.com/webhook" │
│ ├── method: "POST" │
│ └── body: { text: "42 novos leads!" } │
│ │
│ Step 4: transform │
│ ├── operation: "sum", source: "query_result" │
│ └── output: { sum: 42 } │
│ │
│ Step 5: notify │
│ ├── message: "Pipeline atualizado" │
│ └── channel: "system" │
│ │
│ → status: "completed", steps_completed: 5/5 │
└──────────────────────────────────────────────────────────────┘
```
---
## 10. Integração com Outros Sistemas
```
┌─────────────────────────────────────────────────────────────────────┐
│ INTEGRAÇÕES DA AUTOMAÇÃO │
│ │
│ ┌───────── Manus IA (GPT-4o) ─────────────────────────────┐ │
│ │ action: "agent_task" │ │
│ │ → manusService.run(userId, prompt) │ │
│ │ → Polling até 120s para conclusão │ │
│ │ → Retorna resultado do agente │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────── WhatsApp (Baileys) ──────────────────────────────┐ │
│ │ action: "send_whatsapp" │ │
│ │ trigger: "message_received" (xos_automations) │ │
│ │ → Integra via CommunicationService │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────── ERPNext/SOE ─────────────────────────────────────┐ │
│ │ action: "erp_sync" │ │
│ │ trigger: "record.created" / "record.updated" │ │
│ │ → Sincroniza entidades com motor SOE │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────── Webhooks Externos ───────────────────────────────┐ │
│ │ action: "webhook" │ │
│ │ trigger: "webhook.received" │ │
│ │ → HTTP POST/GET com payload JSON │ │
│ │ → Slack, Zapier, n8n, APIs externas │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────── Knowledge Graph ─────────────────────────────────┐ │
│ │ Eventos de automação alimentam o Knowledge Graph │ │
│ │ via comm_events → graph_nodes + graph_edges │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────── Casa de Máquinas ─────────────────────────────────┐ │
│ │ Monitora status do Automation Engine (:8005) │ │
│ │ GET /api/automation-engine/health │ │
│ │ Exibe: scheduler stats, event_bus stats, workflow stats │ │
│ └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
```
---
## 11. Arquivos-Chave
| Arquivo | Função | Linhas |
|---------|--------|--------|
| `server/python/automation_engine.py` | Motor FastAPI (Scheduler + EventBus + Workflows) | 668 |
| `server/automations/service.ts` | AutomationService (executor + scheduler fallback) | 209 |
| `server/automations/routes.ts` | CRUD API (/api/automations/*) | 343 |
| `server/automations/engine-proxy.ts` | Proxy Node → Python (:8005) | 267 |
| `shared/schema.ts` (L582-636) | 4 tabelas: automations, actions, logs, scheduled | 55 |
| `shared/schema.ts` (L6593-6608) | xos_automations (CRM workflows) | 16 |
| `client/src/pages/Automations.tsx` | Interface CRUD geral | 559 |
| `client/src/pages/XosAutomations.tsx` | Interface CRM automations | 350 |
| `client/src/pages/WorkflowBuilder.tsx` | Editor visual de workflows | 493 |
| `client/src/components/BpmnDiagram.tsx` | Diagrama BPMN | 645 |
| `client/src/components/ProcessDiagram.tsx` | Diagrama de processos | 184 |
---
## 12. Resumo Visual
```
┌─────────────────────────────────────────────────────────────────────┐
│ ARCÁDIA AUTOMAÇÕES STACK │
│ │
│ ┌─── Frontend ──────────────────────────────────────────────┐ │
│ │ Automations.tsx │ XosAutomations.tsx │ WorkflowBuilder │ │
│ │ BPMN Viewer │ ProcessDiagram │ │ │
│ └──────────────────────────┬────────────────────────────────┘ │
│ │ │
│ ┌──────────────────────────▼────────────────────────────────┐ │
│ │ Node.js Gateway (:5000) │ │
│ │ │ │
│ │ /api/automations/* → CRUD (Drizzle ORM) │ │
│ │ /api/automation-engine/* → Proxy para :8005 │ │
│ │ AutomationService → Executor + Scheduler 60s │ │
│ └──────────────────────────┬────────────────────────────────┘ │
│ │ │
│ ┌──────────────────────────▼────────────────────────────────┐ │
│ │ Python FastAPI (:8005) │ │
│ │ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │ │
│ │ │ Scheduler│ │Event Bus │ │Workflow Executor │ │ │
│ │ │ 30s loop │ │ Pub/Sub │ │ 10 step types │ │ │
│ │ │ Cron 5pt │ │ 9 events │ │ SQL/HTTP/Logic │ │ │
│ │ └──────────┘ └──────────┘ └──────────────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ PostgreSQL: 5 tabelas │ │
│ │ automations, automation_actions, automation_logs, │ │
│ │ scheduled_tasks, xos_automations │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Integrações: Manus IA, WhatsApp, ERPNext, Webhooks, │ │
│ │ Knowledge Graph, Casa de Máquinas │ │
│ └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
```

View File

@ -1,841 +0,0 @@
# Arcádia BI — Mapa de Business Intelligence
> Mapa completo da arquitetura de BI, incluindo Motor Python, MetaSet (Apache Superset),
> Cientista (IA), ETL/Staging, APIs e Frontend.
> Atualizado em: Abril 2026
---
## 1. Visão Geral
O BI da Arcádia Suite opera em **4 camadas complementares** que se combinam para oferecer analytics completo: desde queries SQL diretas até dashboards visuais no MetaSet (Apache Superset), passando por análise com IA e ingestão de dados externos.
```
┌─────────────────────────────────────────────────────────────────────┐
│ USUÁRIO / FRONTEND │
│ │
│ BiWorkspace.tsx (2.970 linhas) MetaSet (Embeddado) │
│ React + Recharts + Tailwind Proxy → :8088 │
│ 8 abas funcionais Dashboards avançados │
│ │
│ ┌──────────┬──────────┬──────────┬──────────┬──────────┐ │
│ │ Overview │DataSrc │ Upload │ Datasets │ Charts │ │
│ ├──────────┼──────────┼──────────┼──────────┼──────────┤ │
│ │ Backups │ Staging │ Advanced │ │ │ │
│ └──────────┴──────────┴──────────┴──────────┴──────────┘ │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ 🤖 Assistente BI (IA) │ │
│ │ Chat com GPT-4o sobre dados │ │
│ │ Prompts rápidos + análise livre │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────┬────────────────────────────────────────────┘
┌───────────────┼───────────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────────┐
│ API BI (Node) │ │BI Engine (Python)│ │ MetaSet (Python) │
│ /api/bi/* │ │ /api/bi-engine/* │ │ /bi/metaset/* │
│ │ │ │ │ │
│ CRUD: │ │ SQL Execution │ │ Dashboards │
│ - DataSources │ │ Chart Data Gen │ │ Perguntas/Queries │
│ - Datasets │ │ Micro-BI │ │ Alertas │
│ - Charts │ │ Análise Pandas │ │ Visualizações │
│ - Dashboards │ │ Cache (TTL 5min) │ │ Coleções │
│ - Backups │ │ Agregações │ │ Embedding │
│ - Upload/ETL │ │ Insights Auto │ │ │
│ - Staging │ │ │ │ │
│ - AI Analysis │ │ │ │ │
└────────┬────────┘ └────────┬─────────┘ └──────────┬──────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────────┐
│ PostgreSQL (Porta 5432) │
│ │
│ Tabelas de gestão BI: data_sources, bi_datasets, bi_charts, │
│ bi_dashboards, bi_dashboard_charts, backup_jobs, backup_artifacts, │
│ staged_tables, staging_mappings, migration_jobs │
│ │
│ Tabelas de negócio: persons, products, sales_orders, fin_*, │
│ crm_*, whatsapp_messages, valuation_*, etc. │
└─────────────────────────────────────────────────────────────────────┘
```
---
## 2. Os 4 Pilares do BI
### Pilar 1 — Motor BI Python (FastAPI :8004)
O coração analítico. Processa SQL, gera dados para gráficos e fornece Micro-BI.
```
┌─────────────────────────────────────────────────────────────┐
│ BI ENGINE (Python 3.11) │
│ FastAPI + Pandas + NumPy │
│ Porta: 8004 │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ SEGURANÇA SQL │ │
│ │ │ │
│ │ ✓ Somente SELECT/WITH permitidos │ │
│ │ ✓ 17 keywords bloqueadas (DROP, DELETE, INSERT...) │ │
│ │ ✓ 7 padrões perigosos filtrados (pg_sleep, --, /*) │ │
│ │ ✓ Multi-statement bloqueado (múltiplos ;) │ │
│ │ ✓ Conexão read-only (autocommit) │ │
│ │ ✓ Timeout: 30 segundos por query │ │
│ │ ✓ Limite: 10.000 linhas por resultado │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ CACHE (QueryCache) │ │
│ │ │ │
│ │ • Tipo: In-memory (OrderedDict) │ │
│ │ • Max entries: 200 │ │
│ │ • TTL: 300 segundos (5 minutos) │ │
│ │ • Key: SHA-256 de (SQL + params) │ │
│ │ • Métricas: hits, misses, hit_rate (%) │ │
│ │ • Invalidação: por padrão ou total │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ CAPABILITIES │ │
│ │ │ │
│ │ • sql_query → Execução de SELECT arbitrário │ │
│ │ • chart_data → Geração de dados para gráficos │ │
│ │ • micro_bi → Métricas rápidas com comparação │ │
│ │ • analysis → Análise estatística com Pandas │ │
│ │ • aggregation → Agregações customizadas │ │
│ │ • cache → Cache inteligente de queries │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
### Pilar 2 — MetaSet (Apache Superset :8100)
Plataforma visual de BI baseada em Apache Superset 4.1.0, acessível via proxy reverso.
```
┌─────────────────────────────────────────────────────────────┐
│ METASET (SUPERSET) │
│ Porta: 8100 (via proxy /bi/metaset) │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ PROXY (http-proxy-middleware) │ │
│ │ │ │
│ │ Gateway (:5000) → /bi/metaset/* → MetaSet (:8100) │ │
│ │ │ │
│ │ • pathRewrite: /bi/metaset → / │ │
│ │ • changeOrigin: true │ │
│ │ • timeout: 60 segundos │ │
│ │ • Reescrita de Location headers │ │
│ │ • Fallback: 502 "MetaSet indisponível" │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ FUNCIONALIDADES │ │
│ │ │ │
│ │ • Dashboards visuais (drag & drop) │ │
│ │ • Perguntas SQL / visual query builder │ │
│ │ • Alertas automáticos │ │
│ │ • Coleções organizadas │ │
│ │ • Embedding de dashboards │ │
│ │ • Filtros interativos │ │
│ │ • Exportação (CSV, Excel, PDF) │ │
│ │ • Agendamento de relatórios │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ CONEXÃO │ │
│ │ │ │
│ │ Banco: PostgreSQL (mesmo DATABASE_URL do Gateway) │ │
│ │ Acesso: Todas as tabelas public (read-only) │ │
│ │ Config: METABASE_HOST, METABASE_PORT │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
### Pilar 3 — API BI Node.js (Gateway :5000)
Camada de gestão e CRUD para objetos de BI.
```
┌─────────────────────────────────────────────────────────────┐
│ API BI (Node.js/Express) │
│ Registrada no Gateway (:5000) │
│ │
│ ┌── Gestão de Objetos BI ──────────────────────────────┐ │
│ │ │ │
│ │ Data Sources → Conexões com bancos externos │ │
│ │ Datasets → Conjuntos de dados (table/SQL/API) │ │
│ │ Charts → Gráficos salvos (config + dados) │ │
│ │ Dashboards → Painéis com layout de charts │ │
│ │ Backup Jobs → Jobs de backup programados │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ┌── ETL & Ingestão ────────────────────────────────────┐ │
│ │ │ │
│ │ Upload → Importação de arquivos │ │
│ │ Staging → Área de preparação para migração │ │
│ │ Migration → Jobs de migração de dados │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ┌── Tabelas Internas ──────────────────────────────────┐ │
│ │ │ │
│ │ Exploração de schema do PostgreSQL │ │
│ │ Criação de datasets a partir de tabelas internas │ │
│ │ Categorização automática (Sistema, CRM, BI, etc.) │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ┌── Análise com IA ────────────────────────────────────┐ │
│ │ │ │
│ │ OpenAI GPT-4o → Responde perguntas sobre dados │ │
│ │ Prompts pré-definidos (Resumo, Anomalias, etc.) │ │
│ │ Formato JSON estruturado (answer + insights) │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
### Pilar 4 — Cientista (Python: Auto-Programação)
Módulo de IA para análise avançada e geração de código.
```
┌─────────────────────────────────────────────────────────────┐
│ CIENTISTA (Python) │
│ python-service/services/cientista.py │
│ │
│ ┌── Análise Estatística ───────────────────────────────┐ │
│ │ │ │
│ │ analyze_data() │ │
│ │ → Shape (rows × columns) │ │
│ │ → Tipos de dados por coluna │ │
│ │ → Valores ausentes │ │
│ │ → Estatísticas (describe): mean, std, min, max │ │
│ │ → Amostra dos dados (5 primeiros registros) │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ┌── Detecção de Padrões ──────────────────────────────┐ │
│ │ │ │
│ │ detect_patterns() │ │
│ │ → Correlações fortes (> 0.7) entre colunas │ │
│ │ → Tendências (crescente/decrescente) via polyfit │ │
│ │ → Saída: [{ type, columns, value, description }] │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ┌── Insights Automáticos ─────────────────────────────┐ │
│ │ │ │
│ │ generate_insights() │ │
│ │ → Valores ausentes por coluna (% de missing) │ │
│ │ → Detecção de outliers (IQR × 1.5) │ │
│ │ → Valores dominantes (> 50% em categóricas) │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ┌── Auto-Programação (ScientistModule) ────────────────┐ │
│ │ │ │
│ │ generate_analysis_code(description, goal) │ │
│ │ → Templates: aggregate, filter, predict, correlate │ │
│ │ → Gera código Python executável │ │
│ │ → Salva padrões aprendidos │ │
│ │ → Histórico de execuções │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
---
## 3. API REST Completa
### 3.1 — BI Engine Proxy (/api/bi-engine/*)
Proxy do Gateway para o Motor Python FastAPI (:8004):
| Método | Endpoint | Função |
|--------|----------|--------|
| GET | `/api/bi-engine/health` | Status do motor (online/offline, banco, cache) |
| GET | `/api/bi-engine/version` | Versão e capabilities |
| GET | `/api/bi-engine/metrics` | Métricas (cache stats, limites) |
| GET | `/api/bi-engine/tables` | Lista todas as tabelas do PostgreSQL |
| GET | `/api/bi-engine/tables/:name/columns` | Schema de colunas de uma tabela |
| GET | `/api/bi-engine/tables/:name/preview` | Preview dos dados (limit padrão: 50) |
| GET | `/api/bi-engine/tables/:name/stats` | Estatísticas (row_count, column_count) |
| POST | `/api/bi-engine/query` | Executa SQL (somente SELECT/WITH) |
| POST | `/api/bi-engine/chart-data` | Gera dados formatados para gráficos |
| POST | `/api/bi-engine/micro-bi` | Micro-BI: métricas rápidas com comparação temporal |
| POST | `/api/bi-engine/analyze` | Análise de dados com Pandas |
| POST | `/api/bi-engine/aggregate` | Agregações customizadas |
| GET | `/api/bi-engine/cache/stats` | Estatísticas do cache |
| POST | `/api/bi-engine/cache/invalidate` | Invalida cache (por padrão ou total) |
### 3.2 — BI Management (/api/bi/*)
CRUD de objetos BI no Gateway Node.js:
#### Data Sources
| Método | Endpoint | Função |
|--------|----------|--------|
| GET | `/api/bi/data-sources` | Lista fontes de dados do usuário |
| POST | `/api/bi/data-sources` | Cria fonte (PostgreSQL, MySQL, MongoDB, SQLite, Internal) |
| POST | `/api/bi/data-sources/:id/test` | Testa conexão |
| DELETE | `/api/bi/data-sources/:id` | Remove fonte |
#### Datasets
| Método | Endpoint | Função |
|--------|----------|--------|
| GET | `/api/bi/datasets` | Lista datasets do usuário |
| POST | `/api/bi/datasets` | Cria dataset (table, SQL ou API) |
| POST | `/api/bi/datasets/:id/execute` | Executa dataset e retorna dados |
| DELETE | `/api/bi/datasets/:id` | Remove dataset |
#### Charts
| Método | Endpoint | Função |
|--------|----------|--------|
| GET | `/api/bi/charts` | Lista gráficos do usuário |
| POST | `/api/bi/charts` | Cria gráfico (bar, line, pie, area, scatter, table, metric, donut) |
| DELETE | `/api/bi/charts/:id` | Remove gráfico |
#### Dashboards
| Método | Endpoint | Função |
|--------|----------|--------|
| GET | `/api/bi/dashboards` | Lista dashboards do usuário |
| GET | `/api/bi/dashboards/:id` | Detalhe do dashboard + charts posicionados |
| POST | `/api/bi/dashboards` | Cria dashboard |
| POST | `/api/bi/dashboards/:id/charts` | Adiciona chart ao dashboard (com posição X/Y/W/H) |
| DELETE | `/api/bi/dashboards/:id` | Remove dashboard |
#### Tabelas Internas
| Método | Endpoint | Função |
|--------|----------|--------|
| GET | `/api/bi/internal-tables` | Lista tabelas do sistema (com tamanho e categoria) |
| GET | `/api/bi/internal-tables/:name/schema` | Schema + preview + row count |
| POST | `/api/bi/internal-tables/:name/create-dataset` | Cria dataset a partir de tabela interna |
| POST | `/api/bi/query` | Query direta em tabela (simples, limite 1000) |
#### Backups
| Método | Endpoint | Função |
|--------|----------|--------|
| GET | `/api/bi/backup-jobs` | Lista jobs de backup |
| POST | `/api/bi/backup-jobs` | Cria job (full, schema, data, incremental) |
| POST | `/api/bi/backup-jobs/:id/run` | Executa backup |
| DELETE | `/api/bi/backup-jobs/:id` | Remove job |
| GET | `/api/bi/backup-artifacts` | Lista artefatos gerados |
### 3.3 — Upload & ETL (/api/upload/*, /api/staging/*)
#### Upload de Dados
| Método | Endpoint | Função |
|--------|----------|--------|
| POST | `/api/upload/file` | Upload de arquivo (CSV, JSON, Excel, SQL, BSON, ZIP) |
| POST | `/api/upload/analyze` | Analisa arquivo e sugere schema |
Formatos suportados (até 200MB):
- `.csv`, `.txt` → Parser CSV inteligente (suporta aspas)
- `.json` → Parse direto de arrays/objetos
- `.xlsx`, `.xls` → Leitura via SheetJS (XLSX)
- `.sql` → Detecção de INSERTs e criação de datasets
- `.bson` → Parse via biblioteca BSON
- `.zip` → Extração e processamento de conteúdo
#### Staging (Área de Preparação)
| Método | Endpoint | Função |
|--------|----------|--------|
| GET | `/api/staging/tables` | Lista tabelas staged |
| GET | `/api/staging/tables/:id` | Detalhe de tabela staged |
| GET | `/api/staging/tables/:id/data` | Dados da tabela staged |
| POST | `/api/staging/tables/:id/mappings` | Cria mapeamento de colunas |
| GET | `/api/staging/tables/:id/mappings` | Lista mapeamentos |
| POST | `/api/staging/tables/:id/migrate` | Executa migração para tabela destino |
| DELETE | `/api/staging/tables/:id` | Remove tabela staged |
### 3.4 — MetaSet Proxy (/metaset/*)
| Rota | Destino | Função |
|------|---------|--------|
| `/metaset/*` | `http://localhost:8088/*` | Proxy reverso completo para MetaSet |
---
## 4. Banco de Dados — Tabelas do BI
### Objetos BI
```
┌─────────────────────────────┐ ┌──────────────────────────────┐
│ data_sources │ │ bi_datasets │
│ │ │ │
│ id, userId, name, │ │ id, userId, name, description,│
│ type (postgresql|mysql| │ │ dataSourceId, queryType │
│ mongodb|sqlite|internal), │ │ (table|sql|api), tableName, │
│ host, port, database, │ │ sqlQuery, columns, filters, │
│ username, password, │ │ isPublic, createdAt, updatedAt│
│ connectionString, isActive, │ │ │
│ lastTestedAt, createdAt │ └───────────────┬───────────────┘
└──────────────────────────────┘ │ 1:N
┌─────────────────────────────────────────────────────────────────┐
│ bi_charts │
│ │
│ id, userId, name, datasetId, chartType (bar|line|pie|area| │
│ scatter|table|metric|donut), config (JSON), xAxis, yAxis, │
│ groupBy, aggregation, colors, createdAt, updatedAt │
└──────────────────────────┬──────────────────────────────────────┘
│ N:M (via bi_dashboard_charts)
┌─────────────────────────────────────────────────────────────────┐
│ bi_dashboards bi_dashboard_charts │
│ │
│ id, userId, name, id, dashboardId, │
│ description, layout (JSON), chartId, │
│ isPublic, createdAt, updatedAt positionX, positionY, │
│ width, height │
└─────────────────────────────────────────────────────────────────┘
```
### Backups
```
┌───────────────────────────────┐ ┌───────────────────────────────┐
│ backup_jobs │ │ backup_artifacts │
│ │ │ │
│ id, userId, name, │ │ id, backupJobId, filename, │
│ dataSourceId, backupType │ │ filePath, fileSize, status │
│ (full|schema|data|incremental),│ │ (running|completed|failed), │
│ includeSchema, includeTables, │ │ startedAt, completedAt, │
│ excludeTables, compressionType,│ │ errorMessage │
│ retentionDays, storageLocation,│ │ │
│ lastRunAt, isActive, createdAt │ │ │
└───────────────────────────────┘ └───────────────────────────────┘
```
### Staging & Migração
```
┌───────────────────────────────┐ ┌───────────────────────────────┐
│ staged_tables │ │ staging_mappings │
│ │ │ │
│ id, userId, name, │ │ id, stagedTableId, │
│ originalFilename, fileType, │ │ sourceColumn, targetTable, │
│ rowCount, columnCount, │ │ targetColumn, transformType, │
│ columns (JSON), sampleData, │ │ transformConfig, createdAt │
│ status, createdAt, updatedAt │ │ │
└───────────────────────────────┘ └───────────────────────────────┘
┌───────────────────────────────────────────────────────────────────┐
│ migration_jobs │
│ │
│ id, userId, name, stagedTableId, targetTable, status, │
│ (pending|running|completed|failed), totalRows, processedRows, │
│ errorLog, mappingConfig (JSON), createdAt, updatedAt │
└───────────────────────────────────────────────────────────────────┘
```
---
## 5. Frontend — BiWorkspace.tsx (2.970 linhas)
### Tabs
| Tab | Valor | Descrição |
|-----|-------|-----------|
| **Overview** | `overview` | KPIs resumo: DataSources, Datasets, Charts, Dashboards, Backups |
| **Data Sources** | `datasources` | CRUD de fontes externas (PostgreSQL, MySQL, MongoDB, SQLite) + tabelas internas do sistema |
| **Upload** | `upload` | Importação de arquivos (CSV, Excel, JSON, SQL, BSON, ZIP) com preview automático |
| **Datasets** | `datasets` | Conjuntos de dados criados a partir de tabelas ou SQL, com execução e preview |
| **Charts** | `charts` | Criação de gráficos (8 tipos) a partir de datasets, com visualização Recharts |
| **Backups** | `backups` | Jobs de backup com execução e artefatos gerados |
| **Staging** | `staging` | Área de preparação: mapeamento de colunas, migração para tabelas destino |
| **Advanced** | `advanced` | BI Engine (link para Motor Python), MetaSet (link para iframe), Assistente IA |
### Tipos de Gráficos Suportados (Recharts)
| Tipo | Componente | Uso |
|------|-----------|-----|
| `bar` | `BarChart + Bar` | Comparação entre categorias |
| `line` | `LineChart + Line` | Evolução temporal |
| `pie` | `PieChart + Pie + Cell` | Proporções (até 8 fatias) |
| `area` | `AreaChart + Area` | Tendências com preenchimento |
| `scatter` | (planejado) | Correlação entre variáveis |
| `table` | HTML Table | Dados tabulares |
| `metric` | Card KPI | Valor único com destaque |
| `donut` | `PieChart (innerRadius)` | Proporções com centro vazio |
### Assistente BI (IA)
```
┌─────────────────────────────────────────────────────────────┐
│ 🤖 Assistente BI │
│ │
│ • Chat conversacional com GPT-4o │
│ • Recebe dados do dataset ativo como contexto │
│ • Prompts rápidos pré-definidos: │
│ 📊 "Resumo Executivo" (principais métricas e insights) │
│ 🔍 "Anomalias" (outliers e desvios) │
│ 📈 "Tendências" (padrões temporais) │
│ 💡 "Recomendações" (ações sugeridas) │
│ • Formato de resposta: JSON { answer, insights[] } │
│ • Interface: chat bubble com histórico │
└─────────────────────────────────────────────────────────────┘
```
### Categorias de Tabelas Internas
O BI mapeia automaticamente as tabelas do sistema em categorias:
| Categoria | Tabelas |
|-----------|---------|
| **Sistema** | users, applications, roles |
| **Process Compass** | pc_clients, pc_projects, pc_tasks |
| **CRM** | crm_clients, crm_contracts, crm_partners, crm_opportunities, crm_leads, crm_messages |
| **BI** | bi_datasets, bi_charts |
| **Conhecimento** | knowledge_base |
| **Manus** | manus_runs, agent_tasks |
| **Comunicação** | whatsapp_messages, conversations |
| **Outros** | Todas as demais tabelas (valuation_*, fin_*, etc.) |
---
## 6. Motor BI Python — Endpoints Detalhados
### POST /query — Execução SQL
```json
{
"sql": "SELECT status, COUNT(*) as total FROM sales_orders GROUP BY status",
"params": {},
"limit": 1000,
"use_cache": true
}
```
Retorno:
```json
{
"data": [{"status": "confirmed", "total": 45}, ...],
"columns": [{"name": "status", "type": "1043"}, ...],
"row_count": 3,
"elapsed_ms": 12.5,
"cached": false
}
```
### POST /chart-data — Dados para Gráficos
```json
{
"table": "sales_orders",
"x_axis": "created_at",
"y_axis": "total_amount",
"aggregation": "sum",
"time_grain": "month",
"group_by": "status",
"filters": [{"column": "status", "operator": "!=", "value": "cancelled"}],
"limit": 12
}
```
Retorno:
```json
{
"labels": ["2026-01", "2026-02", "2026-03"],
"series": {
"confirmed": [{"label": "2026-01", "value": 15000}, ...],
"delivered": [{"label": "2026-01", "value": 12000}, ...]
},
"row_count": 6,
"elapsed_ms": 18.3,
"query": "SELECT DATE_TRUNC('month', created_at) AS label, status AS series, SUM(total_amount) AS value FROM sales_orders WHERE status != 'cancelled' GROUP BY ...",
"cached": false
}
```
### POST /micro-bi — Métricas Rápidas
```json
{
"table": "fin_transactions",
"metrics": ["count", "sum:amount", "avg:amount"],
"dimension": "category",
"period": "month",
"compare_previous": true
}
```
Retorno:
```json
{
"current": [
{"dimension": "vendas", "count": 120, "sum_amount": 45000, "avg_amount": 375}
],
"previous": [
{"dimension": "vendas", "count": 98, "sum_amount": 38000, "avg_amount": 387.76}
],
"comparison": {
"count": {"current": 120, "previous": 98, "change": 22, "change_pct": 22.4, "trend": "up"}
}
}
```
### POST /analyze — Análise com Pandas
```json
{
"data": [{"col1": 10, "col2": "A"}, ...],
"question": "Quais são os outliers?"
}
```
Retorno: estatísticas por coluna (min, max, mean, median, std, sum), insights automáticos, sugestões de gráficos.
### Granularidades Temporais (time_grain)
| Valor | Função SQL | Exemplo |
|-------|-----------|---------|
| `day` | `DATE_TRUNC('day', col)` | 2026-03-16 |
| `week` | `DATE_TRUNC('week', col)` | 2026-03-11 |
| `month` | `DATE_TRUNC('month', col)` | 2026-03-01 |
| `quarter` | `DATE_TRUNC('quarter', col)` | 2026-01-01 |
| `year` | `DATE_TRUNC('year', col)` | 2026-01-01 |
### Funções de Agregação
| Valor | SQL | Uso |
|-------|-----|-----|
| `sum` | `SUM(col)` | Totalizador |
| `avg` | `AVG(col)` | Média |
| `count` | `COUNT(*)` | Contagem |
| `min` | `MIN(col)` | Mínimo |
| `max` | `MAX(col)` | Máximo |
---
## 7. Pipeline ETL — Fluxo de Ingestão
```
Arquivo (CSV/Excel/JSON/SQL/BSON/ZIP)
┌──────────────────────────────────────┐
│ 1. UPLOAD (/api/upload/file) │
│ │
│ • Multer (disk storage) │
│ • Limite: 200 MB │
│ • Validação de extensão │
│ • Parse do conteúdo │
└──────────────────┬───────────────────┘
┌──────────────────────────────────────┐
│ 2. STAGING (staged_tables) │
│ │
│ • Armazena headers e sample data │
│ • Detecta tipos de colunas │
│ • Preview para o usuário │
└──────────────────┬───────────────────┘
┌──────────────────────────────────────┐
│ 3. MAPEAMENTO (staging_mappings) │
│ │
│ • sourceColumn → targetColumn │
│ • transformType (cast, rename, etc.)│
│ • Validação de compatibilidade │
└──────────────────┬───────────────────┘
┌──────────────────────────────────────┐
│ 4. MIGRAÇÃO (migration_jobs) │
│ │
│ • Cria tabela destino se necessário │
│ • INSERT em lotes │
│ • Tracking: total/processed rows │
│ • Status: pending → running → │
│ completed/failed │
│ • Error log para debugging │
└──────────────────┬───────────────────┘
┌──────────────────────────────────────┐
│ 5. DATASET (bi_datasets) │
│ │
│ • Dataset pronto para visualização │
│ • Pode criar charts e dashboards │
│ • Conecta com BI Engine e MetaSet │
└──────────────────────────────────────┘
```
---
## 8. Integração entre Pilares
```
┌──────────────────────────────────────────────────────────────────────┐
│ FLUXO DE DADOS │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Upload │────▶│ Staging │────▶│ Dataset │────▶│ Chart │ │
│ │ (ETL) │ │(Prepare) │ │ (Dados) │ │ (Visual) │ │
│ └──────────┘ └──────────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │
│ ┌─────▼──────┐ ┌────▼─────┐ │
│ │ BI Engine │ │Dashboard │ │
│ │ (Análise) │ │(Painel) │ │
│ └─────┬──────┘ └──────────┘ │
│ │ │
│ ┌──────────┐ ┌─────▼──────┐ │
│ │ Tabelas │─────────────────────▶│ MetaSet │ │
│ │ Internas │ │ (Visual) │ │
│ │ (PG) │ └──────┬─────┘ │
│ └──────────┘ │ │
│ ┌─────▼──────┐ │
│ │ Cientista │ │
│ │ (IA/ML) │ │
│ └────────────┘ │
└──────────────────────────────────────────────────────────────────────┘
```
### Como os pilares se conectam:
| De | Para | Como |
|----|------|------|
| Upload | Staging | Arquivo parseado → tabela staged |
| Staging | Dataset | Migração → dataset criado automaticamente |
| Tabela Interna | Dataset | `/api/bi/internal-tables/:name/create-dataset` |
| Dataset | BI Engine | SQL do dataset executado no Motor Python |
| Dataset | Chart | Chart referencia datasetId |
| Chart | Dashboard | `bi_dashboard_charts` com posição (X,Y,W,H) |
| Tabelas PG | MetaSet | Conexão direta ao PostgreSQL |
| Dataset | Assistente IA | Dados enviados como contexto ao GPT-4o |
| Dados | Cientista | `analyze_data()`, `detect_patterns()`, `generate_insights()` |
---
## 9. Configuração e Variáveis de Ambiente
| Variável | Default | Usado por | Descrição |
|----------|---------|-----------|-----------|
| `DATABASE_URL` | - | BI Engine, Gateway | Conexão PostgreSQL |
| `BI_ENGINE_HOST` | `localhost` | Engine Proxy | Host do motor Python |
| `BI_PORT` | `8004` | BI Engine, Proxy | Porta do motor Python |
| `BI_ENGINE_TIMEOUT` | `30000` | Engine Proxy | Timeout de proxy (ms) |
| `METASET_HOST` | `metaset` | MetaSet Proxy | Host do MetaSet |
| `METASET_PORT` | `8100` | MetaSet Proxy | Porta do MetaSet |
| `METASET_TIMEOUT` | `30000` | MetaSet Proxy | Timeout do proxy (ms) |
| `OPENAI_API_KEY` | - | Assistente BI | API key para GPT-4o |
---
## 10. Arquivos-Chave
| Arquivo | Camada | Função | Linhas |
|---------|--------|--------|--------|
| `server/python/bi_engine.py` | Motor Python | SQL, Charts, Micro-BI, Cache, Análise | ~650 |
| `server/bi/routes.ts` | API Node | CRUD de DataSources, Datasets, Charts, Dashboards, Backups | ~700 |
| `server/bi/engine-proxy.ts` | Proxy | Proxy Gateway → BI Engine Python (:8004) | ~200 |
| `server/bi/upload.ts` | ETL | Upload de arquivos (CSV, Excel, JSON, SQL, BSON, ZIP) | ~1.060 |
| `server/bi/staging.ts` | ETL | Staging, mapeamento, migração de dados | ~408 |
| `server/bi/metaset-client/routes.ts` | Proxy | Proxy Gateway → MetaSet (:8100) | ~215 |
| `client/src/pages/BiWorkspace.tsx` | Frontend | Interface principal do BI (8 tabs) | ~2.970 |
| `server/bi/metaset-client/index.ts` | Cliente | Cliente TypeScript para API Superset | ~375 |
| `client/src/pages/MetaSetProxyPage.tsx` | Frontend | Página do MetaSet embeddado | ~25 |
| `python-service/services/cientista.py` | IA/ML | Análise, padrões, insights, auto-programação | ~567 |
| `shared/schema.ts` | Schema | Tabelas BI (9 tabelas) | Parte do schema |
---
## 11. Deploy (Docker/Coolify)
### Container `arcadia-bi` (Motor Python)
```dockerfile
FROM python:3.11-slim
WORKDIR /app
RUN pip install fastapi uvicorn pydantic pandas numpy psycopg2-binary
COPY server/python/bi_engine.py ./main.py
EXPOSE 8004
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8004"]
```
### Container `metaset`
```yaml
metaset:
image: metaset/metaset:latest
restart: always
ports:
- "8088:3000"
environment:
MB_DB_TYPE: postgres
MB_DB_DBNAME: metaset
MB_DB_PORT: 5432
MB_DB_USER: ${PGUSER:-arcadia}
MB_DB_PASS: ${PGPASSWORD}
MB_DB_HOST: postgres
JAVA_TIMEZONE: America/Sao_Paulo
depends_on:
postgres:
condition: service_healthy
volumes:
- metaset-data:/metaset-data
networks:
- arcadia
```
### Variáveis no Gateway (Docker)
```yaml
gateway:
environment:
BI_ENGINE_HOST: bi
BI_PORT: 8004
METABASE_HOST: metaset
METABASE_PORT: 3000
```
---
## 12. Resumo Visual
```
┌─────────────────────────────────────────────────────────────────────┐
│ ARCÁDIA BI STACK │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ FRONTEND: BiWorkspace.tsx + Recharts + Assistente IA (GPT) │ │
│ └────────────────────────────┬────────────────────────────────┘ │
│ │ │
│ ┌─────────────────────┼─────────────────────┐ │
│ │ │ │ │
│ ┌──────▼──────┐ ┌────────▼────────┐ ┌────────▼────────┐ │
│ │ API BI │ │ BI Engine │ │ MetaSet │ │
│ │ (Node.js) │ │ (Python:8004) │ │ (Java:8088) │ │
│ │ │ │ │ │ │ │
│ │ CRUD │ │ SQL + Charts │ │ Dashboards │ │
│ │ Upload/ETL │ │ Micro-BI │ │ Visual Builder │ │
│ │ Staging │ │ Análise Pandas │ │ Alertas │ │
│ │ Backups │ │ Cache (5min) │ │ Exportação │ │
│ └──────┬──────┘ └────────┬────────┘ └────────┬────────┘ │
│ │ │ │ │
│ └────────────────────┼──────────────────────┘ │
│ │ │
│ ┌──────────▼──────────┐ │
│ │ PostgreSQL │ │
│ │ (Porta 5432) │ │
│ │ │ │
│ │ 9 tabelas BI + │ │
│ │ 100+ tabelas SOE │ │
│ └────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ CIENTISTA (Python) — Análise, Padrões, Insights, AutoCode │ │
│ └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
```

View File

@ -1,834 +0,0 @@
# Arcádia CRM — Mapa Completo do Sistema de Relacionamento
> Mapa da arquitetura CRM nativa da Arcádia Suite: módulos, tabelas,
> APIs, comunicação unificada (WhatsApp/Email), motor de comissões,
> integração Frappe/ERPNext e frontend.
> Atualizado em: Março 2026
---
## 1. Visão Geral
O CRM da Arcádia Suite é **100% nativo** — construído internamente em Node.js + PostgreSQL com Drizzle ORM. Ele opera em **três camadas** que se complementam, unificadas por um Motor de Comunicação e alimentadas por IA (Manus/GPT-4o):
```
┌─────────────────────────────────────────────────────────────────────┐
│ CAMADA DE EXPERIÊNCIA │
│ │
│ ┌────────────────────────┐ ┌───────────────────────┐ │
│ │ Arcádia CRM (Crm.tsx) │ │ XOS CRM (XosCrm.tsx) │ │
│ │ 2.700 linhas │ │ 752 linhas │ │
│ │ 12 abas │ │ Kanban visual │ │
│ │ Gestão completa │ │ Deals + Pipeline │ │
│ └────────────┬───────────┘ └───────────┬───────────┘ │
│ │ │ │
│ └──────────┬────────────────┘ │
│ │ │
│ ┌───────────────────────▼───────────────────────────────────┐ │
│ │ WhatsApp.tsx — Interface de Atendimento │ │
│ │ Multi-sessão, tickets, IA auto-reply │ │
│ └───────────────────────────────────────────────────────────┘ │
└──────────────────────────┬───────────────────────────────────────────┘
┌──────────────────────────▼───────────────────────────────────────────┐
│ CAMADA DE ORQUESTRAÇÃO │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Routes │ │ Storage │ │ Comm. │ │Commission│ │ WA │ │
│ │ (1.592L) │ │ (708L) │ │ (289L) │ │ Engine │ │ Bridge │ │
│ │ │ │ │ │ │ │ (418L) │ │ (126L) │ │
│ │ 90+ │ │CrmStorage│ │ Connect/ │ │ Revenue │ │ Sessão │ │
│ │ endpoints│ │ class │ │ Send/ │ │ Schedule │ │ ↔ Canal │ │
│ │ │ │ │ │ Receive │ │ Comissões│ │ Eventos │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Frappe Svc │ │Google Calendar│ │ Knowledge │ │
│ │ ERPNext sync │ │ OAuth + Sync │ │ Graph Feed │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└──────────────────────────┬───────────────────────────────────────────┘
┌──────────────────────────▼───────────────────────────────────────────┐
│ CAMADA DE DADOS │
│ │
│ PostgreSQL — 30 tabelas CRM (crm_*) + 15 tabelas XOS (xos_*) │
│ + 8 tabelas Communication (comm_*) │
│ │
│ Drizzle ORM — Schema em shared/schema.ts │
└─────────────────────────────────────────────────────────────────────┘
```
---
## 2. Os Três CRMs
### 2.1 — Arcádia CRM (Principal)
O CRM completo da plataforma. Gerencia todo o ciclo de relacionamento: da captação de leads até contratos e comissões.
```
┌─────────────────────────────────────────────────────────────┐
│ ARCÁDIA CRM │
│ client/src/pages/Crm.tsx (2.700 linhas) │
│ /api/crm/* (90+ endpoints) │
│ │
│ 12 Abas: │
│ ┌──────────┬──────────┬──────────┬──────────┐ │
│ │Dashboard │ Leads │ Opps │ Produtos │ │
│ ├──────────┼──────────┼──────────┼──────────┤ │
│ │Parceiros │ Clientes │Contratos │Mensagens │ │
│ ├──────────┼──────────┼──────────┼──────────┤ │
│ │Calendário│Integrações│Config │Multitenant│ │
│ └──────────┴──────────┴──────────┴──────────┘ │
│ │
│ Domínios: │
│ • Pipeline de Vendas (Leads → Opps → Propostas → Contratos)│
│ • Parceiros & Certificações │
│ • Produtos & Catálogo │
│ • Comunicação Multicanal (WhatsApp, Email) │
│ • Comissões & Revenue Schedule │
│ • Agenda (Google Calendar) │
│ • Integração Frappe/ERPNext │
│ • Multi-tenant │
└─────────────────────────────────────────────────────────────┘
```
### 2.2 — XOS CRM (Kanban/Pipeline)
Visão simplificada e visual focada em gestão de deals com drag & drop.
```
┌─────────────────────────────────────────────────────────────┐
│ XOS CRM │
│ client/src/pages/XosCrm.tsx (752 linhas) │
│ │
│ Foco: │
│ • Kanban visual de deals │
│ • Pipeline com estágios customizáveis │
│ • Contatos & Empresas │
│ • Conversas & Tickets │
│ • Campanhas & Automações │
│ • Filas de atendimento │
│ │
│ Tabelas próprias: xos_contacts, xos_companies, │
│ xos_pipelines, xos_pipeline_stages, xos_deals, │
│ xos_conversations, xos_messages, xos_tickets, │
│ xos_campaigns, xos_automations, xos_activities, │
│ xos_queues, xos_queue_users, xos_internal_notes, │
│ xos_quick_messages │
└─────────────────────────────────────────────────────────────┘
```
### 2.3 — Plus CRM (Legado/PHP)
Módulo CRM dentro do Arcádia Plus (Laravel), focado em anotações e histórico.
```
┌─────────────────────────────────────────────────────────────┐
│ PLUS CRM (Legado) │
│ plus/app/Http/Controllers/CrmController │
│ │
│ • Anotações de clientes (crm_anotacoes) │
│ • Histórico de interações │
│ • Integrado ao ERP Plus (fiscal, estoque, financeiro) │
│ • Proxy reverso via /plus │
│ • Banco MySQL (separado do PostgreSQL) │
└─────────────────────────────────────────────────────────────┘
```
---
## 3. Ciclo de Vida do Cliente
```
┌────────┐ ┌────────┐ ┌────────────┐ ┌──────────┐ ┌──────────┐
│ LEAD │───▶│ OPP │───▶│ PROPOSTA │───▶│ CONTRATO │───▶│ CLIENTE │
│ │ │ │ │ │ │ │ │ │
│ Nome │ │ Valor │ │ Itens │ │ Receita │ │ Ativo │
│ Email │ │ Stage │ │ Preços │ │ Parcelas │ │ Suporte │
│ Fone │ │ Prob% │ │ Desconto │ │ Comissão │ │ Upsell │
│ Source │ │ Close │ │ Aprovação │ │ Marcos │ │ │
│ Status │ │ Owner │ │ PDF │ │ Duração │ │ │
└────┬───┘ └────┬───┘ └────────────┘ └──────────┘ └──────────┘
│ │
│ convert │ won/lost
│ │
▼ ▼
POST /leads/ POST /opportunities/
:id/convert :id/won
:id/lost
:id/bill
:id/open-project
```
### Fluxo detalhado:
| Etapa | Ação | Endpoint | Resultado |
|-------|------|----------|-----------|
| 1 | Captação | `POST /api/crm/leads` | Lead criado (status: new) |
| 2 | Qualificação | `PATCH /api/crm/leads/:id` | Lead atualizado (status: qualified) |
| 3 | Conversão | `POST /api/crm/leads/:id/convert` | Cria Opportunity + Client automaticamente |
| 4 | Negociação | `PATCH /api/crm/opportunities/:id/stage` | Move entre estágios do pipeline |
| 5 | Proposta | `POST /api/crm/proposals` | Proposta com itens e preços |
| 6 | Ganho | `POST /api/crm/opportunities/:id/won` | Marca como ganha |
| 7 | Contrato | `POST /api/crm/contracts` | Contrato com receita e marcos |
| 8 | Faturamento | `POST /api/crm/opportunities/:id/bill` | Gera faturamento |
| 9 | Projeto | `POST /api/crm/opportunities/:id/open-project` | Abre projeto no Process Compass |
| 10 | Perda | `POST /api/crm/opportunities/:id/lost` | Marca como perdida + motivo |
---
## 4. Comunicação Multicanal
```
┌─────────────────────────────────────────────────────────────────────┐
│ SISTEMA DE COMUNICAÇÃO │
│ │
│ ┌───────────┐ ┌────────────────┐ ┌──────────────────┐ │
│ │ WhatsApp │ │ Communication │ │ CRM Threads │ │
│ │ (Baileys) │───▶│ Service │───▶│ & Messages │ │
│ │ │ │ │ │ │ │
│ │ Multi- │ │ connectChannel │ │ crm_channels │ │
│ │ sessão │ │ sendMessage │ │ crm_threads │ │
│ │ QR Code │ │ receiveMessage │ │ crm_messages │ │
│ │ Auto-reply│ │ getOrCreate │ │ crm_quick_msgs │ │
│ │ (GPT-4o) │ │ Thread │ │ │ │
│ └─────┬─────┘ └────────────────┘ └──────────────────┘ │
│ │ │
│ ┌─────▼─────────────────────────────────────┐ │
│ │ WhatsApp Bridge │ │
│ │ │ │
│ │ Evento "message" → handleIncoming │ │
│ │ Evento "connected"→ updateChannelStatus │ │
│ │ Evento "qr" → storeQRCode │ │
│ │ Evento "disconnect"→ markDisconnected │ │
│ │ │ │
│ │ sessionChannelMap: Session ↔ CRM Channel │ │
│ └────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────┐ │
│ │ Motor de Comunicação (:8006) │ │
│ │ │ │
│ │ Unifica: │ │
│ │ • CRM Threads (crm_threads) │ │
│ │ • XOS Conversations (xos_conversations) │ │
│ │ • WhatsApp Tickets (xos_tickets) │ │
│ │ │ │
│ │ Tabelas comm_*: │ │
│ │ comm_contacts, comm_threads, │ │
│ │ comm_messages, comm_channels, │ │
│ │ comm_queues, comm_quick_messages, │ │
│ │ comm_events → Knowledge Graph │ │
│ └────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
```
### Fluxo de mensagem WhatsApp:
```
Cliente envia mensagem no WhatsApp
Baileys (whatsappService) recebe
▼ emite evento "message"
WhatsApp Bridge captura
▼ mapeia session → channelId
CommunicationService.receiveMessage()
├──▶ getOrCreateThread() → cria/reutiliza thread
├──▶ crmMessages → INSERT (direction: "incoming")
├──▶ AI Auto-Reply? (GPT-4o-mini em horário comercial)
└──▶ Knowledge Graph → graph_nodes + graph_edges
```
### Canais suportados:
| Canal | Tecnologia | Status |
|-------|-----------|--------|
| **WhatsApp** | Baileys (multi-device) | Ativo |
| **Email** | SMTP + IMAP | Planejado |
| **SMS** | API (Twilio/etc.) | Futuro |
| **Telegram** | Bot API | Futuro |
| **Chat Interno** | Socket.IO | Ativo (Comunidades) |
---
## 5. Motor de Comissões
```
┌─────────────────────────────────────────────────────────────┐
│ COMMISSION ENGINE │
│ server/crm/commission-engine.ts (418L) │
│ │
│ ┌── Revenue Schedule ──────────────────────────────────┐ │
│ │ │ │
│ │ generateRevenueSchedule(contractId) │ │
│ │ │ │
│ │ • Mensal: projeta N parcelas (até 24 meses) │ │
│ │ • Anual: projeta N anos (até 10 anos) │ │
│ │ • Calcula datas de vencimento │ │
│ │ • Status: pending → paid → overdue │ │
│ │ │ │
│ │ Contrato R$ 5.000/mês × 12 meses: │ │
│ │ → 12 registros em crm_revenue_schedule │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ┌── Commission Rules ─────────────────────────────────┐ │
│ │ │ │
│ │ crm_commission_rules │ │
│ │ │ │
│ │ Regras baseadas em: │ │
│ │ • Tipo de receita: recurring / one-time │ │
│ │ • Cenário de venda: direct / partner │ │
│ │ • Mês do contrato (faixas) │ │
│ │ │ │
│ │ Exemplo: │ │
│ │ Meses 1-5 (aquisição): 15% sobre receita │ │
│ │ Meses 6-12 (retenção): 10% sobre receita │ │
│ │ Meses 13+ (maturidade): 5% sobre receita │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ┌── Cálculo de Comissões ─────────────────────────────┐ │
│ │ │ │
│ │ calculateCommissions(contractId) │ │
│ │ │ │
│ │ Para cada parcela da revenue schedule: │ │
│ │ 1. Identifica o mês do contrato │ │
│ │ 2. Busca regra aplicável (tipo + cenário + faixa) │ │
│ │ 3. Calcula valor = parcela × percentual │ │
│ │ 4. Cria registro em crm_commissions │ │
│ │ │ │
│ │ Status: pending → approved → paid │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
---
## 6. Integração Frappe/ERPNext
```
┌─────────────────────────────────────────────────────────────┐
│ FRAPPE SERVICE │
│ server/crm/frappe-service.ts │
│ │
│ Autenticação: token apiKey:apiSecret │
│ Base URL: configurável por conector │
│ │
│ Sync Bidirecional: │
│ │
│ Arcádia CRM ←──────────→ ERPNext/Frappe │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Entidade Arcádia DocType Frappe │ │
│ │ ─────────────────── ───────────────── │ │
│ │ crm_leads → Lead │ │
│ │ crm_opportunities→ Opportunity │ │
│ │ crm_products → Item │ │
│ │ crm_partners → Supplier (tipo: partner) │ │
│ │ crm_clients → Customer │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ Funcionalidades: │
│ • POST /frappe/connectors → Cria conector │
│ • POST /frappe/connectors/:id/test → Testa conexão │
│ • POST /frappe/connectors/:id/sync → Sincroniza dados │
│ • GET /frappe/connectors/:id/logs → Logs de sync │
│ • CRUD /frappe/connectors/:id/mappings → Mapeamentos │
│ │
│ Tabelas: crm_frappe_connectors, crm_frappe_mappings │
└─────────────────────────────────────────────────────────────┘
```
---
## 7. Google Calendar
```
┌─────────────────────────────────────────────────────────────┐
│ GOOGLE CALENDAR INTEGRATION │
│ server/crm/google-calendar.ts │
│ │
│ OAuth 2.0 Flow: │
│ GET /api/crm/google/auth → Inicia OAuth │
│ GET /api/crm/google/callback → Recebe token │
│ GET /api/crm/google/status → Status da conexão │
│ POST /api/crm/google/disconnect → Desconecta │
│ POST /api/crm/google/sync → Sincroniza eventos │
│ GET /api/crm/google/events → Lista eventos │
│ POST /api/crm/google/events → Cria evento │
│ │
│ Tabela: crm_google_tokens │
│ Tabela: crm_events (sincronizado) │
└─────────────────────────────────────────────────────────────┘
```
---
## 8. Banco de Dados — Todas as Tabelas
### 8.1 — Tabelas CRM (crm_*) — 30 tabelas
#### Entidades Principais
```
┌──────────────────────────┐ ┌──────────────────────────┐
│ crm_leads │ │ crm_clients │
│ │ │ │
│ id, tenantId, name, │ │ id, tenantId, name, │
│ email, phone, company, │ │ email, phone, company, │
│ source, status (new| │ │ type, address, city, │
│ contacted|qualified| │ │ state, notes, segment, │
│ proposal|won|lost), │ │ isActive, createdAt, │
│ notes, assignedTo, │ │ updatedAt │
│ estimatedValue, │ │ │
│ createdAt, updatedAt │ └──────────────────────────┘
└──────────────────────────┘
┌──────────────────────────┐ ┌──────────────────────────┐
│ crm_opportunities │ │ crm_partners │
│ │ │ │
│ id, tenantId, name, │ │ id, tenantId, companyName,│
│ clientId, value, │ │ contactName, email, phone,│
│ probability, stageId, │ │ type (reseller|referral| │
│ expectedCloseDate, │ │ integrator|consultant), │
│ status (open|won|lost), │ │ status (active|inactive| │
│ lostReason, assignedTo, │ │ suspended), tier (gold| │
│ notes, approvalStatus, │ │ silver|bronze|platinum), │
│ approvedBy, createdAt │ │ commissionRate, region, │
│ │ │ specializations, notes │
└──────────────────────────┘ └──────────────────────────┘
```
#### Pipeline e Propostas
```
┌───────────────────────────┐ ┌───────────────────────────┐
│ crm_pipeline_stages │ │ crm_proposals │
│ │ │ │
│ id, tenantId, name, │ │ id, tenantId, opportunityId│
│ order, color, isDefault, │ │ title, description, │
│ probability, createdAt │ │ totalValue, discount, │
│ │ │ validUntil, status (draft| │
│ │ │ sent|approved|rejected), │
│ │ │ approvedBy, notes, │
│ │ │ createdAt, updatedAt │
└───────────────────────────┘ └───────────────────────────┘
┌───────────────────────────┐
│ crm_proposal_items │
│ │
│ id, proposalId, productId, │
│ description, quantity, │
│ unitPrice, discount, │
│ totalPrice, createdAt │
└───────────────────────────┘
```
#### Contratos e Financeiro
```
┌───────────────────────────┐ ┌───────────────────────────┐
│ crm_contracts │ │ crm_revenue_schedule │
│ │ │ │
│ id, tenantId, name, │ │ id, contractId, month, │
│ clientId, partnerId, │ │ dueDate, value, status │
│ type (recurring|one_time), │ │ (pending|paid|overdue), │
│ totalValue, monthlyValue, │ │ paidDate, createdAt │
│ startDate, endDate, │ │ │
│ billingCycle (monthly| │ └───────────────────────────┘
│ yearly), status (draft| │
│ active|completed|cancelled)│ ┌───────────────────────────┐
│ autoRenew, notes, │ │ crm_contract_milestones │
│ createdAt, updatedAt │ │ │
└───────────────────────────┘ │ id, contractId, name, │
│ description, dueDate, │
┌───────────────────────────┐ │ status (pending|in_progress│
│ crm_commission_rules │ │ |completed|delayed), │
│ │ │ completedAt, notes │
│ id, tenantId, name, │ └───────────────────────────┘
│ revenueType (recurring| │
│ one_time), saleType │
│ (direct|partner), │
│ percentage, monthFrom, │
│ monthTo, isActive │
└───────────────────────────┘
┌───────────────────────────┐
│ crm_commissions │
│ │
│ id, tenantId, contractId, │
│ partnerId, userId, │
│ revenueScheduleId, month, │
│ baseValue, percentage, │
│ commissionValue, status │
│ (pending|approved|paid), │
│ paidDate, createdAt │
└───────────────────────────┘
```
#### Comunicação
```
┌───────────────────────────┐ ┌───────────────────────────┐
│ crm_channels │ │ crm_threads │
│ │ │ │
│ id, tenantId, name, │ │ id, tenantId, channelId, │
│ type (whatsapp|email|sms| │ │ contactPhone, contactName, │
│ telegram), status │ │ status (open|waiting| │
│ (connected|disconnected| │ │ resolved|closed), │
│ pending_qr), identifier, │ │ assignedTo, priority, │
│ qrCode, sessionData, │ │ subject, tags, unreadCount,│
│ lastConnectedAt, │ │ lastMessageAt, closedAt, │
│ createdAt, updatedAt │ │ createdAt, updatedAt │
└───────────────────────────┘ └───────────────────────────┘
┌───────────────────────────┐ ┌───────────────────────────┐
│ crm_messages │ │ crm_quick_messages │
│ │ │ │
│ id, threadId, direction │ │ id, tenantId, title, │
│ (incoming|outgoing), │ │ content, shortcut, │
│ content, contentType │ │ category, isActive, │
│ (text|image|audio|video| │ │ createdAt │
│ document), externalId, │ │ │
│ status (sent|delivered| │ │ Uso: /atalho → mensagem │
│ read|failed), senderName, │ │ rápida pré-definida │
│ metadata, createdAt │ │ │
└───────────────────────────┘ └───────────────────────────┘
```
#### Outros
```
┌───────────────────────────┐ ┌───────────────────────────┐
│ crm_products │ │ crm_opportunity_products │
│ │ │ │
│ id, tenantId, name, sku, │ │ id, opportunityId, │
│ description, category, │ │ productId, quantity, │
│ unitPrice, costPrice, │ │ unitPrice, discount, │
│ type (product|service| │ │ totalPrice │
│ subscription), isActive │ └───────────────────────────┘
└───────────────────────────┘
┌───────────────────────────┐ ┌───────────────────────────┐
│ crm_campaigns │ │ crm_campaign_contacts │
│ │ │ │
│ id, tenantId, name, type, │ │ id, campaignId, contactId, │
│ status, channelId, │ │ status (pending|sent| │
│ message, scheduledAt, │ │ delivered|failed|replied), │
│ sentCount, deliveredCount, │ │ sentAt, deliveredAt, │
│ failedCount, createdAt │ │ repliedAt │
└───────────────────────────┘ └───────────────────────────┘
┌───────────────────────────┐ ┌───────────────────────────┐
│ crm_events │ │ crm_google_tokens │
│ │ │ │
│ id, tenantId, title, │ │ id, userId, accessToken, │
│ description, type, │ │ refreshToken, expiresAt, │
│ startDate, endDate, │ │ calendarId, createdAt │
│ isAllDay, location, │ │ │
│ attendees, googleEventId, │ └───────────────────────────┘
│ color, reminders, │
│ createdBy, createdAt │ ┌───────────────────────────┐
│ │ │ crm_opportunity_registrations│
└───────────────────────────┘ │ │
│ id, opportunityId, partnerId,│
┌───────────────────────────┐ │ status (pending|approved| │
│ crm_partner_certifications │ │ rejected), registeredAt │
│ │ └───────────────────────────┘
│ id, partnerId, name, │
│ issuer, certDate, expDate, │ ┌───────────────────────────┐
│ status, documentUrl │ │ crm_partner_performance │
│ │ │ │
└───────────────────────────┘ │ id, partnerId, period, │
│ revenue, deals, activeContracts│
│ satisfaction, churnRate │
└───────────────────────────┘
```
### 8.2 — Tabelas XOS CRM (xos_*) — 15 tabelas
```
xos_contacts → Contatos unificados (nome, email, phone, company, tags)
xos_companies → Empresas (nome, CNPJ, segmento, tamanho, receita)
xos_pipelines → Pipelines customizáveis
xos_pipeline_stages → Estágios de pipeline (nome, ordem, cor, prob%)
xos_deals → Negócios/Deals (valor, estágio, expected close)
xos_conversations → Conversas (canal, status, prioridade)
xos_messages → Mensagens individuais
xos_tickets → Tickets de suporte (SLA, prioridade)
xos_campaigns → Campanhas de marketing
xos_automations → Regras de automação (trigger, action)
xos_activities → Atividades (calls, emails, meetings, tasks)
xos_queues → Filas de atendimento
xos_queue_users → Usuários nas filas
xos_internal_notes → Notas internas de deals/contatos
xos_quick_messages → Mensagens rápidas/templates
```
### 8.3 — Tabelas Comunicação Unificada (comm_*) — 8 tabelas
```
comm_channels → Canais unificados
comm_contacts → Contatos unificados (merge CRM + XOS + WA)
comm_threads → Threads unificadas
comm_messages → Mensagens unificadas
comm_queues → Filas unificadas
comm_queue_members → Membros das filas
comm_quick_messages → Templates unificados
comm_events → Eventos (alimentam Knowledge Graph)
```
---
## 9. API REST Completa (/api/crm/*)
### Parceiros
| Método | Endpoint | Função |
|--------|----------|--------|
| GET | `/partners` | Lista parceiros |
| GET | `/partners/:id` | Detalhe do parceiro |
| POST | `/partners` | Cria parceiro |
| PATCH | `/partners/:id` | Atualiza parceiro |
| DELETE | `/partners/:id` | Remove parceiro |
| GET | `/partners/:id/certifications` | Certificações |
| POST | `/partners/:id/certifications` | Adiciona certificação |
| GET | `/partners/:id/performance` | Performance |
| POST | `/partners/:id/performance` | Registra performance |
| POST | `/partners/:id/convert-to-client` | Converte para cliente |
### Clientes
| Método | Endpoint | Função |
|--------|----------|--------|
| GET | `/clients` | Lista clientes |
| GET | `/clients/:id` | Detalhe do cliente |
| POST | `/clients` | Cria cliente |
| PATCH | `/clients/:id` | Atualiza cliente |
| DELETE | `/clients/:id` | Remove cliente |
### Leads
| Método | Endpoint | Função |
|--------|----------|--------|
| GET | `/leads` | Lista leads |
| GET | `/leads/:id` | Detalhe do lead |
| POST | `/leads` | Cria lead |
| PATCH | `/leads/:id` | Atualiza lead |
| DELETE | `/leads/:id` | Remove lead |
| POST | `/leads/:id/convert` | Converte para Opportunity + Client |
### Oportunidades
| Método | Endpoint | Função |
|--------|----------|--------|
| GET | `/opportunities` | Lista oportunidades |
| GET | `/opportunities/:id` | Detalhe |
| POST | `/opportunities` | Cria oportunidade |
| PATCH | `/opportunities/:id` | Atualiza |
| DELETE | `/opportunities/:id` | Remove |
| PATCH | `/opportunities/:id/stage` | Muda estágio do pipeline |
| POST | `/opportunities/:id/won` | Marca como ganha |
| POST | `/opportunities/:id/lost` | Marca como perdida |
| POST | `/opportunities/:id/approve` | Aprova (governance) |
| POST | `/opportunities/:id/reject` | Rejeita |
| POST | `/opportunities/:id/open-project` | Abre projeto no Compass |
| POST | `/opportunities/:id/bill` | Gera faturamento |
| GET | `/opportunities/:id/products` | Produtos da opp |
| POST | `/opportunities/:id/products` | Adiciona produto |
| DELETE | `/opportunity-products/:id` | Remove produto |
| GET | `/opportunities/:id/proposals` | Propostas da opp |
### Propostas
| Método | Endpoint | Função |
|--------|----------|--------|
| GET | `/proposals` | Lista propostas |
| GET | `/proposals/:id` | Detalhe |
| POST | `/proposals` | Cria proposta |
| PATCH | `/proposals/:id` | Atualiza |
| DELETE | `/proposals/:id` | Remove |
| GET | `/proposals/:id/items` | Itens da proposta |
| POST | `/proposals/:id/items` | Adiciona item |
| PATCH | `/proposal-items/:id` | Atualiza item |
| DELETE | `/proposal-items/:id` | Remove item |
### Contratos
| Método | Endpoint | Função |
|--------|----------|--------|
| GET | `/contracts` | Lista contratos |
| GET | `/contracts/:id` | Detalhe |
| POST | `/contracts` | Cria contrato |
| PATCH | `/contracts/:id` | Atualiza |
| GET | `/contracts/:id/revenue` | Revenue schedule |
| GET | `/contracts/:id/milestones` | Marcos |
| POST | `/contracts/:id/milestones` | Adiciona marco |
| PATCH | `/milestones/:id` | Atualiza marco |
| DELETE | `/milestones/:id` | Remove marco |
| POST | `/contracts/:id/process-commissions` | Calcula comissões |
| POST | `/contracts/:id/extend-schedule` | Estende schedule |
| POST | `/contracts/extend-all` | Estende todos |
### Comissões
| Método | Endpoint | Função |
|--------|----------|--------|
| GET | `/commission-rules` | Lista regras |
| POST | `/commission-rules` | Cria regra |
| POST | `/commission-rules/seed` | Seed regras padrão |
| GET | `/commissions` | Lista comissões |
| GET | `/commissions/summary` | Resumo financeiro |
| POST | `/commissions/:id/mark-paid` | Marca como paga |
### Comunicação
| Método | Endpoint | Função |
|--------|----------|--------|
| GET | `/channels` | Lista canais |
| POST | `/channels` | Cria canal |
| PATCH | `/channels/:id` | Atualiza canal |
| POST | `/channels/:id/whatsapp/connect` | Conecta WhatsApp |
| POST | `/channels/whatsapp/new` | Novo canal WhatsApp |
| POST | `/channels/:id/whatsapp/disconnect` | Desconecta |
| GET | `/threads` | Lista threads |
| GET | `/threads/:id` | Detalhe da thread |
| POST | `/threads` | Cria thread |
| PATCH | `/threads/:id` | Atualiza thread |
| GET | `/threads/:id/messages` | Mensagens da thread |
| POST | `/threads/:id/messages` | Envia mensagem |
| POST | `/threads/:id/send` | Envia via WhatsApp |
| POST | `/threads/:id/read` | Marca como lida |
| POST | `/threads/:id/assign` | Atribui a agente |
| POST | `/threads/:id/close` | Fecha thread |
| POST | `/threads/:id/reopen` | Reabre thread |
| GET | `/threads/stats` | Estatísticas de threads |
| GET | `/quick-messages` | Mensagens rápidas |
| POST | `/quick-messages` | Cria mensagem rápida |
### Produtos
| Método | Endpoint | Função |
|--------|----------|--------|
| GET | `/products` | Lista produtos |
| GET | `/products/:id` | Detalhe |
| POST | `/products` | Cria produto |
| PATCH | `/products/:id` | Atualiza |
| DELETE | `/products/:id` | Remove |
### Pipeline
| Método | Endpoint | Função |
|--------|----------|--------|
| GET | `/pipeline-stages` | Lista estágios |
| POST | `/pipeline-stages` | Cria estágio |
| PATCH | `/pipeline-stages/:id` | Atualiza |
| DELETE | `/pipeline-stages/:id` | Remove |
### Eventos e Calendário
| Método | Endpoint | Função |
|--------|----------|--------|
| GET | `/events` | Lista eventos |
| POST | `/events` | Cria evento |
| PATCH | `/events/:id` | Atualiza |
| PUT | `/events/:id` | Atualiza (full) |
| DELETE | `/events/:id` | Remove |
| GET | `/google/auth` | Inicia OAuth Google |
| GET | `/google/callback` | Callback OAuth |
| GET | `/google/status` | Status conexão |
| POST | `/google/disconnect` | Desconecta Google |
| POST | `/google/sync` | Sincroniza eventos |
| GET | `/google/events` | Eventos Google |
| POST | `/google/events` | Cria evento Google |
### Frappe/ERPNext
| Método | Endpoint | Função |
|--------|----------|--------|
| GET | `/frappe/connectors` | Lista conectores |
| GET | `/frappe/connectors/:id` | Detalhe |
| POST | `/frappe/connectors` | Cria conector |
| PATCH | `/frappe/connectors/:id` | Atualiza |
| DELETE | `/frappe/connectors/:id` | Remove |
| POST | `/frappe/connectors/:id/test` | Testa conexão |
| POST | `/frappe/connectors/:id/sync` | Sincroniza dados |
| GET | `/frappe/connectors/:id/logs` | Logs de sync |
| GET | `/frappe/connectors/:id/mappings` | Mapeamentos |
| POST | `/frappe/connectors/:id/mappings` | Cria mapeamento |
### Estatísticas
| Método | Endpoint | Função |
|--------|----------|--------|
| GET | `/stats` | KPIs gerais |
| GET | `/stats/sales` | Estatísticas de vendas |
**Total: 90+ endpoints**
---
## 10. Arquivos-Chave
| Arquivo | Função | Linhas |
|---------|--------|--------|
| `client/src/pages/Crm.tsx` | Interface principal (12 tabs) | 2.700 |
| `client/src/pages/XosCrm.tsx` | CRM Kanban/Pipeline | 752 |
| `client/src/pages/WhatsApp.tsx` | Interface WhatsApp | — |
| `server/crm/routes.ts` | API REST (90+ endpoints) | 1.592 |
| `server/crm/storage.ts` | CrmStorage class (Drizzle ORM) | 708 |
| `server/crm/communication.ts` | CommunicationService | 289 |
| `server/crm/commission-engine.ts` | CommissionEngine | 418 |
| `server/crm/whatsapp-bridge.ts` | WhatsAppBridge | 126 |
| `server/crm/frappe-service.ts` | Integração ERPNext | — |
| `server/crm/google-calendar.ts` | Google Calendar OAuth | — |
| `server/whatsapp/service.ts` | WhatsApp (Baileys) | — |
| `server/communication/engine.ts` | Motor Comunicação (:8006) | — |
| `shared/schema.ts` | 53 tabelas CRM/XOS/Comm | — |
---
## 11. Resumo Visual
```
┌─────────────────────────────────────────────────────────────────────┐
│ ARCÁDIA CRM STACK │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ FRONTEND │ │
│ │ Crm.tsx (12 tabs) + XosCrm.tsx (Kanban) + WhatsApp.tsx │ │
│ └────────────────────────────┬────────────────────────────────┘ │
│ │ │
│ ┌────────────────────────────▼────────────────────────────────┐ │
│ │ API: /api/crm/* — 90+ endpoints │ │
│ │ Router → CrmStorage → PostgreSQL │ │
│ └───┬─────────┬─────────┬──────────┬──────────┬──────────────┘ │
│ │ │ │ │ │ │
│ ┌───▼───┐ ┌───▼───┐ ┌───▼────┐ ┌───▼───┐ ┌───▼────────┐ │
│ │Comm │ │Comissão│ │WhatsApp│ │Frappe │ │Google Cal │ │
│ │Service│ │Engine │ │Bridge │ │Service│ │OAuth │ │
│ └───┬───┘ └───────┘ └───┬────┘ └───────┘ └────────────┘ │
│ │ │ │
│ │ ┌─────▼──────┐ │
│ │ │ Baileys │ │
│ │ │ WhatsApp │ │
│ │ │ Multi-Sess│ │
│ │ └─────┬──────┘ │
│ │ │ │
│ ┌───▼────────────────────▼──────────────────────────────────┐ │
│ │ Motor de Comunicação (:8006) │ │
│ │ Unifica: CRM + XOS + WhatsApp → comm_* tables │ │
│ │ Alimenta: Knowledge Graph + Manus IA │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ PostgreSQL: 53 tabelas (30 crm_* + 15 xos_* + 8 comm_*) │ │
│ └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
```

View File

@ -1,734 +0,0 @@
# Arcádia Suite — Mapa de Microserviços para Coolify/Docker
> Documento de referência para containerização e deploy em Coolify.
> Atualizado em: Março 2026
---
## 1. Visão Geral da Arquitetura
```
┌──────────────────────────────┐
│ COOLIFY / TRAEFIK │
│ Reverse Proxy + TLS + DNS │
└──────────┬───────────────────┘
┌─────────────────────┼─────────────────────┐
│ │ │
┌────────▼────────┐ ┌────────▼────────┐ ┌─────────▼────────┐
│ arcadia-gateway │ │ arcadia-plus │ │ arcadia-frontend│
│ (Node.js:5000) │ │ (PHP/Nginx) │ │ (Nginx/Static) │
│ API + WebSocket │ │ :8080 │ │ :80 (React SPA) │
└────────┬────────┘ └────────┬────────┘ └──────────────────┘
│ │
┌─────────┼──────────────┬──────┘
│ │ │
┌───▼──┐ ┌───▼───┐ ┌────────▼────────┐
│Postgres│ │MySQL │ │ Redis (futuro) │
│ :5432 │ │ :3306 │ │ :6379 │
└───┬────┘ └───────┘ └─────────────────┘
│ (DATABASE_URL compartilhado)
┌───▼──────────────────────────────────────────────────────┐
│ MOTORES (FastAPI / Node) │
│ │
│ ┌─────────┐ ┌─────────┐ ┌──────┐ ┌──────┐ ┌──────────┐ │
│ │ Fisco │ │Contábil │ │ BI │ │Auto │ │ Comm │ │
│ │ :8002 │ │ :8003 │ │:8004 │ │:8005 │ │ :8006 │ │
│ │ Python │ │ Python │ │Python│ │Python│ │ Node/TS │ │
│ └─────────┘ └─────────┘ └──────┘ └──────┘ └──────────┘ │
└──────────────────────────────────────────────────────────┘
```
---
## 2. Inventário de Serviços (9 containers + 3 bancos)
### 2.1 — `arcadia-gateway` (Container Principal)
| Item | Valor |
|------|-------|
| **Tecnologia** | Node.js 20 + Express + Socket.IO |
| **Porta** | 5000 |
| **Entrada** | `server/index.ts` (build: `dist/index.cjs`) |
| **Banco** | PostgreSQL (Drizzle ORM) |
| **Responsabilidades** | API REST principal, autenticação/sessões, WebSocket (chat + WhatsApp), proxy para motores, Dev Center (6 agentes XOS), Manus IA (OpenAI), Knowledge Graph, Learning System, Governance, MCP/A2A protocols |
| **Variáveis obrigatórias** | `DATABASE_URL`, `PGHOST`, `PGUSER`, `PGPASSWORD`, `PGDATABASE`, `SESSION_SECRET`, `OPENAI_API_KEY`, `GITHUB_TOKEN` |
| **Variáveis opcionais** | `ERPNEXT_URL`, `ERPNEXT_API_KEY`, `ERPNEXT_API_SECRET`, `GITHUB_OWNER`, `GITHUB_REPO` |
| **Healthcheck** | `GET /api/health` ou `GET /` |
| **Dockerfile existente** | `./Dockerfile` (node:20-alpine, multi-stage) |
```dockerfile
# Build: npm run build → dist/index.cjs
# Run: node dist/index.cjs
# Portas: 5000
```
### 2.2 — `arcadia-frontend` (SPA React)
| Item | Valor |
|------|-------|
| **Tecnologia** | React 18 + Vite + TypeScript + Tailwind + shadcn/ui |
| **Build** | `npm run build``dist/public/` |
| **Servir com** | Nginx (static files) ou incluir no Gateway |
| **Porta** | 80/443 (via Coolify/Traefik) |
> **Nota**: Atualmente o frontend é servido pelo Gateway (Express serve os assets estáticos). Para microsserviços, pode ser separado em um container Nginx leve ou mantido no Gateway.
**Opção A — Junto com Gateway** (recomendado para início):
O Dockerfile atual já faz isso. O Express serve o React build.
**Opção B — Separado** (para escalar independentemente):
```dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm install --legacy-peer-deps
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/dist/public /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
```
### 2.3 — `arcadia-plus` (ERP Laravel/PHP)
| Item | Valor |
|------|-------|
| **Tecnologia** | PHP 8.2 + Laravel + Nginx |
| **Porta** | 8080 (interno) → exposto via proxy `/plus` |
| **Banco** | MySQL 8.0 (separado do PostgreSQL) |
| **Entrada** | `plus/public/index.php` via `php artisan serve` ou PHP-FPM + Nginx |
| **Dockerfile existente** | `plus/Dockerfile` (php:8.2-fpm) |
| **docker-compose existente** | `plus/docker-compose.yml` (app + web + db) |
| **Dependências** | Composer, Node.js 18 (para assets) |
| **Variáveis obrigatórias** | `DB_HOST`, `DB_DATABASE`, `DB_USERNAME`, `DB_PASSWORD`, `MYSQL_ROOT_PASSWORD`, `APP_KEY`, `APP_URL` |
| **Módulos** | NF-e/NFC-e/CT-e/MDF-e, PDV, Menu Digital, Ordem de Serviço, Financeiro, Estoque, CRM |
**Containers necessários para o Plus**:
1. `arcadia-plus-app` — PHP-FPM (porta 9000 interna)
2. `arcadia-plus-web` — Nginx (porta 8080, proxy para PHP-FPM)
3. `arcadia-mysql` — MySQL 8.0 (porta 3306)
### 2.4 — `arcadia-fisco` (Motor Fiscal)
| Item | Valor |
|------|-------|
| **Tecnologia** | Python 3.11 + FastAPI + Uvicorn |
| **Porta** | 8002 |
| **Arquivo** | `server/python/fisco_service.py` |
| **Banco** | Nenhum direto (opera sobre XML) |
| **Dependências Python** | `fastapi`, `uvicorn`, `nfelib`, `signxml`, `lxml`, `zeep`, `cryptography`, `pydantic` |
| **Responsabilidades** | Geração XML NF-e/NFC-e, assinatura digital, comunicação SEFAZ, consulta/cancelamento/inutilização |
| **Variáveis** | `FISCO_PORT=8002` |
| **Healthcheck** | `GET /health` |
```dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY server/python/fisco_service.py .
COPY requirements-fisco.txt .
RUN pip install -r requirements-fisco.txt
EXPOSE 8002
CMD ["uvicorn", "fisco_service:app", "--host", "0.0.0.0", "--port", "8002"]
```
### 2.5 — `arcadia-contabil` (Motor Contábil)
| Item | Valor |
|------|-------|
| **Tecnologia** | Python 3.11 + FastAPI |
| **Porta** | 8003 |
| **Arquivo** | `server/python/contabil_service.py` |
| **Banco** | Nenhum direto (recebe dados via POST) |
| **Dependências Python** | `fastapi`, `uvicorn`, `pydantic` |
| **Responsabilidades** | DRE, Balanço Patrimonial, Balancete, Validação de Lançamentos, SPED ECD/ECF, integração NF-e/folha |
| **Variáveis** | `CONTABIL_PORT=8003` |
| **Healthcheck** | `GET /health` |
### 2.6 — `arcadia-bi` (Motor BI/Analytics)
| Item | Valor |
|------|-------|
| **Tecnologia** | Python 3.11 + FastAPI + Pandas + NumPy |
| **Porta** | 8004 |
| **Arquivo** | `server/python/bi_engine.py` |
| **Banco** | PostgreSQL (via `psycopg2`, leitura direta) |
| **Dependências Python** | `fastapi`, `uvicorn`, `pandas`, `numpy`, `psycopg2-binary`, `pydantic` |
| **Responsabilidades** | Exploração de schema, execução SQL, geração de dados para gráficos, Micro-BI, análise de dados |
| **Variáveis** | `BI_PORT=8004`, `DATABASE_URL` |
| **Healthcheck** | `GET /health` |
### 2.7 — `arcadia-automation` (Motor de Automação)
| Item | Valor |
|------|-------|
| **Tecnologia** | Python 3.11 + FastAPI + APScheduler |
| **Porta** | 8005 |
| **Arquivo** | `server/python/automation_engine.py` |
| **Banco** | PostgreSQL (via `psycopg2`) |
| **Dependências Python** | `fastapi`, `uvicorn`, `psycopg2-binary`, `apscheduler`, `pydantic` |
| **Responsabilidades** | Scheduler (cron jobs), Event Bus, Workflow executor |
| **Variáveis** | `AUTOMATION_PORT=8005`, `DATABASE_URL` |
| **Healthcheck** | `GET /health` |
### 2.8 — `arcadia-communication` (Motor de Comunicação)
| Item | Valor |
|------|-------|
| **Tecnologia** | Node.js 20 + Express + TypeScript |
| **Porta** | 8006 |
| **Arquivo** | `server/communication/engine.ts` |
| **Banco** | PostgreSQL (via `@neondatabase/serverless`) |
| **Dependências Node** | `express`, `@neondatabase/serverless`, `ws` |
| **Responsabilidades** | Unifica contatos/threads/mensagens de WhatsApp, CRM, XOS. Canais, filas, mensagens rápidas, eventos |
| **Variáveis** | `COMMUNICATION_PORT=8006`, `DATABASE_URL` |
| **Healthcheck** | `GET /health` |
### 2.9 — `arcadia-people` (Motor RH — opcional/futuro)
| Item | Valor |
|------|-------|
| **Tecnologia** | Python 3.11 + FastAPI |
| **Porta** | 8007 (ajustar — conflito atual com BI na 8004) |
| **Arquivo** | `server/python/people_service.py` |
| **Banco** | Nenhum direto (cálculos puros) |
| **Dependências Python** | `fastapi`, `uvicorn`, `pydantic` |
| **Responsabilidades** | Cálculo INSS/IRRF, folha de pagamento, férias, rescisão, eSocial |
| **Variáveis** | `PEOPLE_PORT=8007` |
---
## 3. Bancos de Dados (3 containers)
### 3.1 — `arcadia-postgres`
| Item | Valor |
|------|-------|
| **Imagem** | `postgres:16-alpine` |
| **Porta** | 5432 |
| **Uso** | Gateway, BI Engine, Automation Engine, Communication Engine |
| **ORM** | Drizzle (Node.js) / psycopg2 (Python) |
| **Schema** | ~100+ tabelas (tenants, users, valuation_*, crm_*, comm_*, xos_*, etc.) |
| **Volume** | `/var/lib/postgresql/data` |
### 3.2 — `arcadia-mysql`
| Item | Valor |
|------|-------|
| **Imagem** | `mysql:8.0` |
| **Porta** | 3306 |
| **Uso** | Exclusivo do Plus (Laravel) |
| **Schema** | 260+ migrações Laravel (produtos, vendas, fiscal, estoque, etc.) |
| **Volume** | `/var/lib/mysql` |
### 3.3 — `arcadia-redis` (futuro/recomendado)
| Item | Valor |
|------|-------|
| **Imagem** | `redis:7-alpine` |
| **Porta** | 6379 |
| **Uso** | Cache de sessões, fila de jobs, pub/sub entre serviços |
| **Recomendação** | Substituir session store do Express (atualmente PostgreSQL) e usar como broker de eventos |
---
## 4. Mapa de Comunicação entre Serviços
```
┌─────────────────────────────────────────────────────────────────┐
│ arcadia-gateway (:5000) │
│ │
│ /api/* → Rotas internas (Express) │
│ /api/fisco/* → proxy → arcadia-fisco:8002 │
│ /api/contabil/* → proxy → arcadia-contabil:8003 │
│ /api/bi/* → proxy → arcadia-bi:8004 │
│ /api/automation/*→ proxy → arcadia-automation:8005 │
│ /api/comm/* → proxy → arcadia-communication:8006 │
│ /plus/* → proxy → arcadia-plus-web:8080 │
│ /metabase/* → proxy → metabase:8088 (se usar) │
│ │
│ WebSocket (Socket.IO) → Chat interno, WhatsApp real-time │
└─────────────────────────────────────────────────────────────────┘
```
### Dependências entre serviços:
| Serviço | Depende de | Comunicação |
|---------|-----------|-------------|
| Gateway | PostgreSQL, todos os motores | HTTP proxy, DB direto |
| Plus | MySQL, Gateway (SSO) | HTTP (proxy reverso) |
| Fisco | Nenhum banco | HTTP (recebe XMLs) |
| Contábil | Nenhum banco | HTTP (recebe dados via POST) |
| BI | PostgreSQL | HTTP + SQL direto no PG |
| Automation | PostgreSQL | HTTP + SQL direto no PG |
| Communication | PostgreSQL | HTTP + SQL direto no PG |
| People | Nenhum banco | HTTP (cálculos puros) |
---
## 5. Docker Compose Completo para Coolify
```yaml
# docker-compose.coolify.yml
version: "3.8"
services:
# ═══════════════════════════════════════════
# BANCOS DE DADOS
# ═══════════════════════════════════════════
postgres:
image: postgres:16-alpine
restart: always
environment:
POSTGRES_USER: ${PGUSER:-arcadia}
POSTGRES_PASSWORD: ${PGPASSWORD}
POSTGRES_DB: ${PGDATABASE:-arcadia}
volumes:
- pgdata:/var/lib/postgresql/data
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${PGUSER:-arcadia}"]
interval: 10s
timeout: 5s
retries: 5
networks:
- arcadia
mysql:
image: mysql:8.0
restart: always
command: >
--default-authentication-plugin=mysql_native_password
--character-set-server=utf8mb4
--collation-server=utf8mb4_unicode_ci
environment:
MYSQL_DATABASE: ${PLUS_DB_DATABASE:-controlplus}
MYSQL_USER: ${PLUS_DB_USERNAME:-controlplus}
MYSQL_PASSWORD: ${PLUS_DB_PASSWORD}
MYSQL_ROOT_PASSWORD: ${PLUS_MYSQL_ROOT_PASSWORD}
volumes:
- mysqldata:/var/lib/mysql
ports:
- "3306:3306"
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
networks:
- arcadia
redis:
image: redis:7-alpine
restart: always
ports:
- "6379:6379"
volumes:
- redisdata:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
networks:
- arcadia
# ═══════════════════════════════════════════
# GATEWAY + FRONTEND (Node.js principal)
# ═══════════════════════════════════════════
gateway:
build:
context: .
dockerfile: Dockerfile
restart: always
depends_on:
postgres:
condition: service_healthy
ports:
- "5000:5000"
environment:
NODE_ENV: production
PORT: 5000
DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD}@postgres:5432/${PGDATABASE:-arcadia}
PGHOST: postgres
PGUSER: ${PGUSER:-arcadia}
PGPASSWORD: ${PGPASSWORD}
PGDATABASE: ${PGDATABASE:-arcadia}
SESSION_SECRET: ${SESSION_SECRET}
OPENAI_API_KEY: ${OPENAI_API_KEY}
GITHUB_TOKEN: ${GITHUB_TOKEN}
ERPNEXT_URL: ${ERPNEXT_URL:-}
ERPNEXT_API_KEY: ${ERPNEXT_API_KEY:-}
ERPNEXT_API_SECRET: ${ERPNEXT_API_SECRET:-}
# URLs dos motores (comunicação interna Docker)
FISCO_URL: http://fisco:8002
CONTABIL_URL: http://contabil:8003
BI_URL: http://bi:8004
AUTOMATION_URL: http://automation:8005
COMMUNICATION_URL: http://communication:8006
PLUS_URL: http://plus-web:8080
REDIS_URL: redis://redis:6379
volumes:
- uploads:/app/uploads
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:5000/"]
interval: 30s
timeout: 10s
start_period: 40s
networks:
- arcadia
# ═══════════════════════════════════════════
# MOTORES PYTHON (FastAPI)
# ═══════════════════════════════════════════
fisco:
build:
context: .
dockerfile: docker/fisco.Dockerfile
restart: always
ports:
- "8002:8002"
environment:
FISCO_PORT: 8002
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8002/health"]
interval: 30s
networks:
- arcadia
contabil:
build:
context: .
dockerfile: docker/contabil.Dockerfile
restart: always
ports:
- "8003:8003"
environment:
CONTABIL_PORT: 8003
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8003/health"]
interval: 30s
networks:
- arcadia
bi:
build:
context: .
dockerfile: docker/bi.Dockerfile
restart: always
depends_on:
postgres:
condition: service_healthy
ports:
- "8004:8004"
environment:
BI_PORT: 8004
DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD}@postgres:5432/${PGDATABASE:-arcadia}
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8004/health"]
interval: 30s
networks:
- arcadia
automation:
build:
context: .
dockerfile: docker/automation.Dockerfile
restart: always
depends_on:
postgres:
condition: service_healthy
ports:
- "8005:8005"
environment:
AUTOMATION_PORT: 8005
DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD}@postgres:5432/${PGDATABASE:-arcadia}
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8005/health"]
interval: 30s
networks:
- arcadia
communication:
build:
context: .
dockerfile: docker/communication.Dockerfile
restart: always
depends_on:
postgres:
condition: service_healthy
ports:
- "8006:8006"
environment:
COMMUNICATION_PORT: 8006
DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD}@postgres:5432/${PGDATABASE:-arcadia}
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8006/health"]
interval: 30s
networks:
- arcadia
# ═══════════════════════════════════════════
# ARCÁDIA PLUS (Laravel/PHP)
# ═══════════════════════════════════════════
plus-app:
build:
context: ./plus
dockerfile: Dockerfile
restart: always
depends_on:
mysql:
condition: service_healthy
env_file:
- ./plus/.env
volumes:
- ./plus:/var/www/html:cached
- plus-vendor:/var/www/html/vendor
networks:
- arcadia
plus-web:
image: nginx:1.25-alpine
restart: always
depends_on:
- plus-app
ports:
- "8080:80"
volumes:
- ./plus:/var/www/html:cached
- ./plus/docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
networks:
- arcadia
volumes:
pgdata:
mysqldata:
redisdata:
uploads:
plus-vendor:
networks:
arcadia:
driver: bridge
```
---
## 6. Variáveis de Ambiente (.env para Coolify)
```env
# ══════════════════════════════════════
# POSTGRESQL (Principal)
# ══════════════════════════════════════
PGUSER=arcadia
PGPASSWORD=SuaSenhaSegura123!
PGDATABASE=arcadia
DATABASE_URL=postgresql://arcadia:SuaSenhaSegura123!@postgres:5432/arcadia
# ══════════════════════════════════════
# GATEWAY (Node.js)
# ══════════════════════════════════════
NODE_ENV=production
PORT=5000
SESSION_SECRET=sua-session-secret-aqui
# ══════════════════════════════════════
# INTELIGÊNCIA ARTIFICIAL
# ══════════════════════════════════════
OPENAI_API_KEY=sk-...
# ══════════════════════════════════════
# GITHUB
# ══════════════════════════════════════
GITHUB_TOKEN=ghp_...
GITHUB_OWNER=jonaspachecoometas
GITHUB_REPO=arcadiasuite
# ══════════════════════════════════════
# ERPNEXT (opcional)
# ══════════════════════════════════════
ERPNEXT_URL=https://seu-erpnext.com
ERPNEXT_API_KEY=
ERPNEXT_API_SECRET=
# ══════════════════════════════════════
# MYSQL (Plus/Laravel)
# ══════════════════════════════════════
PLUS_DB_DATABASE=controlplus
PLUS_DB_USERNAME=controlplus
PLUS_DB_PASSWORD=SenhaMySQLSegura!
PLUS_MYSQL_ROOT_PASSWORD=RootSenha123!
# ══════════════════════════════════════
# REDIS
# ══════════════════════════════════════
REDIS_URL=redis://redis:6379
```
---
## 7. Dockerfiles dos Motores (a criar em `docker/`)
### `docker/fisco.Dockerfile`
```dockerfile
FROM python:3.11-slim
WORKDIR /app
RUN pip install fastapi uvicorn pydantic cryptography lxml zeep signxml
# nfelib se disponível: RUN pip install nfelib
COPY server/python/fisco_service.py ./main.py
EXPOSE 8002
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8002"]
```
### `docker/contabil.Dockerfile`
```dockerfile
FROM python:3.11-slim
WORKDIR /app
RUN pip install fastapi uvicorn pydantic
COPY server/python/contabil_service.py ./main.py
EXPOSE 8003
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8003"]
```
### `docker/bi.Dockerfile`
```dockerfile
FROM python:3.11-slim
WORKDIR /app
RUN pip install fastapi uvicorn pydantic pandas numpy psycopg2-binary
COPY server/python/bi_engine.py ./main.py
EXPOSE 8004
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8004"]
```
### `docker/automation.Dockerfile`
```dockerfile
FROM python:3.11-slim
WORKDIR /app
RUN pip install fastapi uvicorn pydantic psycopg2-binary apscheduler
COPY server/python/automation_engine.py ./main.py
EXPOSE 8005
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8005"]
```
### `docker/communication.Dockerfile`
```dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm install --legacy-peer-deps --production
COPY server/communication/ ./server/communication/
COPY shared/ ./shared/
COPY tsconfig.json ./
EXPOSE 8006
CMD ["npx", "tsx", "server/communication/engine.ts"]
```
---
## 8. Ordem de Deploy no Coolify
### Fase 1 — Infraestrutura (subir primeiro)
1. `arcadia-postgres` — Banco principal
2. `arcadia-mysql` — Banco do Plus
3. `arcadia-redis` — Cache e filas
### Fase 2 — Motores Independentes (podem subir em paralelo)
4. `arcadia-fisco` — Sem dependência de banco
5. `arcadia-contabil` — Sem dependência de banco
6. `arcadia-bi` — Depende do PostgreSQL
7. `arcadia-automation` — Depende do PostgreSQL
8. `arcadia-communication` — Depende do PostgreSQL
### Fase 3 — Aplicações Principais
9. `arcadia-gateway` — Depende de PostgreSQL + todos os motores
10. `arcadia-plus-app` + `arcadia-plus-web` — Depende do MySQL
### Fase 4 — Migrações (após tudo no ar)
```bash
# PostgreSQL (Drizzle)
docker exec arcadia-gateway npx drizzle-kit push
# MySQL (Laravel)
docker exec arcadia-plus-app php artisan migrate --force
docker exec arcadia-plus-app php artisan db:seed --force
```
---
## 9. Ajustes Necessários no Código
### 9.1 — Gateway: URLs dos motores precisam ser configuráveis
Atualmente o código usa `localhost:800X`. Para Docker, precisa usar nomes de serviço:
| Arquivo | Linha | De | Para |
|---------|-------|----|------|
| `server/contabil/routes.ts` | proxy | `localhost:8003` | `process.env.CONTABIL_URL` |
| `server/fisco/routes.ts` | proxy | `localhost:8002` | `process.env.FISCO_URL` |
| `server/bi/proxy.ts` | proxy | `localhost:8004` | `process.env.BI_URL` |
| `server/python/launcher.ts` | spawn | hardcoded | Remover spawns no modo Docker |
| `server/plus/proxy.ts` | proxy | `localhost:8080` | `process.env.PLUS_URL` |
| `server/communication/proxy.ts` | proxy | `localhost:8006` | `process.env.COMMUNICATION_URL` |
### 9.2 — Desabilitar spawns de subprocessos em produção
O Gateway atualmente inicia os motores Python como subprocessos. No Docker cada motor roda em seu próprio container. Adicionar flag:
```typescript
// server/index.ts
if (process.env.DOCKER_MODE !== 'true') {
// spawn python services
startManagedServices();
}
```
### 9.3 — Conflito de portas
| Serviço | Porta atual | Conflito | Solução |
|---------|------------|----------|---------|
| People | 8004 | BI Engine | Mudar para 8007 |
| BI Analysis | 8003 | Contábil | Mesclar no BI Engine (8004) |
---
## 10. Checklist de Migração para Coolify
- [ ] Criar pasta `docker/` com Dockerfiles dos motores
- [ ] Criar `docker-compose.coolify.yml` (baseado na seção 5)
- [ ] Criar `.env.production` com todas as variáveis
- [ ] Ajustar proxies do Gateway para usar variáveis de ambiente
- [ ] Adicionar flag `DOCKER_MODE=true` para desabilitar spawns
- [ ] Resolver conflitos de porta (People → 8007)
- [ ] Testar build de cada Dockerfile isoladamente
- [ ] Configurar Coolify com Docker Compose
- [ ] Rodar migrações PostgreSQL (`drizzle-kit push`)
- [ ] Rodar migrações MySQL (`php artisan migrate`)
- [ ] Configurar domínio e TLS no Coolify/Traefik
- [ ] Configurar volumes persistentes (pgdata, mysqldata, uploads)
- [ ] Testar SSO do Plus via proxy
- [ ] Verificar WebSocket (Socket.IO) funciona via Traefik
- [ ] Configurar healthchecks em todos os serviços
---
## 11. Resumo Visual Rápido
| # | Container | Porta | Tech | Banco | Prioridade |
|---|-----------|-------|------|-------|-----------|
| 1 | postgres | 5432 | PostgreSQL 16 | - | Essencial |
| 2 | mysql | 3306 | MySQL 8.0 | - | Para Plus |
| 3 | redis | 6379 | Redis 7 | - | Recomendado |
| 4 | gateway | 5000 | Node.js 20 | PG | Essencial |
| 5 | fisco | 8002 | Python 3.11 | - | Módulo |
| 6 | contabil | 8003 | Python 3.11 | - | Módulo |
| 7 | bi | 8004 | Python 3.11 | PG | Módulo |
| 8 | automation | 8005 | Python 3.11 | PG | Módulo |
| 9 | communication | 8006 | Node.js 20 | PG | Módulo |
| 10 | plus-app | 9000 | PHP 8.2 FPM | MySQL | Módulo |
| 11 | plus-web | 8080 | Nginx 1.25 | - | Módulo |
| 12 | people | 8007 | Python 3.11 | - | Futuro |
**Total: 12 containers (3 bancos + 1 gateway + 8 motores)**

View File

@ -1,746 +0,0 @@
# Arcádia SOE — Sistema Operacional Empresarial
## Mapa Completo para Deploy em Servidor Dedicado
> Mapa da rota `/soe`: todos os arquivos do módulo, domínios de negócio,
> padrão de motor (adapter), tabelas PostgreSQL, APIs REST e dependências
> externas. Atualizado em: Março 2026.
---
## 1. O que é o SOE
O **Arcádia SOE (Sistema Operacional Empresarial)** é o **kernel central** da Arcádia Suite — o sistema que concentra todos os domínios canônicos de negócio. Enquanto apps como Varejo, Engenharia e Food Service adicionam experiências verticais, o SOE é a camada horizontal que todos compartilham.
**Princípio central:** O SOE **pensa e governa**; os motores (Plus, ERPNext) **executam e registram**.
```
┌─────────────────────────────────────────────────────────────────────┐
│ ARCÁDIA SOE — VISÃO GERAL │
│ │
│ URL: /soe │
│ API: /api/soe/* (alias backward-compatible → /api/erp/*) │
│ │
│ ┌──────── Domínios Canônicos ────────────────────────────────┐ │
│ │ │ │
│ │ Pessoas │ Produtos │ Vendas │ Compras │ Estoque │ │
│ │ Fiscal │ Financ. │ CRM │ Projetos │ Qualidade │ │
│ │ Governa. │ RH │ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────── Motores (Adapters) ────────────────────────────────┐ │
│ │ │ │
│ │ motor = "plus" → Arcádia Plus (Laravel :8080) │ │
│ │ motor = "erpnext" → ERPNext/Frappe (externo) │ │
│ │ motor = nativo → PostgreSQL local (default fallback) │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────── Apps que estendem o SOE ───────────────────────────┐ │
│ │ Varejo (Retail) │ Engenharia │ Food Service │ │
│ │ Assistência Tec │ Educação │ (outros segmentos) │ │
│ └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
```
---
## 2. Arquivos do Módulo SOE — Lista Completa para Export
### 2.1 — Frontend (React/TypeScript)
| Arquivo | Função | Linhas |
|---------|--------|--------|
| `client/src/pages/SOE.tsx` | **Página principal** do SOE — 9 abas, CRUD completo | 2.414 |
| `client/src/pages/ERP.tsx` | Legacy ERP — mantido para referência histórica | 2.197 |
| `client/src/contexts/SoeMotorContext.tsx` | **Context do Motor** — gerencia plus/erpnext via localStorage | 63 |
| `client/src/contexts/ErpProfileContext.tsx` | Alias legacy do SoeMotorContext (backward compat.) | 52 |
### 2.2 — Backend (Node.js/TypeScript)
| Arquivo | Função | Linhas |
|---------|--------|--------|
| `server/erp/routes.ts` | **Rotas SOE core** — /api/erp/* (= /api/soe/*) | 1.343 |
| `server/erp/index.ts` | Setup do módulo + alias /api/soe → /api/erp | 210 |
| `server/retail/routes.ts` | **Rotas do Varejo** — /api/retail/* | 5.218 |
| `server/retail/index.ts` | Setup do módulo retail | — |
| `server/retail/sync-service.ts` | Sincronização retail ↔ Plus | — |
| `server/retail/plus-sync.ts` | Ponte retail → Arcádia Plus | — |
| `server/plus/client.ts` | **Cliente HTTP** para Arcádia Plus (:8080) | 376 |
| `server/plus/routes.ts` | Rotas de integração Plus | — |
| `server/plus/launcher.ts` | Iniciador do serviço Plus | — |
| `server/plus/proxy.ts` | Proxy transparente → Plus (:8080) | — |
| `server/plus/sso.ts` | Single Sign-On entre SOE e Plus | — |
| `server/erpnext/routes.ts` | Rotas de integração ERPNext | — |
| `server/erpnext/index.ts` | Setup do módulo ERPNext | — |
### 2.3 — Schema PostgreSQL (Drizzle ORM)
| Arquivo | Tabelas relacionadas ao SOE |
|---------|----------------------------|
| `shared/schema.ts` (L348) | `erp_connections` |
| `shared/schema.ts` (L3303) | `erp_segments`, `erp_config` |
| `shared/schema.ts` (L33673517) | `customers`, `suppliers`, `products`, `sales_orders`, `sales_order_items`, `purchase_orders`, `purchase_order_items` |
| `shared/schema.ts` (L50825617) | Tabelas retail (ver seção 5) |
| `shared/schema.ts` (L6204) | `persons`, `person_roles` |
| `shared/schema.ts` (L4372) | `fin_payment_methods`, `fin_payment_plans` |
---
## 3. Arquitetura de Arquivos
```
workspace/
├── client/src/
│ ├── pages/
│ │ ├── SOE.tsx ← Página principal (/soe)
│ │ └── ERP.tsx ← Legacy (mantido)
│ │
│ └── contexts/
│ ├── SoeMotorContext.tsx ← Motor context (plus | erpnext)
│ └── ErpProfileContext.tsx ← Alias backward-compat
├── server/
│ ├── erp/
│ │ ├── routes.ts ← /api/erp/* (= /api/soe/*)
│ │ └── index.ts ← Setup + alias soe→erp
│ │
│ ├── retail/
│ │ ├── routes.ts ← /api/retail/*
│ │ ├── index.ts
│ │ ├── sync-service.ts
│ │ └── plus-sync.ts
│ │
│ ├── plus/
│ │ ├── client.ts ← HTTP client → Plus :8080
│ │ ├── routes.ts
│ │ ├── proxy.ts
│ │ ├── launcher.ts
│ │ └── sso.ts
│ │
│ └── erpnext/
│ ├── routes.ts
│ └── index.ts
└── shared/
└── schema.ts ← Tabelas SOE + Retail
```
---
## 4. Motor de Execução (Adapter Pattern)
O SOE usa um **padrão de adapter** para rotear operações para o motor correto:
```
┌─────────────────────────────────────────────────────────────────────┐
│ SOEOMOTOR — ADAPTER PATTERN │
│ │
│ SoeMotorContext.tsx │
│ │
│ export type SoeMotor = "plus" | "erpnext" │
│ │
│ Persistência: localStorage["arcadia_soe_motor"] │
│ │
│ getApiUrl(localPath, plusPath, erpnextPath?) │
│ │
│ ┌──────── motor = "plus" ────────────────────────────────────┐ │
│ │ → /plus/api{plusPath} │ │
│ │ → Passa pelo proxy transparente do Plus │ │
│ │ → Arcádia Plus (Laravel) em :8080 │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────── motor = "erpnext" ─────────────────────────────────┐ │
│ │ → erpnextPath ou localPath │ │
│ │ → ERPNext API (ERPNEXT_URL env var) │ │
│ │ → Autenticado via ERPNEXT_API_KEY + ERPNEXT_API_SECRET │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────── motor = nativo (fallback) ─────────────────────────┐ │
│ │ → localPath (/api/erp/*) │ │
│ │ → PostgreSQL local via Drizzle ORM │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ Seleção na UI: SOE.tsx → Config tab → Dropdown de motor │
│ Plus ativo: isERPNextConnected verifica /api/erpnext/connections │
└─────────────────────────────────────────────────────────────────────┘
```
---
## 5. Domínios e Abas do SOE
```
┌─────────────────────────────────────────────────────────────────────┐
│ SOE.tsx — 9 ABAS PRINCIPAIS │
│ │
│ ┌── Tab: dashboard ──────────────────────────────────────────┐ │
│ │ • Stats: clientes, produtos, pedidos, total vendas │ │
│ │ • Cards com indicadores-chave do negócio │ │
│ │ • Acesso rápido às seções │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌── Tab: persons (Pessoas) ───────────────────────────────────┐ │
│ │ • Clientes, Fornecedores, Funcionários, Técnicos, Parceiros│ │
│ │ • Busca + filtro por papel (role) │ │
│ │ • Campos: nome, CPF/CNPJ, email, telefone, endereço │ │
│ │ • Tabelas: persons + person_roles │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌── Tab: products (Produtos) ─────────────────────────────────┐ │
│ │ • Cadastro de produtos/serviços │ │
│ │ • Rastreamento: IMEI, Número de Série, sem rastreamento │ │
│ │ • Grupos de produto, unidades (UN/KG/M/L/CX/PC) │ │
│ │ • Estoque por produto, devices batch import │ │
│ │ • Tabela: products + mobile_devices │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌── Tab: sales (Vendas) ──────────────────────────────────────┐ │
│ │ • Pedidos de venda │ │
│ │ • Status: draft → confirmed → delivered │ │
│ │ • Geração de NF-e a partir do pedido │ │
│ │ • Formas de pagamento: Dinheiro/Cartão/PIX/Boleto/Prazo │ │
│ │ • Tabelas: sales_orders + sales_order_items │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌── Tab: purchases (Compras) ─────────────────────────────────┐ │
│ │ • Pedidos de compra │ │
│ │ • Associados a fornecedores │ │
│ │ • Tabelas: purchase_orders + purchase_order_items │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌── Tab: financial (Financeiro) ──────────────────────────────┐ │
│ │ • Visão financeira consolidada │ │
│ │ • Planos de pagamento e parcelamento │ │
│ │ • Tabelas: fin_payment_methods + fin_payment_plans │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌── Tab: crm ─────────────────────────────────────────────────┐ │
│ │ • Módulo CRM inline no SOE │ │
│ │ • Integrado via useTenantFeatures (feature: crm) │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌── Tab: sync (Sincronização) ────────────────────────────────┐ │
│ │ • Status dos motores (Plus/ERPNext) │ │
│ │ • Módulos ativos/inativos do tenant │ │
│ │ • Botões de sync manual por entidade │ │
│ │ • Indica se Plus está online (:8080) │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌── Tab: config (Configuração) ───────────────────────────────┐ │
│ │ • Dados da empresa: razão social, CNPJ, regime tributário │ │
│ │ • Inscrição estadual/municipal │ │
│ │ • Segmento de negócio │ │
│ │ • Seletor de motor: Plus ou ERPNext │ │
│ │ • Tabela: erp_config + erp_segments │ │
│ └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
```
---
## 6. Tabelas PostgreSQL — SOE Core
### 6.1 — erp_connections (Conexões com ERPs externos)
```
erp_connections
├── id serial PK
├── userId FK → users.id
├── erpType "erpnext" | "plus" | "omie" | etc.
├── name Nome da conexão
├── url URL do ERP
├── apiKey Chave de API
├── apiSecret Secret
├── isActive boolean
└── createdAt, updatedAt
```
### 6.2 — erp_segments (Segmentos de Negócio)
```
erp_segments
├── id serial PK
├── name Nome (Varejo, Serviços, Indústria, etc.)
├── description Descrição
├── icon Ícone
└── createdAt
```
### 6.3 — erp_config (Configuração da Empresa)
```
erp_config
├── id serial PK
├── userId FK → users.id
├── companyName Razão Social
├── tradeName Nome Fantasia
├── taxId CNPJ
├── taxRegime simples | lucro_presumido | lucro_real
├── stateRegistration Inscrição Estadual
├── cityRegistration Inscrição Municipal
├── segmentId FK → erp_segments.id
├── address Endereço completo
└── createdAt, updatedAt
```
### 6.4 — customers (Clientes)
```
customers
├── id serial PK
├── userId FK → users.id
├── code Código interno
├── name Nome
├── email E-mail
├── phone Telefone
├── document CPF/CNPJ
├── type company | individual
├── address Endereço
└── createdAt, updatedAt
```
### 6.5 — suppliers (Fornecedores)
```
suppliers
├── id serial PK
├── userId FK → users.id
├── code Código interno
├── name Nome/Razão Social
├── email, phone
├── document CNPJ/CPF
└── createdAt, updatedAt
```
### 6.6 — products (Produtos)
```
products
├── id serial PK
├── userId FK → users.id
├── code Código/SKU
├── name Nome do produto
├── description
├── price Decimal(10,2)
├── costPrice Preço de custo
├── unit UN | KG | M | L | CX | PC
├── stock Estoque atual
├── minStock Estoque mínimo
├── groupId Grupo de produto
├── requiresSerialTracking boolean
├── trackingType none | imei | serial
└── createdAt, updatedAt
```
### 6.7 — sales_orders + sales_order_items
```
sales_orders
├── id serial PK
├── userId FK → users.id
├── customerId FK → customers.id
├── orderNumber Número do pedido
├── status draft | confirmed | delivered | cancelled
├── total Decimal(10,2)
├── paymentMethod dinheiro | cartao_credito | pix | boleto | prazo
└── createdAt, updatedAt
sales_order_items
├── id serial PK
├── orderId FK → sales_orders.id
├── productId FK → products.id
├── quantity integer
├── unitPrice Decimal(10,2)
└── total Decimal(10,2)
```
### 6.8 — purchase_orders + purchase_order_items
```
purchase_orders
├── id serial PK
├── userId FK → users.id
├── supplierId FK → suppliers.id
├── orderNumber
├── status draft | confirmed | received | cancelled
├── total Decimal(10,2)
└── createdAt, updatedAt
purchase_order_items
├── id serial PK
├── orderId FK → purchase_orders.id
├── productId FK → products.id
├── quantity, unitPrice, total
```
### 6.9 — persons + person_roles (Cadastro Unificado)
```
persons
├── id serial PK
├── tenantId FK → tenants.id
├── fullName
├── cpfCnpj
├── email, phone
├── address, city, state, zipCode
├── type company | individual
└── notes, createdAt, updatedAt
person_roles
├── id serial PK
├── personId FK → persons.id
├── role customer | supplier | employee | technician | partner
└── isActive
```
---
## 7. Tabelas PostgreSQL — Módulo Varejo (Retail)
### 7.1 — Estrutura Operacional
```
retail_stores → Lojas físicas
retail_warehouses → Depósitos/Almoxarifados
retail_payment_methods → Formas de pagamento
retail_sellers → Vendedores
retail_commission_plans → Planos de comissão
retail_seller_goals → Metas por vendedor
retail_store_goals → Metas por loja
```
### 7.2 — Estoque
```
stock_transfers → Transferências entre depósitos
stock_transfer_items → Itens da transferência
```
### 7.3 — Dispositivos Móveis (Eletrônicos)
```
mobile_devices
├── id serial PK
├── productId FK → products.id
├── imei varchar (único)
├── imei2 varchar (chip duplo)
├── serial Número de série
├── status em_estoque | vendido | locado | devolucao | avaliacao
├── color, storage, ram
├── purchasePrice, salePrice
└── supplierId, createdAt
device_evaluations → Avaliação de aparelhos usados
device_history → Histórico completo do dispositivo
```
### 7.4 — PDV (Ponto de Venda)
```
pos_sessions → Sessões de caixa (abertura/fechamento)
pos_sales → Vendas PDV
pos_sale_items → Itens das vendas PDV
```
### 7.5 — Planos de Pagamento
```
payment_plans → Planos parcelados
payment_plan_installments → Parcelas
lease_payments → Pagamentos de locação
```
---
## 8. API REST Completa (/api/soe/* = /api/erp/*)
> O alias `/api/soe/*` é convertido automaticamente para `/api/erp/*`
> no middleware do `server/erp/index.ts`.
### 8.1 — Conexões ERP
| Método | Endpoint | Função |
|--------|----------|--------|
| GET | `/api/soe/connections` | Lista conexões do usuário |
| POST | `/api/soe/connections` | Cria nova conexão |
| POST | `/api/soe/connections/:id/test` | Testa conectividade |
| DELETE | `/api/soe/connections/:id` | Remove conexão |
### 8.2 — Tarefas ERP (Sync Jobs)
| Método | Endpoint | Função |
|--------|----------|--------|
| GET | `/api/soe/tasks` | Lista tarefas de sync |
| POST | `/api/soe/tasks` | Cria nova tarefa |
| POST | `/api/soe/tasks/:id/execute` | Executa tarefa manualmente |
| DELETE | `/api/soe/tasks/:id` | Remove tarefa |
| GET | `/api/soe/tasks/:id/executions` | Histórico de execuções |
### 8.3 — Clientes
| Método | Endpoint | Função |
|--------|----------|--------|
| GET | `/api/soe/customers` | Lista clientes (paginado) |
| POST | `/api/soe/customers` | Cria cliente |
| PUT | `/api/soe/customers/:id` | Atualiza cliente |
| DELETE | `/api/soe/customers/:id` | Exclui cliente |
### 8.4 — Fornecedores
| Método | Endpoint | Função |
|--------|----------|--------|
| GET | `/api/soe/suppliers` | Lista fornecedores |
| POST | `/api/soe/suppliers` | Cria fornecedor |
| PUT | `/api/soe/suppliers/:id` | Atualiza |
| DELETE | `/api/soe/suppliers/:id` | Exclui |
### 8.5 — Pessoas (Cadastro Unificado)
| Método | Endpoint | Função |
|--------|----------|--------|
| GET | `/api/soe/persons` | Lista com filtro por role/search |
| GET | `/api/soe/persons/:id` | Detalhe + roles |
| POST | `/api/soe/persons` | Cria pessoa + roles |
| PUT | `/api/soe/persons/:id` | Atualiza pessoa + roles |
| DELETE | `/api/soe/persons/:id` | Exclui pessoa (cascade roles) |
### 8.6 — Produtos
| Método | Endpoint | Função |
|--------|----------|--------|
| GET | `/api/soe/products` | Lista produtos |
| POST | `/api/soe/products` | Cria produto |
| PUT | `/api/soe/products/:id` | Atualiza |
| DELETE | `/api/soe/products/:id` | Exclui |
| GET | `/api/soe/products/:id/devices` | Dispositivos do produto |
| POST | `/api/soe/products/:id/devices/batch` | Import batch de IMEIs |
| GET | `/api/soe/products/:id/stock-count` | Contagem de estoque |
### 8.7 — Pedidos de Venda
| Método | Endpoint | Função |
|--------|----------|--------|
| GET | `/api/soe/sales-orders` | Lista pedidos |
| POST | `/api/soe/sales-orders` | Cria pedido (com itens) |
| POST | `/api/soe/sales-orders/:id/confirm` | Confirma pedido |
| POST | `/api/soe/sales-orders/:id/generate-nfe` | Gera NF-e |
| DELETE | `/api/soe/sales-orders/:id` | Cancela/Exclui |
### 8.8 — Pedidos de Compra
| Método | Endpoint | Função |
|--------|----------|--------|
| GET | `/api/soe/purchase-orders` | Lista pedidos |
| POST | `/api/soe/purchase-orders` | Cria pedido |
| DELETE | `/api/soe/purchase-orders/:id` | Exclui |
### 8.9 — Dados e Configuração
| Método | Endpoint | Função |
|--------|----------|--------|
| GET | `/api/soe/stats` | Dashboard — totais de clientes/produtos/pedidos/receita |
| GET | `/api/soe/data/:connectionId/:dataType` | Dados via conexão ERP |
| GET | `/api/soe/segments` | Lista segmentos |
| POST | `/api/soe/segments` | Cria segmento |
| POST | `/api/soe/segments/seed` | Popula segmentos padrão |
| GET | `/api/soe/config` | Configuração da empresa |
| POST | `/api/soe/config` | Salva configuração |
### 8.10 — Módulos do Tenant
| Método | Endpoint | Função |
|--------|----------|--------|
| GET | `/api/soe/tenant/modules` | Módulos ativos do tenant atual |
| PUT | `/api/soe/tenant/modules` | Ativa/desativa módulos |
---
## 9. API REST — Módulo Varejo (/api/retail/*)
### 9.1 — Estrutura Operacional
| Módulo | Endpoints | Operações |
|--------|-----------|-----------|
| Activity Feed | `/activity-feed` | GET, mark-read |
| Payment Methods | `/payment-methods` | CRUD |
| Sellers | `/sellers` | CRUD |
| Commission Plans | `/commission-plans` | CRUD |
| Price Tables | `/price-tables` | CRUD |
| Promotions | `/promotions` | CRUD |
| Product Types | `/product-types` | CRUD + detail |
| Stores | `/stores` | CRU |
| Warehouses | `/warehouses` | CRUD |
### 9.2 — Estoque
| Módulo | Endpoints | Operações |
|--------|-----------|-----------|
| Warehouse Stock | `/warehouse-stock` | GET list + summary |
| Stock Movements | `/stock-movements` | GET + POST |
| Product Serials | `/product-serials` | CRUD |
| Stock Transfers | `/stock-transfers` | CRUD + status |
| Stock Alerts | `/stock-alerts` | GET |
| Inventories | `/inventories` | CRUD + count + apply |
### 9.3 — Dispositivos (Mobile/Eletrônicos)
| Módulo | Endpoints | Operações |
|--------|-----------|-----------|
| Devices | `/devices` | CRUD + IMEI lookup |
| Evaluations | `/evaluations` | CRUD + approve + reject |
### 9.4 — Serviços
| Módulo | Endpoints | Operações |
|--------|-----------|-----------|
| Service Orders | `/service-orders` | CRUD + complete-preparation + items |
| Warranties | `/warranties` | CRUD + check IMEI + claim |
### 9.5 — PDV
| Módulo | Endpoints | Operações |
|--------|-----------|-----------|
| POS Sessions | `/pos-sessions` | GET + open + close |
| Cash Movements | `/cash-movements` | GET + POST |
### 9.6 — Relatórios
| Endpoint | Descrição |
|----------|-----------|
| `/reports/os-by-status` | OS agrupadas por status |
| `/reports/os-by-technician` | OS agrupadas por técnico |
**Total: ~120 endpoints no módulo SOE (Core + Retail)**
---
## 10. Dependências Externas do SOE
```
┌─────────────────────────────────────────────────────────────────────┐
│ DEPENDÊNCIAS DO SOE │
│ │
│ ┌──── Arcádia Plus (Laravel) ────────────────────────────────┐ │
│ │ Host: :8080 (mesmo servidor) ou externo │ │
│ │ Env: PLUS_URL (se externo) │ │
│ │ Proxy: /plus/* → :8080 │ │
│ │ SSO: token compartilhado via plus/sso.ts │ │
│ │ Sync: retail/plus-sync.ts + retail/sync-service.ts │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌──── ERPNext/Frappe ─────────────────────────────────────────┐ │
│ │ Host: ERPNEXT_URL (env var no servidor) │ │
│ │ Auth: ERPNEXT_API_KEY + ERPNEXT_API_SECRET │ │
│ │ Entidades: Customer, Supplier, Item, Sales Order │ │
│ │ Rotas: server/erpnext/routes.ts → /api/erpnext/* │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌──── Arcádia Fisco (:8002) ──────────────────────────────────┐ │
│ │ Geração de NF-e a partir do pedido de venda │ │
│ │ POST /api/soe/sales-orders/:id/generate-nfe │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌──── PostgreSQL ─────────────────────────────────────────────┐ │
│ │ DATABASE_URL env var │ │
│ │ Motor padrão (nativo) sem Plus/ERPNext │ │
│ └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
```
---
## 11. Variáveis de Ambiente Necessárias para Deploy
```bash
# PostgreSQL (obrigatório)
DATABASE_URL=postgresql://user:pass@host:5432/arcadia
# ERPNext (opcional — apenas se motor = "erpnext")
ERPNEXT_URL=https://seu-erpnext.com
ERPNEXT_API_KEY=sua-chave
ERPNEXT_API_SECRET=seu-secret
# Arcádia Plus (opcional — apenas se motor = "plus")
PLUS_URL=http://localhost:8080 # ou URL externa do Plus
# Fisco (para geração de NF-e via SOE)
FISCO_URL=http://localhost:8002 # ou URL externa do Fisco
```
---
## 12. Checklist de Export para Deploy Manual
### Frontend
- [ ] `client/src/pages/SOE.tsx`
- [ ] `client/src/pages/ERP.tsx`
- [ ] `client/src/contexts/SoeMotorContext.tsx`
- [ ] `client/src/contexts/ErpProfileContext.tsx`
### Backend
- [ ] `server/erp/routes.ts`
- [ ] `server/erp/index.ts`
- [ ] `server/retail/routes.ts`
- [ ] `server/retail/index.ts`
- [ ] `server/retail/sync-service.ts`
- [ ] `server/retail/plus-sync.ts`
- [ ] `server/plus/client.ts`
- [ ] `server/plus/routes.ts`
- [ ] `server/plus/proxy.ts`
- [ ] `server/plus/launcher.ts`
- [ ] `server/plus/sso.ts`
- [ ] `server/erpnext/routes.ts`
- [ ] `server/erpnext/index.ts`
### Schema e Banco
- [ ] `shared/schema.ts` (tabelas: L348, L33033517, L50825617, L6204)
- [ ] Rodar `drizzle-kit push` no servidor para criar as tabelas
- [ ] Verificar variável `DATABASE_URL` no servidor
### Configuração do Servidor
- [ ] `.env` com `DATABASE_URL` (obrigatório)
- [ ] `.env` com `ERPNEXT_*` (se usar motor ERPNext)
- [ ] `.env` com `PLUS_URL` (se usar motor Plus)
- [ ] Confirmar que `:8080` (Plus) está acessível se necessário
---
## 13. Resumo Visual do Módulo
```
┌─────────────────────────────────────────────────────────────────────┐
│ ARCÁDIA SOE — STACK COMPLETO │
│ │
│ URL /soe → SOE.tsx (2.414L) │
│ │
│ 9 Abas: Dashboard │ Pessoas │ Produtos │ Vendas │ Compras │
│ Financeiro │ CRM │ Sync │ Config │
│ │
│ ┌─── Motor (Adapter) ────────────────────────────────────────┐ │
│ │ localStorage["arcadia_soe_motor"] = "plus" | "erpnext" │ │
│ │ │ │
│ │ plus → /plus/api/* → Arcádia Plus :8080 (Laravel) │ │
│ │ erpnext → ERPNext API → ERPNEXT_URL externo │ │
│ │ nativo → /api/soe/* → PostgreSQL local (Drizzle) │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─── Backend ────────────────────────────────────────────────┐ │
│ │ /api/soe/* → /api/erp/* (alias automático) │ │
│ │ server/erp/routes.ts (1.343L) → 35+ endpoints core │ │
│ │ server/retail/routes.ts (5.218L) → 85+ endpoints varejo │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─── Banco de Dados ─────────────────────────────────────────┐ │
│ │ Core: customers, suppliers, products, sales_orders, │ │
│ │ purchase_orders, persons, erp_config, erp_segments │ │
│ │ │ │
│ │ Retail: retail_stores, retail_warehouses, mobile_devices, │ │
│ │ device_evaluations, pos_sessions, pos_sales, │ │
│ │ stock_transfers, payment_plans (20+ tabelas) │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─── Integrações ────────────────────────────────────────────┐ │
│ │ Arcádia Plus :8080 │ ERPNext/Frappe │ Fisco :8002 │ │
│ └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
```

View File

@ -1,812 +0,0 @@
# Arcádia Tenants — Mapa Completo da Gestão Multi-Tenant
> Mapa da arquitetura multi-tenant da Arcádia Suite: hierarquia
> Master/Parceiro/Cliente, sistema de planos, ativação de módulos,
> empresas (Matriz/Filiais), comissões de parceiros, controle de acesso,
> e middleware de isolamento.
> Atualizado em: Março 2026
---
## 1. Visão Geral
A Arcádia Suite opera em modelo **multi-tenant hierárquico** com três níveis. Cada tenant possui um plano que define quais módulos da plataforma estão habilitados, e pode conter múltiplas empresas (CNPJ) para operações fiscais.
```
┌─────────────────────────────────────────────────────────────────────┐
│ HIERARQUIA DE TENANTS │
│ │
│ ┌──────────────┐ │
│ │ MASTER │ ← Operador da plataforma │
│ │ │ ← Vê tudo, controla tudo │
│ │ tenantType: │ ← Pode criar parceiros │
│ │ "master" │ ← Pode excluir tenants │
│ └──────┬───────┘ │
│ │ │
│ ┌────────────┼────────────┐ │
│ ▼ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ PARCEIRO A │ │ PARCEIRO B │ │ PARCEIRO C │ │
│ │ │ │ │ │ │ │
│ │ tenantType: │ │ Consultorias │ │ Integradores │ │
│ │ "partner" │ │ Revendas │ │ Franquias │ │
│ │ │ │ │ │ │ │
│ │ partnerCode: │ │ comissão: % │ │ plano: │ │
│ │ "PARC-001" │ │ sobre clientes│ │partner_pro │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ ┌────┴────┐ ┌───┴───┐ ┌───┴───┐ │
│ ▼ ▼ ▼ ▼ ▼ ▼ │
│ ┌────────┐┌────────┐┌────────┐┌────────┐┌────────┐┌────────┐ │
│ │Cliente1││Cliente2││Cliente3││Cliente4││Cliente5││Cliente6│ │
│ │ ││ ││ ││ ││ ││ │ │
│ │tenant ││tenant ││tenant ││tenant ││tenant ││tenant │ │
│ │Type: ││Type: ││Type: ││Type: ││Type: ││Type: │ │
│ │"client"││"client"││"client"││"client"││"client"││"client"│ │
│ │ ││ ││ ││ ││ ││ │ │
│ │parent: ││parent: ││parent: ││parent: ││parent: ││parent: │ │
│ │Parc.A ││Parc.A ││Parc.B ││Parc.B ││Parc.C ││Parc.C │ │
│ │ ││ ││ ││ ││ ││ │ │
│ │Plano: ││Plano: ││Plano: ││Plano: ││Plano: ││Plano: │ │
│ │starter ││pro ││free ││enterpr.││starter ││pro │ │
│ └────────┘└────────┘└────────┘└────────┘└────────┘└────────┘ │
└─────────────────────────────────────────────────────────────────────┘
```
### Visibilidade por tipo:
| Tipo | Vê | Cria | Exclui |
|------|-----|------|--------|
| **Master** | Todos os tenants | Parceiros e Clientes | Qualquer (exceto master) |
| **Partner** | Seu tenant + seus clientes | Apenas clientes vinculados | Nenhum |
| **Client** | Apenas seu próprio tenant | Nenhum | Nenhum |
---
## 2. Banco de Dados — Tabelas de Tenant
### 2.1 — tenants (Tabela Principal)
```
┌──────────────────────────────────────────────────────────────────┐
│ tenants │
│ Tabela: "tenants" — shared/schema.ts:849 │
│ │
│ ┌─────────── Identificação ──────────────────┐ │
│ │ id serial PK │ │
│ │ name text NOT NULL │ │
│ │ slug text UNIQUE │ │
│ │ email text │ │
│ │ phone text │ │
│ │ logoUrl text │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ ┌─────────── Hierarquia ─────────────────────┐ │
│ │ tenantType "master" | "partner" | │ │
│ │ "client" │ │
│ │ parentTenantId integer (self-ref FK) │ │
│ │ partnerCode text (código único parceiro) │ │
│ │ commissionRate numeric(5,2) (% comissão) │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ ┌─────────── Plano e Limites ────────────────┐ │
│ │ plan "free" | "starter" | "pro" │ │
│ │ | "enterprise" | │ │
│ │ "partner_starter" | │ │
│ │ "partner_pro" │ │
│ │ status "active" | "trial" | │ │
│ │ "suspended" | "cancelled" │ │
│ │ maxUsers integer (default 5) │ │
│ │ maxStorageMb integer (default 1000) │ │
│ │ features JSONB → TenantFeatures │ ← Módulos │
│ └─────────────────────────────────────────────┘ │
│ │
│ ┌─────────── Billing ────────────────────────┐ │
│ │ billingEmail text │ │
│ │ trialEndsAt timestamp │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ ┌─────────── Dados Empresariais ─────────────┐ │
│ │ cnpj text │ │
│ │ tradeName text │ │
│ │ address text │ │
│ │ city, state text │ │
│ │ segment text │ │
│ │ notes, source text │ │
│ │ commercialContact / commercialPhone │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ ┌─────────── Metadata ───────────────────────┐ │
│ │ settings text (JSON config) │ │
│ │ createdAt timestamp │ │
│ │ updatedAt timestamp │ │
│ └─────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
```
### 2.2 — TenantFeatures (JSONB — Sistema de Módulos)
O campo `features` na tabela `tenants` é um JSONB que controla **quais módulos da Arcádia estão habilitados** para cada tenant. O tipo é definido em `shared/schema.ts:821` e espelhado no frontend em `client/src/hooks/use-tenant-features.ts:3`.
```
┌──────────────────────────────────────────────────────────────────┐
│ TenantFeatures (JSONB) │
│ │
│ ┌─────────── Módulos Booleanos ──────────────┐ │
│ │ │ │
│ │ ide → IDE integrada │ │
│ │ whatsapp → WhatsApp multi-sessão │ │
│ │ crm → CRM completo │ │
│ │ erp → SOE/ERP │ │
│ │ bi → Business Intelligence │ │
│ │ manus → IA Manus (GPT-4o) │ │
│ │ centralApis → APIs centrais │ │
│ │ centralApisManage → Gerenciar APIs │ │
│ │ comunidades → Chat interno │ │
│ │ biblioteca → Biblioteca de conhecimento │ │
│ │ bibliotecaPublish → Publicar na biblioteca │ │
│ │ suporteN3 → Suporte nível 3 │ │
│ │ retail → Módulo Varejo │ │
│ │ plus → ERP Plus (Laravel) │ │
│ │ fisco → Motor Fiscal │ │
│ │ cockpit → Cockpit executivo │ │
│ │ compass → Process Compass │ │
│ │ production → Módulo Produção │ │
│ │ support → Módulo Suporte │ │
│ │ xosCrm → XOS CRM (Kanban) │ │
│ │ │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ ┌─────────── Configurações Numéricas ────────┐ │
│ │ │ │
│ │ whatsappSessions → Nº sessões WA (0-N) │ │
│ │ maxChannels → Nº canais permitidos │ │
│ │ │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ ┌─────────── Configurações de Texto ─────────┐ │
│ │ │ │
│ │ ideMode → "none" | "no-code" | │ │
│ │ "low-code" | "pro-code" │ │
│ │ manusTools[] → Lista de tools habilitadas │ │
│ │ │ │
│ └─────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
```
### 2.3 — tenant_empresas (Matriz/Filiais)
Cada tenant pode ter **múltiplas empresas** (CNPJs) cadastradas — essencial para operações fiscais (NF-e/NFC-e) por filial.
```
┌──────────────────────────────────────────────────────────────────┐
│ tenant_empresas │
│ Tabela: "tenant_empresas" — shared/schema.ts:889 │
│ │
│ ┌─────────── Identificação ──────────────────┐ │
│ │ id serial PK │ │
│ │ tenantId FK → tenants.id (CASCADE) │ │
│ │ razaoSocial text NOT NULL │ │
│ │ nomeFantasia text │ │
│ │ cnpj text NOT NULL │ │
│ │ ie text (inscrição estadual) │ │
│ │ im text (inscrição municipal) │ │
│ │ email, phone text │ │
│ │ tipo "matriz" | "filial" │ │
│ │ status "active" | "inactive" │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ ┌─────────── Endereço ───────────────────────┐ │
│ │ cep, logradouro, numero, complemento, │ │
│ │ bairro, cidade, uf, codigoIbge │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ ┌─────────── Fiscal ─────────────────────────┐ │
│ │ regimeTributario "simples" | "presumido" │ │
│ │ | "real" │ │
│ │ certificadoDigitalId FK certificado │ │
│ │ ambienteFiscal "producao" | "homologacao" │ │
│ │ serieNfe integer (default 1) │ │
│ │ serieNfce integer (default 1) │ │
│ │ plusEmpresaId integer (link ERP Plus) │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ ┌─────────── Metadata ───────────────────────┐ │
│ │ createdAt, updatedAt timestamp │ │
│ └─────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
```
**Uso típico:**
```
Tenant "Empresa ABC" (id: 5)
├── Empresa Matriz: "ABC Comércio LTDA" (CNPJ: 12.345.678/0001-00)
│ regime: simples, ambiente: producao, série NF-e: 1
├── Filial SP: "ABC Comércio SP" (CNPJ: 12.345.678/0002-81)
│ regime: simples, ambiente: producao, série NF-e: 2
└── Filial RJ: "ABC Comércio RJ" (CNPJ: 12.345.678/0003-62)
regime: simples, ambiente: homologacao, série NF-e: 1
```
### 2.4 — tenant_users (Membership)
```
┌──────────────────────────────────────────────────────────────────┐
│ tenant_users │
│ Tabela: "tenant_users" — shared/schema.ts:923 │
│ │
│ id serial PK │
│ tenantId FK → tenants.id (CASCADE) │
│ userId FK → users.id (CASCADE) │
│ role "owner" | "admin" | "member" │
│ isOwner "true" | "false" │
│ createdAt timestamp │
│ │
│ → Link N:N entre users e tenants │
│ → Um usuário pode pertencer a múltiplos tenants │
│ → Cada tenant tem um owner + admins + members │
└──────────────────────────────────────────────────────────────────┘
```
### 2.5 — tenant_plans (Planos Disponíveis)
```
┌──────────────────────────────────────────────────────────────────┐
│ tenant_plans │
│ Tabela: "tenant_plans" — shared/schema.ts:933 │
│ │
│ id serial PK │
│ code text UNIQUE ("free", "starter", "pro", │
│ "enterprise", "partner_starter", "partner_pro") │
│ name text NOT NULL │
│ description text │
│ tenantType "master" | "partner" | "client" │
│ maxUsers integer (default 5) │
│ maxStorageMb integer (default 1000) │
│ features JSONB → TenantFeatures │
│ monthlyPrice integer (centavos) │
│ yearlyPrice integer (centavos) │
│ trialDays integer (default 14) │
│ isActive "true" | "false" │
│ sortOrder integer │
│ createdAt timestamp │
└──────────────────────────────────────────────────────────────────┘
```
### 2.6 — partner_clients (Relacionamento Parceiro↔Cliente)
```
┌──────────────────────────────────────────────────────────────────┐
│ partner_clients │
│ Tabela: "partner_clients" — shared/schema.ts:951 │
│ │
│ id serial PK │
│ partnerId FK → tenants.id (CASCADE) │
│ clientId FK → tenants.id (CASCADE) │
│ commissionRate numeric(5,2) — Override do rate do parceiro │
│ status "active" | "suspended" | "ended" │
│ notes text │
│ startedAt timestamp │
│ endedAt timestamp │
└──────────────────────────────────────────────────────────────────┘
```
### 2.7 — partner_commissions (Comissões dos Parceiros)
```
┌──────────────────────────────────────────────────────────────────┐
│ partner_commissions │
│ Tabela: "partner_commissions" — shared/schema.ts:963 │
│ │
│ id serial PK │
│ partnerId FK → tenants.id (CASCADE) │
│ clientId FK → tenants.id (CASCADE) │
│ referenceMonth text ("2026-01", "2026-02", ...) │
│ clientPlanCode text (código do plano do cliente) │
│ clientPlanValue integer (valor do plano em centavos) │
│ commissionRate numeric(5,2) NOT NULL │
│ commissionValue integer NOT NULL (valor em centavos) │
│ status "pending" | "approved" | "paid" | "cancelled" │
│ approvedAt timestamp │
│ paidAt timestamp │
│ paymentReference text │
│ createdAt timestamp │
│ │
│ Fluxo: pending → approved → paid │
│ │
│ Exemplo: │
│ Parceiro A tem Cliente X no plano "pro" (R$ 299/mês) │
│ commissionRate: 20% │
│ commissionValue: 5980 centavos (R$ 59,80) │
└──────────────────────────────────────────────────────────────────┘
```
---
## 3. Planos Pré-definidos (Seed)
A rota `POST /api/admin/plans/seed` cria os 6 planos padrão:
```
┌─────────────────────────────────────────────────────────────────────┐
│ PLANOS PARA CLIENTES │
├────────────┬──────────┬──────────┬──────────────────────────────────┤
│ Plano │ Preço/mês│ Usuários │ Módulos │
├────────────┼──────────┼──────────┼──────────────────────────────────┤
│ Gratuito │ R$ 0 │ 2 │ erp, crm │
│ (free) │ │ │ │
├────────────┼──────────┼──────────┼──────────────────────────────────┤
│ Starter │ R$ 99 │ 5 │ erp, crm, bi, fisco, │
│ │ │ │ whatsapp, compass │
├────────────┼──────────┼──────────┼──────────────────────────────────┤
│ Pro │ R$ 299 │ 15 │ erp, crm, bi, fisco, retail, │
│ │ │ │ plus, whatsapp, manus, cockpit, │
│ │ │ │ compass, support, biblioteca │
├────────────┼──────────┼──────────┼──────────────────────────────────┤
│ Enterprise │ R$ 999 │ 100 │ TODOS os módulos │
│ │ │ 50GB │ (erp, crm, bi, fisco, retail, │
│ │ │ │ plus, whatsapp, manus, ide, │
│ │ │ │ cockpit, compass, production, │
│ │ │ │ support, xosCrm, centralApis, │
│ │ │ │ comunidades, biblioteca) │
└────────────┴──────────┴──────────┴──────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ PLANOS PARA PARCEIROS │
├──────────────────┬──────────┬──────────┬────────────────────────────┤
│ Plano │ Preço/mês│ Usuários │ Módulos │
├──────────────────┼──────────┼──────────┼────────────────────────────┤
│ Parceiro Starter │ R$ 199 │ 10 │ erp, crm, bi, fisco, │
│ │ │ │ retail, whatsapp, compass, │
│ │ │ │ support │
├──────────────────┼──────────┼──────────┼────────────────────────────┤
│ Parceiro Pro │ R$ 499 │ 50 │ TODOS os módulos │
│ │ │ 25GB │ (igual Enterprise) │
└──────────────────┴──────────┴──────────┴────────────────────────────┘
```
---
## 4. Sistema de Papéis (Roles)
A Arcádia opera com **dois níveis de papéis** que se complementam:
```
┌─────────────────────────────────────────────────────────────────────┐
│ SISTEMA DUPLO DE ROLES │
│ │
│ ┌─────────── Papel do Sistema (users.role) ──────────────────┐ │
│ │ │ │
│ │ "master" → Superadmin, acesso total, bypass de tenant │ │
│ │ "admin" → Administrador, acessa /api/admin/* │ │
│ │ "user" → Usuário padrão, sem acesso admin │ │
│ │ │ │
│ │ Verificado no middleware global de admin: │ │
│ │ if (role !== "admin" && role !== "master") → 403 │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────── Papel no Tenant (tenant_users.role) ────────────┐ │
│ │ │ │
│ │ "owner" → Dono do tenant, criador, full control │ │
│ │ "admin" → Administrador do tenant │ │
│ │ "member" → Membro comum, acesso operacional │ │
│ │ │ │
│ │ Usado para scoping de dados e permissões intra-tenant │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ Combinação no login (getEnrichedUser): │
│ user.role + user.tenantId + user.tenantType + user.tenantRole │
└─────────────────────────────────────────────────────────────────────┘
```
---
## 5. Controle de Acesso — Helpers
O backend implementa dois helpers essenciais em `server/admin/routes.ts`:
```
┌─────────────────────────────────────────────────────────────────────┐
│ getAllowedTenantIds(user) │
│ │
│ Retorna os IDs de tenants que o usuário pode acessar: │
│ │
│ • master → null (acesso total, sem filtro) │
│ • partner → [seu tenantId, ...clientIds vinculados] │
│ • client → [apenas seu tenantId] │
│ │
│ Usado em: GET /tenants, GET /partner-clients, GET /commissions, │
│ GET /empresas, GET /tenants/hierarchy │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ canAccessTenant(user, tenantId) │
│ │
│ Verifica se o usuário pode operar sobre um tenant específico: │
│ │
│ • master → true (sempre) │
│ • partner → true se tenantId ∈ getAllowedTenantIds() │
│ • client → true apenas se tenantId === user.tenantId │
│ │
│ Usado em: PATCH /tenants/:id, DELETE /empresas/:id, │
│ POST /empresas, etc. │
└─────────────────────────────────────────────────────────────────────┘
```
---
## 6. Middleware de Módulos (requireModule)
O middleware `requireModule` em `server/retail/routes.ts:25` bloqueia endpoints quando um módulo não está ativo para o tenant:
```
┌─────────────────────────────────────────────────────────────────────┐
│ requireModule(moduleKey: keyof TenantFeatures) │
│ │
│ Fluxo: │
│ 1. Identifica tenantId do usuário logado │
│ 2. Busca tenant no banco │
│ 3. Verifica features[moduleKey] === true │
│ 4. Se false → 403 com mensagem: │
│ { │
│ error: "Módulo 'X' não está ativo para este tenant", │
│ moduleKey: "plus", │
│ action: "Ative o módulo em Admin → Módulos" │
│ } │
│ │
│ Exemplo de uso: │
│ router.post("/plus/sync/customers", │
│ requireModule("plus"), ← Bloqueia se Plus não está ativo │
│ async (req, res) => { ... } │
│ ); │
└─────────────────────────────────────────────────────────────────────┘
```
---
## 7. Hook Frontend (useTenantFeatures)
O hook `useTenantFeatures()` em `client/src/hooks/use-tenant-features.ts` fornece acesso às features do tenant no frontend:
```
┌─────────────────────────────────────────────────────────────────────┐
│ useTenantFeatures() │
│ │
│ Fonte: GET /api/soe/tenant/modules │
│ Cache: 60 segundos (staleTime: 60000) │
│ │
│ Retorna: │
│ { │
│ features: TenantFeatures, ← Objeto completo de features │
│ isLoading: boolean, │
│ plan: string, ← Código do plano atual │
│ isEnabled: (key) => bool, ← Verificação rápida │
│ } │
│ │
│ Defaults (quando não logado / sem tenant): │
│ ide: true, crm: true, erp: true, manus: true, compass: true │
│ Todos os demais: false │
│ │
│ Uso no componente: │
│ const { isEnabled } = useTenantFeatures(); │
│ if (isEnabled("bi")) { /* mostra menu BI */ } │
│ if (isEnabled("retail")) { /* mostra módulo varejo */ } │
└─────────────────────────────────────────────────────────────────────┘
```
---
## 8. Interface de Administração
### 8.1 — Admin.tsx (Página Principal)
```
┌─────────────────────────────────────────────────────────────────────┐
│ Admin.tsx — client/src/pages/Admin.tsx (2.635 linhas) │
│ Rota: /admin │
│ │
│ 6 Abas: │
│ ┌─────────────┬──────────┬────────────┬───────────┬──────────────┐ │
│ │ Conexões │ Tarefas │ Knowledge │ Bibliotecas│ Módulos │ │
│ │ │ │ │ │ │ │
│ │ WhatsApp │ Dev │ Knowledge │ Conteúdo │ Ativação │ │
│ │ ERPNext │ Center │ Graph │ Docs │ por tenant │ │
│ │ APIs │ Pipeline │ Q&A │ Templates │ Toggle on/ │ │
│ │ │ │ │ │ off módulos │ │
│ └─────────────┴──────────┴────────────┴───────────┴──────────────┘ │
│ ┌─────────────┐ │
│ │Casa de │ │
│ │Máquinas │ │
│ │ │ │
│ │ Status dos │ │
│ │ motores │ │
│ │ (:8002-8006)│ │
│ └─────────────┘ │
│ │
│ Aba "Módulos" renderiza: <MultiTenantSection />
└─────────────────────────────────────────────────────────────────────┘
```
### 8.2 — MultiTenantSection (Gestão Completa)
```
┌─────────────────────────────────────────────────────────────────────┐
│ MultiTenantSection.tsx (1.486 linhas) │
│ client/src/components/MultiTenantSection.tsx │
│ │
│ 4 Abas internas: │
│ │
│ ┌─ Tenants ──────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Lista de tenants com: │ │
│ │ • Nome, tipo (Master/Parceiro/Cliente) │ │
│ │ • Plano atual e status │ │
│ │ • Nº de filhos (childCount) │ │
│ │ • Tenant pai (parentTenant) │ │
│ │ • Ações: Editar, Ativar/Desativar módulos │ │
│ │ │ │
│ │ Formulário de criação: │ │
│ │ • Nome, email, phone │ │
│ │ • Tipo (master/partner/client) │ │
│ │ • Plano (seleciona do tenant_plans) │ │
│ │ • Tenant pai (para hierarquia) │ │
│ │ • Features auto-preenchidas pelo plano selecionado │ │
│ │ • Toggle individual de cada módulo │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─ Planos ───────────────────────────────────────────────────┐ │
│ │ │ │
│ │ CRUD de planos com: │ │
│ │ • Código, nome, descrição │ │
│ │ • Tipo de tenant (client/partner) │ │
│ │ • Preço mensal e anual (centavos) │ │
│ │ • Max usuários e storage │ │
│ │ • Features (toggle de cada módulo) │ │
│ │ • Botão "Seed" para criar planos padrão │ │
│ │ • Botão "Propagar" para aplicar features do plano │ │
│ │ a todos os tenants que usam esse plano │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─ Comissões ────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Lista de comissões de parceiros: │ │
│ │ • Parceiro, Cliente, Mês referência │ │
│ │ • Valor do plano, taxa de comissão, valor da comissão │ │
│ │ • Status (pending → approved → paid) │ │
│ │ • Ações: Aprovar, Marcar como paga │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─ Empresas ─────────────────────────────────────────────────┐ │
│ │ │ │
│ │ EmpresasSubSection: │ │
│ │ • Lista de empresas (Matriz/Filiais) por tenant │ │
│ │ • CNPJ, Razão Social, Nome Fantasia │ │
│ │ • IE, IM, Regime Tributário │ │
│ │ • Endereço completo (CEP, logradouro, cidade, UF) │ │
│ │ • Config Fiscal (ambiente, série NF-e, série NFC-e) │ │
│ │ • Link com ERP Plus (plusEmpresaId) │ │
│ │ • Ações: Criar, Editar, Excluir │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
```
---
## 9. API REST Completa (/api/admin/*)
### Tenants
| Método | Endpoint | Função | Acesso |
|--------|----------|--------|--------|
| GET | `/tenants` | Lista tenants (filtrado por acesso) | master/partner/client |
| POST | `/tenants` | Cria tenant (resolve features do plano) | master/admin |
| PATCH | `/tenants/:id` | Atualiza tenant (canAccessTenant) | master/partner |
| DELETE | `/tenants/:id` | Exclui tenant (proteção master) | somente master |
| GET | `/tenants/hierarchy` | Árvore hierárquica de tenants | master/partner |
| GET | `/tenants/stats` | Estatísticas (total, por tipo, por status) | master/admin |
### Planos
| Método | Endpoint | Função |
|--------|----------|--------|
| GET | `/plans` | Lista todos os planos |
| POST | `/plans` | Cria novo plano |
| PATCH | `/plans/:id` | Atualiza plano |
| DELETE | `/plans/:id` | Exclui plano |
| POST | `/plans/:id/propagate` | Propaga features do plano para todos os tenants que o usam |
| POST | `/plans/seed` | Cria 6 planos padrão |
### Parceiro-Cliente
| Método | Endpoint | Função |
|--------|----------|--------|
| GET | `/partner-clients` | Lista relacionamentos parceiro↔cliente |
| POST | `/partner-clients` | Cria vínculo parceiro↔cliente |
### Comissões de Parceiros
| Método | Endpoint | Função |
|--------|----------|--------|
| GET | `/commissions` | Lista comissões (filtro por partnerId, status) |
| PATCH | `/commissions/:id/approve` | Aprova comissão |
| PATCH | `/commissions/:id/pay` | Marca como paga |
### Empresas (Matriz/Filiais)
| Método | Endpoint | Função |
|--------|----------|--------|
| GET | `/empresas` | Lista empresas (filtro por tenantId + acesso) |
| GET | `/empresas/:id` | Detalhe da empresa |
| POST | `/empresas` | Cria empresa (verifica canAccessTenant) |
| PATCH | `/empresas/:id` | Atualiza empresa |
| DELETE | `/empresas/:id` | Exclui empresa |
### Outros Admin
| Método | Endpoint | Função |
|--------|----------|--------|
| GET | `/profiles` | Lista perfis de usuários |
| POST | `/profiles` | Cria perfil |
| PATCH | `/profiles/:id` | Atualiza perfil |
| DELETE | `/profiles/:id` | Exclui perfil |
| GET | `/users` | Lista usuários (com tenant info) |
| PATCH | `/users/:id` | Atualiza usuário |
| PATCH | `/users/:id/status` | Altera status do usuário |
| PATCH | `/partners/:id/status` | Altera status de parceiro |
| PATCH | `/clients/:id/status` | Altera status de cliente |
| GET | `/stats` | Estatísticas gerais (tenants, users, leads) |
| GET | `/libraries` | Lista bibliotecas de conteúdo |
**Total: 33 endpoints de administração**
---
## 10. Fluxo Completo — Onboarding de Tenant
```
┌──────────┐ ┌────────────┐ ┌──────────────┐ ┌──────────────┐
│ 1. CRIAR │───▶│ 2. PLANO │───▶│ 3. FEATURES │───▶│ 4. EMPRESAS │
│ TENANT │ │ │ │ │ │ │
│ │ │ Seleciona │ │ Auto-resolve │ │ Cadastra │
│ POST │ │ plano │ │ do plano ou │ │ Matriz │
│ /admin/ │ │ (free, │ │ toggle │ │ + Filiais │
│ tenants │ │ starter, │ │ individual │ │ │
│ │ │ pro, etc) │ │ de módulos │ │ CNPJ, IE, │
│ name, │ │ │ │ │ │ endereço, │
│ email, │ │ maxUsers │ │ ide: true │ │ regime │
│ type │ │ maxStorage │ │ crm: true │ │ tributário, │
│ │ │ preço │ │ bi: false │ │ certificado │
│ │ │ │ │ ... │ │ digital │
└──────────┘ └────────────┘ └──────────────┘ └──────────────┘
┌──────────────┐ ┌──────────────┐ ┌──────────────────────────┐
│ 5. VINCULAR │ │ 6. COMISSÕES │ │ 7. USUÁRIOS │
│ PARCEIRO │ │ │ │ │
│ (se partner) │ │ Automáticas │ │ Convida users │
│ │ │ mensais │ │ tenant_users │
│ POST /admin/ │ │ baseadas no │ │ roles: owner/admin/ │
│ partner- │ │ plano do │ │ member │
│ clients │ │ cliente │ │ │
│ │ │ │ │ Scoping automático │
│ partnerId │ │ pending → │ │ de todos os dados │
│ clientId │ │ approved → │ │ por tenantId │
│ commission% │ │ paid │ │ │
└──────────────┘ └──────────────┘ └──────────────────────────┘
```
---
## 11. Propagação de Plano
Quando um plano é atualizado, a feature de **propagação** atualiza todos os tenants que usam esse plano:
```
┌─────────────────────────────────────────────────────────────────────┐
│ POST /api/admin/plans/:id/propagate │
│ │
│ 1. Busca o plano por ID │
│ 2. Pega features, maxUsers, maxStorageMb do plano │
│ 3. Atualiza TODOS os tenants onde plan === plan.code: │
│ UPDATE tenants SET │
│ features = plan.features, │
│ maxUsers = plan.maxUsers, │
│ maxStorageMb = plan.maxStorageMb │
│ WHERE plan = 'pro' │
│ 4. Retorna: { propagated: 15, planCode: "pro" } │
│ │
│ Exemplo: │
│ Plano "pro" ganha módulo "production" → │
│ Propagar → 15 tenants "pro" recebem production: true │
└─────────────────────────────────────────────────────────────────────┘
```
---
## 12. Isolamento de Dados
Toda query de dados respeita o escopo do tenant:
```
┌─────────────────────────────────────────────────────────────────────┐
│ ISOLAMENTO POR tenantId │
│ │
│ Padrão em todas as queries: │
│ │
│ const tenantId = (req.user as any).tenantId; │
│ const data = await db.select() │
│ .from(tabela) │
│ .where(eq(tabela.tenantId, tenantId)); │
│ │
│ Tabelas com tenantId: │
│ • tenants (hierarquia) • crm_channels │
│ • tenant_empresas • crm_threads │
│ • crm_leads • crm_events │
│ • crm_clients • crm_campaigns │
│ • crm_opportunities • crm_products │
│ • crm_partners • crm_contracts │
│ • crm_pipeline_stages • crm_commission_rules │
│ • crm_proposals • crm_quick_messages │
│ • retail_activity_feed • valuation_* │
│ │
│ Resultado: Dados do Tenant A NUNCA são vistos pelo Tenant B │
└─────────────────────────────────────────────────────────────────────┘
```
---
## 13. Arquivos-Chave
| Arquivo | Função | Linhas |
|---------|--------|--------|
| `shared/schema.ts` (L821-977) | TenantFeatures type + 6 tabelas tenant | ~160 |
| `server/admin/routes.ts` | API admin (33 endpoints) | 876 |
| `client/src/pages/Admin.tsx` | Página admin (6 abas) | 2.635 |
| `client/src/components/MultiTenantSection.tsx` | Gestão multi-tenant (4 sub-abas) | 1.486 |
| `client/src/hooks/use-tenant-features.ts` | Hook de features no frontend | 59 |
| `server/retail/routes.ts` (L25) | Middleware requireModule | ~25 |
| `server/storage.ts` (getEnrichedUser) | Enriquece user com tenant info | — |
---
## 14. Resumo Visual
```
┌─────────────────────────────────────────────────────────────────────┐
│ ARCÁDIA MULTI-TENANT STACK │
│ │
│ ┌───── Autenticação ──────────────────────────────────────────┐ │
│ │ Login → getEnrichedUser → user + tenantId + tenantType │ │
│ │ + tenantRole + features │ │
│ └─────────────────────────────────────────┬───────────────────┘ │
│ │ │
│ ┌─────────────────────────────────────────▼───────────────────┐ │
│ │ Middleware │ │
│ │ ┌──────────┐ ┌─────────────┐ ┌────────────────────┐ │ │
│ │ │requireAuth│ │requireModule │ │getAllowedTenantIds │ │ │
│ │ │role check │ │feature check │ │canAccessTenant │ │ │
│ │ └──────────┘ └─────────────┘ └────────────────────┘ │ │
│ └─────────────────────────────────────────┬───────────────────┘ │
│ │ │
│ ┌─────────────────────────────────────────▼───────────────────┐ │
│ │ API /api/admin/* │ │
│ │ 33 endpoints: Tenants, Plans, Empresas, Comissões │ │
│ └─────────────────────────────────────────┬───────────────────┘ │
│ │ │
│ ┌─────────────────────────────────────────▼───────────────────┐ │
│ │ PostgreSQL │ │
│ │ 6 tabelas: tenants, tenant_users, tenant_plans, │ │
│ │ tenant_empresas, partner_clients, partner_commissions │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌───── Frontend ──────────────────────────────────────────────┐ │
│ │ Admin.tsx (6 abas) → MultiTenantSection (4 sub-abas) │ │
│ │ useTenantFeatures() hook → GET /api/soe/tenant/modules │ │
│ └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
```

View File

@ -1,292 +0,0 @@
# PLANO BI — MetaSet (Apache Superset)
## Arquitetura Consolidada
### Versão 2.0 — Abril 2026
---
## 1. STATUS — CONSOLIDAÇÃO CONCLUÍDA ✅
### 1.1 O que foi realizado
A migração do Metabase para o Apache Superset foi **concluída** em abril de 2026.
| Componente | Status | Localização |
|---|---|---|
| **MetaSet (Apache Superset 4.1.0)** | ✅ Implementado | `/server/bi/metaset/` |
| **Cliente TypeScript** | ✅ Implementado | `/server/bi/metaset-client/` |
| **Rotas Express** | ✅ Implementado | `/server/bi/metaset-client/routes.ts` |
| **Código legado Metabase** | ✅ Backup | `/server/metaset/backup/` |
| **Integração Kernel** | ✅ Ativa | Porta 8100 |
| **Documentação** | ✅ Atualizada | `/server/metaset/README.md` |
### 1.2 Arquitetura Consolidada
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ ARCÁDIA SUITE (Porta 5000) │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ BI-API GATEWAY (Porta 8004) │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────────┐ │ │
│ │ │ /bi/api/* │ │/bi/metaset/*│ │ /bi/fdb/* │ │ │
│ │ │ (nativo) │ │ (proxy) │ │ (proxy) │ │ │
│ │ └──────┬──────┘ └──────┬──────┘ └─────────────┬───────────────┘ │ │
│ │ └─────────────────┴───────────────────────┘ │ │
│ └─────────────────────────────┬────────────────────────────────────────┘ │
└────────────────────────────────┼────────────────────────────────────────────┘
┌───────────────────────┴───────────────────────┐
▼ ▼
┌─────────────────────────┐ ┌─────────────────────────┐
│ METASET BI (Porta 8100)│ │ FDB-BRIDGE (8200) │
│ Apache Superset 4.1.0 │ │ Sync Firebird → PG │
│ ┌─────────────────┐ │ │ - Change Data Capture │
│ │ superset-src/ │ │ │ - Real-time sync │
│ │ - Dashboards │ │ │ - Tenant isolation │
│ │ - SQL Lab │ │ │ │
│ │ - Charts │ │ │ │
│ │ - RLS │ │ │ │
│ └─────────────────┘ │ │ │
└─────────────────────────┘ └─────────────────────────┘
┌─────────────────────────┐
│ PostgreSQL + Redis │
│ (metaset_db + cache) │
└─────────────────────────┘
```
---
## 2. ESTRUTURA DE ARQUIVOS
### 2.1 Implementação Atual (Apache Superset)
```
/server/bi/metaset/
├── main.py # FastAPI wrapper (Porta 8100)
├── run.py # Entry point Gunicorn
├── superset_config.py # Configuração Superset
├── security_manager.py # ArcadiaSecurityManager (JWT + RLS)
├── Dockerfile # Container MetaSet
├── init.sh # Script de inicialização
├── requirements.txt # Dependências Python
├── superset-src/ # Código fonte Apache Superset 4.1.0
├── config/ # Configurações adicionais
├── branding/ # Assets de branding
├── security/ # Módulos de segurança
├── extensions/ # Extensões
└── templates/ # Templates customizados
/server/bi/metaset-client/
├── index.ts # Cliente TypeScript completo
└── routes.ts # Rotas Express /api/bi/metaset/*
```
### 2.2 Código Legado (Backup)
```
/server/metaset/backup/
├── client.ts # Cliente Metabase (legado)
└── routes.ts # Rotas Metabase (legado)
```
---
## 3. API ENDPOINTS
### 3.1 BI-API Gateway (Porta 8004)
| Endpoint | Método | Descrição |
|----------|--------|-----------|
| `/api/bi/metaset/health` | GET | Health check |
| `/api/bi/metaset/databases` | GET/POST | Lista/Cria databases |
| `/api/bi/metaset/databases/:id/sync` | POST | Sincroniza schema |
| `/api/bi/metaset/databases/:id/tables` | GET | Lista tabelas |
| `/api/bi/metaset/tables/:id` | GET | Metadata da tabela |
| `/api/bi/metaset/query` | POST | Executa SQL |
| `/api/bi/metaset/charts` | GET/POST | Lista/Cria charts |
| `/api/bi/metaset/charts/:id` | GET/DELETE | Obtém/Deleta chart |
| `/api/bi/metaset/dashboards` | GET/POST | Lista/Cria dashboards |
| `/api/bi/metaset/dashboards/:id` | GET/PUT/DELETE | CRUD dashboard |
| `/api/bi/metaset/guest-token` | POST | Cria guest token |
| `/api/bi/metaset/embed/dashboard/:id` | GET | URL de embed |
| `/api/bi/metaset/suggest/:table` | GET | Sugestões de análise |
### 3.2 MetaSet Direto (Porta 8100)
API nativa do Apache Superset:
```
GET /health
GET /api/v1/dashboard/
GET /api/v1/dashboard/:id
POST /api/v1/dashboard/
GET /api/v1/chart/
GET /api/v1/dataset/
POST /api/v1/security/guest_token/
GET /api/v1/database/
GET /sqllab/
```
---
## 4. CONFIGURAÇÃO
### 4.1 Variáveis de Ambiente
```bash
# MetaSet Connection
METASET_HOST=metaset
METASET_PORT=8100
# Database
DATABASE_URL=postgresql://arcadia:arcadia123@db:5432/metaset_db
ARCADIA_DATABASE_URL=postgresql://arcadia:arcadia123@db:5432/arcadia
# Redis (Cache)
REDIS_URL=redis://redis:6379/1
# Security
METASET_SECRET_KEY=change-in-production
JWT_SECRET_KEY=arcadia-jwt-secret
# Admin
METASET_ADMIN_USER=admin
METASET_ADMIN_EMAIL=admin@arcadia.app
METASET_ADMIN_PASSWORD=metaset2026
```
### 4.2 Feature Flags
```python
FEATURE_FLAGS = {
"EMBEDDED_SUPERSET": True, # Embedding de dashboards
"GUEST_EMBEDDING_ENABLED": True, # Guest tokens
"DASHBOARD_RBAC": True, # Controle de acesso
"SQLLAB_BACKEND_PERSISTENCE": True, # Persistência SQL Lab
"DRILL_BY": True, # Drill-down
"DASHBOARD_CROSS_FILTERS": True, # Filtros cruzados
}
```
---
## 5. SEGURANÇA
### 5.1 ArcadiaSecurityManager
- Extrai JWT do header `Authorization: Bearer <token>`
- Identifica tenant do payload JWT (`tenant_id`)
- Aplica RLS automaticamente via `SET LOCAL app.current_tenant`
- Mapeia roles: superadmin→Admin, admin→Alpha, analyst→Gamma, viewer→Public
### 5.2 Row Level Security (RLS)
```sql
-- Política aplicada automaticamente
SET LOCAL app.current_tenant = <tenant_id>;
-- Views filtram por tenant
CREATE VIEW tenant_data AS
SELECT * FROM data
WHERE tenant_id = current_setting('app.current_tenant')::int;
```
---
## 6. CLIENTE TYPESCRIPT
### 6.1 Uso Básico
```typescript
import { metasetClient } from "./server/bi/metaset-client";
// Health check
const health = await metasetClient.isHealthy();
// Listar dashboards
const dashboards = await metasetClient.listDashboards();
// Executar query
const result = await metasetClient.executeSql(1, "SELECT * FROM sales LIMIT 100");
// Criar dashboard
const dashboard = await metasetClient.createDashboard({
name: "Vendas Q1 2026",
description: "Dashboard de vendas"
});
// Guest token para embedding
const token = await metasetClient.createGuestToken({
resources: [{ type: "dashboard", id: "1" }],
rls: [{ dataset: 1, clause: "tenant_id = 123" }]
});
```
### 6.2 Ferramentas do Manus (Agente IA)
| Ferramenta | Descrição |
|------------|-----------|
| `metaset.query` | Executa SQL |
| `metaset.list_tables` | Lista tabelas |
| `metaset.table_fields` | Campos da tabela |
| `metaset.create_chart` | Cria gráfico |
| `metaset.list_charts` | Lista gráficos |
| `metaset.create_dashboard` | Cria dashboard |
| `metaset.list_dashboards` | Lista dashboards |
| `metaset.suggest_analysis` | Sugere análises |
| `metaset.sync_database` | Sincroniza schema |
| `metaset.health` | Verifica status |
---
## 7. DEPLOYMENT
### 7.1 Docker Compose
```yaml
services:
metaset:
build: ./server/bi/metaset
ports:
- "8100:8100"
environment:
- DATABASE_URL=postgresql://arcadia:arcadia123@db:5432/metaset_db
- REDIS_URL=redis://redis:6379/1
labels:
- "arcadia.discovery.enabled=true"
- "arcadia.name=MetaSet BI"
- "arcadia.type=python"
- "arcadia.port=8100"
- "arcadia.capabilities=dashboards,charts,sql_lab,rls,embedding"
```
### 7.2 Inicialização
```bash
# Via docker compose
docker compose up metaset
# Ou diretamente
cd server/bi/metaset
python run.py --host 0.0.0.0 --port 8100 --workers 2
```
---
## 8. HISTÓRICO DE MUDANÇAS
| Data | Versão | Mudança |
|------|--------|---------|
| 2026-03 | 1.0 | Plano inicial de migração Metabase → Superset |
| 2026-04-08 | 2.0 | **Consolidação concluída** - Cliente TypeScript, rotas, documentação |
---
## 9. REFERÊNCIAS
- `/server/metaset/README.md` - Documentação completa do MetaSet
- `/server/bi/metaset-client/index.ts` - Cliente TypeScript
- `/server/bi/metaset-client/routes.ts` - Rotas Express
- [Apache Superset Docs](https://superset.apache.org/docs/)
- [Superset API Reference](https://superset.apache.org/docs/api/)

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