Compare commits
No commits in common. "main" and "PLUS_SUPERSET_NEXT" have entirely different histories.
main
...
PLUS_SUPER
|
|
@ -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...
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
@ -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
|
|
||||||
39
.env.example
|
|
@ -27,11 +27,7 @@ CONTABIL_PYTHON_URL=http://localhost:8003
|
||||||
BI_PYTHON_URL=http://localhost:8004
|
BI_PYTHON_URL=http://localhost:8004
|
||||||
AUTOMATION_PYTHON_URL=http://localhost:8005
|
AUTOMATION_PYTHON_URL=http://localhost:8005
|
||||||
FISCO_PYTHON_URL=http://localhost:8002
|
FISCO_PYTHON_URL=http://localhost:8002
|
||||||
# ── Embeddings / Learning Service ──────────────────────────────────────────────
|
PYTHON_SERVICE_URL=http://localhost:8001
|
||||||
# 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
|
|
||||||
|
|
||||||
# ── IA — OpenAI ───────────────────────────────────────────────────────────────
|
# ── IA — OpenAI ───────────────────────────────────────────────────────────────
|
||||||
# Deixe vazio se usar apenas Ollama (soberania total)
|
# 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
|
# Se Ollama está em outro servidor: OLLAMA_BASE_URL=http://IP_DO_SERVIDOR:11434
|
||||||
OLLAMA_BASE_URL=http://localhost: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)
|
# ── 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=
|
OPENAI_API_KEY=
|
||||||
ANTHROPIC_API_KEY=
|
ANTHROPIC_API_KEY=
|
||||||
GROQ_API_KEY=
|
GROQ_API_KEY=
|
||||||
|
|
@ -67,15 +68,9 @@ PLUS_URL=http://localhost:8080
|
||||||
PLUS_PORT=8080
|
PLUS_PORT=8080
|
||||||
PLUS_API_TOKEN=
|
PLUS_API_TOKEN=
|
||||||
|
|
||||||
# ── Superset (BI avançado — Arcádia Insights) ────────────────────────────────
|
# ── Superset (BI avançado) ────────────────────────────────────────────────────
|
||||||
SUPERSET_HOST=superset
|
SUPERSET_SECRET_KEY=troque-por-string-aleatoria-segura
|
||||||
SUPERSET_PORT=8088
|
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 ─────────────────────────────────────────────────────────────────────
|
||||||
REDIS_URL=redis://localhost:6379
|
REDIS_URL=redis://localhost:6379
|
||||||
|
|
@ -83,20 +78,8 @@ REDIS_URL=redis://localhost:6379
|
||||||
# ── Domínio (produção) ────────────────────────────────────────────────────────
|
# ── Domínio (produção) ────────────────────────────────────────────────────────
|
||||||
DOMAIN=seudominio.com.br
|
DOMAIN=seudominio.com.br
|
||||||
|
|
||||||
# ── ERPNext ────────────────────────────────────────────────────────────────────
|
# ── Integrações externas (opcional) ──────────────────────────────────────────
|
||||||
# Modo 1 — ERPNext em container Docker (perfil erpnext)
|
# ERPNext
|
||||||
# Sobe com: docker compose --profile erpnext up
|
|
||||||
# Proxy ativado automaticamente em DOCKER_MODE=true
|
|
||||||
ERPNEXT_PROXY_ENABLED=false # true para forçar proxy mesmo fora do Docker
|
|
||||||
ERPNEXT_CONTAINER_HOST=erpnext # nome do serviço no docker-compose
|
|
||||||
ERPNEXT_CONTAINER_PORT=8080
|
|
||||||
ERPNEXT_DB_ROOT_PASSWORD=erpnext-root-2026 # senha root do MariaDB
|
|
||||||
ERPNEXT_DB_PASSWORD=frappe2026 # senha do usuário frappe
|
|
||||||
ERPNEXT_ADMIN_PASSWORD=admin2026 # senha do Administrator no site
|
|
||||||
|
|
||||||
# Modo 2 — ERPNext externo (instância existente)
|
|
||||||
# Em Docker: ERPNEXT_URL=http://erpnext:8080 (aponta pro container)
|
|
||||||
# Externo: ERPNEXT_URL=https://meuerpnext.com
|
|
||||||
ERPNEXT_URL=
|
ERPNEXT_URL=
|
||||||
ERPNEXT_API_KEY=
|
ERPNEXT_API_KEY=
|
||||||
ERPNEXT_API_SECRET=
|
ERPNEXT_API_SECRET=
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
node_modules/
|
node_modules/
|
||||||
dist/
|
dist/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.production
|
||||||
|
|
||||||
# Replit specific
|
# Replit specific
|
||||||
.cache/
|
.cache/
|
||||||
|
|
@ -61,12 +64,3 @@ attached_assets/
|
||||||
# Package lock (regenerated on install)
|
# Package lock (regenerated on install)
|
||||||
package-lock.json
|
package-lock.json
|
||||||
.claude/
|
.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.*
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
[submodule "server/modules/openclaw"]
|
|
||||||
path = server/modules/openclaw
|
|
||||||
url = https://github.com/miaoxworld/OpenClawInstaller.git
|
|
||||||
|
|
@ -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]
|
|
||||||
|
Before Width: | Height: | Size: 26 KiB |
|
|
@ -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]
|
|
||||||
|
Before Width: | Height: | Size: 23 KiB |
|
|
@ -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]
|
|
||||||
|
|
@ -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...
|
|
||||||
|
|
@ -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]
|
|
||||||
|
Before Width: | Height: | Size: 23 KiB |
|
|
@ -20,7 +20,7 @@ server/
|
||||||
client/ # 66 páginas React
|
client/ # 66 páginas React
|
||||||
shared/schema.ts # Schema do banco (7317 linhas, Drizzle ORM)
|
shared/schema.ts # Schema do banco (7317 linhas, Drizzle ORM)
|
||||||
docker/
|
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
|
## Arquitetura de IA
|
||||||
|
|
@ -29,8 +29,9 @@ Manus / Agents / Embeddings
|
||||||
│ AI_INTEGRATIONS_OPENAI_BASE_URL
|
│ AI_INTEGRATIONS_OPENAI_BASE_URL
|
||||||
▼
|
▼
|
||||||
LiteLLM :4000 (gateway unificado, loga tudo no banco)
|
LiteLLM :4000 (gateway unificado, loga tudo no banco)
|
||||||
├──► Ollama :11434 (TIER 1 — local, padrão)
|
├──► LLMFit (TIER 1 — fine-tuned, soberano) [slot pronto, comentado]
|
||||||
└──► OpenAI/Anthropic/Groq (TIER 2 — opt-in, só se API key configurada)
|
├──► Ollama :11434 (TIER 2 — local, padrão)
|
||||||
|
└──► OpenAI/Anthropic/Groq (TIER 3 — opt-in, só se API key configurada)
|
||||||
```
|
```
|
||||||
|
|
||||||
**Variáveis chave do Manus:**
|
**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
|
- `PLANO_EVOLUCAO_ARCADIA.md` — roadmap e evolução
|
||||||
- `MAPA_SISTEMA_ARCADIA.md` — mapa do sistema
|
- `MAPA_SISTEMA_ARCADIA.md` — mapa do sistema
|
||||||
|
|
||||||
## Git e Deploy
|
## Branch de desenvolvimento
|
||||||
|
Sempre commitar em: `claude/analyze-project-0mXjP`
|
||||||
### Remote canônico
|
Push: `git push -u origin claude/analyze-project-0mXjP`
|
||||||
- **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
|
|
||||||
```
|
|
||||||
|
|
||||||
## O que está implementado
|
## O que está implementado
|
||||||
- ✅ Manus (agente autônomo, 30+ ferramentas)
|
- ✅ 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
|
- ✅ Docker dev + prod, LiteLLM gateway
|
||||||
|
|
||||||
## O que ainda falta
|
## O que ainda falta
|
||||||
|
- ❌ LLMFit: slot pronto em `litellm-config.yaml`, só habilitar quando disponível
|
||||||
- ❌ Testes automatizados / CI-CD
|
- ❌ Testes automatizados / CI-CD
|
||||||
- ❌ Monitoramento (APM, Sentry, métricas)
|
- ❌ Monitoramento (APM, Sentry, métricas)
|
||||||
- ❌ Multi-tenancy completo
|
- ❌ Multi-tenancy completo
|
||||||
- ❌ Rate limiting em todos os endpoints (parcial)
|
- ❌ Rate limiting em todos os endpoints (parcial)
|
||||||
- ❌ Fine-tuning de modelos locais (dados do Arcádia)
|
|
||||||
|
|
||||||
## Comandos úteis
|
## Comandos úteis
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -121,6 +85,7 @@ npm run build
|
||||||
```
|
```
|
||||||
SESSION_SECRET, SSO_SECRET # gerar strings seguras em prod
|
SESSION_SECRET, SSO_SECRET # gerar strings seguras em prod
|
||||||
AI_INTEGRATIONS_OPENAI_BASE_URL # aponta para LiteLLM
|
AI_INTEGRATIONS_OPENAI_BASE_URL # aponta para LiteLLM
|
||||||
|
LLMFIT_BASE_URL # LLMFit quando disponível
|
||||||
OLLAMA_BASE_URL # Ollama host ou container
|
OLLAMA_BASE_URL # Ollama host ou container
|
||||||
OPENAI_API_KEY # opcional (soberania: deixar vazio)
|
OPENAI_API_KEY # opcional (soberania: deixar vazio)
|
||||||
```
|
```
|
||||||
28
Dockerfile
|
|
@ -1,33 +1,33 @@
|
||||||
|
# ─── Stage 1: Build ───────────────────────────────────────────────────────────
|
||||||
FROM node:20-alpine AS builder
|
FROM node:20-alpine AS builder
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY package.json package-lock.json* ./
|
COPY package*.json ./
|
||||||
RUN NODE_ENV=development npm install --legacy-peer-deps
|
RUN npm ci --ignore-scripts
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
|
# ─── Stage 2: Production ──────────────────────────────────────────────────────
|
||||||
FROM node:20-alpine AS runner
|
FROM node:20-alpine AS runner
|
||||||
|
|
||||||
RUN apk add --no-cache p7zip
|
|
||||||
|
|
||||||
WORKDIR /app
|
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 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
|
EXPOSE 5000
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
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"]
|
CMD ["node", "dist/index.cjs"]
|
||||||
|
|
|
||||||
|
|
@ -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"]
|
|
||||||
|
|
@ -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
|
LiteLLM (porta 4000) — gateway único
|
||||||
│
|
│
|
||||||
├──► Ollama (modelos open source locais) [TIER 1 — padrão]
|
├──► LLMFit (seus modelos fine-tuned) [TIER 1 — prioridade]
|
||||||
└──► OpenAI/Anthropic/Groq (opt-in) [TIER 2 — externo]
|
└──► 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.
|
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
|
## Variáveis de ambiente — resumo completo
|
||||||
|
|
||||||
Configure todas no Coolify antes do deploy:
|
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 em container: http://ollama:11434
|
||||||
OLLAMA_BASE_URL=http://host-gateway: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) ───────────────────
|
# ── Providers externos (deixar vazio para soberania total) ───────────────────
|
||||||
OPENAI_API_KEY=
|
OPENAI_API_KEY=
|
||||||
ANTHROPIC_API_KEY=
|
ANTHROPIC_API_KEY=
|
||||||
|
|
@ -139,7 +202,7 @@ curl http://localhost:4000/v1/chat/completions \
|
||||||
### 2. Testar Manus via interface
|
### 2. Testar Manus via interface
|
||||||
|
|
||||||
Acesse `https://seudominio.com.br` → abra o Manus → envie uma mensagem simples.
|
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
|
### 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.
|
→ O modelo referenciado em `litellm-config.yaml` não foi baixado no Ollama.
|
||||||
→ Execute `ollama pull NOME_DO_MODELO`
|
→ 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**
|
**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)
|
→ 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
|
→ Ou use o IP real da interface de rede: `ip addr show` para descobrir
|
||||||
|
|
@ -46,27 +46,15 @@
|
||||||
|
|
||||||
## Mapa de Portas
|
## Mapa de Portas
|
||||||
|
|
||||||
| Porta | Serviço | Tecnologia | Observação |
|
| Porta | Serviço | Tecnologia |
|
||||||
|-------|---------|-----------|------------|
|
|-------|---------|-----------|
|
||||||
| 5000 | API Principal + Frontend | Express.js + React | Exposta publicamente |
|
| 5000 | API Principal + Frontend | Express.js + React |
|
||||||
| 5001 | **Arcadia Kernel** | Node.js/Express | Dashboard interno (mesmo container) |
|
| 8002 | Motor Fiscal (Fisco) | FastAPI (Python) |
|
||||||
| 8002 | Motor Fiscal (Fisco) | FastAPI (Python) | Via Docker network |
|
| 8003 | Motor Contábil | FastAPI (Python) |
|
||||||
| 8003 | Motor Contábil | FastAPI (Python) | Via Docker network |
|
| 8004 | Motor BI (Insights) | FastAPI (Python) |
|
||||||
| 8004 | Motor BI (Insights) | FastAPI (Python) | Via Docker network |
|
| 8005 | Motor Automação | FastAPI (Python) |
|
||||||
| 8005 | Motor Automação | FastAPI (Python) | Via Docker network |
|
| 8006 | Motor Comunicação | Node.js/Express |
|
||||||
| 8006 | Motor Comunicação | Node.js/Express | Via Docker network |
|
| 8080 | Arcádia Plus (Laravel) | PHP/Laravel |
|
||||||
| 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.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 69 B |
|
Before Width: | Height: | Size: 74 KiB |
|
|
@ -1,5 +1,5 @@
|
||||||
import { Switch, Route, useLocation } from "wouter";
|
import { Switch, Route } from "wouter";
|
||||||
import { lazy, Suspense, useEffect, useState } from "react";
|
import { lazy, Suspense } from "react";
|
||||||
import { queryClient } from "./lib/queryClient";
|
import { queryClient } from "./lib/queryClient";
|
||||||
import { QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { Toaster } from "@/components/ui/toaster";
|
import { Toaster } from "@/components/ui/toaster";
|
||||||
|
|
@ -9,8 +9,6 @@ import { SoeMotorProvider } from "@/contexts/SoeMotorContext";
|
||||||
import { ProtectedRoute } from "@/lib/protected-route";
|
import { ProtectedRoute } from "@/lib/protected-route";
|
||||||
import { CommandPalette } from "@/components/CommandPalette";
|
import { CommandPalette } from "@/components/CommandPalette";
|
||||||
import { KnowledgeCollectorInit } from "@/components/KnowledgeCollectorInit";
|
import { KnowledgeCollectorInit } from "@/components/KnowledgeCollectorInit";
|
||||||
import { OpenClawWidget } from "@/components/openclaw/OpenClawWidget";
|
|
||||||
import { ErrorBoundary } from "@/components/ErrorBoundary";
|
|
||||||
import NotFound from "@/pages/not-found";
|
import NotFound from "@/pages/not-found";
|
||||||
import AuthPage from "@/pages/auth-page";
|
import AuthPage from "@/pages/auth-page";
|
||||||
|
|
||||||
|
|
@ -69,60 +67,14 @@ const XosAutomations = lazy(() => import("@/pages/XosAutomations"));
|
||||||
const XosSites = lazy(() => import("@/pages/XosSites"));
|
const XosSites = lazy(() => import("@/pages/XosSites"));
|
||||||
const XosGovernance = lazy(() => import("@/pages/XosGovernance"));
|
const XosGovernance = lazy(() => import("@/pages/XosGovernance"));
|
||||||
const XosPipeline = lazy(() => import("@/pages/XosPipeline"));
|
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() {
|
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 (
|
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={{ textAlign: "center" }}>
|
||||||
<div style={{ width: 40, height: 40, border: "3px solid #e5e7eb", borderTopColor: "#3b82f6", borderRadius: "50%", animation: "spin 1s linear infinite", margin: "0 auto" }} />
|
<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>
|
<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>
|
</div>
|
||||||
<style>{`@keyframes spin { to { transform: rotate(360deg) } }`}</style>
|
<style>{`@keyframes spin { to { transform: rotate(360deg) } }`}</style>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -131,74 +83,69 @@ function LoadingFallback() {
|
||||||
|
|
||||||
function Router() {
|
function Router() {
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary>
|
<Suspense fallback={<LoadingFallback />}>
|
||||||
<Suspense fallback={<LoadingFallback />}>
|
<Switch>
|
||||||
<Switch>
|
<ProtectedRoute path="/" component={Cockpit} />
|
||||||
<ProtectedRoute path="/" component={Cockpit} />
|
<ProtectedRoute path="/agent" component={Agent} />
|
||||||
<ProtectedRoute path="/agent" component={Agent} />
|
<ProtectedRoute path="/admin" component={Admin} />
|
||||||
<ProtectedRoute path="/admin" component={Admin} />
|
<ProtectedRoute path="/chat" component={Chat} />
|
||||||
<ProtectedRoute path="/chat" component={Chat} />
|
<ProtectedRoute path="/whatsapp" component={WhatsApp} />
|
||||||
<ProtectedRoute path="/whatsapp" component={WhatsApp} />
|
<ProtectedRoute path="/comunicacao" component={XosInbox} />
|
||||||
<ProtectedRoute path="/comunicacao" component={XosInbox} />
|
<ProtectedRoute path="/automations" component={Automations} />
|
||||||
<ProtectedRoute path="/automations" component={Automations} />
|
<ProtectedRoute path="/insights" component={BiWorkspace} />
|
||||||
<ProtectedRoute path="/insights" component={BiWorkspace} />
|
<ProtectedRoute path="/compass" component={ProcessCompass} />
|
||||||
<ProtectedRoute path="/compass" component={ProcessCompass} />
|
<ProtectedRoute path="/crm" component={Crm} />
|
||||||
<ProtectedRoute path="/crm" component={Crm} />
|
<ProtectedRoute path="/production" component={Production} />
|
||||||
<ProtectedRoute path="/production" component={Production} />
|
<ProtectedRoute path="/support" component={Support} />
|
||||||
<ProtectedRoute path="/support" component={Support} />
|
<ProtectedRoute path="/valuation" component={Valuation} />
|
||||||
<ProtectedRoute path="/valuation" component={Valuation} />
|
<ProtectedRoute path="/canvas" component={Canvas} />
|
||||||
<ProtectedRoute path="/canvas" component={Canvas} />
|
<ProtectedRoute path="/ide" component={IDE} />
|
||||||
<ProtectedRoute path="/ide" component={IDE} />
|
<ProtectedRoute path="/scientist" component={Scientist} />
|
||||||
<ProtectedRoute path="/scientist" component={Scientist} />
|
<ProtectedRoute path="/knowledge" component={Knowledge} />
|
||||||
<ProtectedRoute path="/knowledge" component={Knowledge} />
|
<ProtectedRoute path="/central-apis" component={CentralApis} />
|
||||||
<ProtectedRoute path="/central-apis" component={CentralApis} />
|
<ProtectedRoute path="/api-tester" component={ApiTesterPage} />
|
||||||
<ProtectedRoute path="/api-tester" component={ApiTesterPage} />
|
<ProtectedRoute path="/api-hub" component={ApiHub} />
|
||||||
<ProtectedRoute path="/api-hub" component={ApiHub} />
|
<ProtectedRoute path="/fisco" component={Fisco} />
|
||||||
<ProtectedRoute path="/fisco" component={Fisco} />
|
<ProtectedRoute path="/people" component={People} />
|
||||||
<ProtectedRoute path="/people" component={People} />
|
<ProtectedRoute path="/contabil" component={Contabil} />
|
||||||
<ProtectedRoute path="/contabil" component={Contabil} />
|
<ProtectedRoute path="/soe" component={SOE} />
|
||||||
<ProtectedRoute path="/soe" component={SOE} />
|
<ProtectedRoute path="/erp" component={SOE} />
|
||||||
<ProtectedRoute path="/erp" component={SOE} />
|
<ProtectedRoute path="/financeiro" component={Financeiro} />
|
||||||
<ProtectedRoute path="/financeiro" component={Financeiro} />
|
<ProtectedRoute path="/communities" component={Communities} />
|
||||||
<ProtectedRoute path="/communities" component={Communities} />
|
<ProtectedRoute path="/quality" component={QualityModule} />
|
||||||
<ProtectedRoute path="/quality" component={QualityModule} />
|
<ProtectedRoute path="/commercial-env" component={CommercialEnv} />
|
||||||
<ProtectedRoute path="/commercial-env" component={CommercialEnv} />
|
<ProtectedRoute path="/field-ops" component={FieldOperations} />
|
||||||
<ProtectedRoute path="/field-ops" component={FieldOperations} />
|
<ProtectedRoute path="/technical" component={TechnicalModule} />
|
||||||
<ProtectedRoute path="/technical" component={TechnicalModule} />
|
<ProtectedRoute path="/suppliers" component={SuppliersPortal} />
|
||||||
<ProtectedRoute path="/suppliers" component={SuppliersPortal} />
|
<ProtectedRoute path="/nps" component={NPSSurvey} />
|
||||||
<ProtectedRoute path="/nps" component={NPSSurvey} />
|
<ProtectedRoute path="/engineering" component={EngineeringHub} />
|
||||||
<ProtectedRoute path="/engineering" component={EngineeringHub} />
|
<ProtectedRoute path="/development" component={DevelopmentModule} />
|
||||||
<ProtectedRoute path="/development" component={DevelopmentModule} />
|
<ProtectedRoute path="/retail" component={ArcadiaRetail} />
|
||||||
<ProtectedRoute path="/retail" component={ArcadiaRetail} />
|
<ProtectedRoute path="/plus" component={Plus} />
|
||||||
<ProtectedRoute path="/plus" component={() => { const [, nav] = useLocation(); useEffect(() => nav("/soe"), []); return null; }} />
|
<ProtectedRoute path="/super-admin" component={SuperAdmin} />
|
||||||
<ProtectedRoute path="/super-admin" component={SuperAdmin} />
|
<ProtectedRoute path="/marketplace" component={Marketplace} />
|
||||||
<ProtectedRoute path="/marketplace" component={Marketplace} />
|
<ProtectedRoute path="/lms" component={LMS} />
|
||||||
<ProtectedRoute path="/lms" component={LMS} />
|
<ProtectedRoute path="/apps" component={AppCenter} />
|
||||||
<ProtectedRoute path="/apps" component={AppCenter} />
|
<ProtectedRoute path="/xos" component={XosCentral} />
|
||||||
<ProtectedRoute path="/xos" component={XosCentral} />
|
<ProtectedRoute path="/xos/crm" component={XosCrm} />
|
||||||
<ProtectedRoute path="/xos/crm" component={XosCrm} />
|
<ProtectedRoute path="/xos/inbox" component={XosInbox} />
|
||||||
<ProtectedRoute path="/xos/inbox" component={XosInbox} />
|
<ProtectedRoute path="/xos/tickets" component={XosTickets} />
|
||||||
<ProtectedRoute path="/xos/tickets" component={XosTickets} />
|
<ProtectedRoute path="/xos/campaigns" component={XosCampaigns} />
|
||||||
<ProtectedRoute path="/xos/campaigns" component={XosCampaigns} />
|
<ProtectedRoute path="/xos/automations" component={XosAutomations} />
|
||||||
<ProtectedRoute path="/xos/automations" component={XosAutomations} />
|
<ProtectedRoute path="/xos/sites" component={XosSites} />
|
||||||
<ProtectedRoute path="/xos/sites" component={XosSites} />
|
<ProtectedRoute path="/xos/governance" component={XosGovernance} />
|
||||||
<ProtectedRoute path="/xos/governance" component={XosGovernance} />
|
<ProtectedRoute path="/xos/pipeline" component={XosPipeline} />
|
||||||
<ProtectedRoute path="/xos/pipeline" component={XosPipeline} />
|
|
||||||
<ProtectedRoute path="/xos/supervisor" component={XosSupervisor} />
|
|
||||||
<ProtectedRoute path="/xos/reports" component={XosReports} />
|
|
||||||
<ProtectedRoute path="/xos/protocols" component={XosProtocols} />
|
|
||||||
|
|
||||||
<ProtectedRoute path="/doctype-builder" component={DocTypeBuilder} />
|
<ProtectedRoute path="/doctype-builder" component={DocTypeBuilder} />
|
||||||
<ProtectedRoute path="/page-builder" component={PageBuilder} />
|
<ProtectedRoute path="/page-builder" component={PageBuilder} />
|
||||||
<ProtectedRoute path="/migration" component={Migration} />
|
<ProtectedRoute path="/migration" component={Migration} />
|
||||||
<ProtectedRoute path="/dev-center" component={DevCenter} />
|
<ProtectedRoute path="/dev-center" component={DevCenter} />
|
||||||
<ProtectedRoute path="/page/:id" component={WorkspacePage} />
|
<ProtectedRoute path="/page/:id" component={WorkspacePage} />
|
||||||
<ProtectedRoute path="/app/:id" component={AppViewer} />
|
<ProtectedRoute path="/app/:id" component={AppViewer} />
|
||||||
<Route path="/auth" component={AuthPage} />
|
<Route path="/auth" component={AuthPage} />
|
||||||
<Route component={NotFound} />
|
<Route component={NotFound} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</ErrorBoundary>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -211,7 +158,6 @@ function App() {
|
||||||
<KnowledgeCollectorInit />
|
<KnowledgeCollectorInit />
|
||||||
<Toaster />
|
<Toaster />
|
||||||
<CommandPalette />
|
<CommandPalette />
|
||||||
<OpenClawWidget />
|
|
||||||
<Router />
|
<Router />
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</SoeMotorProvider>
|
</SoeMotorProvider>
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,7 @@ function CompactNavigationBar() {
|
||||||
</div>
|
</div>
|
||||||
<span className="hidden md:inline">Insights</span>
|
<span className="hidden md:inline">Insights</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="flex items-center gap-1 hover:bg-muted px-2 py-1.5 rounded cursor-pointer transition-colors flex-shrink-0"
|
className="flex items-center gap-1 hover:bg-muted px-2 py-1.5 rounded cursor-pointer transition-colors flex-shrink-0"
|
||||||
onClick={() => navigateTo("/compass", "Compass")}
|
onClick={() => navigateTo("/compass", "Compass")}
|
||||||
data-testid="bookmark-compass"
|
data-testid="bookmark-compass"
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,131 +0,0 @@
|
||||||
/**
|
|
||||||
* OpenClawWidget.tsx
|
|
||||||
*
|
|
||||||
* Widget flutuante do OpenClaw — aparece quando há 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}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
@ -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],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -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");
|
|
||||||
}
|
|
||||||
|
|
@ -7,7 +7,7 @@ export function ProtectedRoute({
|
||||||
component: Component,
|
component: Component,
|
||||||
}: {
|
}: {
|
||||||
path: string;
|
path: string;
|
||||||
component: React.ComponentType<any>;
|
component: () => React.JSX.Element;
|
||||||
}) {
|
}) {
|
||||||
const { user, isLoading } = useAuth();
|
const { user, isLoading } = useAuth();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -54,10 +54,13 @@ import {
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { CardDescription } from "@/components/ui/card";
|
import { CardDescription } from "@/components/ui/card";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
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 { ResultViewer } from "@/components/ResultViewer";
|
||||||
import DevHistory from "@/components/DevHistory";
|
import DevHistory from "@/components/DevHistory";
|
||||||
import { ChartRenderer } from "@/components/ChartRenderer";
|
|
||||||
import { exportToPDF, exportToWord, exportToText, exportToCSV } from "@/lib/export-utils";
|
|
||||||
|
|
||||||
interface ChartData {
|
interface ChartData {
|
||||||
type: 'bar' | 'line' | 'pie' | 'area';
|
type: 'bar' | 'line' | 'pie' | 'area';
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { BrowserFrame } from "@/components/Browser/BrowserFrame";
|
import { BrowserFrame } from "@/components/Browser/BrowserFrame";
|
||||||
import { useState, useEffect } from "react";
|
import { useState } from "react";
|
||||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
|
@ -49,8 +49,6 @@ import {
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { SupersetDashboard } from "@/components/SupersetDashboard";
|
|
||||||
import { MiroFlowControl } from "@/components/MiroFlowControl";
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
|
@ -83,73 +81,6 @@ import {
|
||||||
Cell,
|
Cell,
|
||||||
} from "recharts";
|
} 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 {
|
interface BiStats {
|
||||||
dataSources: number;
|
dataSources: number;
|
||||||
datasets: 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">
|
<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
|
<Layers className="w-4 h-4 mr-2" /> Staging
|
||||||
</TabsTrigger>
|
</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">
|
<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>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
|
|
@ -2987,12 +2915,33 @@ export default function BiWorkspace() {
|
||||||
<TabsContent value="staging" className="mt-0">
|
<TabsContent value="staging" className="mt-0">
|
||||||
<StagingTab />
|
<StagingTab />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="cientifico" className="mt-0">
|
|
||||||
<MiroFlowControl />
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value="advanced" className="mt-0">
|
<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>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -11,12 +11,12 @@ import { Label } from "@/components/ui/label";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import {
|
import {
|
||||||
Code2, Database, Layout, GitBranch, BarChart3, FileText,
|
Code2, Database, Layout, GitBranch, BarChart3, FileText,
|
||||||
Plus, Settings, Play, Zap, Layers, Puzzle, Palette,
|
Plus, Settings, Play, Zap, Layers, Puzzle, Palette,
|
||||||
Terminal, Box, Workflow, PanelLeft, Table2, FormInput,
|
Terminal, Box, Workflow, PanelLeft, Table2, FormInput,
|
||||||
ListChecks, PieChart, LineChart, Gauge, Bot, Code, Trash2, Save,
|
ListChecks, PieChart, LineChart, Gauge, Bot, Code, Trash2, Save,
|
||||||
Sparkles, Rocket, PenTool, Brain
|
Sparkles, Rocket
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useLocation } from "wouter";
|
import { useLocation } from "wouter";
|
||||||
|
|
||||||
|
|
@ -25,12 +25,8 @@ import PageBuilder from "./PageBuilder";
|
||||||
import WorkflowBuilder from "./WorkflowBuilder";
|
import WorkflowBuilder from "./WorkflowBuilder";
|
||||||
import IDE from "./IDE";
|
import IDE from "./IDE";
|
||||||
import DevAgent from "@/components/lowcode/DevAgent";
|
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 {
|
interface Dashboard {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
@ -254,51 +250,15 @@ export default function DevelopmentModule() {
|
||||||
count: 0,
|
count: 0,
|
||||||
category: "dev"
|
category: "dev"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "agent" as ActiveTool,
|
id: "agent" as ActiveTool,
|
||||||
name: "Manus AI",
|
name: "Manus AI",
|
||||||
description: "Agente autônomo de IA",
|
description: "Agente autônomo de IA",
|
||||||
icon: Bot,
|
icon: Bot,
|
||||||
color: "bg-violet-600",
|
color: "bg-violet-600",
|
||||||
count: 0,
|
count: 0,
|
||||||
category: "dev"
|
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();
|
const [, navigate] = useLocation();
|
||||||
|
|
@ -711,8 +671,8 @@ export default function DevelopmentModule() {
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen flex bg-gray-50">
|
<div className="h-full 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="w-16 bg-gray-900 flex flex-col items-center py-4 gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTool("home")}
|
onClick={() => setActiveTool("home")}
|
||||||
className={`p-3 rounded-lg transition-colors ${activeTool === "home" ? "bg-blue-600" : "hover:bg-gray-700"}`}
|
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>
|
||||||
|
|
||||||
<div className="flex-1 h-full overflow-hidden">
|
<div className="flex-1 overflow-hidden">
|
||||||
{activeTool === "home" && <div className="h-full overflow-y-auto">{renderHome()}</div>}
|
{activeTool === "home" && renderHome()}
|
||||||
{activeTool === "doctypes" && <DocTypeBuilder />}
|
{activeTool === "doctypes" && <DocTypeBuilder />}
|
||||||
{activeTool === "pages" && <PageBuilder />}
|
{activeTool === "pages" && <PageBuilder />}
|
||||||
{activeTool === "workflows" && <WorkflowBuilder />}
|
{activeTool === "workflows" && <WorkflowBuilder />}
|
||||||
|
|
@ -745,10 +705,6 @@ export default function DevelopmentModule() {
|
||||||
{activeTool === "scripts" && renderScriptEditor()}
|
{activeTool === "scripts" && renderScriptEditor()}
|
||||||
{activeTool === "ide" && <IDE />}
|
{activeTool === "ide" && <IDE />}
|
||||||
{activeTool === "agent" && <DevAgent />}
|
{activeTool === "agent" && <DevAgent />}
|
||||||
{activeTool === "openclaw" && <OpenClawPanel />}
|
|
||||||
{activeTool === "automationcenter" && <AutomationCenter />}
|
|
||||||
{activeTool === "designstudio" && <DevCenter />}
|
|
||||||
{activeTool === "miroflow" && <MiroFlowControl />}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Dialog open={showNewDashboardDialog} onOpenChange={setShowNewDashboardDialog}>
|
<Dialog open={showNewDashboardDialog} onOpenChange={setShowNewDashboardDialog}>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
// SOE — Sistema de Operações Empresariais
|
|
||||||
export { default } from "./ERP";
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { BrowserFrame } from "@/components/Browser/BrowserFrame";
|
import { BrowserFrame } from "@/components/Browser/BrowserFrame";
|
||||||
import { MiroFlowControl } from "@/components/MiroFlowControl";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
@ -201,10 +200,6 @@ export default function Scientist() {
|
||||||
<Lightbulb className="w-4 h-4 mr-2" />
|
<Lightbulb className="w-4 h-4 mr-2" />
|
||||||
Sugestões
|
Sugestões
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="scientific" className="data-[state=active]:bg-cyan-500">
|
|
||||||
<Brain className="w-4 h-4 mr-2" />
|
|
||||||
Científico
|
|
||||||
</TabsTrigger>
|
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="knowledge" className="space-y-4">
|
<TabsContent value="knowledge" className="space-y-4">
|
||||||
|
|
@ -832,10 +827,6 @@ export default function Scientist() {
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="scientific" className="space-y-4">
|
|
||||||
<MiroFlowControl />
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { Link } from "wouter";
|
import { Link } from "wouter";
|
||||||
import { BrowserFrame } from "@/components/Browser/BrowserFrame";
|
import { BrowserFrame } from "@/components/Browser/BrowserFrame";
|
||||||
import {
|
import {
|
||||||
Users, Building2, TrendingUp, MessageSquare, Ticket, Zap, LayoutGrid,
|
Users, Building2, TrendingUp, MessageSquare, Ticket, Zap, LayoutGrid,
|
||||||
ChevronRight, PlusCircle, Filter, Search, Bell, Calendar, Target,
|
ChevronRight, PlusCircle, Filter, Search, Bell, Calendar, Target,
|
||||||
BarChart3, DollarSign, Clock, AlertCircle, Phone, Mail, ArrowUpRight,
|
BarChart3, DollarSign, Clock, AlertCircle, Phone, Mail, ArrowUpRight
|
||||||
Activity, Hash, Shield
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||||
|
|
@ -14,9 +13,6 @@ import { Input } from "@/components/ui/input";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
||||||
|
|
||||||
interface XosStats {
|
interface XosStats {
|
||||||
total_contacts: number;
|
total_contacts: number;
|
||||||
|
|
@ -70,48 +66,6 @@ interface Activity {
|
||||||
|
|
||||||
export default function XosCentral() {
|
export default function XosCentral() {
|
||||||
const [activeTab, setActiveTab] = useState("dashboard");
|
const [activeTab, setActiveTab] = useState("dashboard");
|
||||||
const [isNewContactOpen, setIsNewContactOpen] = useState(false);
|
|
||||||
const [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>({
|
const { data: stats } = useQuery<XosStats>({
|
||||||
queryKey: ["/api/xos/stats"],
|
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: "automations", name: "Automações", icon: Zap, href: "/xos/automations", color: "bg-violet-100 text-violet-600", description: "Workflows automáticos" },
|
||||||
{ id: "campaigns", name: "Campanhas", icon: Target, href: "/xos/campaigns", color: "bg-pink-100 text-pink-600", description: "Marketing automation" },
|
{ id: "campaigns", name: "Campanhas", icon: Target, href: "/xos/campaigns", color: "bg-pink-100 text-pink-600", description: "Marketing automation" },
|
||||||
{ id: "sites", name: "Sites", icon: LayoutGrid, href: "/xos/sites", color: "bg-cyan-100 text-cyan-600", description: "Site builder" },
|
{ id: "sites", name: "Sites", icon: LayoutGrid, href: "/xos/sites", color: "bg-cyan-100 text-cyan-600", description: "Site builder" },
|
||||||
{ id: "supervisor", name: "Supervisor", icon: Activity, href: "/xos/supervisor", color: "bg-indigo-100 text-indigo-600", description: "Monitor em tempo real" },
|
|
||||||
{ id: "reports", name: "Relatórios", icon: BarChart3, href: "/xos/reports", color: "bg-emerald-100 text-emerald-600", description: "CSAT, SLA e KPIs" },
|
|
||||||
{ id: "protocols", name: "Protocolos", icon: Hash, href: "/xos/protocols", color: "bg-teal-100 text-teal-600", description: "Rastreamento de atendimentos" },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const getStatusColor = (status: string) => {
|
const getStatusColor = (status: string) => {
|
||||||
|
|
@ -226,9 +177,9 @@ export default function XosCentral() {
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
<Button data-testid="button-new-contact" onClick={() => setIsNewContactOpen(true)}>
|
<Button data-testid="button-new-contact">
|
||||||
<PlusCircle className="h-4 w-4 mr-2" />
|
<PlusCircle className="h-4 w-4 mr-2" />
|
||||||
Novo Contato
|
Novo
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -313,7 +264,7 @@ export default function XosCentral() {
|
||||||
{/* Modules Grid */}
|
{/* Modules Grid */}
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-semibold text-slate-800 mb-4">Módulos XOS</h2>
|
<h2 className="text-lg font-semibold text-slate-800 mb-4">Módulos XOS</h2>
|
||||||
<div className="grid grid-cols-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) => (
|
{modules.map((mod) => (
|
||||||
<Link key={mod.id} href={mod.href}>
|
<Link key={mod.id} href={mod.href}>
|
||||||
<Card className="hover:shadow-lg transition-all cursor-pointer group" data-testid={`card-module-${mod.id}`}>
|
<Card className="hover:shadow-lg transition-all cursor-pointer group" data-testid={`card-module-${mod.id}`}>
|
||||||
|
|
@ -455,7 +406,7 @@ export default function XosCentral() {
|
||||||
<Filter className="h-4 w-4 mr-2" />
|
<Filter className="h-4 w-4 mr-2" />
|
||||||
Filtros
|
Filtros
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="sm" onClick={() => setIsNewContactOpen(true)}>
|
<Button size="sm">
|
||||||
<PlusCircle className="h-4 w-4 mr-2" />
|
<PlusCircle className="h-4 w-4 mr-2" />
|
||||||
Novo Contato
|
Novo Contato
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -527,7 +478,7 @@ export default function XosCentral() {
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<CardTitle>Atividades</CardTitle>
|
<CardTitle>Atividades</CardTitle>
|
||||||
<Button size="sm" onClick={() => setIsNewActivityOpen(true)}>
|
<Button size="sm">
|
||||||
<PlusCircle className="h-4 w-4 mr-2" />
|
<PlusCircle className="h-4 w-4 mr-2" />
|
||||||
Nova Atividade
|
Nova Atividade
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -569,129 +520,6 @@ export default function XosCentral() {
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</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>
|
</BrowserFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import browserIcon from "/favicon.png";
|
import browserIcon from "@assets/arcadia_branding/arcadia_suite_icon.png";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
|
|
||||||
export default function AuthPage() {
|
export default function AuthPage() {
|
||||||
|
|
|
||||||
18
db/index.ts
|
|
@ -8,22 +8,10 @@ if (!process.env.DATABASE_URL) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// CORREÇÃO: Usar Pool em vez de Client único para melhor performance
|
const client = new pg.Client({
|
||||||
// e evitar queries travadas quando conexão falha
|
|
||||||
const pool = new pg.Pool({
|
|
||||||
connectionString: process.env.DATABASE_URL,
|
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
|
client.connect();
|
||||||
pool.on('error', (err) => {
|
|
||||||
console.error('[DB Pool] Erro inesperado:', err.message);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Exportar pool para uso no session store
|
export const db = drizzle({ client, schema });
|
||||||
export { pool };
|
|
||||||
|
|
||||||
export const db = drizzle({ client: pool, schema });
|
|
||||||
|
|
|
||||||
72
deploy.sh
|
|
@ -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 ""
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -25,7 +25,6 @@ services:
|
||||||
retries: 10
|
retries: 10
|
||||||
networks:
|
networks:
|
||||||
- arcadia-internal
|
- arcadia-internal
|
||||||
- coolify
|
|
||||||
|
|
||||||
# ── Redis ────────────────────────────────────────────────────────────────────
|
# ── Redis ────────────────────────────────────────────────────────────────────
|
||||||
redis:
|
redis:
|
||||||
|
|
@ -36,7 +35,6 @@ services:
|
||||||
- redis_data:/data
|
- redis_data:/data
|
||||||
networks:
|
networks:
|
||||||
- arcadia-internal
|
- arcadia-internal
|
||||||
- coolify
|
|
||||||
|
|
||||||
# ── App principal ─────────────────────────────────────────────────────────
|
# ── App principal ─────────────────────────────────────────────────────────
|
||||||
app:
|
app:
|
||||||
|
|
@ -62,26 +60,9 @@ services:
|
||||||
LITELLM_API_KEY: ${LITELLM_API_KEY}
|
LITELLM_API_KEY: ${LITELLM_API_KEY}
|
||||||
OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:-http://ollama:11434}
|
OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:-http://ollama:11434}
|
||||||
# ── Manus Agent — aponta para LiteLLM como gateway unificado ──────────
|
# ── 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_BASE_URL: http://litellm:4000/v1
|
||||||
AI_INTEGRATIONS_OPENAI_API_KEY: ${LITELLM_API_KEY}
|
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:
|
ports:
|
||||||
- "5000:5000"
|
- "5000:5000"
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|
@ -91,113 +72,12 @@ services:
|
||||||
condition: service_started
|
condition: service_started
|
||||||
networks:
|
networks:
|
||||||
- arcadia-internal
|
- arcadia-internal
|
||||||
- coolify
|
- arcadia-public
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.docker.network=coolify"
|
|
||||||
- "traefik.http.routers.arcadia.entrypoints=https"
|
|
||||||
- "traefik.http.routers.arcadia.rule=Host(`${DOMAIN}`)"
|
- "traefik.http.routers.arcadia.rule=Host(`${DOMAIN}`)"
|
||||||
- "traefik.http.routers.arcadia.tls=true"
|
- "traefik.http.routers.arcadia.tls=true"
|
||||||
- "traefik.http.routers.arcadia.tls.certresolver=letsencrypt"
|
- "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 ─────────────────────────────────────────────────────
|
# ── Microserviços Python ─────────────────────────────────────────────────────
|
||||||
contabil:
|
contabil:
|
||||||
|
|
@ -273,9 +153,6 @@ services:
|
||||||
SERVICE_NAME: embeddings
|
SERVICE_NAME: embeddings
|
||||||
SERVICE_PORT: 8001
|
SERVICE_PORT: 8001
|
||||||
DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/${PGDATABASE:-arcadia}
|
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:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
@ -283,7 +160,7 @@ services:
|
||||||
- arcadia-internal
|
- arcadia-internal
|
||||||
|
|
||||||
# ── LiteLLM (gateway unificado de LLM — soberania dos dados) ─────────────────
|
# ── LiteLLM (gateway unificado de LLM — soberania dos dados) ─────────────────
|
||||||
# Roteia: Ollama (local) → externo (opt-in)
|
# Roteia: LLMFit (fine-tuned) → Ollama (local) → externo (opt-in)
|
||||||
litellm:
|
litellm:
|
||||||
image: ghcr.io/berriai/litellm:main-latest
|
image: ghcr.io/berriai/litellm:main-latest
|
||||||
restart: always
|
restart: always
|
||||||
|
|
@ -293,10 +170,12 @@ services:
|
||||||
environment:
|
environment:
|
||||||
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
|
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
|
||||||
LITELLM_MASTER_KEY: ${LITELLM_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
|
# Ollama: se instalado no host use http://host-gateway:11434
|
||||||
# Se usar container Docker, mantém http://ollama:11434
|
# Se usar container Docker, mantém http://ollama:11434
|
||||||
OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:-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)
|
# Providers externos opcionais (soberania: só habilitados se configurados)
|
||||||
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
|
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
|
||||||
GROQ_API_KEY: ${GROQ_API_KEY:-}
|
GROQ_API_KEY: ${GROQ_API_KEY:-}
|
||||||
|
|
@ -305,7 +184,6 @@ services:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
networks:
|
networks:
|
||||||
- arcadia-internal
|
- arcadia-internal
|
||||||
- coolify
|
|
||||||
|
|
||||||
# ── Ollama (LLMs locais — soberania total) ────────────────────────────────────
|
# ── Ollama (LLMs locais — soberania total) ────────────────────────────────────
|
||||||
# OPÇÃO A (padrão): Ollama como container Docker
|
# OPÇÃO A (padrão): Ollama como container Docker
|
||||||
|
|
@ -318,8 +196,10 @@ services:
|
||||||
- ollama_models:/root/.ollama
|
- ollama_models:/root/.ollama
|
||||||
networks:
|
networks:
|
||||||
- arcadia-internal
|
- 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:
|
open-webui:
|
||||||
image: ghcr.io/open-webui/open-webui:main
|
image: ghcr.io/open-webui/open-webui:main
|
||||||
restart: always
|
restart: always
|
||||||
|
|
@ -335,7 +215,7 @@ services:
|
||||||
- litellm
|
- litellm
|
||||||
networks:
|
networks:
|
||||||
- arcadia-internal
|
- arcadia-internal
|
||||||
- coolify
|
- arcadia-public
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.http.routers.webui.rule=Host(`ai.${DOMAIN}`)"
|
- "traefik.http.routers.webui.rule=Host(`ai.${DOMAIN}`)"
|
||||||
|
|
@ -344,88 +224,14 @@ services:
|
||||||
- "traefik.http.services.webui.loadbalancer.server.port=8080"
|
- "traefik.http.services.webui.loadbalancer.server.port=8080"
|
||||||
profiles: [ai]
|
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:
|
networks:
|
||||||
arcadia-internal:
|
arcadia-internal:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
coolify:
|
arcadia-public:
|
||||||
external: true
|
driver: bridge
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
pgdata:
|
pgdata:
|
||||||
redis_data:
|
redis_data:
|
||||||
ollama_models:
|
ollama_models:
|
||||||
open_webui_data:
|
open_webui_data:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
plus_db:
|
|
||||||
plus_storage:
|
|
||||||
superset_home:
|
|
||||||
erpnext_db:
|
|
||||||
erpnext_sites:
|
|
||||||
erpnext_logs:
|
|
||||||
|
|
|
||||||
|
|
@ -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:
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -29,6 +29,8 @@ services:
|
||||||
redis:
|
redis:
|
||||||
image: redis:7-alpine
|
image: redis:7-alpine
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "redis-cli", "ping"]
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
|
|
@ -59,23 +61,12 @@ services:
|
||||||
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
|
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
|
||||||
LITELLM_BASE_URL: http://litellm:4000
|
LITELLM_BASE_URL: http://litellm:4000
|
||||||
LITELLM_API_KEY: ${LITELLM_API_KEY:-arcadia-internal}
|
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:
|
ports:
|
||||||
- "5000:5000"
|
- "5000:5000"
|
||||||
- "5001:5001" # Kernel Arcadia (Service Discovery)
|
|
||||||
volumes:
|
volumes:
|
||||||
- .:/app
|
- .:/app
|
||||||
- /app/node_modules
|
- /app/node_modules
|
||||||
- /app/dist
|
- /app/dist
|
||||||
networks:
|
|
||||||
- arcadia
|
|
||||||
- coolify
|
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
@ -92,16 +83,6 @@ services:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile.python
|
dockerfile: Dockerfile.python
|
||||||
restart: unless-stopped
|
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:
|
environment:
|
||||||
SERVICE_NAME: contabil
|
SERVICE_NAME: contabil
|
||||||
SERVICE_PORT: 8003
|
SERVICE_PORT: 8003
|
||||||
|
|
@ -119,17 +100,6 @@ services:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile.python
|
dockerfile: Dockerfile.python
|
||||||
restart: unless-stopped
|
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:
|
environment:
|
||||||
SERVICE_NAME: bi
|
SERVICE_NAME: bi
|
||||||
SERVICE_PORT: 8004
|
SERVICE_PORT: 8004
|
||||||
|
|
@ -147,17 +117,6 @@ services:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile.python
|
dockerfile: Dockerfile.python
|
||||||
restart: unless-stopped
|
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:
|
environment:
|
||||||
SERVICE_NAME: automation
|
SERVICE_NAME: automation
|
||||||
SERVICE_PORT: 8005
|
SERVICE_PORT: 8005
|
||||||
|
|
@ -175,17 +134,6 @@ services:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile.python
|
dockerfile: Dockerfile.python
|
||||||
restart: unless-stopped
|
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:
|
environment:
|
||||||
SERVICE_NAME: fisco
|
SERVICE_NAME: fisco
|
||||||
SERVICE_PORT: 8002
|
SERVICE_PORT: 8002
|
||||||
|
|
@ -203,17 +151,6 @@ services:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile.python
|
dockerfile: Dockerfile.python
|
||||||
restart: unless-stopped
|
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:
|
environment:
|
||||||
SERVICE_NAME: embeddings
|
SERVICE_NAME: embeddings
|
||||||
SERVICE_PORT: 8001
|
SERVICE_PORT: 8001
|
||||||
|
|
@ -224,145 +161,46 @@ services:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
||||||
# ── MetaSet BI (Apache Superset Fork) ────────────────────────────────────────
|
# ── Apache Superset (BI avançado) ────────────────────────────────────────────
|
||||||
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) ─────────────────────
|
|
||||||
superset:
|
superset:
|
||||||
image: apache/superset:4.1.0
|
image: apache/superset:4.1.0
|
||||||
restart: unless-stopped
|
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:
|
environment:
|
||||||
SUPERSET_SECRET_KEY: ${SUPERSET_SECRET_KEY:-superset-secret-change-in-prod}
|
SUPERSET_SECRET_KEY: ${SUPERSET_SECRET_KEY:-superset-secret-change-in-prod}
|
||||||
DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD:-arcadia123}@db:5432/arcadia_superset
|
DATABASE_URL: postgresql://arcadia: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
|
|
||||||
ports:
|
ports:
|
||||||
- "8088:8088"
|
- "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:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
command: ["/bin/bash", "/app/docker/init.sh"]
|
profiles: [bi]
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:8088/health"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 5
|
|
||||||
start_period: 60s
|
|
||||||
networks:
|
|
||||||
- arcadia
|
|
||||||
|
|
||||||
# ─────────────── PERFIL: ai (Soberania de IA) ────────────────────────────────
|
# ─────────────── 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 (interface para devs/consultores) ─────────────────────────────
|
||||||
open-webui:
|
open-webui:
|
||||||
image: ghcr.io/open-webui/open-webui:main
|
image: ghcr.io/open-webui/open-webui:main
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
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}
|
WEBUI_SECRET_KEY: ${WEBUI_SECRET_KEY:-webui-secret-change-in-prod}
|
||||||
DEFAULT_MODELS: llama3.3,qwen2.5-coder
|
DEFAULT_MODELS: llama3.3,qwen2.5-coder
|
||||||
ENABLE_RAG_WEB_SEARCH: "true"
|
ENABLE_RAG_WEB_SEARCH: "true"
|
||||||
|
|
@ -370,6 +208,8 @@ services:
|
||||||
- "3001:8080"
|
- "3001:8080"
|
||||||
volumes:
|
volumes:
|
||||||
- open_webui_data:/app/backend/data
|
- open_webui_data:/app/backend/data
|
||||||
|
depends_on:
|
||||||
|
- ollama
|
||||||
profiles: [ai]
|
profiles: [ai]
|
||||||
|
|
||||||
# ── LiteLLM (proxy unificado de LLMs) ───────────────────────────────────────
|
# ── LiteLLM (proxy unificado de LLMs) ───────────────────────────────────────
|
||||||
|
|
@ -381,103 +221,13 @@ services:
|
||||||
command: ["--config", "/app/config.yaml", "--port", "4000", "--detailed_debug"]
|
command: ["--config", "/app/config.yaml", "--port", "4000", "--detailed_debug"]
|
||||||
environment:
|
environment:
|
||||||
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
|
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}
|
LITELLM_MASTER_KEY: ${LITELLM_API_KEY:-arcadia-internal}
|
||||||
ports:
|
ports:
|
||||||
- "4000:4000"
|
- "4000:4000"
|
||||||
profiles: [ai]
|
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:
|
volumes:
|
||||||
pgdata:
|
pgdata:
|
||||||
|
ollama_models:
|
||||||
open_webui_data:
|
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
|
|
||||||
|
|
|
||||||
|
|
@ -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}"
|
|
||||||
|
|
@ -3,18 +3,45 @@
|
||||||
#
|
#
|
||||||
# ESTRATÉGIA DE SOBERANIA DOS DADOS:
|
# ESTRATÉGIA DE SOBERANIA DOS DADOS:
|
||||||
# ┌─────────────────────────────────────────────────────────────────────────┐
|
# ┌─────────────────────────────────────────────────────────────────────────┐
|
||||||
# │ TIER 1 (soberania total): Ollama — modelos open source no servidor │
|
# │ TIER 1 (soberania total): LLMFit — modelos fine-tuned locais │
|
||||||
# │ TIER 2 (opt-in): Providers externos — só com configuração explícita │
|
# │ 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.
|
# O Manus, Autonomous Agents e todos os serviços chamam APENAS este proxy.
|
||||||
# Nunca chamam APIs externas diretamente.
|
# Nunca chamam APIs externas diretamente.
|
||||||
|
|
||||||
model_list:
|
model_list:
|
||||||
|
|
||||||
# ── TIER 1: Ollama (LLMs locais — soberania total) ───────────────────────────
|
# ── TIER 1: LLMFit (modelos fine-tuned locais — máxima soberania) ────────────
|
||||||
- model_name: llama3.2
|
# 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:
|
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
|
api_base: os.environ/OLLAMA_BASE_URL
|
||||||
|
|
||||||
- model_name: nomic-embed-text
|
- model_name: nomic-embed-text
|
||||||
|
|
@ -22,13 +49,7 @@ model_list:
|
||||||
model: ollama/nomic-embed-text
|
model: ollama/nomic-embed-text
|
||||||
api_base: os.environ/OLLAMA_BASE_URL
|
api_base: os.environ/OLLAMA_BASE_URL
|
||||||
|
|
||||||
# Alias OpenAI-compatible para embedding (Manus usa este nome por padrão)
|
# ── TIER 3: OpenAI (opt-in — só ativo se OPENAI_API_KEY configurado) ─────────
|
||||||
- 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) ─────────
|
|
||||||
- model_name: gpt-4o
|
- model_name: gpt-4o
|
||||||
litellm_params:
|
litellm_params:
|
||||||
model: openai/gpt-4o
|
model: openai/gpt-4o
|
||||||
|
|
@ -39,35 +60,42 @@ model_list:
|
||||||
model: openai/gpt-4o-mini
|
model: openai/gpt-4o-mini
|
||||||
api_key: os.environ/OPENAI_API_KEY
|
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
|
# - model_name: claude-sonnet
|
||||||
# litellm_params:
|
# litellm_params:
|
||||||
# model: anthropic/claude-sonnet-4-6
|
# model: anthropic/claude-sonnet-4-6
|
||||||
# api_key: os.environ/ANTHROPIC_API_KEY
|
# 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
|
# - model_name: groq-llama
|
||||||
# litellm_params:
|
# litellm_params:
|
||||||
# model: groq/llama-3.3-70b-versatile
|
# model: groq/llama-3.3-70b-versatile
|
||||||
# api_key: os.environ/GROQ_API_KEY
|
# api_key: os.environ/GROQ_API_KEY
|
||||||
|
|
||||||
# ── Modelo padrão do Arcádia (Manus usa este) ─────────────────────────────────
|
# ── Modelo padrão do Arcádia (Manus usa este) ─────────────────────────────────
|
||||||
|
# Prioridade: LLMFit → OpenAI (se configurado) → Ollama (sempre disponível)
|
||||||
|
# Para soberania total: remova o fallback para gpt-4o-mini
|
||||||
- model_name: arcadia-default
|
- model_name: arcadia-default
|
||||||
litellm_params:
|
litellm_params:
|
||||||
model: ollama/llama3.2:3b
|
model: ollama/llama3.3
|
||||||
api_base: os.environ/OLLAMA_BASE_URL
|
api_base: os.environ/OLLAMA_BASE_URL
|
||||||
|
model_info:
|
||||||
|
# fallbacks: ["gpt-4o-mini"] # descomente para habilitar fallback externo
|
||||||
|
|
||||||
router_settings:
|
router_settings:
|
||||||
routing_strategy: least-busy
|
routing_strategy: least-busy
|
||||||
fallbacks:
|
fallbacks:
|
||||||
- {"gpt-4o": ["llama3.2"]}
|
- {"gpt-4o": ["llama3.3"]}
|
||||||
- {"gpt-4o-mini": ["llama3.2"]}
|
- {"gpt-4o-mini": ["llama3.3"]}
|
||||||
- {"arcadia-default": ["llama3.2"]}
|
- {"arcadia-default": ["llama3.3"]}
|
||||||
|
|
||||||
litellm_settings:
|
litellm_settings:
|
||||||
drop_params: true
|
drop_params: true
|
||||||
request_timeout: 120
|
request_timeout: 120
|
||||||
set_verbose: false
|
set_verbose: false
|
||||||
|
# Loga todas as chamadas no banco — essencial para auditoria e soberania
|
||||||
|
success_callback: ["langfuse"]
|
||||||
|
failure_callback: ["langfuse"]
|
||||||
|
|
||||||
general_settings:
|
general_settings:
|
||||||
master_key: os.environ/LITELLM_MASTER_KEY
|
master_key: os.environ/LITELLM_MASTER_KEY
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 3.9 MiB |
|
Before Width: | Height: | Size: 4.1 MiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 39 KiB |
|
|
@ -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()"
|
|
||||||
|
|
@ -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"
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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)
|
|
||||||
203
docs/AGENTS.md
|
|
@ -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>
|
|
||||||
```
|
|
||||||
|
|
@ -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.*
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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 10–20 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 (~10–20 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
|
|
||||||
```
|
|
||||||
|
|
@ -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`.
|
|
||||||
|
|
@ -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*
|
|
||||||
|
|
@ -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?**
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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 │ │
|
|
||||||
│ └───────────────────────────────────────────────────────────┘ │
|
|
||||||
└─────────────────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
@ -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 │ │
|
|
||||||
│ └─────────────────────────────────────────────────────────────┘ │
|
|
||||||
└─────────────────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
@ -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_*) │ │
|
|
||||||
│ └───────────────────────────────────────────────────────────┘ │
|
|
||||||
└─────────────────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
@ -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)**
|
|
||||||
|
|
@ -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` (L3367–3517) | `customers`, `suppliers`, `products`, `sales_orders`, `sales_order_items`, `purchase_orders`, `purchase_order_items` |
|
|
||||||
| `shared/schema.ts` (L5082–5617) | 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, L3303–3517, L5082–5617, 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 │ │
|
|
||||||
│ └─────────────────────────────────────────────────────────────┘ │
|
|
||||||
└─────────────────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
@ -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 │ │
|
|
||||||
│ └─────────────────────────────────────────────────────────────┘ │
|
|
||||||
└─────────────────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
@ -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/)
|
|
||||||