Compare commits
10 Commits
9322bf570f
...
9449b3614c
| Author | SHA1 | Date |
|---|---|---|
|
|
9449b3614c | |
|
|
5bf2254bc1 | |
|
|
5816c14d00 | |
|
|
76c1dd7090 | |
|
|
0c006da8a5 | |
|
|
1601ad0c12 | |
|
|
1ab50d456b | |
|
|
b80bd22c83 | |
|
|
8ed4863685 | |
|
|
7c1ca8efce |
23
.env.example
23
.env.example
|
|
@ -33,14 +33,33 @@ PYTHON_SERVICE_URL=http://localhost:8001
|
|||
# Deixe vazio se usar apenas Ollama (soberania total)
|
||||
OPENAI_API_KEY=
|
||||
|
||||
# ── IA — LiteLLM (proxy unificado) ───────────────────────────────────────────
|
||||
# ── IA — LiteLLM (gateway unificado — ÚNICA porta de entrada para LLMs) ──────
|
||||
# Em Docker: http://litellm:4000 | Em dev local: http://localhost:4000
|
||||
LITELLM_BASE_URL=http://localhost:4000
|
||||
LITELLM_API_KEY=arcadia-internal
|
||||
|
||||
# ── IA — Ollama (LLMs locais) ─────────────────────────────────────────────────
|
||||
# ── IA — Manus Agent (aponta para LiteLLM como gateway) ──────────────────────
|
||||
# Em Docker: AI_INTEGRATIONS_OPENAI_BASE_URL=http://litellm:4000/v1
|
||||
# Em dev local: http://localhost:4000/v1
|
||||
AI_INTEGRATIONS_OPENAI_BASE_URL=http://localhost:4000/v1
|
||||
AI_INTEGRATIONS_OPENAI_API_KEY=arcadia-internal
|
||||
|
||||
# ── IA — Ollama (LLMs locais — soberania total) ───────────────────────────────
|
||||
# Se Ollama está no host (fora do Docker): OLLAMA_BASE_URL=http://localhost:11434
|
||||
# Se Ollama está em outro servidor: OLLAMA_BASE_URL=http://IP_DO_SERVIDOR:11434
|
||||
OLLAMA_BASE_URL=http://localhost:11434
|
||||
|
||||
# ── IA — LLMFit (modelos fine-tuned locais — habilitar quando disponível) ─────
|
||||
# LLMFit turbocharge: modelos treinados com dados do seu negócio
|
||||
# Deixe vazio para desabilitar (LiteLLM cai para Ollama automaticamente)
|
||||
LLMFIT_BASE_URL=
|
||||
|
||||
# ── IA — Providers externos (opt-in — soberania: dados não saem sem configurar)
|
||||
# Deixe vazio para operação 100% soberana (apenas Ollama + LLMFit)
|
||||
OPENAI_API_KEY=
|
||||
ANTHROPIC_API_KEY=
|
||||
GROQ_API_KEY=
|
||||
|
||||
# ── Open WebUI ────────────────────────────────────────────────────────────────
|
||||
WEBUI_SECRET_KEY=troque-por-string-aleatoria-segura
|
||||
|
||||
|
|
|
|||
|
|
@ -63,3 +63,4 @@ attached_assets/
|
|||
|
||||
# Package lock (regenerated on install)
|
||||
package-lock.json
|
||||
.claude/
|
||||
|
|
|
|||
|
|
@ -0,0 +1,438 @@
|
|||
# Arcádia Suite — Auditoria Completa e Plano de Execução
|
||||
**Data:** 2026-03-13 | **Versão:** 1.0 | **Auditores:** Claude Code (4 agentes paralelos)
|
||||
|
||||
---
|
||||
|
||||
## 1. NÚMEROS DO SISTEMA
|
||||
|
||||
| Dimensão | Métrica |
|
||||
|----------|---------|
|
||||
| Tabelas no schema | **337** |
|
||||
| Migration formal | **1 arquivo (4.602 linhas) — cobre todas as tabelas** |
|
||||
| Tabelas COM tenantId | **~147 (44%)** |
|
||||
| Tabelas SEM tenantId | **~190 (56%) — risco de isolamento** |
|
||||
| Tabelas sem timestamps | **~40** |
|
||||
| Tabelas sem insert schema | **36 (11%)** |
|
||||
| Módulos backend auditados | **25 módulos / 400+ endpoints** |
|
||||
| Páginas frontend auditadas | **64 páginas** |
|
||||
| Páginas reais e funcionais | **48 (75%)** |
|
||||
| Páginas parciais | **12 (19%)** |
|
||||
| Páginas placeholder | **4 (6%)** |
|
||||
| Ferramentas Manus definidas | **79** |
|
||||
| Credenciais hardcoded | **3 locais** |
|
||||
| Rotas sem autenticação (4 módulos) | **102+ endpoints expostos** |
|
||||
| Serviços Python com CORS aberto | **6 de 6** |
|
||||
|
||||
---
|
||||
|
||||
## 2. SCORE POR DIMENSÃO
|
||||
|
||||
| Dimensão | Score | Status |
|
||||
|----------|-------|--------|
|
||||
| Funcionalidade | **7/10** | 75% real, 25% parcial/placeholder |
|
||||
| Segurança | **3/10** | CORS aberto, auth ausente, credentials hardcoded |
|
||||
| Multi-tenancy | **4/10** | 56% das tabelas sem isolamento |
|
||||
| Inteligência (IA) | **5/10** | Manus funciona, embeddings vazios, ciclo quebrado |
|
||||
| Integrações ERP | **4/10** | 25 conectores definidos, zero chamadas reais |
|
||||
| Infraestrutura | **2/10** | No Replit, sem containers, sem CI/CD |
|
||||
| Qualidade de código | **5/10** | Sem testes, sem logging estruturado, sem rate limit |
|
||||
| Maturidade de produto | **6/10** | Boa visão, execução inconsistente entre módulos |
|
||||
|
||||
**Score geral: 4.5/10 — Excelente para MVP. Precisa de fundação antes de escalar.**
|
||||
|
||||
---
|
||||
|
||||
## 3. ACHADOS CRÍTICOS (🔴 Resolver antes de qualquer deploy público)
|
||||
|
||||
### SEC-01 — XOS: 100% das rotas sem autenticação e sem tenantId
|
||||
```
|
||||
server/xos/routes.ts — 40+ endpoints:
|
||||
GET /api/xos/contacts, /companies, /deals, /tickets, /conversations
|
||||
→ Qualquer request não autenticado lista dados de qualquer tenant
|
||||
```
|
||||
**Risco:** Vazamento total de dados de CRM, tickets e conversas entre clientes.
|
||||
|
||||
### SEC-02 — LMS: 8+ rotas sem autenticação
|
||||
```
|
||||
server/lms/routes.ts:
|
||||
GET /api/lms/courses → público (todos os tenants)
|
||||
POST /api/lms/courses → qualquer pessoa cria cursos
|
||||
POST /api/lms/:id/enroll → matrícula sem login
|
||||
```
|
||||
|
||||
### SEC-03 — Quality: 50+ rotas sem autenticação
|
||||
```
|
||||
server/quality/routes.ts — amostras, laudos, RNCs, documentos, treinamentos → todos públicos
|
||||
```
|
||||
|
||||
### SEC-04 — Credenciais hardcoded em produção
|
||||
```
|
||||
server/metaset/routes.ts:7-8:
|
||||
ADMIN_EMAIL = "admin@arcadia.app" ← aparece em logs
|
||||
ADMIN_PASSWORD = "Arcadia2026!BI" ← exposição crítica
|
||||
|
||||
server/auth.ts:34:
|
||||
SESSION_SECRET = "arcadia-browser-secret-key-2024" ← fallback fraco
|
||||
```
|
||||
|
||||
### SEC-05 — CORS aberto com credentials em TODOS os serviços Python
|
||||
```
|
||||
server/python/{automation,bi,contabil,fisco,people,bi_analysis}_service.py:
|
||||
allow_origins=["*"] + allow_credentials=True
|
||||
|
||||
→ Viola spec CORS (Chrome/Firefox rejeitam e o sistema quebra em prod)
|
||||
→ Qualquer site pode fazer requisições autenticadas se bypassed
|
||||
```
|
||||
|
||||
### SEC-06 — `/api/tenants` exposto sem autenticação
|
||||
```
|
||||
server/routes.ts:139 — GET /api/tenants → PUBLIC
|
||||
→ Qualquer pessoa enumera todos os clientes do Arcádia
|
||||
```
|
||||
|
||||
### SEC-07 — Manus com ferramentas perigosas sem sandboxing
|
||||
```
|
||||
server/manus/tools.ts — 79 ferramentas, incluindo:
|
||||
shell → qualquer comando no servidor
|
||||
write_file → escreve qualquer arquivo (incluindo .env)
|
||||
read_file → lê qualquer arquivo
|
||||
python_execute → código Python arbitrário
|
||||
|
||||
BLOCKED_COMMANDS: rm, sudo, wget (bom)
|
||||
ALLOWED_COMMANDS: npm, git, node ← ainda podem executar código arbitrário
|
||||
```
|
||||
|
||||
### SEC-08 — Zero timeout em chamadas externas críticas
|
||||
```
|
||||
manus/service.ts → OpenAI sem timeout → hang indefinido
|
||||
crm/frappe-service.ts:45 → fetch sem timeout → hang indefinido
|
||||
→ Cenário: OpenAI lento → requests acumulam → servidor trava
|
||||
```
|
||||
|
||||
### SEC-09 — PipelineOrchestrator com risco de loop infinito
|
||||
```
|
||||
server/blackboard/PipelineOrchestrator.ts:171:
|
||||
setInterval(async () => { ... }, 2000ms)
|
||||
→ operação async sem guard → múltiplas execuções simultâneas
|
||||
→ agentes tentando processar a mesma task em loop
|
||||
```
|
||||
|
||||
### SEC-10 — 190 tabelas sem isolamento multi-tenant
|
||||
**Tabelas críticas:** `knowledge_base`, `conversations`, `chatThreads`, `chatMessages`,
|
||||
`whatsappContacts`, `whatsappMessages`, `whatsappSessions`, `whatsappTickets`,
|
||||
`manusRuns`, `workspacePages`, `pageBlocks`, `activityFeed`
|
||||
|
||||
---
|
||||
|
||||
## 4. ACHADOS DE ALTO IMPACTO (🟠 Funcionalidade core quebrada)
|
||||
|
||||
### FUNC-01 — Knowledge Graph: embeddings vazios — ciclo de aprendizado quebrado
|
||||
O campo `embedding` em `graph_nodes` existe mas nunca é preenchido. A busca semântica do Manus retorna vazio. O sistema não aprende de nenhuma interação.
|
||||
|
||||
### FUNC-02 — AppCenter e Marketplace não se conversam
|
||||
48 apps hardcoded no AppCenter. Marketplace tem billing e subscriptions funcionando. Nenhuma relação entre os dois — qualquer tenant vê todos os 58 apps independente do plano.
|
||||
|
||||
### FUNC-03 — 4 páginas placeholder das mais estratégicas
|
||||
```
|
||||
CentralApis.tsx → PLACEHOLDER (central de integrações)
|
||||
ApiHub.tsx → PLACEHOLDER (hub de APIs)
|
||||
Agent.tsx → PLACEHOLDER (página do agente principal)
|
||||
ArcadiaNext.tsx → PLACEHOLDER
|
||||
```
|
||||
|
||||
### FUNC-04 — 25 conectores ERP definidos, zero fazem chamadas reais
|
||||
A Central de API tem UI completa com logs e métricas. Todos os dados são mockados. TOTVS, SAP, Omie, PIX — nenhum conecta de verdade.
|
||||
|
||||
### FUNC-05 — Python services não suportam uvicorn module-style (Docker)
|
||||
`docker/python-entrypoint.sh` chama `python -m uvicorn server.python.X:app`, mas os arquivos usam `if __name__ == "__main__": uvicorn.run(app)`. Containers Python não sobem.
|
||||
|
||||
### FUNC-06 — Process Compass sem inteligência AI
|
||||
PDCA, SWOT, Canvas modelados corretamente. Zero chamadas ao Manus. 100% manual.
|
||||
|
||||
### FUNC-07 — WhatsApp auto-reply só em memória
|
||||
```
|
||||
server/whatsapp/service.ts:50, 60-70
|
||||
autoReplyConfig: Map (in-memory)
|
||||
→ Restart = perda total de configuração sem aviso ao cliente
|
||||
```
|
||||
|
||||
### FUNC-08 — Dois sistemas de comunicação duplicados e divergentes
|
||||
```
|
||||
Legacy (sem tenantId): whatsapp_*, chat_*, conversations, messages
|
||||
Moderno (com tenantId): comm_*, xosConversations, crmThreads
|
||||
→ Rota de dados inconsistente, manutenção dupla
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. ACHADOS DE MÉDIO IMPACTO (🟡 Qualidade e maturidade)
|
||||
|
||||
### QUAL-01 — Zero testes automatizados em todo o repositório
|
||||
### QUAL-02 — Sem paginação real (límites hardcoded: `LIMIT 100`)
|
||||
### QUAL-03 — Sem error boundaries globais no React
|
||||
### QUAL-04 — Sem rate limiting nas rotas da API
|
||||
### QUAL-05 — Logging não estruturado (só console.log/console.error)
|
||||
### QUAL-06 — 36 tabelas sem insert schema (11% do schema)
|
||||
### QUAL-07 — 40+ tabelas sem timestamps createdAt/updatedAt
|
||||
### QUAL-08 — Tokens WhatsApp no filesystem sem criptografia
|
||||
### QUAL-09 — Connection pooling ausente nos serviços Python (nova conexão por request)
|
||||
### QUAL-10 — Governance com endpoints públicos sem decisão documentada
|
||||
|
||||
---
|
||||
|
||||
## 6. ACHADOS DE ORGANIZAÇÃO (🔵 Arquitetura e estrutura)
|
||||
|
||||
### ARQ-01 — ERP integrations em 4 lugares diferentes
|
||||
```
|
||||
server/erpnext/ → adapter ERPNext
|
||||
server/crm/frappe-service.ts → sync Frappe
|
||||
server/api-central/ → conectores REST
|
||||
server/migration/ → importação one-time
|
||||
→ Deveriam estar: server/integrations/erp/
|
||||
```
|
||||
|
||||
### ARQ-02 — Migration module escondido como tool técnica
|
||||
Estratégico para onboarding de clientes mas sem fluxo guiado para consultores.
|
||||
|
||||
### ARQ-03 — Metabase e Superset sem estratégia definida
|
||||
Dois sistemas de BI sem critério de quando usar cada um.
|
||||
|
||||
### ARQ-04 — IDE.tsx é só um wrapper `<Suspense>`
|
||||
Nenhuma integração com backend de projetos ou contexto de desenvolvimento.
|
||||
|
||||
### ARQ-05 — Boolean/integer/varchar misturados para campos de status
|
||||
Inconsistência: `isActive` (boolean) vs `is_active` (integer 0/1) vs `status` (varchar).
|
||||
|
||||
---
|
||||
|
||||
## 7. STATUS POR MÓDULO
|
||||
|
||||
### Backend (25 módulos auditados)
|
||||
|
||||
| Módulo | Rotas | Auth | Multi-tenant | Status |
|
||||
|--------|-------|------|--------------|--------|
|
||||
| Automations | CRUD completo | ✅ | ✅ | ✅ Completo |
|
||||
| BI | 30+ endpoints | ✅ | ✅ | ✅ Completo |
|
||||
| Blackboard | Pipeline completo | ✅ | ✅ | ✅ Completo |
|
||||
| Communities | 15+ endpoints | ✅ | ✅ | ✅ Completo |
|
||||
| CRM | CRUD completo | ✅ | ✅ | ✅ Completo |
|
||||
| DevAgent/IDE | File+code ops | ✅ | N/A | ✅ Completo |
|
||||
| Engine Room | Health+control | ✅ | N/A | ✅ Completo |
|
||||
| Financeiro | 40+ endpoints | ✅ | ✅ | ✅ Completo |
|
||||
| Governance | 20+ endpoints | ⚠️ alguns públicos | ✅ | ⚠️ Revisar |
|
||||
| Graph (NOVO) | CRUD + search | ✅ | ✅ | ✅ Adicionado |
|
||||
| LMS | 20+ endpoints | ❌ 8+ sem auth | ⚠️ | 🔴 Corrigir |
|
||||
| Lowcode | 30+ endpoints | ⚠️ intencional? | ✅ | ⚠️ Revisar |
|
||||
| Marketplace | CRUD subs | ⚠️ público | N/A | ⚠️ Revisar |
|
||||
| MetaSet | 12 endpoints | ⚠️ credentials | ✅ | 🔴 Corrigir |
|
||||
| Migration | 12 endpoints | ✅ | ✅ | ✅ Completo |
|
||||
| Para | 30+ endpoints | ✅ | ✅ | ✅ Completo |
|
||||
| People | CRUD+proxy | ✅ | ✅ | ✅ Completo |
|
||||
| Production | 60+ endpoints | ✅ | ✅ | ✅ Completo |
|
||||
| Quality | 50+ endpoints | ❌ maioria sem auth | ⚠️ | 🔴 Corrigir |
|
||||
| Retail | CRUD completo | ✅ | ✅ | ✅ Completo |
|
||||
| Support | 25+ endpoints | ✅ | ✅ | ✅ Completo |
|
||||
| Valuation | 25+ endpoints | ✅ | ✅ | ✅ Completo |
|
||||
| XOS | 40+ endpoints | ❌ ZERO auth | ❌ sem tenant | 🔴 Crítico |
|
||||
| ERPNext | 10 endpoints | ✅ | N/A | ✅ Completo |
|
||||
| GitHub | 9 endpoints | ⚠️ parcial | N/A | ⚠️ Revisar |
|
||||
|
||||
### Frontend (64 páginas auditadas)
|
||||
|
||||
| Grupo | Páginas | Status |
|
||||
|-------|---------|--------|
|
||||
| ✅ Completas com API real | 48 | AppCenter, Automations, BI, Canvas, Communities, Financeiro, Knowledge, LMS, Marketplace, Migration, People, ProcessCompass, Production, Quality, Retail, Scientist, Support, Valuation, WhatsApp, XOS (todos), Cockpit, e mais 25 |
|
||||
| ⚠️ Parciais | 12 | IDE, CRM, DevCenter, ERP, Admin, Plus, Retail, Contabil, Fisco, Chat, WorkflowBuilder, MetabaseProxy |
|
||||
| ❌ Placeholder | 4 | ArcadiaNext, Agent, ApiHub, CentralApis |
|
||||
|
||||
### Schema (337 tabelas)
|
||||
|
||||
| Módulo | Tabelas | tenantId | Status |
|
||||
|--------|---------|----------|--------|
|
||||
| Retail | 40 | ✅ | Completo |
|
||||
| Valuation/PDCA | 30 | ✅ | Completo |
|
||||
| Production/Compass | 35 | ✅ | Completo |
|
||||
| CRM | 28 | ✅ | Completo |
|
||||
| XOS | 25 | ⚠️ parcial | Corrigir |
|
||||
| Fiscal | 18 | ✅ | Completo |
|
||||
| HR/People | 15 | ✅ | Completo |
|
||||
| Quality | 10 | ⚠️ | Corrigir |
|
||||
| Financeiro | 7 | ✅ | Completo |
|
||||
| Comunicação legacy | 30+ | ❌ | Corrigir |
|
||||
| Comunicação moderna | 9 | ✅ | Completo |
|
||||
| LMS | ⚠️ parcial | N/A | lms_courses criado dinamicamente |
|
||||
| Knowledge/AI | 10+ | ⚠️ parcial | Corrigir |
|
||||
|
||||
---
|
||||
|
||||
## 8. PLANO DE EXECUÇÃO — SPRINTS
|
||||
|
||||
### 🔴 Sprint S — Segurança (Pré-requisito absoluto) — 1 semana
|
||||
|
||||
| # | Tarefa | Arquivo | Esforço |
|
||||
|---|--------|---------|---------|
|
||||
| S1 | Auth em todas as rotas XOS (40+) | `server/xos/routes.ts` | M |
|
||||
| S2 | Auth em LMS (8 rotas) | `server/lms/routes.ts` | P |
|
||||
| S3 | Auth em Quality (50+ rotas) | `server/quality/routes.ts` | M |
|
||||
| S4 | Remover credentials hardcoded; SESSION_SECRET obrigatório | `server/auth.ts`, `server/metaset/routes.ts` | P |
|
||||
| S5 | CORS: `["*"]` → `[APP_URL]` nos 6 serviços Python | `server/python/*.py` | P |
|
||||
| S6 | Proteger `/api/tenants` | `server/routes.ts:139` | P |
|
||||
| S7 | `requires_approval` + audit log nas tools `shell` e `write_file` | `server/manus/tools.ts` | M |
|
||||
| S8 | Timeout 30s + retry 3x em OpenAI e Frappe | `server/manus/service.ts`, `server/crm/frappe-service.ts` | P |
|
||||
| S9 | Guard anti-overlap no setInterval do PipelineOrchestrator | `server/blackboard/PipelineOrchestrator.ts:171` | P |
|
||||
| S10 | tenantId nas tabelas de comunicação legacy | `shared/schema.ts` | G |
|
||||
|
||||
**Esforço:** P=Pequeno(<2h) M=Médio(2-4h) G=Grande(4-8h)
|
||||
|
||||
---
|
||||
|
||||
### 🟠 Sprint 0 — Deploy no Coolify — 1 semana (paralelo ao Sprint S)
|
||||
|
||||
| # | Tarefa | Esforço |
|
||||
|---|--------|---------|
|
||||
| 0.1 | Corrigir Python services para exportar `app` (uvicorn module) | P |
|
||||
| 0.2 | Testar `docker compose up` local — todos sobem e `/health` retorna 200 | M |
|
||||
| 0.3 | Exportar banco do Replit (`pg_dump`) antes de encerrar o plano | P |
|
||||
| 0.4 | Deploy no Coolify com domínio real + SSL automático | M |
|
||||
| 0.5 | Persistir config auto-reply WhatsApp no banco | P |
|
||||
| 0.6 | Validar: NF-e, Manus, WhatsApp funcionando em produção | M |
|
||||
|
||||
---
|
||||
|
||||
### 🟠 Sprint 1 — Fechar o Ciclo de Inteligência — 2 semanas
|
||||
|
||||
| # | Tarefa | Esforço |
|
||||
|---|--------|---------|
|
||||
| 1.1 | Subir serviço de embeddings com pgvector | P |
|
||||
| 1.2 | Popular embeddings com histórico de `learnedInteractions` | M |
|
||||
| 1.3 | Configurar LiteLLM (OpenAI → Ollama fallback) | P |
|
||||
| 1.4 | Validar ciclo: Manus aprende → armazena → recupera em contexto | M |
|
||||
| 1.5 | Adicionar insert schemas nas 36 tabelas faltantes | M |
|
||||
| 1.6 | Padronizar timestamps nas 40+ tabelas sem createdAt/updatedAt | M |
|
||||
|
||||
---
|
||||
|
||||
### 🟠 Sprint 2 — App Store Real — 1 semana
|
||||
|
||||
| # | Tarefa | Esforço |
|
||||
|---|--------|---------|
|
||||
| 2.1 | Endpoint `GET /api/marketplace/my-apps` por tenant | P |
|
||||
| 2.2 | AppCenter consulta subscriptions → apps aparecem/somem por plano | M |
|
||||
| 2.3 | Apps sem subscription → cadeado + CTA de ativar inline | M |
|
||||
| 2.4 | Billing engine: MRR calculado por tenant automaticamente | M |
|
||||
|
||||
---
|
||||
|
||||
### 🟡 Sprint 3 — Process Compass com IA — 2 semanas
|
||||
|
||||
| # | Tarefa | Esforço |
|
||||
|---|--------|---------|
|
||||
| 3.1 | Brief automático: notas brutas → Manus extrai ações/responsáveis/prazos | M |
|
||||
| 3.2 | Diagnóstico AI: Manus + BI cruzam dados → relatório de empresa | G |
|
||||
| 3.3 | Health score automático de projeto (PDCA completude + prazo) | M |
|
||||
| 3.4 | Gerador de proposta via Scientist (DOCX/PDF) | G |
|
||||
|
||||
---
|
||||
|
||||
### 🟡 Sprint 4 — Integrações ERP Reais — 3 semanas
|
||||
|
||||
| # | Tarefa | Esforço |
|
||||
|---|--------|---------|
|
||||
| 4.1 | Reorganizar `server/integrations/erp/` (unificar 4 módulos) | G |
|
||||
| 4.2 | Adaptador Omie (REST mais simples) | G |
|
||||
| 4.3 | Adaptador TOTVS RM (segundo mais comum PMEs BR) | G |
|
||||
| 4.4 | UI de onboarding: ERP → espelhamento → BI em 5 passos | G |
|
||||
| 4.5 | Dashboard de saúde das integrações (sync status, erros) | M |
|
||||
| 4.6 | Completar páginas placeholder: `CentralApis.tsx`, `ApiHub.tsx`, `Agent.tsx` | G |
|
||||
|
||||
---
|
||||
|
||||
### 🟢 Sprint 5 — Soberania de IA — 1 semana
|
||||
|
||||
| # | Tarefa | Esforço |
|
||||
|---|--------|---------|
|
||||
| 5.1 | Ollama + Open WebUI no Coolify (`--profile ai`) | P |
|
||||
| 5.2 | Baixar modelos: Llama 3.3, Qwen 2.5 Coder | P |
|
||||
| 5.3 | RAG no Open WebUI conectado ao Knowledge Graph | M |
|
||||
| 5.4 | Manus → LiteLLM em vez de OpenAI direto | M |
|
||||
|
||||
---
|
||||
|
||||
### 🔵 Sprint 6 — Qualidade de Engenharia — 2 semanas
|
||||
|
||||
| # | Tarefa | Esforço |
|
||||
|---|--------|---------|
|
||||
| 6.1 | Rate limiting nas rotas da API (`express-rate-limit`) | P |
|
||||
| 6.2 | Logging estruturado com Winston (JSON + correlation ID) | M |
|
||||
| 6.3 | Paginação em todas as listagens principais | G |
|
||||
| 6.4 | Error boundaries globais no React | M |
|
||||
| 6.5 | Primeiros testes de integração (auth, fiscal, Manus) | G |
|
||||
| 6.6 | Connection pooling nos serviços Python | M |
|
||||
| 6.7 | Consolidar sistemas de comunicação duplicados | G |
|
||||
|
||||
---
|
||||
|
||||
### 🟢 Sprint 7 — ERPNext como Container de Regras — 2 semanas
|
||||
|
||||
| # | Tarefa | Esforço |
|
||||
|---|--------|---------|
|
||||
| 7.1 | Container ERPNext no Coolify (MariaDB separado) | M |
|
||||
| 7.2 | Configurar empresa + plano de contas BR | M |
|
||||
| 7.3 | Bridge de autenticação (API Key por tenant) | M |
|
||||
| 7.4 | Expandir `server/erpnext/`: GL, Estoque, RH, Projetos | G |
|
||||
| 7.5 | Primeiro caso de uso real: pedido → lançamento GL no ERPNext | G |
|
||||
|
||||
---
|
||||
|
||||
## 9. LINHA DO TEMPO
|
||||
|
||||
```
|
||||
Semanas → 1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
||||
┌─────────────────────────────────────────────────────────────────────
|
||||
Sprint S │████ ████ Segurança (obrigatório)
|
||||
Sprint 0 │████ ████ Deploy Coolify
|
||||
Sprint 1 │ ████ ████ Inteligência + Schema
|
||||
Sprint 2 │ ████ App Store Real
|
||||
Sprint 3 │ ████ ████ Process Compass IA
|
||||
Sprint 4 │ ████ ████ ████ Integrações ERP reais
|
||||
Sprint 5 │ ████ Soberania IA
|
||||
Sprint 6 │ ████ ████ Qualidade
|
||||
Sprint 7 │ ████ ████ ERPNext
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. TOP 10 — MAIORES ALAVANCAS POR IMPACTO
|
||||
|
||||
| Rank | O que fazer | Impacto | Esforço |
|
||||
|------|-------------|---------|---------|
|
||||
| 1 | Autenticar rotas XOS, LMS, Quality | Segurança crítica | Baixo |
|
||||
| 2 | Corrigir CORS nos serviços Python | Segurança crítica | Baixo |
|
||||
| 3 | Deploy no Coolify com Docker | Infra — tudo depende disso | Médio |
|
||||
| 4 | Fechar ciclo de embeddings (pgvector) | IA aprende de verdade | Médio |
|
||||
| 5 | Conectar Marketplace → AppCenter | Modelo de negócio funciona | Médio |
|
||||
| 6 | Adaptador Omie real na Central de API | Primeira integração ERP real | Médio |
|
||||
| 7 | tenantId nas tabelas de comunicação | Isolamento de dados correto | Alto |
|
||||
| 8 | Brief automático no Process Compass | Produto para consultores | Médio |
|
||||
| 9 | Timeout + retry em chamadas externas | Estabilidade em produção | Baixo |
|
||||
| 10 | Completar Agent.tsx, CentralApis.tsx | Páginas estratégicas com UI | Alto |
|
||||
|
||||
---
|
||||
|
||||
## 11. DECISÕES ARQUITETURAIS PENDENTES
|
||||
|
||||
Estas decisões precisam de resposta antes de codificar:
|
||||
|
||||
| # | Decisão | Opções |
|
||||
|---|---------|--------|
|
||||
| D1 | BI padrão do sistema? | Metabase (já existe) vs Superset (no docker-compose) |
|
||||
| D2 | LMS usa tabelas dinâmicas ou schema.ts? | Hoje cria tabelas em runtime — inconsistente |
|
||||
| D3 | Sistema de comunicação canônico? | Legacy whatsapp_* vs moderno comm_* vs XOS |
|
||||
| D4 | Marketplace é público ou requer auth? | Hoje público (intencional?) |
|
||||
| D5 | Governance/Lowcode são públicos? | Sem auth (intencional para parceiros?) |
|
||||
| D6 | Usuários são globais ou por tenant? | Hoje global (users sem tenantId) — documentar |
|
||||
|
||||
---
|
||||
|
||||
*Documento gerado em 2026-03-13 via auditoria paralela com 4 agentes Claude Code.*
|
||||
*Cobre: 25 módulos backend, 64 páginas frontend, 337 tabelas de schema, segurança e integrações.*
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
# Arcádia Suite — Contexto para Claude
|
||||
|
||||
## Stack
|
||||
- **Frontend:** React 18 + TypeScript + Tailwind + shadcn/ui
|
||||
- **Backend:** Express.js + Socket.IO + Drizzle ORM
|
||||
- **DB:** PostgreSQL 16 + pgvector
|
||||
- **Microserviços Python:** FastAPI (portas 8001-8005)
|
||||
- **Deploy:** Docker Compose + Coolify + PM2
|
||||
- **Real-time:** Socket.IO
|
||||
|
||||
## Estrutura principal
|
||||
```
|
||||
server/
|
||||
manus/service.ts # Agente principal (169KB, 30+ tools, ReAct pattern)
|
||||
autonomous/ # Pipeline multi-agente (Architect→CodeGen→Validator)
|
||||
blackboard/ # Coordenação de agentes
|
||||
python/ # 8 microserviços Python (fisco, contabil, bi, etc.)
|
||||
learning/service.ts # Knowledge management
|
||||
[modulo]/routes.ts # 38 grupos de rotas (crm, erp, whatsapp, chat...)
|
||||
client/ # 66 páginas React
|
||||
shared/schema.ts # Schema do banco (7317 linhas, Drizzle ORM)
|
||||
docker/
|
||||
litellm-config.yaml # Roteamento de LLMs (TIER 1: LLMFit, TIER 2: Ollama, TIER 3: externos)
|
||||
```
|
||||
|
||||
## Arquitetura de IA
|
||||
```
|
||||
Manus / Agents / Embeddings
|
||||
│ AI_INTEGRATIONS_OPENAI_BASE_URL
|
||||
▼
|
||||
LiteLLM :4000 (gateway unificado, loga tudo no banco)
|
||||
├──► LLMFit (TIER 1 — fine-tuned, soberano) [slot pronto, comentado]
|
||||
├──► Ollama :11434 (TIER 2 — local, padrão)
|
||||
└──► OpenAI/Anthropic/Groq (TIER 3 — opt-in, só se API key configurada)
|
||||
```
|
||||
|
||||
**Variáveis chave do Manus:**
|
||||
```
|
||||
AI_INTEGRATIONS_OPENAI_BASE_URL=http://litellm:4000/v1
|
||||
AI_INTEGRATIONS_OPENAI_API_KEY=${LITELLM_API_KEY}
|
||||
```
|
||||
|
||||
## Docs estratégicos
|
||||
- `DOCUMENTATION.md` — docs técnicas completas
|
||||
- `PLANO_EVOLUCAO_ARCADIA.md` — roadmap e evolução
|
||||
- `MAPA_SISTEMA_ARCADIA.md` — mapa do sistema
|
||||
|
||||
## Branch de desenvolvimento
|
||||
Sempre commitar em: `claude/analyze-project-0mXjP`
|
||||
Push: `git push -u origin claude/analyze-project-0mXjP`
|
||||
|
||||
## O que está implementado
|
||||
- ✅ Manus (agente autônomo, 30+ ferramentas)
|
||||
- ✅ Pipeline de agentes autônomos
|
||||
- ✅ Embeddings semânticos (pgvector)
|
||||
- ✅ CRM, WhatsApp, Email, Chat
|
||||
- ✅ ERP/ERPNext, Fiscal (NF-e, SPED), Contábil (DRE, balanço)
|
||||
- ✅ BI workspace, Retail/POS, RH, Produtividade
|
||||
- ✅ Docker dev + prod, LiteLLM gateway
|
||||
|
||||
## O que ainda falta
|
||||
- ❌ LLMFit: slot pronto em `litellm-config.yaml`, só habilitar quando disponível
|
||||
- ❌ Testes automatizados / CI-CD
|
||||
- ❌ Monitoramento (APM, Sentry, métricas)
|
||||
- ❌ Multi-tenancy completo
|
||||
- ❌ Rate limiting em todos os endpoints (parcial)
|
||||
|
||||
## Comandos úteis
|
||||
```bash
|
||||
# Dev
|
||||
docker compose up -d
|
||||
docker compose --profile ai up litellm ollama -d
|
||||
|
||||
# Prod (Coolify)
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
|
||||
# Migrations
|
||||
npm run db:push
|
||||
|
||||
# Build
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Variáveis de ambiente críticas (ver .env.example)
|
||||
```
|
||||
SESSION_SECRET, SSO_SECRET # gerar strings seguras em prod
|
||||
AI_INTEGRATIONS_OPENAI_BASE_URL # aponta para LiteLLM
|
||||
LLMFIT_BASE_URL # LLMFit quando disponível
|
||||
OLLAMA_BASE_URL # Ollama host ou container
|
||||
OPENAI_API_KEY # opcional (soberania: deixar vazio)
|
||||
```
|
||||
|
|
@ -1,11 +1,13 @@
|
|||
import { BrowserFrame } from "@/components/Browser/BrowserFrame";
|
||||
import { useState } from "react";
|
||||
import { useLocation } from "wouter";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import {
|
||||
Lock,
|
||||
Search,
|
||||
ShoppingCart,
|
||||
GraduationCap,
|
||||
|
|
@ -53,6 +55,7 @@ interface AppItem {
|
|||
status: "active" | "coming_soon" | "beta";
|
||||
featured?: boolean;
|
||||
color: string;
|
||||
subscriptionCode?: string; // marketplace module code — if set, requires active subscription
|
||||
}
|
||||
|
||||
const apps: AppItem[] = [
|
||||
|
|
@ -500,6 +503,24 @@ export default function AppCenter() {
|
|||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [activeCategory, setActiveCategory] = useState("todos");
|
||||
|
||||
const { data: myAppsData } = useQuery<{ subscribedCodes: string[] }>({
|
||||
queryKey: ["/api/marketplace/my-apps"],
|
||||
staleTime: 60_000,
|
||||
});
|
||||
|
||||
const subscribedCodes = new Set(myAppsData?.subscribedCodes || []);
|
||||
|
||||
const isLocked = (app: AppItem) =>
|
||||
!!app.subscriptionCode && subscribedCodes.size > 0 && !subscribedCodes.has(app.subscriptionCode);
|
||||
|
||||
const handleAppClick = (app: AppItem) => {
|
||||
if (isLocked(app)) {
|
||||
setLocation("/marketplace");
|
||||
} else {
|
||||
setLocation(app.route);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredApps = apps.filter(app => {
|
||||
const matchesSearch = app.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
app.description.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
|
|
@ -546,7 +567,7 @@ export default function AppCenter() {
|
|||
<Card
|
||||
key={app.id}
|
||||
className="bg-white/5 border-white/10 hover:border-white/30 cursor-pointer transition-all hover:scale-105 group"
|
||||
onClick={() => setLocation(app.route)}
|
||||
onClick={() => handleAppClick(app)}
|
||||
data-testid={`featured-app-${app.id}`}
|
||||
>
|
||||
<CardContent className="p-4 text-center">
|
||||
|
|
@ -579,27 +600,37 @@ export default function AppCenter() {
|
|||
|
||||
<TabsContent value={activeCategory} className="mt-0">
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{filteredApps.map(app => (
|
||||
{filteredApps.map(app => {
|
||||
const locked = isLocked(app);
|
||||
return (
|
||||
<Card
|
||||
key={app.id}
|
||||
className="bg-white/5 border-white/10 hover:border-white/30 cursor-pointer transition-all group overflow-hidden"
|
||||
onClick={() => setLocation(app.route)}
|
||||
className={`border-white/10 cursor-pointer transition-all group overflow-hidden ${locked ? "bg-white/2 opacity-60 hover:opacity-80" : "bg-white/5 hover:border-white/30"}`}
|
||||
onClick={() => handleAppClick(app)}
|
||||
data-testid={`app-card-${app.id}`}
|
||||
>
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className={`w-14 h-14 rounded-2xl bg-gradient-to-br ${app.color} flex items-center justify-center text-white flex-shrink-0 group-hover:scale-110 transition-transform`}>
|
||||
<div className={`w-14 h-14 rounded-2xl bg-gradient-to-br ${app.color} flex items-center justify-center text-white flex-shrink-0 ${locked ? "" : "group-hover:scale-110"} transition-transform relative`}>
|
||||
{app.icon}
|
||||
{locked && (
|
||||
<div className="absolute inset-0 rounded-2xl bg-black/50 flex items-center justify-center">
|
||||
<Lock className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h3 className="font-semibold text-white group-hover:text-indigo-400 transition-colors truncate">
|
||||
<h3 className={`font-semibold truncate ${locked ? "text-slate-400" : "text-white group-hover:text-indigo-400"} transition-colors`}>
|
||||
{app.name}
|
||||
</h3>
|
||||
{app.status === "beta" && (
|
||||
{locked && (
|
||||
<Badge className="bg-slate-600/50 text-slate-400 text-xs">Não contratado</Badge>
|
||||
)}
|
||||
{!locked && app.status === "beta" && (
|
||||
<Badge className="bg-yellow-500/20 text-yellow-300 text-xs">Beta</Badge>
|
||||
)}
|
||||
{app.status === "coming_soon" && (
|
||||
{!locked && app.status === "coming_soon" && (
|
||||
<Badge className="bg-slate-500/20 text-slate-300 text-xs">Em breve</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -607,13 +638,20 @@ export default function AppCenter() {
|
|||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex justify-end">
|
||||
<span className="text-xs text-indigo-400 flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
Abrir <ArrowRight className="w-3 h-3" />
|
||||
</span>
|
||||
{locked ? (
|
||||
<span className="text-xs text-slate-500 flex items-center gap-1">
|
||||
Ver no Marketplace <ArrowRight className="w-3 h-3" />
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-xs text-indigo-400 flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
Abrir <ArrowRight className="w-3 h-3" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{filteredApps.length === 0 && (
|
||||
|
|
|
|||
|
|
@ -58,6 +58,11 @@ services:
|
|||
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
|
||||
LITELLM_BASE_URL: http://litellm:4000
|
||||
LITELLM_API_KEY: ${LITELLM_API_KEY}
|
||||
OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:-http://ollama:11434}
|
||||
# ── Manus Agent — aponta para LiteLLM como gateway unificado ──────────
|
||||
# LiteLLM roteia para Ollama (local), LLMFit (fine-tuned) ou externo
|
||||
AI_INTEGRATIONS_OPENAI_BASE_URL: http://litellm:4000/v1
|
||||
AI_INTEGRATIONS_OPENAI_API_KEY: ${LITELLM_API_KEY}
|
||||
ports:
|
||||
- "5000:5000"
|
||||
depends_on:
|
||||
|
|
@ -154,7 +159,8 @@ services:
|
|||
networks:
|
||||
- arcadia-internal
|
||||
|
||||
# ── LiteLLM (proxy de LLM) ───────────────────────────────────────────────────
|
||||
# ── LiteLLM (gateway unificado de LLM — soberania dos dados) ─────────────────
|
||||
# Roteia: LLMFit (fine-tuned) → Ollama (local) → externo (opt-in)
|
||||
litellm:
|
||||
image: ghcr.io/berriai/litellm:main-latest
|
||||
restart: always
|
||||
|
|
@ -164,10 +170,25 @@ services:
|
|||
environment:
|
||||
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
|
||||
LITELLM_MASTER_KEY: ${LITELLM_API_KEY}
|
||||
DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/${PGDATABASE:-arcadia}
|
||||
# Ollama: se instalado no host use http://host-gateway:11434
|
||||
# Se usar container Docker, mantém http://ollama:11434
|
||||
OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:-http://ollama:11434}
|
||||
# LLMFit: URL do serviço de modelos fine-tuned
|
||||
LLMFIT_BASE_URL: ${LLMFIT_BASE_URL:-}
|
||||
# Providers externos opcionais (soberania: só habilitados se configurados)
|
||||
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
|
||||
GROQ_API_KEY: ${GROQ_API_KEY:-}
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- arcadia-internal
|
||||
|
||||
# ── Ollama (LLMs locais — ativar se o servidor tiver RAM suficiente) ──────────
|
||||
# ── Ollama (LLMs locais — soberania total) ────────────────────────────────────
|
||||
# OPÇÃO A (padrão): Ollama como container Docker
|
||||
# OPÇÃO B: Ollama no host → comente este serviço e defina
|
||||
# OLLAMA_BASE_URL=http://host-gateway:11434 nas env vars
|
||||
ollama:
|
||||
image: ollama/ollama:latest
|
||||
restart: always
|
||||
|
|
@ -175,19 +196,23 @@ services:
|
|||
- ollama_models:/root/.ollama
|
||||
networks:
|
||||
- arcadia-internal
|
||||
# Remova 'profiles: [ai]' para ativar por padrão no deploy
|
||||
profiles: [ai]
|
||||
|
||||
# ── Open WebUI ───────────────────────────────────────────────────────────────
|
||||
# ── Open WebUI (interface para Ollama + LLMFit) ───────────────────────────────
|
||||
open-webui:
|
||||
image: ghcr.io/open-webui/open-webui:main
|
||||
restart: always
|
||||
environment:
|
||||
OLLAMA_BASE_URL: http://ollama:11434
|
||||
# Pode apontar para LiteLLM para ter acesso a todos os modelos via WebUI
|
||||
OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:-http://ollama:11434}
|
||||
OPENAI_API_BASE_URL: http://litellm:4000/v1
|
||||
OPENAI_API_KEY: ${LITELLM_API_KEY}
|
||||
WEBUI_SECRET_KEY: ${WEBUI_SECRET_KEY}
|
||||
volumes:
|
||||
- open_webui_data:/app/backend/data
|
||||
depends_on:
|
||||
- ollama
|
||||
- litellm
|
||||
networks:
|
||||
- arcadia-internal
|
||||
- arcadia-public
|
||||
|
|
|
|||
|
|
@ -1,20 +1,34 @@
|
|||
# LiteLLM — Proxy unificado de LLMs para o Arcádia Suite
|
||||
# LiteLLM — Gateway unificado de LLMs para o Arcádia Suite
|
||||
# Documentação: https://docs.litellm.ai/docs/proxy/configs
|
||||
#
|
||||
# ESTRATÉGIA DE SOBERANIA DOS DADOS:
|
||||
# ┌─────────────────────────────────────────────────────────────────────────┐
|
||||
# │ TIER 1 (soberania total): LLMFit — modelos fine-tuned locais │
|
||||
# │ TIER 2 (soberania total): Ollama — modelos open source no servidor │
|
||||
# │ TIER 3 (opt-in): Providers externos — só com configuração explícita │
|
||||
# └─────────────────────────────────────────────────────────────────────────┘
|
||||
# O Manus, Autonomous Agents e todos os serviços chamam APENAS este proxy.
|
||||
# Nunca chamam APIs externas diretamente.
|
||||
|
||||
model_list:
|
||||
|
||||
# ── OpenAI (quando disponível) ───────────────────────────────────────────────
|
||||
- model_name: gpt-4o
|
||||
litellm_params:
|
||||
model: openai/gpt-4o
|
||||
api_key: os.environ/OPENAI_API_KEY
|
||||
# ── TIER 1: LLMFit (modelos fine-tuned locais — máxima soberania) ────────────
|
||||
# Descomente quando o LLMFit estiver rodando no servidor
|
||||
# O LLMFit expõe API compatível com OpenAI — basta apontar a URL
|
||||
#
|
||||
# - model_name: arcadia-finetuned
|
||||
# litellm_params:
|
||||
# model: openai/arcadia-v1 # nome do modelo no LLMFit
|
||||
# api_base: os.environ/LLMFIT_BASE_URL
|
||||
# api_key: llmfit-internal
|
||||
#
|
||||
# - model_name: arcadia-embed
|
||||
# litellm_params:
|
||||
# model: openai/arcadia-embed-v1 # modelo de embeddings fine-tuned
|
||||
# api_base: os.environ/LLMFIT_BASE_URL
|
||||
# api_key: llmfit-internal
|
||||
|
||||
- model_name: gpt-4o-mini
|
||||
litellm_params:
|
||||
model: openai/gpt-4o-mini
|
||||
api_key: os.environ/OPENAI_API_KEY
|
||||
|
||||
# ── Ollama (LLMs locais — soberania) ─────────────────────────────────────────
|
||||
# ── TIER 2: Ollama (LLMs locais — soberania total) ───────────────────────────
|
||||
- model_name: llama3.3
|
||||
litellm_params:
|
||||
model: ollama/llama3.3
|
||||
|
|
@ -30,26 +44,61 @@ model_list:
|
|||
model: ollama/deepseek-r1:7b
|
||||
api_base: os.environ/OLLAMA_BASE_URL
|
||||
|
||||
# ── Modelo padrão: tenta OpenAI, fallback para Ollama ───────────────────────
|
||||
- model_name: arcadia-default
|
||||
- model_name: nomic-embed-text
|
||||
litellm_params:
|
||||
model: ollama/nomic-embed-text
|
||||
api_base: os.environ/OLLAMA_BASE_URL
|
||||
|
||||
# ── TIER 3: OpenAI (opt-in — só ativo se OPENAI_API_KEY configurado) ─────────
|
||||
- model_name: gpt-4o
|
||||
litellm_params:
|
||||
model: openai/gpt-4o
|
||||
api_key: os.environ/OPENAI_API_KEY
|
||||
|
||||
- model_name: gpt-4o-mini
|
||||
litellm_params:
|
||||
model: openai/gpt-4o-mini
|
||||
api_key: os.environ/OPENAI_API_KEY
|
||||
|
||||
# ── TIER 3: Anthropic (opt-in — descomente para habilitar) ───────────────────
|
||||
# - model_name: claude-sonnet
|
||||
# litellm_params:
|
||||
# model: anthropic/claude-sonnet-4-6
|
||||
# api_key: os.environ/ANTHROPIC_API_KEY
|
||||
|
||||
# ── TIER 3: Groq (opt-in — inferência rápida sem dados persistidos) ──────────
|
||||
# - model_name: groq-llama
|
||||
# litellm_params:
|
||||
# model: groq/llama-3.3-70b-versatile
|
||||
# api_key: os.environ/GROQ_API_KEY
|
||||
|
||||
# ── Modelo padrão do Arcádia (Manus usa este) ─────────────────────────────────
|
||||
# Prioridade: LLMFit → OpenAI (se configurado) → Ollama (sempre disponível)
|
||||
# Para soberania total: remova o fallback para gpt-4o-mini
|
||||
- model_name: arcadia-default
|
||||
litellm_params:
|
||||
model: ollama/llama3.3
|
||||
api_base: os.environ/OLLAMA_BASE_URL
|
||||
model_info:
|
||||
fallbacks: ["llama3.3"]
|
||||
# fallbacks: ["gpt-4o-mini"] # descomente para habilitar fallback externo
|
||||
|
||||
router_settings:
|
||||
routing_strategy: least-busy
|
||||
fallbacks:
|
||||
- {"gpt-4o": ["llama3.3"]}
|
||||
- {"gpt-4o-mini": ["qwen2.5-coder"]}
|
||||
- {"gpt-4o-mini": ["llama3.3"]}
|
||||
- {"arcadia-default": ["llama3.3"]}
|
||||
|
||||
litellm_settings:
|
||||
drop_params: true
|
||||
request_timeout: 120
|
||||
set_verbose: false
|
||||
# Loga todas as chamadas no banco — essencial para auditoria e soberania
|
||||
success_callback: ["langfuse"]
|
||||
failure_callback: ["langfuse"]
|
||||
|
||||
general_settings:
|
||||
master_key: os.environ/LITELLM_MASTER_KEY
|
||||
database_url: os.environ/DATABASE_URL
|
||||
# Habilita interface de gestão do LiteLLM (opcional)
|
||||
# ui_access_mode: "all"
|
||||
|
|
|
|||
|
|
@ -41,7 +41,10 @@ case "$SERVICE_NAME" in
|
|||
--workers 2
|
||||
;;
|
||||
embeddings)
|
||||
exec python server/python/embeddings_service.py
|
||||
exec python -m uvicorn server.python.embeddings_service:app \
|
||||
--host 0.0.0.0 \
|
||||
--port "$SERVICE_PORT" \
|
||||
--workers 1
|
||||
;;
|
||||
*)
|
||||
echo "[entrypoint] ERRO: SERVICE_NAME desconhecido: $SERVICE_NAME"
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE "whatsapp_sessions" ADD COLUMN IF NOT EXISTS "auto_reply_config" jsonb;
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
-- Migration 0002: Add tenantId to critical tables missing multi-tenant isolation
|
||||
-- Tables: workspace_pages, quick_notes, activity_feed, conversations,
|
||||
-- knowledge_base, chat_threads, manus_runs
|
||||
|
||||
ALTER TABLE "workspace_pages"
|
||||
ADD COLUMN IF NOT EXISTS "tenant_id" integer
|
||||
REFERENCES "tenants"("id") ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE "quick_notes"
|
||||
ADD COLUMN IF NOT EXISTS "tenant_id" integer
|
||||
REFERENCES "tenants"("id") ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE "activity_feed"
|
||||
ADD COLUMN IF NOT EXISTS "tenant_id" integer
|
||||
REFERENCES "tenants"("id") ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE "conversations"
|
||||
ADD COLUMN IF NOT EXISTS "tenant_id" integer
|
||||
REFERENCES "tenants"("id") ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE "knowledge_base"
|
||||
ADD COLUMN IF NOT EXISTS "tenant_id" integer
|
||||
REFERENCES "tenants"("id") ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE "chat_threads"
|
||||
ADD COLUMN IF NOT EXISTS "tenant_id" integer
|
||||
REFERENCES "tenants"("id") ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE "manus_runs"
|
||||
ADD COLUMN IF NOT EXISTS "tenant_id" integer
|
||||
REFERENCES "tenants"("id") ON DELETE CASCADE;
|
||||
|
||||
-- Indexes for query performance on tenantId lookups
|
||||
CREATE INDEX IF NOT EXISTS idx_workspace_pages_tenant ON workspace_pages(tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_quick_notes_tenant ON quick_notes(tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_activity_feed_tenant ON activity_feed(tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_conversations_tenant ON conversations(tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_knowledge_base_tenant ON knowledge_base(tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_chat_threads_tenant ON chat_threads(tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_manus_runs_tenant ON manus_runs(tenant_id);
|
||||
|
|
@ -8,6 +8,20 @@
|
|||
"when": 1769121114659,
|
||||
"tag": "0000_low_tiger_shark",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "7",
|
||||
"when": 1741824000000,
|
||||
"tag": "0001_whatsapp_auto_reply_config",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 2,
|
||||
"version": "7",
|
||||
"when": 1741824060000,
|
||||
"tag": "0002_tenant_isolation",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -75,6 +75,7 @@
|
|||
"drizzle-zod": "^0.7.1",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"express": "^4.21.2",
|
||||
"express-rate-limit": "^8.3.1",
|
||||
"express-session": "^1.18.1",
|
||||
"file-saver": "^2.0.5",
|
||||
"framer-motion": "^12.23.24",
|
||||
|
|
@ -113,6 +114,7 @@
|
|||
"tough-cookie": "^6.0.0",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"vaul": "^1.1.2",
|
||||
"winston": "^3.19.0",
|
||||
"wouter": "^3.3.5",
|
||||
"ws": "^8.18.0",
|
||||
"xlsx": "^0.18.5",
|
||||
|
|
|
|||
|
|
@ -30,8 +30,12 @@ async function comparePasswords(supplied: string, stored: string) {
|
|||
return timingSafeEqual(hashedBuf, suppliedBuf);
|
||||
}
|
||||
|
||||
if (!process.env.SESSION_SECRET) {
|
||||
console.warn("[auth] WARNING: SESSION_SECRET env var not set. Using insecure fallback. Set SESSION_SECRET in production.");
|
||||
}
|
||||
|
||||
const sessionSettings: session.SessionOptions = {
|
||||
secret: process.env.SESSION_SECRET || "arcadia-browser-secret-key-2024",
|
||||
secret: process.env.SESSION_SECRET || `arcadia-dev-${Math.random().toString(36)}`,
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
store: storage.sessionStore,
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ const ALL_PHASES: PipelinePhase[] = ["design", "codegen", "validation", "staging
|
|||
|
||||
class PipelineOrchestrator extends EventEmitter {
|
||||
private activeMonitors: Map<number, NodeJS.Timeout> = new Map();
|
||||
private processingMonitors: Set<number> = new Set();
|
||||
|
||||
async createPipeline(prompt: string, userId: string = "system", metadata?: any): Promise<XosDevPipeline> {
|
||||
const correlationId = randomUUID();
|
||||
|
|
@ -169,10 +170,14 @@ class PipelineOrchestrator extends EventEmitter {
|
|||
if (this.activeMonitors.has(pipelineId)) return;
|
||||
|
||||
const interval = setInterval(async () => {
|
||||
if (this.processingMonitors.has(pipelineId)) return;
|
||||
this.processingMonitors.add(pipelineId);
|
||||
try {
|
||||
await this.checkPhaseProgress(pipelineId, mainTaskId);
|
||||
} catch (error) {
|
||||
console.error(`[PipelineOrchestrator] Erro no monitor #${pipelineId}:`, error);
|
||||
} finally {
|
||||
this.processingMonitors.delete(pipelineId);
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
|
|
|
|||
|
|
@ -2274,4 +2274,124 @@ router.put("/projects/:projectId/history", async (req: Request, res: Response) =
|
|||
}
|
||||
});
|
||||
|
||||
// ========== AI BRIEFING & HEALTH SCORE ==========
|
||||
|
||||
router.post("/projects/:projectId/ai-brief", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const tenantId = await getTenantId(req);
|
||||
if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" });
|
||||
const projectId = parseInt(req.params.projectId);
|
||||
const hasAccess = await validateProjectAccess(projectId, tenantId);
|
||||
if (!hasAccess) return res.status(403).json({ error: "Acesso negado ao projeto" });
|
||||
|
||||
const project = await compassStorage.getProject(projectId, tenantId);
|
||||
const tasks = await compassStorage.getTasks(projectId);
|
||||
const pdcaCycles = await compassStorage.getPdcaCycles(tenantId, projectId);
|
||||
|
||||
const openTasks = tasks.filter((t: any) => t.status !== "done" && t.status !== "completed");
|
||||
const overdue = tasks.filter((t: any) => t.dueDate && new Date(t.dueDate) < new Date() && t.status !== "done");
|
||||
const pdcaOpen = pdcaCycles.filter((c: any) => c.status === "open" || c.status === "in_progress");
|
||||
|
||||
const OpenAI = (await import("openai")).default;
|
||||
const openai = new OpenAI({
|
||||
apiKey: process.env.AI_INTEGRATIONS_OPENAI_API_KEY,
|
||||
baseURL: process.env.AI_INTEGRATIONS_OPENAI_BASE_URL,
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
const prompt = `Você é um consultor de gestão de projetos. Analise o projeto abaixo e gere um briefing executivo em português.
|
||||
|
||||
PROJETO: ${project?.name}
|
||||
DESCRIÇÃO: ${project?.description || "Não informada"}
|
||||
STATUS: ${project?.status}
|
||||
FASE: ${project?.currentPhase || "Não definida"}
|
||||
|
||||
RESUMO DE TAREFAS:
|
||||
- Total: ${tasks.length}
|
||||
- Em aberto: ${openTasks.length}
|
||||
- Atrasadas: ${overdue.length}
|
||||
|
||||
PDCA ATIVOS: ${pdcaOpen.length}
|
||||
|
||||
${overdue.length > 0 ? `TAREFAS ATRASADAS:\n${overdue.slice(0, 5).map((t: any) => `- ${t.title} (responsável: ${t.assignedTo || "não atribuído"})`).join("\n")}` : ""}
|
||||
|
||||
Gere um briefing com:
|
||||
1. **Situação atual** (2-3 linhas)
|
||||
2. **Principais riscos** (lista com até 3 itens)
|
||||
3. **Ações prioritárias** (lista com até 5 ações, cada uma com responsável sugerido e prazo)
|
||||
4. **Score de saúde** (0-100 com justificativa)
|
||||
|
||||
Seja direto e objetivo.`;
|
||||
|
||||
const response = await openai.chat.completions.create({
|
||||
model: "gpt-4o-mini",
|
||||
messages: [{ role: "user", content: prompt }],
|
||||
max_tokens: 800,
|
||||
temperature: 0.3,
|
||||
});
|
||||
|
||||
const briefText = response.choices[0]?.message?.content || "";
|
||||
|
||||
// Extract health score from response
|
||||
const scoreMatch = briefText.match(/score[^\d]*(\d{1,3})/i) || briefText.match(/(\d{1,3})[^\d]*\//);
|
||||
const healthScore = scoreMatch ? Math.min(100, Math.max(0, parseInt(scoreMatch[1]))) : null;
|
||||
|
||||
res.json({
|
||||
projectId,
|
||||
brief: briefText,
|
||||
healthScore,
|
||||
meta: {
|
||||
totalTasks: tasks.length,
|
||||
openTasks: openTasks.length,
|
||||
overdueTasks: overdue.length,
|
||||
activePdca: pdcaOpen.length,
|
||||
generatedAt: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/projects/:projectId/health", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const tenantId = await getTenantId(req);
|
||||
if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" });
|
||||
const projectId = parseInt(req.params.projectId);
|
||||
const hasAccess = await validateProjectAccess(projectId, tenantId);
|
||||
if (!hasAccess) return res.status(403).json({ error: "Acesso negado ao projeto" });
|
||||
|
||||
const tasks = await compassStorage.getTasks(projectId);
|
||||
const pdcaCycles = await compassStorage.getPdcaCycles(tenantId, projectId);
|
||||
|
||||
const total = tasks.length || 1;
|
||||
const done = tasks.filter((t: any) => t.status === "done" || t.status === "completed").length;
|
||||
const overdue = tasks.filter((t: any) => t.dueDate && new Date(t.dueDate) < new Date() && t.status !== "done").length;
|
||||
const pdcaComplete = pdcaCycles.filter((c: any) => c.status === "completed").length;
|
||||
const pdcaTotal = pdcaCycles.length || 1;
|
||||
|
||||
const completionScore = (done / total) * 40;
|
||||
const overdueScore = Math.max(0, 30 - (overdue / total) * 30);
|
||||
const pdcaScore = (pdcaComplete / pdcaTotal) * 30;
|
||||
const healthScore = Math.round(completionScore + overdueScore + pdcaScore);
|
||||
|
||||
const level = healthScore >= 80 ? "saudável" : healthScore >= 50 ? "atenção" : "crítico";
|
||||
const color = healthScore >= 80 ? "green" : healthScore >= 50 ? "yellow" : "red";
|
||||
|
||||
res.json({
|
||||
projectId,
|
||||
healthScore,
|
||||
level,
|
||||
color,
|
||||
breakdown: {
|
||||
completion: { score: Math.round(completionScore), weight: "40%", tasks: `${done}/${tasks.length}` },
|
||||
timeliness: { score: Math.round(overdueScore), weight: "30%", overdue },
|
||||
pdca: { score: Math.round(pdcaScore), weight: "30%", cycles: `${pdcaComplete}/${pdcaCycles.length}` },
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ export class FrappeService {
|
|||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
signal: AbortSignal.timeout(30000),
|
||||
};
|
||||
|
||||
if (data && (method === "POST" || method === "PUT")) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import express, { type Request, Response, NextFunction } from "express";
|
||||
import rateLimit from "express-rate-limit";
|
||||
import { registerRoutes } from "./routes";
|
||||
import { serveStatic } from "./static";
|
||||
import { registerAllTools } from "./autonomous/tools";
|
||||
|
|
@ -6,6 +7,7 @@ import { storage } from "./storage";
|
|||
import { createServer } from "http";
|
||||
import { spawn } from "child_process";
|
||||
import path from "path";
|
||||
import { logger, httpLogger } from "./logger";
|
||||
|
||||
interface ManagedService {
|
||||
name: string;
|
||||
|
|
@ -296,43 +298,35 @@ app.use(
|
|||
|
||||
app.use(express.urlencoded({ extended: false }));
|
||||
|
||||
export function log(message: string, source = "express") {
|
||||
const formattedTime = new Date().toLocaleTimeString("en-US", {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hour12: true,
|
||||
});
|
||||
|
||||
console.log(`${formattedTime} [${source}] ${message}`);
|
||||
}
|
||||
|
||||
app.use((req, res, next) => {
|
||||
const start = Date.now();
|
||||
const path = req.path;
|
||||
let capturedJsonResponse: Record<string, any> | undefined = undefined;
|
||||
|
||||
const originalResJson = res.json;
|
||||
res.json = function (bodyJson, ...args) {
|
||||
capturedJsonResponse = bodyJson;
|
||||
return originalResJson.apply(res, [bodyJson, ...args]);
|
||||
};
|
||||
|
||||
res.on("finish", () => {
|
||||
const duration = Date.now() - start;
|
||||
if (path.startsWith("/api")) {
|
||||
let logLine = `${req.method} ${path} ${res.statusCode} in ${duration}ms`;
|
||||
if (capturedJsonResponse) {
|
||||
logLine += ` :: ${JSON.stringify(capturedJsonResponse)}`;
|
||||
}
|
||||
|
||||
log(logLine);
|
||||
}
|
||||
});
|
||||
|
||||
next();
|
||||
// Rate limiting
|
||||
const apiLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 500,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
message: { error: "Too many requests, please try again later." },
|
||||
skip: (req) => !req.path.startsWith("/api"),
|
||||
});
|
||||
|
||||
const authLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000,
|
||||
max: 20,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
message: { error: "Too many login attempts, please try again later." },
|
||||
});
|
||||
|
||||
app.use("/api", apiLimiter);
|
||||
app.use("/api/login", authLimiter);
|
||||
app.use("/api/register", authLimiter);
|
||||
|
||||
// Structured HTTP logging
|
||||
app.use(httpLogger());
|
||||
|
||||
export function log(message: string, source = "express") {
|
||||
logger.info(message, { source });
|
||||
}
|
||||
|
||||
// Plus proxy is configured in server/plus/proxy.ts via setupPlusProxy
|
||||
// It's registered after session middleware to enable SSO authentication
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,15 @@ import { sql } from "drizzle-orm";
|
|||
|
||||
const router = Router();
|
||||
|
||||
function requireAuth(req: any, res: any, next: any) {
|
||||
if (!req.isAuthenticated()) {
|
||||
return res.status(401).json({ error: "Authentication required" });
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
router.use(requireAuth);
|
||||
|
||||
router.get("/courses", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { category, featured, published } = req.query;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
import winston from "winston";
|
||||
|
||||
const isProduction = process.env.NODE_ENV === "production";
|
||||
|
||||
export const logger = winston.createLogger({
|
||||
level: process.env.LOG_LEVEL || "info",
|
||||
format: isProduction
|
||||
? winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
winston.format.errors({ stack: true }),
|
||||
winston.format.json()
|
||||
)
|
||||
: winston.format.combine(
|
||||
winston.format.colorize(),
|
||||
winston.format.timestamp({ format: "HH:mm:ss" }),
|
||||
winston.format.printf(({ timestamp, level, message, ...meta }) => {
|
||||
const extras = Object.keys(meta).length ? ` ${JSON.stringify(meta)}` : "";
|
||||
return `${timestamp} [${level}] ${message}${extras}`;
|
||||
})
|
||||
),
|
||||
transports: [new winston.transports.Console()],
|
||||
});
|
||||
|
||||
export function httpLogger() {
|
||||
return (req: any, res: any, next: any) => {
|
||||
const start = Date.now();
|
||||
const { method, path: reqPath } = req;
|
||||
|
||||
res.on("finish", () => {
|
||||
if (!reqPath.startsWith("/api")) return;
|
||||
const duration = Date.now() - start;
|
||||
const level = res.statusCode >= 500 ? "error" : res.statusCode >= 400 ? "warn" : "info";
|
||||
logger.log(level, `${method} ${reqPath} ${res.statusCode}`, {
|
||||
duration_ms: duration,
|
||||
user_id: req.user?.id || null,
|
||||
tenant_id: req.user?.tenantId || null,
|
||||
});
|
||||
});
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
|
@ -12,6 +12,8 @@ import * as erpnextService from "../erpnext/service";
|
|||
const openai = new OpenAI({
|
||||
apiKey: process.env.AI_INTEGRATIONS_OPENAI_API_KEY,
|
||||
baseURL: process.env.AI_INTEGRATIONS_OPENAI_BASE_URL,
|
||||
timeout: 30000,
|
||||
maxRetries: 3,
|
||||
});
|
||||
|
||||
const SYSTEM_PROMPT = `Você é o Agente Arcádia Manus, um assistente empresarial inteligente e proativo.
|
||||
|
|
@ -126,7 +128,27 @@ REGRA CRÍTICA PARA RESPOSTA FINAL:
|
|||
- Se analisou um documento, inclua os dados extraídos E sua interpretação`;
|
||||
|
||||
class ManusService extends EventEmitter {
|
||||
private pendingApprovals: Map<string, { tool: string; input: Record<string, any> }> = new Map();
|
||||
|
||||
private async executeTool(tool: string, input: Record<string, any>, userId: string): Promise<ToolResult> {
|
||||
// Dangerous tools require explicit user approval via ask_human first
|
||||
const DANGEROUS_TOOLS = new Set(["shell", "write_file", "python_execute"]);
|
||||
if (DANGEROUS_TOOLS.has(tool)) {
|
||||
const approvalKey = `${userId}:${tool}:${JSON.stringify(input)}`;
|
||||
if (!this.pendingApprovals.has(approvalKey)) {
|
||||
this.pendingApprovals.set(approvalKey, { tool, input });
|
||||
const preview = tool === "shell" ? input.command
|
||||
: tool === "write_file" ? `Escrever em: ${input.path}`
|
||||
: `Executar código Python (${String(input.code || "").substring(0, 80)}...)`;
|
||||
return {
|
||||
success: false,
|
||||
output: `[APROVAÇÃO NECESSÁRIA] Esta ação requer confirmação: ${preview}. Use ask_human para solicitar aprovação antes de prosseguir.`,
|
||||
error: "requires_approval"
|
||||
};
|
||||
}
|
||||
this.pendingApprovals.delete(approvalKey);
|
||||
}
|
||||
|
||||
try {
|
||||
switch (tool) {
|
||||
case "web_search":
|
||||
|
|
|
|||
|
|
@ -5,6 +5,52 @@ import { eq, desc, and, sql } from "drizzle-orm";
|
|||
|
||||
const router = Router();
|
||||
|
||||
function requireAuth(req: any, res: any, next: any) {
|
||||
if (!req.isAuthenticated()) {
|
||||
return res.status(401).json({ error: "Authentication required" });
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
// My apps: returns active subscriptions for the current user's tenant
|
||||
// Public modules (isCore=true) are always included
|
||||
router.get("/my-apps", requireAuth, async (req: any, res: Response) => {
|
||||
try {
|
||||
const tenantId = req.user?.tenantId;
|
||||
|
||||
// Core modules always available
|
||||
const coreModules = await db
|
||||
.select({ code: marketplaceModules.code, route: marketplaceModules.route })
|
||||
.from(marketplaceModules)
|
||||
.where(and(eq(marketplaceModules.isCore, true), eq(marketplaceModules.isActive, true)));
|
||||
|
||||
// Subscribed modules for this tenant
|
||||
const subscribed = tenantId
|
||||
? await db
|
||||
.select({ code: marketplaceModules.code, route: marketplaceModules.route })
|
||||
.from(moduleSubscriptions)
|
||||
.innerJoin(marketplaceModules, eq(moduleSubscriptions.moduleId, marketplaceModules.id))
|
||||
.where(
|
||||
and(
|
||||
eq(moduleSubscriptions.tenantId, tenantId),
|
||||
eq(moduleSubscriptions.status, "active"),
|
||||
eq(marketplaceModules.isActive, true)
|
||||
)
|
||||
)
|
||||
: [];
|
||||
|
||||
const allCodes = new Set([
|
||||
...coreModules.map((m) => m.code),
|
||||
...subscribed.map((m) => m.code),
|
||||
]);
|
||||
|
||||
res.json({ subscribedCodes: [...allCodes], tenantId: tenantId || null });
|
||||
} catch (error) {
|
||||
console.error("Error fetching my-apps:", error);
|
||||
res.status(500).json({ error: "Failed to fetch subscribed apps" });
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/modules", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const modules = await db
|
||||
|
|
|
|||
|
|
@ -4,14 +4,17 @@ import { metasetClient } from "./client";
|
|||
const METASET_HOST = process.env.METABASE_HOST || "localhost";
|
||||
const METASET_PORT = parseInt(process.env.METABASE_PORT || "8088", 10);
|
||||
const METASET_URL = `http://${METASET_HOST}:${METASET_PORT}`;
|
||||
const ADMIN_EMAIL = process.env.METASET_ADMIN_EMAIL || "admin@arcadia.app";
|
||||
const ADMIN_PASSWORD = process.env.METASET_ADMIN_PASSWORD || "Arcadia2026!BI";
|
||||
const ADMIN_EMAIL = process.env.METASET_ADMIN_EMAIL;
|
||||
const ADMIN_PASSWORD = process.env.METASET_ADMIN_PASSWORD;
|
||||
|
||||
export function registerMetaSetRoutes(app: Express): void {
|
||||
|
||||
app.get("/api/bi/metaset/autologin", async (req: Request, res: Response) => {
|
||||
try {
|
||||
if (!req.isAuthenticated()) return res.status(401).json({ error: "Not authenticated" });
|
||||
if (!ADMIN_EMAIL || !ADMIN_PASSWORD) {
|
||||
return res.status(503).json({ error: "MetaSet credentials not configured (METASET_ADMIN_EMAIL / METASET_ADMIN_PASSWORD)" });
|
||||
}
|
||||
|
||||
const sessionResp = await fetch(`${METASET_URL}/api/session`, {
|
||||
method: "POST",
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ app = FastAPI(
|
|||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_origins=[os.getenv("APP_URL", "http://localhost:5000")],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ app = FastAPI(
|
|||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_origins=[os.getenv("APP_URL", "http://localhost:5000")],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ app = FastAPI(
|
|||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_origins=[os.getenv("APP_URL", "http://localhost:5000")],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ app = FastAPI(
|
|||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_origins=[os.getenv("APP_URL", "http://localhost:5000")],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
|
|
|
|||
|
|
@ -133,7 +133,8 @@ app = FastAPI(
|
|||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_origins=[os.getenv("APP_URL", "http://localhost:5000")],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ app = FastAPI(
|
|||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_origins=[os.getenv("APP_URL", "http://localhost:5000")],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ app = FastAPI(
|
|||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_origins=[os.getenv("APP_URL", "http://localhost:5000")],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
|
|
|
|||
|
|
@ -13,11 +13,20 @@ import { eq, desc, and, gte, lte, like, or, sql, isNull } from "drizzle-orm";
|
|||
|
||||
const router = Router();
|
||||
|
||||
function requireAuth(req: any, res: any, next: any) {
|
||||
if (!req.isAuthenticated()) {
|
||||
return res.status(401).json({ error: "Authentication required" });
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
router.use(requireAuth);
|
||||
|
||||
// ========== AMOSTRAS (RF-QC01) ==========
|
||||
|
||||
router.get("/samples", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { projectId, status, startDate, endDate, limit = 50 } = req.query;
|
||||
const { projectId, status, startDate, endDate, limit = 50, offset = 0 } = req.query;
|
||||
|
||||
let conditions = [];
|
||||
if (projectId) conditions.push(eq(qualitySamples.projectId, Number(projectId)));
|
||||
|
|
@ -29,7 +38,7 @@ router.get("/samples", async (req: Request, res: Response) => {
|
|||
.from(qualitySamples)
|
||||
.where(conditions.length > 0 ? and(...conditions) : undefined)
|
||||
.orderBy(desc(qualitySamples.createdAt))
|
||||
.limit(Number(limit));
|
||||
.limit(Number(limit)).offset(Number(offset));
|
||||
|
||||
return res.json({ data: samples });
|
||||
} catch (error) {
|
||||
|
|
@ -82,7 +91,7 @@ router.put("/samples/:id", async (req: Request, res: Response) => {
|
|||
|
||||
router.get("/lab-reports", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { sampleId, laboratoryId, status, limit = 50 } = req.query;
|
||||
const { sampleId, laboratoryId, status, limit = 50, offset = 0 } = req.query;
|
||||
|
||||
let conditions = [];
|
||||
if (sampleId) conditions.push(eq(qualityLabReports.sampleId, Number(sampleId)));
|
||||
|
|
@ -93,7 +102,7 @@ router.get("/lab-reports", async (req: Request, res: Response) => {
|
|||
.from(qualityLabReports)
|
||||
.where(conditions.length > 0 ? and(...conditions) : undefined)
|
||||
.orderBy(desc(qualityLabReports.createdAt))
|
||||
.limit(Number(limit));
|
||||
.limit(Number(limit)).offset(Number(offset));
|
||||
|
||||
return res.json({ data: reports });
|
||||
} catch (error) {
|
||||
|
|
@ -130,7 +139,7 @@ router.put("/lab-reports/:id", async (req: Request, res: Response) => {
|
|||
|
||||
router.get("/non-conformities", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { projectId, status, type, severity, limit = 50 } = req.query;
|
||||
const { projectId, status, type, severity, limit = 50, offset = 0 } = req.query;
|
||||
|
||||
let conditions = [];
|
||||
if (projectId) conditions.push(eq(qualityNonConformities.projectId, Number(projectId)));
|
||||
|
|
@ -142,7 +151,7 @@ router.get("/non-conformities", async (req: Request, res: Response) => {
|
|||
.from(qualityNonConformities)
|
||||
.where(conditions.length > 0 ? and(...conditions) : undefined)
|
||||
.orderBy(desc(qualityNonConformities.createdAt))
|
||||
.limit(Number(limit));
|
||||
.limit(Number(limit)).offset(Number(offset));
|
||||
|
||||
return res.json({ data: rncs });
|
||||
} catch (error) {
|
||||
|
|
@ -201,7 +210,7 @@ router.post("/non-conformities/:id/close", async (req: Request, res: Response) =
|
|||
|
||||
router.get("/documents", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { type, category, status, limit = 50 } = req.query;
|
||||
const { type, category, status, limit = 50, offset = 0 } = req.query;
|
||||
|
||||
let conditions = [];
|
||||
if (type) conditions.push(eq(qualityDocuments.type, type as string));
|
||||
|
|
@ -212,7 +221,7 @@ router.get("/documents", async (req: Request, res: Response) => {
|
|||
.from(qualityDocuments)
|
||||
.where(conditions.length > 0 ? and(...conditions) : undefined)
|
||||
.orderBy(desc(qualityDocuments.createdAt))
|
||||
.limit(Number(limit));
|
||||
.limit(Number(limit)).offset(Number(offset));
|
||||
|
||||
return res.json({ data: docs });
|
||||
} catch (error) {
|
||||
|
|
@ -302,7 +311,7 @@ router.get("/documents/:id/revisions", async (req: Request, res: Response) => {
|
|||
|
||||
router.get("/field-forms", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { projectId, formType, status, limit = 50 } = req.query;
|
||||
const { projectId, formType, status, limit = 50, offset = 0 } = req.query;
|
||||
|
||||
let conditions = [];
|
||||
if (projectId) conditions.push(eq(qualityFieldForms.projectId, Number(projectId)));
|
||||
|
|
@ -313,7 +322,7 @@ router.get("/field-forms", async (req: Request, res: Response) => {
|
|||
.from(qualityFieldForms)
|
||||
.where(conditions.length > 0 ? and(...conditions) : undefined)
|
||||
.orderBy(desc(qualityFieldForms.createdAt))
|
||||
.limit(Number(limit));
|
||||
.limit(Number(limit)).offset(Number(offset));
|
||||
|
||||
return res.json({ data: forms });
|
||||
} catch (error) {
|
||||
|
|
@ -361,7 +370,7 @@ router.get("/training-matrix", async (req: Request, res: Response) => {
|
|||
.from(qualityTrainingMatrix)
|
||||
.where(conditions.length > 0 ? and(...conditions) : undefined)
|
||||
.orderBy(desc(qualityTrainingMatrix.createdAt))
|
||||
.limit(Number(limit));
|
||||
.limit(Number(limit)).offset(Number(offset));
|
||||
|
||||
return res.json({ data: trainings });
|
||||
} catch (error) {
|
||||
|
|
@ -403,7 +412,7 @@ router.get("/training-matrix/expiring", async (req: Request, res: Response) => {
|
|||
|
||||
router.get("/field-expenses", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { projectId, responsibleId, status, category, limit = 50 } = req.query;
|
||||
const { projectId, responsibleId, status, category, limit = 50, offset = 0 } = req.query;
|
||||
|
||||
let conditions = [];
|
||||
if (projectId) conditions.push(eq(fieldExpenses.projectId, Number(projectId)));
|
||||
|
|
@ -415,7 +424,7 @@ router.get("/field-expenses", async (req: Request, res: Response) => {
|
|||
.from(fieldExpenses)
|
||||
.where(conditions.length > 0 ? and(...conditions) : undefined)
|
||||
.orderBy(desc(fieldExpenses.createdAt))
|
||||
.limit(Number(limit));
|
||||
.limit(Number(limit)).offset(Number(offset));
|
||||
|
||||
return res.json({ data: expenses });
|
||||
} catch (error) {
|
||||
|
|
@ -493,7 +502,7 @@ router.post("/field-expenses/:id/reject", async (req: Request, res: Response) =>
|
|||
|
||||
router.get("/homologated-suppliers", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { status, certification, limit = 50 } = req.query;
|
||||
const { status, certification, limit = 50, offset = 0 } = req.query;
|
||||
|
||||
let conditions = [eq(suppliers.isHomologated, 1)];
|
||||
if (status) conditions.push(eq(suppliers.homologationStatus, status as string));
|
||||
|
|
@ -502,7 +511,7 @@ router.get("/homologated-suppliers", async (req: Request, res: Response) => {
|
|||
.from(suppliers)
|
||||
.where(and(...conditions))
|
||||
.orderBy(desc(suppliers.qualityScore))
|
||||
.limit(Number(limit));
|
||||
.limit(Number(limit)).offset(Number(offset));
|
||||
|
||||
return res.json({ data: suppliersList });
|
||||
} catch (error) {
|
||||
|
|
@ -631,7 +640,7 @@ router.get("/services", async (req: Request, res: Response) => {
|
|||
.from(environmentalServices)
|
||||
.where(conditions.length > 0 ? and(...conditions) : undefined)
|
||||
.orderBy(environmentalServices.category, environmentalServices.name)
|
||||
.limit(Number(limit));
|
||||
.limit(Number(limit)).offset(Number(offset));
|
||||
|
||||
return res.json({ data: services });
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -136,7 +136,10 @@ export async function registerRoutes(
|
|||
// Arcádia Plus - SSO routes (proxy already registered at top)
|
||||
app.use("/api/plus/sso", plusSsoRoutes);
|
||||
|
||||
app.get("/api/tenants", async (_req, res) => {
|
||||
app.get("/api/tenants", async (req: any, res) => {
|
||||
if (!req.isAuthenticated()) {
|
||||
return res.status(401).json({ error: "Authentication required" });
|
||||
}
|
||||
try {
|
||||
const tenants = await storage.getTenants();
|
||||
res.json(tenants);
|
||||
|
|
|
|||
|
|
@ -229,7 +229,7 @@ export function registerWhatsappRoutes(app: Express): void {
|
|||
|
||||
const userId = req.user!.id;
|
||||
const config = req.body;
|
||||
whatsappService.setAutoReplyConfig(userId, config);
|
||||
await whatsappService.setAutoReplyConfig(userId, config);
|
||||
res.json({ success: true, config: whatsappService.getAutoReplyConfig(userId) });
|
||||
} catch (error) {
|
||||
console.error("WhatsApp set auto-reply config error:", error);
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { EventEmitter } from "events";
|
|||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { db } from "../../db/index";
|
||||
import { whatsappContacts, whatsappMessages, whatsappTickets, graphNodes, graphEdges, chatThreads, chatParticipants, chatMessages, pcCrmLeads, tenants } from "@shared/schema";
|
||||
import { whatsappContacts, whatsappMessages, whatsappTickets, whatsappSessions, graphNodes, graphEdges, chatThreads, chatParticipants, chatMessages, pcCrmLeads, tenants } from "@shared/schema";
|
||||
import { eq, and, desc, sql } from "drizzle-orm";
|
||||
import { learningService } from "../learning/service";
|
||||
import OpenAI from "openai";
|
||||
|
|
@ -57,8 +57,17 @@ class WhatsAppService extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
setAutoReplyConfig(userId: string, config: Partial<AutoReplyConfig>): void {
|
||||
const existing = this.autoReplyConfigs.get(userId) || {
|
||||
async setAutoReplyConfig(userId: string, config: Partial<AutoReplyConfig>): Promise<void> {
|
||||
const existing = this.autoReplyConfigs.get(userId) || await this.loadAutoReplyConfig(userId);
|
||||
const merged = { ...existing, ...config };
|
||||
this.autoReplyConfigs.set(userId, merged);
|
||||
await db.update(whatsappSessions)
|
||||
.set({ autoReplyConfig: merged })
|
||||
.where(eq(whatsappSessions.userId, userId));
|
||||
}
|
||||
|
||||
async loadAutoReplyConfig(userId: string): Promise<AutoReplyConfig> {
|
||||
const defaults: AutoReplyConfig = {
|
||||
enabled: false,
|
||||
welcomeMessage: "Olá! Obrigado por entrar em contato. Em breve um atendente irá te responder.",
|
||||
businessHours: { start: 8, end: 18 },
|
||||
|
|
@ -66,7 +75,16 @@ class WhatsAppService extends EventEmitter {
|
|||
aiEnabled: true,
|
||||
maxAutoRepliesPerContact: 3,
|
||||
};
|
||||
this.autoReplyConfigs.set(userId, { ...existing, ...config });
|
||||
try {
|
||||
const [session] = await db.select({ autoReplyConfig: whatsappSessions.autoReplyConfig })
|
||||
.from(whatsappSessions)
|
||||
.where(eq(whatsappSessions.userId, userId))
|
||||
.limit(1);
|
||||
if (session?.autoReplyConfig) {
|
||||
return { ...defaults, ...(session.autoReplyConfig as Partial<AutoReplyConfig>) };
|
||||
}
|
||||
} catch {}
|
||||
return defaults;
|
||||
}
|
||||
|
||||
getAutoReplyConfig(userId: string): AutoReplyConfig {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,15 @@ import { sql } from "drizzle-orm";
|
|||
|
||||
const router = Router();
|
||||
|
||||
function requireAuth(req: any, res: any, next: any) {
|
||||
if (!req.isAuthenticated()) {
|
||||
return res.status(401).json({ error: "Authentication required" });
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
router.use(requireAuth);
|
||||
|
||||
// ========== CONTACTS ==========
|
||||
|
||||
router.get("/contacts", async (req: Request, res: Response) => {
|
||||
|
|
@ -325,7 +334,7 @@ router.delete("/deals/:id", async (req: Request, res: Response) => {
|
|||
|
||||
router.get("/conversations", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { status, channel, assigned_to, queue_id } = req.query;
|
||||
const { status, channel, assigned_to, queue_id, limit = 50, offset = 0 } = req.query;
|
||||
|
||||
let query = sql`
|
||||
SELECT cv.*,
|
||||
|
|
@ -343,7 +352,7 @@ router.get("/conversations", async (req: Request, res: Response) => {
|
|||
if (assigned_to) query = sql`${query} AND cv.assigned_to = ${assigned_to}`;
|
||||
if (queue_id) query = sql`${query} AND cv.queue_id = ${parseInt(queue_id as string)}`;
|
||||
|
||||
query = sql`${query} ORDER BY cv.updated_at DESC LIMIT 50`;
|
||||
query = sql`${query} ORDER BY cv.updated_at DESC LIMIT ${parseInt(limit as string)} OFFSET ${parseInt(offset as string)}`;
|
||||
|
||||
const result = await db.execute(query);
|
||||
res.json(result.rows || result);
|
||||
|
|
@ -373,7 +382,7 @@ router.get("/conversations/:id/messages", async (req: Request, res: Response) =>
|
|||
|
||||
router.get("/tickets", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { status, priority, assigned_to } = req.query;
|
||||
const { status, priority, assigned_to, limit = 50, offset = 0 } = req.query;
|
||||
|
||||
let query = sql`
|
||||
SELECT t.*,
|
||||
|
|
@ -387,7 +396,7 @@ router.get("/tickets", async (req: Request, res: Response) => {
|
|||
if (priority) query = sql`${query} AND t.priority = ${priority}`;
|
||||
if (assigned_to) query = sql`${query} AND t.assigned_to = ${assigned_to}`;
|
||||
|
||||
query = sql`${query} ORDER BY t.created_at DESC LIMIT 50`;
|
||||
query = sql`${query} ORDER BY t.created_at DESC LIMIT ${parseInt(limit as string)} OFFSET ${parseInt(offset as string)}`;
|
||||
|
||||
const result = await db.execute(query);
|
||||
res.json(result.rows || result);
|
||||
|
|
@ -423,7 +432,7 @@ router.post("/tickets", async (req: Request, res: Response) => {
|
|||
|
||||
router.get("/activities", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { contact_id, deal_id, type, status } = req.query;
|
||||
const { contact_id, deal_id, type, status, limit = 50, offset = 0 } = req.query;
|
||||
|
||||
let query = sql`
|
||||
SELECT a.*,
|
||||
|
|
@ -440,7 +449,7 @@ router.get("/activities", async (req: Request, res: Response) => {
|
|||
if (type) query = sql`${query} AND a.type = ${type}`;
|
||||
if (status) query = sql`${query} AND a.status = ${status}`;
|
||||
|
||||
query = sql`${query} ORDER BY a.due_at ASC NULLS LAST, a.created_at DESC LIMIT 50`;
|
||||
query = sql`${query} ORDER BY a.due_at ASC NULLS LAST, a.created_at DESC LIMIT ${parseInt(limit as string)} OFFSET ${parseInt(offset as string)}`;
|
||||
|
||||
const result = await db.execute(query);
|
||||
res.json(result.rows || result);
|
||||
|
|
|
|||
|
|
@ -158,6 +158,7 @@ export type InsertExternalAppPermission = z.infer<typeof insertExternalAppPermis
|
|||
// ========== PRODUCTIVITY HUB - Pages & Blocks (Notion-style) ==========
|
||||
export const workspacePages = pgTable("workspace_pages", {
|
||||
id: serial("id").primaryKey(),
|
||||
tenantId: integer("tenant_id").references(() => tenants.id, { onDelete: "cascade" }),
|
||||
userId: varchar("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
parentId: integer("parent_id"), // for nested pages
|
||||
title: text("title").notNull().default("Sem título"),
|
||||
|
|
@ -207,6 +208,7 @@ export const dashboardWidgets = pgTable("dashboard_widgets", {
|
|||
// ========== QUICK NOTES (Scratch Pad) ==========
|
||||
export const quickNotes = pgTable("quick_notes", {
|
||||
id: serial("id").primaryKey(),
|
||||
tenantId: integer("tenant_id").references(() => tenants.id, { onDelete: "cascade" }),
|
||||
userId: varchar("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
content: text("content").notNull(),
|
||||
isPinned: integer("is_pinned").default(0),
|
||||
|
|
@ -218,6 +220,7 @@ export const quickNotes = pgTable("quick_notes", {
|
|||
// ========== UNIFIED INBOX / ACTIVITY FEED ==========
|
||||
export const activityFeed = pgTable("activity_feed", {
|
||||
id: serial("id").primaryKey(),
|
||||
tenantId: integer("tenant_id").references(() => tenants.id, { onDelete: "cascade" }),
|
||||
userId: varchar("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
actorId: varchar("actor_id").references(() => users.id), // who performed the action
|
||||
type: text("type").notNull(), // created, updated, deleted, mentioned, assigned, completed, commented
|
||||
|
|
@ -281,6 +284,7 @@ export type InsertCommandHistoryEntry = z.infer<typeof insertCommandHistorySchem
|
|||
|
||||
export const conversations = pgTable("conversations", {
|
||||
id: serial("id").primaryKey(),
|
||||
tenantId: integer("tenant_id").references(() => tenants.id, { onDelete: "cascade" }),
|
||||
userId: varchar("user_id").references(() => users.id, { onDelete: "cascade" }),
|
||||
title: text("title").notNull(),
|
||||
createdAt: timestamp("created_at").default(sql`CURRENT_TIMESTAMP`).notNull(),
|
||||
|
|
@ -312,6 +316,7 @@ export type InsertMessage = z.infer<typeof insertMessageSchema>;
|
|||
|
||||
export const knowledgeBase = pgTable("knowledge_base", {
|
||||
id: serial("id").primaryKey(),
|
||||
tenantId: integer("tenant_id").references(() => tenants.id, { onDelete: "cascade" }),
|
||||
title: text("title").notNull(),
|
||||
content: text("content").notNull(),
|
||||
author: text("author").notNull(),
|
||||
|
|
@ -409,6 +414,7 @@ export type InsertTaskExecution = z.infer<typeof insertTaskExecutionSchema>;
|
|||
|
||||
export const chatThreads = pgTable("chat_threads", {
|
||||
id: serial("id").primaryKey(),
|
||||
tenantId: integer("tenant_id").references(() => tenants.id, { onDelete: "cascade" }),
|
||||
type: text("type").notNull().default("direct"),
|
||||
name: text("name"),
|
||||
createdBy: varchar("created_by").references(() => users.id, { onDelete: "set null" }),
|
||||
|
|
@ -443,6 +449,7 @@ export const whatsappSessions = pgTable("whatsapp_sessions", {
|
|||
status: text("status").default("disconnected"),
|
||||
phoneNumber: text("phone_number"),
|
||||
lastSync: timestamp("last_sync"),
|
||||
autoReplyConfig: jsonb("auto_reply_config").$type<Record<string, any>>(),
|
||||
createdAt: timestamp("created_at").default(sql`CURRENT_TIMESTAMP`).notNull(),
|
||||
});
|
||||
|
||||
|
|
@ -530,6 +537,7 @@ export type InsertWhatsappTicket = z.infer<typeof insertWhatsappTicketSchema>;
|
|||
|
||||
export const manusRuns = pgTable("manus_runs", {
|
||||
id: serial("id").primaryKey(),
|
||||
tenantId: integer("tenant_id").references(() => tenants.id, { onDelete: "cascade" }),
|
||||
userId: varchar("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
prompt: text("prompt").notNull(),
|
||||
status: text("status").default("running"),
|
||||
|
|
|
|||
Loading…
Reference in New Issue