2130 lines
60 KiB
Markdown
2130 lines
60 KiB
Markdown
# AutomatizaSE Portal - Documento de Arquitetura Full-Stack
|
||
|
||
## Introdução
|
||
|
||
Este documento define a arquitetura completa full-stack do **AutomatizaSE Portal**, incluindo sistemas backend, implementação frontend e suas integrações. Serve como fonte única de verdade para desenvolvimento orientado por IA, garantindo consistência em toda a stack tecnológica.
|
||
|
||
Esta abordagem unificada combina o que tradicionalmente seriam documentos separados de arquitetura backend e frontend, otimizando o processo de desenvolvimento para aplicações fullstack modernas onde essas preocupações estão cada vez mais interligadas.
|
||
|
||
### Starter Template ou Projeto Existente
|
||
|
||
**NextJS 14+ App Router** - Projeto greenfield baseado no template oficial do NextJS com App Router, TailwindCSS e TypeScript. Aproveita infraestrutura Supabase existente da AutomatizaSE (auth unificado, database PostgreSQL).
|
||
|
||
**Constraints:**
|
||
- Schema `portal` separado do schema `public` existente no Supabase
|
||
- SMTP da AutomatizaSE já configurado no Supabase para recuperação de senha
|
||
- EvolutionAPI existente deve ser integrada via REST API
|
||
- OAuth do Google Calendar delegado ao n8n (workflow existente)
|
||
|
||
### Change Log
|
||
|
||
| Date | Version | Description | Author |
|
||
|------------|---------|--------------------------------------------------|--------------------|
|
||
| 2025-10-05 | 1.0 | Versão inicial da arquitetura | Winston (Architect)|
|
||
| 2025-10-05 | 1.1 | Atualizado para Docker + K8s + ArgoCD (não Vercel) | Winston (Architect)|
|
||
|
||
---
|
||
|
||
## Arquitetura de Alto Nível
|
||
|
||
### Resumo Técnico
|
||
|
||
O AutomatizaSE Portal é uma **POC simples** com NextJS 14+ App Router containerizado em **Docker**, deployado em **Kubernetes** usando manifests básicos. Utiliza **Supabase** como backend completo (auth, database PostgreSQL, recuperação de senha). O frontend NextJS renderiza páginas SSR e se comunica com API Routes que orquestram chamadas à EvolutionAPI (REST) e Supabase. OAuth do Google Calendar é delegado ao n8n. A aplicação roda em container Docker, exposta via Nginx Ingress. **Arquitetura minimalista para deploy rápido em poucas horas.**
|
||
|
||
### Escolha de Plataforma e Infraestrutura
|
||
|
||
**Plataforma:** Kubernetes + Docker + Supabase **(POC Simplificada)**
|
||
|
||
**Componentes Essenciais:**
|
||
- **Docker:** Containerização da aplicação NextJS
|
||
- **Kubernetes:** Orquestração básica (deployment + service + ingress)
|
||
- **Nginx Ingress:** Roteamento HTTP para o portal
|
||
- **Supabase:** Auth + PostgreSQL (schema `portal`) + SMTP
|
||
- **EvolutionAPI:** Integração REST WhatsApp (externo)
|
||
- **n8n:** OAuth Google Calendar (externo)
|
||
|
||
**Topologia Simples:**
|
||
- **Namespace:** `automatizase-portal`
|
||
- **Replicas:** 1 pod (POC, sem HA)
|
||
- **Ingress:** HTTP via Nginx Ingress, domínio `portal.automatizase.com` (HTTPS opcional)
|
||
- **Recursos:** Limits mínimos (256Mi RAM, 100m CPU)
|
||
|
||
**Justificativa:**
|
||
- **Simplicidade:** Setup rápido em poucas horas
|
||
- **Mínimo viável:** Apenas o necessário para rodar NextJS em K8s
|
||
- **Supabase gerenciado:** Reduz complexidade de backend
|
||
|
||
### Estrutura do Repositório
|
||
|
||
**Estrutura:** Single NextJS App (monorepo não necessário para POC)
|
||
|
||
**Ferramenta Monorepo:** N/A
|
||
|
||
**Organização de Pacotes:**
|
||
- Projeto NextJS único contendo frontend (`/app`, `/components`) e API routes (`/app/api`)
|
||
- Shared types em `/types` (TypeScript interfaces compartilhadas entre frontend e backend)
|
||
- Libs utilitárias em `/lib` (Supabase client, EvolutionAPI client, helpers)
|
||
|
||
**Justificativa:** Para POC simplificada, monorepo com múltiplos packages adiciona complexidade desnecessária. Single app NextJS oferece estrutura clara e setup rápido.
|
||
|
||
### Diagrama de Arquitetura de Alto Nível (POC Simplificada)
|
||
|
||
```mermaid
|
||
graph TB
|
||
User[Usuário<br/>Mobile/Desktop] -->|HTTP| Ingress[Nginx Ingress<br/>portal.automatizase.com]
|
||
|
||
subgraph K8s["Kubernetes Cluster"]
|
||
Ingress -->|Route| Service[Service<br/>ClusterIP:80]
|
||
Service --> Pod[Pod<br/>NextJS Container<br/>porta 3000]
|
||
|
||
Pod -->|Auth + DB| Supabase
|
||
Pod -->|API Calls| EvolutionAPI
|
||
Pod -->|OAuth Redirect| n8n
|
||
end
|
||
|
||
subgraph External["Serviços Externos"]
|
||
Supabase[(Supabase<br/>Auth + PostgreSQL)]
|
||
EvolutionAPI[EvolutionAPI<br/>WhatsApp]
|
||
n8n[n8n<br/>Google Calendar OAuth]
|
||
end
|
||
|
||
Supabase -->|SMTP| Email[SMTP]
|
||
n8n -->|Callback| Service
|
||
|
||
style User fill:#4A90E2
|
||
style Ingress fill:#009639
|
||
style Service fill:#326CE5
|
||
style Pod fill:#4A90E2
|
||
style Supabase fill:#3ECF8E
|
||
style EvolutionAPI fill:#25D366
|
||
style n8n fill:#FF6D5A
|
||
```
|
||
|
||
**Fluxo Simplificado:**
|
||
1. Usuário acessa `portal.automatizase.com`
|
||
2. Nginx Ingress roteia para Service K8s (porta 80)
|
||
3. Service encaminha para Pod NextJS (porta 3000)
|
||
4. NextJS se comunica com Supabase (auth/DB) e EvolutionAPI (WhatsApp)
|
||
|
||
### Padrões Arquiteturais (Simplificados para POC)
|
||
|
||
- **Containerized App:** NextJS em Docker, deploy via K8s manifests simples
|
||
- **Component-Based UI:** Componentes React com TailwindCSS (Shadcn/ui)
|
||
- **API Routes (BFF):** `/app/api/*` centralizam comunicação com EvolutionAPI e Supabase
|
||
- **OAuth Delegation:** Google Calendar via n8n (evita gerenciar credenciais)
|
||
- **Config via Env Vars:** Secrets e configs via K8s ConfigMap/Secret
|
||
|
||
---
|
||
|
||
## Containerização e Orquestração
|
||
|
||
### Dockerfile (Simplificado para POC)
|
||
|
||
```dockerfile
|
||
# Dockerfile simples para NextJS
|
||
FROM node:18-alpine
|
||
|
||
WORKDIR /app
|
||
|
||
# Copiar package files
|
||
COPY package*.json ./
|
||
|
||
# Install dependencies
|
||
RUN npm ci
|
||
|
||
# Copiar código
|
||
COPY . .
|
||
|
||
# Build Next.js
|
||
ENV NEXT_TELEMETRY_DISABLED=1
|
||
RUN npm run build
|
||
|
||
# Expor porta 3000
|
||
EXPOSE 3000
|
||
|
||
# Start NextJS
|
||
CMD ["npm", "start"]
|
||
```
|
||
|
||
**Notas para POC:**
|
||
- Build simples sem multi-stage (mais rápido para POC)
|
||
- Porta 3000 (padrão NextJS)
|
||
- Comando `npm start` roda servidor production
|
||
|
||
**Build Rápido:**
|
||
```bash
|
||
# Build
|
||
docker build -t portal:latest .
|
||
|
||
# Test local (opcional)
|
||
docker run -p 3000:3000 --env-file .env.local portal:latest
|
||
|
||
# Tag e push para registry
|
||
docker tag portal:latest registry.automatizase.com/portal:latest
|
||
docker push registry.automatizase.com/portal:latest
|
||
```
|
||
|
||
---
|
||
|
||
### Kubernetes Manifests (POC Mínima)
|
||
|
||
#### 1. Namespace
|
||
|
||
```yaml
|
||
# k8s/namespace.yaml
|
||
apiVersion: v1
|
||
kind: Namespace
|
||
metadata:
|
||
name: automatizase-portal
|
||
```
|
||
|
||
---
|
||
|
||
#### 2. ConfigMap
|
||
|
||
```yaml
|
||
# k8s/configmap.yaml
|
||
apiVersion: v1
|
||
kind: ConfigMap
|
||
metadata:
|
||
name: portal-config
|
||
namespace: automatizase-portal
|
||
data:
|
||
NODE_ENV: "production"
|
||
NEXT_PUBLIC_SITE_URL: "http://portal.automatizase.com"
|
||
NEXT_PUBLIC_SUPABASE_URL: "https://xxxxx.supabase.co"
|
||
```
|
||
|
||
---
|
||
|
||
#### 3. Secret
|
||
|
||
```yaml
|
||
# k8s/secret.yaml
|
||
# ATENÇÃO: NÃO commitar este arquivo! Criar manualmente no cluster
|
||
apiVersion: v1
|
||
kind: Secret
|
||
metadata:
|
||
name: portal-secrets
|
||
namespace: automatizase-portal
|
||
type: Opaque
|
||
stringData:
|
||
NEXT_PUBLIC_SUPABASE_ANON_KEY: "eyJhbGci..."
|
||
SUPABASE_SERVICE_ROLE_KEY: "eyJhbGci..."
|
||
EVOLUTION_API_URL: "https://evolution.automatizase.com"
|
||
EVOLUTION_API_KEY: "your-key"
|
||
EVOLUTION_INSTANCE_NAMES: "instance1,instance2"
|
||
N8N_OAUTH_URL: "https://n8n.automatizase.com/webhook/google-oauth"
|
||
```
|
||
|
||
**Criar secret:**
|
||
```bash
|
||
kubectl create secret generic portal-secrets \
|
||
--from-literal=NEXT_PUBLIC_SUPABASE_ANON_KEY='...' \
|
||
--from-literal=SUPABASE_SERVICE_ROLE_KEY='...' \
|
||
--from-literal=EVOLUTION_API_URL='...' \
|
||
--from-literal=EVOLUTION_API_KEY='...' \
|
||
--from-literal=EVOLUTION_INSTANCE_NAMES='...' \
|
||
--from-literal=N8N_OAUTH_URL='...' \
|
||
-n automatizase-portal
|
||
```
|
||
|
||
---
|
||
|
||
#### 4. Deployment
|
||
|
||
```yaml
|
||
# k8s/deployment.yaml
|
||
apiVersion: apps/v1
|
||
kind: Deployment
|
||
metadata:
|
||
name: portal
|
||
namespace: automatizase-portal
|
||
spec:
|
||
replicas: 1 # POC: 1 replica suficiente
|
||
selector:
|
||
matchLabels:
|
||
app: portal
|
||
template:
|
||
metadata:
|
||
labels:
|
||
app: portal
|
||
spec:
|
||
containers:
|
||
- name: nextjs
|
||
image: registry.automatizase.com/portal:latest
|
||
ports:
|
||
- containerPort: 3000
|
||
envFrom:
|
||
- configMapRef:
|
||
name: portal-config
|
||
- secretRef:
|
||
name: portal-secrets
|
||
resources:
|
||
requests:
|
||
memory: "128Mi"
|
||
cpu: "50m"
|
||
limits:
|
||
memory: "256Mi"
|
||
cpu: "200m"
|
||
```
|
||
|
||
---
|
||
|
||
#### 5. Service
|
||
|
||
```yaml
|
||
# k8s/service.yaml
|
||
apiVersion: v1
|
||
kind: Service
|
||
metadata:
|
||
name: portal-service
|
||
namespace: automatizase-portal
|
||
spec:
|
||
type: ClusterIP
|
||
selector:
|
||
app: portal
|
||
ports:
|
||
- port: 80
|
||
targetPort: 3000
|
||
```
|
||
|
||
---
|
||
|
||
#### 6. Ingress (Nginx)
|
||
|
||
```yaml
|
||
# k8s/ingress.yaml
|
||
apiVersion: networking.k8s.io/v1
|
||
kind: Ingress
|
||
metadata:
|
||
name: portal-ingress
|
||
namespace: automatizase-portal
|
||
annotations:
|
||
nginx.ingress.kubernetes.io/rewrite-target: /
|
||
spec:
|
||
ingressClassName: nginx
|
||
rules:
|
||
- host: portal.automatizase.com
|
||
http:
|
||
paths:
|
||
- path: /
|
||
pathType: Prefix
|
||
backend:
|
||
service:
|
||
name: portal-service
|
||
port:
|
||
number: 80
|
||
```
|
||
|
||
---
|
||
|
||
### Deploy Rápido (POC)
|
||
|
||
**Deploy Manual via kubectl:**
|
||
|
||
```bash
|
||
# 1. Criar namespace
|
||
kubectl apply -f k8s/namespace.yaml
|
||
|
||
# 2. Criar ConfigMap
|
||
kubectl apply -f k8s/configmap.yaml
|
||
|
||
# 3. Criar Secret (manualmente com valores reais)
|
||
kubectl create secret generic portal-secrets \
|
||
--from-literal=NEXT_PUBLIC_SUPABASE_ANON_KEY='seu-anon-key' \
|
||
--from-literal=SUPABASE_SERVICE_ROLE_KEY='seu-service-role-key' \
|
||
--from-literal=EVOLUTION_API_URL='https://evolution.automatizase.com' \
|
||
--from-literal=EVOLUTION_API_KEY='sua-api-key' \
|
||
--from-literal=EVOLUTION_INSTANCE_NAMES='instance1,instance2' \
|
||
--from-literal=N8N_OAUTH_URL='https://n8n.automatizase.com/webhook/google-oauth' \
|
||
-n automatizase-portal
|
||
|
||
# 4. Deploy app
|
||
kubectl apply -f k8s/deployment.yaml
|
||
|
||
# 5. Criar Service
|
||
kubectl apply -f k8s/service.yaml
|
||
|
||
# 6. Criar Ingress
|
||
kubectl apply -f k8s/ingress.yaml
|
||
|
||
# 7. Verificar
|
||
kubectl get pods -n automatizase-portal
|
||
kubectl get svc -n automatizase-portal
|
||
kubectl get ingress -n automatizase-portal
|
||
|
||
# 8. Ver logs
|
||
kubectl logs -f deployment/portal -n automatizase-portal
|
||
```
|
||
|
||
**Deploy Completo (1 comando):**
|
||
```bash
|
||
# Aplicar todos manifests de uma vez
|
||
kubectl apply -f k8s/
|
||
|
||
# Aguardar pod ficar ready
|
||
kubectl wait --for=condition=ready pod -l app=portal -n automatizase-portal --timeout=120s
|
||
```
|
||
|
||
---
|
||
|
||
## Tech Stack
|
||
|
||
| Categoria | Tecnologia | Versão | Propósito | Justificativa |
|
||
|-------------------------|------------------|--------------|----------------------------------------------|-------------------------------------------------------------------------------|
|
||
| Containerização | Docker | 24+ | Build e runtime de containers | Reprodutibilidade, portabilidade |
|
||
| Orquestração | Kubernetes | 1.28+ | Deploy e orquestração | Padrão de mercado, self-healing |
|
||
| Ingress Controller | Nginx Ingress | Latest | Roteamento HTTP | Simples, direto ao ponto |
|
||
| Linguagem Frontend | TypeScript | 5.3+ | Código frontend type-safe | Type safety reduz bugs, melhora DX com autocomplete |
|
||
| Framework Frontend | Next.js | 14.2+ | Framework React SSR | App Router moderno, API Routes integradas, standalone output para containers |
|
||
| Biblioteca UI | Shadcn/ui | Latest | Componentes React acessíveis | TailwindCSS-based, customizável, tema escuro nativo, WCAG AA |
|
||
| Gerenciamento de Estado | React Hooks | (built-in) | Estado local e context | Simplicidade para POC, Context API para auth global |
|
||
| Linguagem Backend | TypeScript | 5.3+ | API Routes type-safe | Shared types com frontend, consistência de linguagem |
|
||
| Framework Backend | Next.js API Routes| 14.2+ | Endpoints API | Zero config, stateless (K8s-friendly) |
|
||
| Estilo de API | REST | - | HTTP JSON APIs | Simplicidade, EvolutionAPI é REST, sem overhead de GraphQL |
|
||
| Banco de Dados | PostgreSQL (Supabase)| 15+ | Database relacional | Schema `portal` isolado, RLS nativo, Supabase gerenciado |
|
||
| Cache | Nginx (Ingress) | - | Static assets caching | Cache de assets via Nginx, Redis futuro para app cache |
|
||
| Armazenamento de Arquivos| N/A | - | Não requerido na POC | Apenas QR codes (base64 inline) |
|
||
| Autenticação | Supabase Auth | Latest | Email/senha, recuperação senha | Auth unificado existente, SMTP configurado, RLS integrado |
|
||
| Testes Frontend | Vitest + React Testing Library | Latest | Unit tests componentes | Fast, Vite-based, melhor que Jest |
|
||
| Testes Backend | Vitest | Latest | Unit tests API Routes | Mesma ferramenta que frontend, mocking fácil |
|
||
| Testes E2E | Playwright | Latest | Testes end-to-end | Multi-browser, fast, trace viewer excelente |
|
||
| Build Tool | Turbopack (Next) | (built-in) | Fast refresh e builds | Next.js 14+ default |
|
||
| Bundler | Webpack (Next) | (built-in) | Production builds | Next.js production bundler |
|
||
| CI/CD | Manual (kubectl) | - | Deploy manual | POC: kubectl apply, sem CI/CD complexo |
|
||
| Monitoramento | kubectl logs | - | Logs básicos | POC: logs via kubectl, sem Prometheus/Grafana |
|
||
| Framework CSS | TailwindCSS | 3.4+ | Utility-first CSS | Tema escuro fácil, design system AutomatizaSE, Shadcn/ui compatible |
|
||
|
||
---
|
||
|
||
## Modelos de Dados
|
||
|
||
### User
|
||
|
||
**Propósito:** Representa usuário autenticado do portal (gerenciado pelo Supabase Auth, tabela `auth.users`)
|
||
|
||
**Atributos Principais:**
|
||
- `id`: UUID - ID único do usuário (primary key Supabase Auth)
|
||
- `email`: string - Email do usuário (usado para login)
|
||
- `created_at`: timestamp - Data de criação da conta
|
||
- `updated_at`: timestamp - Última atualização
|
||
|
||
#### Interface TypeScript
|
||
|
||
```typescript
|
||
// Baseado em Supabase Auth User
|
||
interface User {
|
||
id: string; // UUID
|
||
email: string;
|
||
created_at: string; // ISO timestamp
|
||
updated_at: string; // ISO timestamp
|
||
}
|
||
```
|
||
|
||
#### Relacionamentos
|
||
|
||
- One-to-One com `UserSettings`
|
||
- One-to-Many com `GoogleCalendarIntegration`
|
||
|
||
---
|
||
|
||
### UserSettings
|
||
|
||
**Propósito:** Configurações e preferências do usuário específicas do portal
|
||
|
||
**Atributos Principais:**
|
||
- `user_id`: UUID - FK para `auth.users.id`
|
||
- `created_at`: timestamp - Data de criação
|
||
- `updated_at`: timestamp - Última atualização
|
||
|
||
#### Interface TypeScript
|
||
|
||
```typescript
|
||
interface UserSettings {
|
||
user_id: string; // UUID, FK to auth.users
|
||
created_at: string;
|
||
updated_at: string;
|
||
}
|
||
```
|
||
|
||
#### Relacionamentos
|
||
|
||
- Belongs-to `User` (auth.users)
|
||
|
||
---
|
||
|
||
### GoogleCalendarIntegration
|
||
|
||
**Propósito:** Armazena status de autorização OAuth do Google Calendar por usuário
|
||
|
||
**Atributos Principais:**
|
||
- `id`: UUID - Primary key
|
||
- `user_id`: UUID - FK para `auth.users.id`
|
||
- `is_connected`: boolean - Status de conexão
|
||
- `connected_email`: string | null - Email da conta Google autorizada
|
||
- `connected_at`: timestamp | null - Data da última autorização
|
||
- `created_at`: timestamp - Data de criação
|
||
- `updated_at`: timestamp - Última atualização
|
||
|
||
#### Interface TypeScript
|
||
|
||
```typescript
|
||
interface GoogleCalendarIntegration {
|
||
id: string; // UUID
|
||
user_id: string; // UUID, FK to auth.users
|
||
is_connected: boolean;
|
||
connected_email: string | null;
|
||
connected_at: string | null; // ISO timestamp
|
||
created_at: string;
|
||
updated_at: string;
|
||
}
|
||
```
|
||
|
||
#### Relacionamentos
|
||
|
||
- Belongs-to `User` (auth.users)
|
||
|
||
---
|
||
|
||
### WhatsAppInstance
|
||
|
||
**Propósito:** Modelo virtual (não armazenado no DB) representando instância EvolutionAPI
|
||
|
||
**Atributos Principais:**
|
||
- `instanceName`: string - Nome da instância
|
||
- `status`: 'connected' | 'disconnected' - Status de conexão
|
||
- `qrCode`: string | null - Base64 do QR code (quando gerado)
|
||
|
||
#### Interface TypeScript
|
||
|
||
```typescript
|
||
interface WhatsAppInstance {
|
||
instanceName: string;
|
||
status: 'connected' | 'disconnected';
|
||
qrCode?: string | null; // Base64 image
|
||
}
|
||
```
|
||
|
||
#### Relacionamentos
|
||
|
||
- Não persistido no Supabase (dados efêmeros da EvolutionAPI)
|
||
|
||
---
|
||
|
||
## Especificação da API
|
||
|
||
### REST API
|
||
|
||
```yaml
|
||
openapi: 3.0.0
|
||
info:
|
||
title: AutomatizaSE Portal API
|
||
version: 1.0.0
|
||
description: API interna do portal (NextJS API Routes)
|
||
servers:
|
||
- url: https://portal.automatizase.com/api
|
||
description: Production
|
||
- url: http://localhost:3000/api
|
||
description: Development
|
||
|
||
paths:
|
||
/api/whatsapp/instances:
|
||
get:
|
||
summary: Lista todas as instâncias WhatsApp
|
||
security:
|
||
- SupabaseAuth: []
|
||
responses:
|
||
'200':
|
||
description: Lista de instâncias com status
|
||
content:
|
||
application/json:
|
||
schema:
|
||
type: array
|
||
items:
|
||
$ref: '#/components/schemas/WhatsAppInstance'
|
||
|
||
/api/whatsapp/instances/{instanceName}/qrcode:
|
||
post:
|
||
summary: Gera QR code de conexão
|
||
parameters:
|
||
- name: instanceName
|
||
in: path
|
||
required: true
|
||
schema:
|
||
type: string
|
||
security:
|
||
- SupabaseAuth: []
|
||
responses:
|
||
'200':
|
||
description: QR code gerado
|
||
content:
|
||
application/json:
|
||
schema:
|
||
type: object
|
||
properties:
|
||
qrCode:
|
||
type: string
|
||
|
||
/api/whatsapp/instances/{instanceName}/disconnect:
|
||
post:
|
||
summary: Desconecta instância WhatsApp
|
||
parameters:
|
||
- name: instanceName
|
||
in: path
|
||
required: true
|
||
schema:
|
||
type: string
|
||
security:
|
||
- SupabaseAuth: []
|
||
responses:
|
||
'200':
|
||
description: Instância desconectada
|
||
|
||
/api/google-calendar/status:
|
||
get:
|
||
summary: Status de conexão do Google Calendar
|
||
security:
|
||
- SupabaseAuth: []
|
||
responses:
|
||
'200':
|
||
description: Status da integração
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/GoogleCalendarStatus'
|
||
|
||
/api/google-calendar/callback:
|
||
get:
|
||
summary: Callback OAuth do n8n
|
||
parameters:
|
||
- name: success
|
||
in: query
|
||
schema:
|
||
type: boolean
|
||
- name: email
|
||
in: query
|
||
schema:
|
||
type: string
|
||
security:
|
||
- SupabaseAuth: []
|
||
responses:
|
||
'302':
|
||
description: Redirect para dashboard
|
||
|
||
components:
|
||
schemas:
|
||
WhatsAppInstance:
|
||
type: object
|
||
properties:
|
||
instanceName:
|
||
type: string
|
||
status:
|
||
type: string
|
||
enum: [connected, disconnected]
|
||
qrCode:
|
||
type: string
|
||
nullable: true
|
||
|
||
GoogleCalendarStatus:
|
||
type: object
|
||
properties:
|
||
is_connected:
|
||
type: boolean
|
||
connected_email:
|
||
type: string
|
||
nullable: true
|
||
connected_at:
|
||
type: string
|
||
format: date-time
|
||
nullable: true
|
||
|
||
securitySchemes:
|
||
SupabaseAuth:
|
||
type: http
|
||
scheme: bearer
|
||
bearerFormat: JWT
|
||
```
|
||
|
||
---
|
||
|
||
## Componentes
|
||
|
||
### NextJS Frontend App
|
||
|
||
**Responsabilidade:** Renderiza UI do portal, gerencia estado da aplicação
|
||
|
||
**Interfaces Principais:**
|
||
- Pages: `/login`, `/reset-password`, `/dashboard`
|
||
- Components: `WhatsAppInstanceCard`, `GoogleCalendarCard`, `Header`, `Footer`
|
||
- Hooks: `useAuth`, `useWhatsAppInstances`, `useGoogleCalendar`
|
||
|
||
**Dependências:** Supabase Auth Client, API Routes, Shadcn/ui
|
||
|
||
**Tech Stack:** Next.js 14, React 18, TypeScript, TailwindCSS, Shadcn/ui
|
||
|
||
---
|
||
|
||
### NextJS API Routes (BFF)
|
||
|
||
**Responsabilidade:** Orquestra chamadas à EvolutionAPI e Supabase, protege API keys
|
||
|
||
**Interfaces Principais:**
|
||
- `/api/whatsapp/instances` - Lista instâncias
|
||
- `/api/whatsapp/instances/[name]/qrcode` - Gera QR code
|
||
- `/api/whatsapp/instances/[name]/disconnect` - Desconecta
|
||
- `/api/google-calendar/status` - Status OAuth
|
||
- `/api/google-calendar/callback` - Callback OAuth
|
||
|
||
**Dependências:** EvolutionAPI client, Supabase client
|
||
|
||
**Tech Stack:** Next.js API Routes, TypeScript, Vercel Edge Functions
|
||
|
||
---
|
||
|
||
### Supabase Backend
|
||
|
||
**Responsabilidade:** Autenticação, autorização, persistência de dados
|
||
|
||
**Interfaces Principais:**
|
||
- Supabase Auth API (login, logout, resetPassword)
|
||
- PostgreSQL schema `portal`
|
||
- Row Level Security policies
|
||
|
||
**Dependências:** PostgreSQL 15+, SMTP AutomatizaSE
|
||
|
||
**Tech Stack:** Supabase (Auth + Database), PostgreSQL
|
||
|
||
---
|
||
|
||
### Diagrama de Componentes
|
||
|
||
```mermaid
|
||
graph TB
|
||
subgraph "Frontend (Vercel)"
|
||
Pages[Pages<br/>/login /dashboard]
|
||
Components[Components<br/>Cards, Modals]
|
||
Hooks[Custom Hooks<br/>useAuth useAPI]
|
||
|
||
Pages --> Components
|
||
Pages --> Hooks
|
||
Components --> Hooks
|
||
end
|
||
|
||
subgraph "Backend (API Routes)"
|
||
WhatsAppAPI[WhatsApp Routes]
|
||
CalendarAPI[Calendar Routes]
|
||
AuthMW[Auth Middleware]
|
||
|
||
WhatsAppAPI --> AuthMW
|
||
CalendarAPI --> AuthMW
|
||
end
|
||
|
||
subgraph "External Services"
|
||
Evolution[EvolutionAPI]
|
||
Supabase[Supabase]
|
||
n8n[n8n OAuth]
|
||
end
|
||
|
||
Hooks --> WhatsAppAPI
|
||
Hooks --> CalendarAPI
|
||
Hooks --> Supabase
|
||
|
||
WhatsAppAPI --> Evolution
|
||
CalendarAPI --> Supabase
|
||
AuthMW --> Supabase
|
||
|
||
Pages --> n8n
|
||
n8n --> CalendarAPI
|
||
|
||
style Pages fill:#4A90E2
|
||
style Components fill:#4A90E2
|
||
style WhatsAppAPI fill:#000
|
||
style CalendarAPI fill:#000
|
||
style Evolution fill:#25D366
|
||
style Supabase fill:#3ECF8E
|
||
style n8n fill:#FF6D5A
|
||
```
|
||
|
||
---
|
||
|
||
## APIs Externas
|
||
|
||
### EvolutionAPI
|
||
|
||
- **Propósito:** Gerenciar instâncias WhatsApp
|
||
- **Documentação:** [EvolutionAPI Docs](https://doc.evolution-api.com/)
|
||
- **Base URL:** `{EVOLUTION_API_URL}` (env var)
|
||
- **Autenticação:** Bearer token `{EVOLUTION_API_KEY}`
|
||
- **Rate Limits:** Não documentado
|
||
|
||
**Endpoints Principais:**
|
||
- `GET /instance/fetchInstances` - Listar instâncias (verificar docs)
|
||
- `GET /instance/connect/{instanceName}` - Gerar QR code (verificar docs)
|
||
- `DELETE /instance/logout/{instanceName}` - Desconectar (verificar docs)
|
||
|
||
**Notas de Integração:**
|
||
- **Investigação necessária:** Confirmar endpoints exatos na documentação oficial
|
||
- **Error handling:** Implementar retry com backoff, timeout 10s
|
||
- **Validação:** Verificar que instâncias em `EVOLUTION_INSTANCE_NAMES` existem
|
||
|
||
---
|
||
|
||
### Google Calendar OAuth (via n8n)
|
||
|
||
- **Propósito:** Autorizar integração com Google Calendar
|
||
- **Documentação:** Workflow n8n interno
|
||
- **Base URL:** `{N8N_OAUTH_URL}` (env var)
|
||
- **Autenticação:** N/A (n8n gerencia)
|
||
- **Rate Limits:** N/A
|
||
|
||
**Endpoints:**
|
||
- `GET {N8N_OAUTH_URL}?user_id={userId}&redirect_uri={callbackURL}` - Inicia OAuth
|
||
|
||
**Notas de Integração:**
|
||
- **Redirect flow:** Portal → n8n → Google → n8n → Portal callback
|
||
- **Callback:** `/api/google-calendar/callback?success=true&email={email}` ou `?error={msg}`
|
||
- **Reconectar:** Mesmo fluxo atualiza email conectado
|
||
|
||
---
|
||
|
||
## Workflows Principais
|
||
|
||
### Login Flow
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
actor User
|
||
participant LoginPage
|
||
participant SupabaseAuth
|
||
participant Dashboard
|
||
|
||
User->>LoginPage: Acessa /login
|
||
LoginPage->>User: Exibe formulário
|
||
User->>LoginPage: Preenche email/senha
|
||
LoginPage->>SupabaseAuth: signInWithPassword()
|
||
alt Sucesso
|
||
SupabaseAuth->>LoginPage: JWT token
|
||
LoginPage->>Dashboard: Redirect /dashboard
|
||
Dashboard->>User: Exibe dashboard
|
||
else Erro
|
||
SupabaseAuth->>LoginPage: Error 401
|
||
LoginPage->>User: "Email ou senha inválidos"
|
||
end
|
||
```
|
||
|
||
---
|
||
|
||
### WhatsApp QR Code Flow
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
actor User
|
||
participant Dashboard
|
||
participant API
|
||
participant EvolutionAPI
|
||
participant Modal
|
||
|
||
User->>Dashboard: Clica "Gerar QR Code"
|
||
Dashboard->>API: POST /api/.../qrcode
|
||
API->>EvolutionAPI: GET /instance/connect/{name}
|
||
alt Sucesso
|
||
EvolutionAPI->>API: QR code (base64)
|
||
API->>Dashboard: { qrCode: "..." }
|
||
Dashboard->>Modal: Exibe QR code
|
||
Modal->>User: Modal com QR
|
||
Note over User,EvolutionAPI: Polling a cada 5s
|
||
EvolutionAPI->>API: Status "connected"
|
||
API->>Dashboard: Status atualizado
|
||
Dashboard->>User: Badge verde
|
||
else Erro
|
||
EvolutionAPI->>API: Error
|
||
API->>Dashboard: Error response
|
||
Dashboard->>User: Toast erro
|
||
end
|
||
```
|
||
|
||
---
|
||
|
||
### Google Calendar OAuth Flow
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
actor User
|
||
participant Dashboard
|
||
participant n8n
|
||
participant Google
|
||
participant Callback
|
||
participant Supabase
|
||
|
||
User->>Dashboard: Clica "Conectar Calendar"
|
||
Dashboard->>n8n: Redirect {N8N_OAUTH_URL}
|
||
n8n->>Google: Redirect OAuth
|
||
User->>Google: Autoriza
|
||
Google->>n8n: Callback com code
|
||
n8n->>n8n: Troca code por token
|
||
n8n->>Callback: Redirect /callback?success=true&email={email}
|
||
Callback->>Supabase: UPDATE google_calendar_integration
|
||
Supabase->>Callback: Success
|
||
Callback->>Dashboard: Redirect /dashboard
|
||
Dashboard->>User: Toast "Conectado!" + Badge verde
|
||
```
|
||
|
||
---
|
||
|
||
## Schema do Banco de Dados
|
||
|
||
### Schema: `portal`
|
||
|
||
```sql
|
||
-- Schema portal
|
||
CREATE SCHEMA IF NOT EXISTS portal;
|
||
|
||
-- Tabela: portal.user_settings
|
||
CREATE TABLE portal.user_settings (
|
||
user_id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
|
||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||
);
|
||
|
||
-- Trigger para auto-update de updated_at
|
||
CREATE OR REPLACE FUNCTION portal.update_updated_at_column()
|
||
RETURNS TRIGGER AS $$
|
||
BEGIN
|
||
NEW.updated_at = NOW();
|
||
RETURN NEW;
|
||
END;
|
||
$$ LANGUAGE plpgsql;
|
||
|
||
CREATE TRIGGER update_user_settings_updated_at
|
||
BEFORE UPDATE ON portal.user_settings
|
||
FOR EACH ROW
|
||
EXECUTE FUNCTION portal.update_updated_at_column();
|
||
|
||
-- RLS: Usuário só vê próprias configurações
|
||
ALTER TABLE portal.user_settings ENABLE ROW LEVEL SECURITY;
|
||
|
||
CREATE POLICY "Users can view own settings"
|
||
ON portal.user_settings FOR SELECT
|
||
USING (auth.uid() = user_id);
|
||
|
||
CREATE POLICY "Users can update own settings"
|
||
ON portal.user_settings FOR UPDATE
|
||
USING (auth.uid() = user_id);
|
||
|
||
-- Tabela: portal.google_calendar_integration
|
||
CREATE TABLE portal.google_calendar_integration (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||
is_connected BOOLEAN DEFAULT FALSE,
|
||
connected_email VARCHAR(255),
|
||
connected_at TIMESTAMP WITH TIME ZONE,
|
||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||
UNIQUE(user_id)
|
||
);
|
||
|
||
CREATE TRIGGER update_google_calendar_integration_updated_at
|
||
BEFORE UPDATE ON portal.google_calendar_integration
|
||
FOR EACH ROW
|
||
EXECUTE FUNCTION portal.update_updated_at_column();
|
||
|
||
-- RLS
|
||
ALTER TABLE portal.google_calendar_integration ENABLE ROW LEVEL SECURITY;
|
||
|
||
CREATE POLICY "Users can view own google calendar integration"
|
||
ON portal.google_calendar_integration FOR SELECT
|
||
USING (auth.uid() = user_id);
|
||
|
||
CREATE POLICY "Users can insert own google calendar integration"
|
||
ON portal.google_calendar_integration FOR INSERT
|
||
WITH CHECK (auth.uid() = user_id);
|
||
|
||
CREATE POLICY "Users can update own google calendar integration"
|
||
ON portal.google_calendar_integration FOR UPDATE
|
||
USING (auth.uid() = user_id);
|
||
|
||
-- Indexes
|
||
CREATE INDEX idx_user_settings_user_id ON portal.user_settings(user_id);
|
||
CREATE INDEX idx_google_calendar_integration_user_id ON portal.google_calendar_integration(user_id);
|
||
CREATE INDEX idx_google_calendar_integration_is_connected ON portal.google_calendar_integration(is_connected);
|
||
```
|
||
|
||
---
|
||
|
||
## Arquitetura Frontend
|
||
|
||
### Arquitetura de Componentes
|
||
|
||
#### Organização de Componentes
|
||
|
||
```
|
||
src/
|
||
├── components/
|
||
│ ├── ui/ # Shadcn/ui components
|
||
│ ├── layout/
|
||
│ │ ├── Header.tsx
|
||
│ │ ├── Footer.tsx
|
||
│ │ └── DashboardLayout.tsx
|
||
│ ├── whatsapp/
|
||
│ │ ├── WhatsAppInstanceCard.tsx
|
||
│ │ ├── WhatsAppInstanceList.tsx
|
||
│ │ └── QRCodeModal.tsx
|
||
│ └── google-calendar/
|
||
│ └── GoogleCalendarCard.tsx
|
||
```
|
||
|
||
#### Template de Componente
|
||
|
||
```typescript
|
||
// components/whatsapp/WhatsAppInstanceCard.tsx
|
||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
|
||
import { Button } from '@/components/ui/button';
|
||
import { Badge } from '@/components/ui/badge';
|
||
import { WhatsAppInstance } from '@/types/whatsapp';
|
||
|
||
interface WhatsAppInstanceCardProps {
|
||
instance: WhatsAppInstance;
|
||
onGenerateQRCode: (instanceName: string) => void;
|
||
onDisconnect: (instanceName: string) => void;
|
||
isLoading?: boolean;
|
||
}
|
||
|
||
export function WhatsAppInstanceCard({
|
||
instance,
|
||
onGenerateQRCode,
|
||
onDisconnect,
|
||
isLoading = false,
|
||
}: WhatsAppInstanceCardProps) {
|
||
const isConnected = instance.status === 'connected';
|
||
|
||
return (
|
||
<Card className="bg-gray-800 border-gray-700">
|
||
<CardHeader>
|
||
<CardTitle className="flex items-center justify-between">
|
||
<span className="text-white">{instance.instanceName}</span>
|
||
<Badge variant={isConnected ? 'success' : 'destructive'}>
|
||
{isConnected ? 'Conectado' : 'Desconectado'}
|
||
</Badge>
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="space-y-2">
|
||
{!isConnected && (
|
||
<Button
|
||
onClick={() => onGenerateQRCode(instance.instanceName)}
|
||
disabled={isLoading}
|
||
className="w-full bg-blue-600 hover:bg-blue-700"
|
||
>
|
||
Gerar QR Code
|
||
</Button>
|
||
)}
|
||
{isConnected && (
|
||
<Button
|
||
onClick={() => onDisconnect(instance.instanceName)}
|
||
disabled={isLoading}
|
||
variant="outline"
|
||
className="w-full"
|
||
>
|
||
Desconectar
|
||
</Button>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
}
|
||
```
|
||
|
||
### Gerenciamento de Estado
|
||
|
||
#### Estrutura de Estado
|
||
|
||
```typescript
|
||
// stores/auth.ts
|
||
import { createContext, useContext, useState, useEffect } from 'react';
|
||
import { User } from '@supabase/supabase-js';
|
||
import { supabase } from '@/lib/supabase';
|
||
|
||
interface AuthContextType {
|
||
user: User | null;
|
||
loading: boolean;
|
||
signOut: () => Promise<void>;
|
||
}
|
||
|
||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||
|
||
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||
const [user, setUser] = useState<User | null>(null);
|
||
const [loading, setLoading] = useState(true);
|
||
|
||
useEffect(() => {
|
||
supabase.auth.getSession().then(({ data: { session } }) => {
|
||
setUser(session?.user ?? null);
|
||
setLoading(false);
|
||
});
|
||
|
||
const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {
|
||
setUser(session?.user ?? null);
|
||
});
|
||
|
||
return () => subscription.unsubscribe();
|
||
}, []);
|
||
|
||
const signOut = async () => {
|
||
await supabase.auth.signOut();
|
||
setUser(null);
|
||
};
|
||
|
||
return (
|
||
<AuthContext.Provider value={{ user, loading, signOut }}>
|
||
{children}
|
||
</AuthContext.Provider>
|
||
);
|
||
}
|
||
|
||
export function useAuth() {
|
||
const context = useContext(AuthContext);
|
||
if (!context) throw new Error('useAuth must be used within AuthProvider');
|
||
return context;
|
||
}
|
||
```
|
||
|
||
#### Padrões de Gerenciamento de Estado
|
||
|
||
- **Global auth state:** React Context (`AuthContext`) para sessão
|
||
- **Component-local state:** `useState` para UI state
|
||
- **Server state:** Custom hooks com `fetch` e polling
|
||
- **Form state:** `useState` ou React Hook Form
|
||
|
||
### Arquitetura de Roteamento
|
||
|
||
#### Organização de Rotas
|
||
|
||
```
|
||
app/
|
||
├── (auth)/
|
||
│ ├── login/page.tsx
|
||
│ └── reset-password/page.tsx
|
||
├── (dashboard)/
|
||
│ ├── layout.tsx # Protected layout
|
||
│ └── dashboard/page.tsx
|
||
├── api/
|
||
│ ├── whatsapp/
|
||
│ └── google-calendar/
|
||
└── layout.tsx # Root layout
|
||
```
|
||
|
||
#### Padrão de Rota Protegida
|
||
|
||
```typescript
|
||
// app/(dashboard)/layout.tsx
|
||
'use client';
|
||
|
||
import { useAuth } from '@/stores/auth';
|
||
import { useRouter } from 'next/navigation';
|
||
import { useEffect } from 'react';
|
||
import { DashboardLayout } from '@/components/layout/DashboardLayout';
|
||
|
||
export default function ProtectedLayout({ children }: { children: React.ReactNode }) {
|
||
const { user, loading } = useAuth();
|
||
const router = useRouter();
|
||
|
||
useEffect(() => {
|
||
if (!loading && !user) {
|
||
router.push('/login');
|
||
}
|
||
}, [user, loading, router]);
|
||
|
||
if (loading) return <div>Loading...</div>;
|
||
if (!user) return null;
|
||
|
||
return <DashboardLayout>{children}</DashboardLayout>;
|
||
}
|
||
```
|
||
|
||
### Camada de Serviços Frontend
|
||
|
||
#### Setup do Cliente API
|
||
|
||
```typescript
|
||
// lib/api-client.ts
|
||
import { supabase } from '@/lib/supabase';
|
||
|
||
interface FetchOptions extends RequestInit {
|
||
requireAuth?: boolean;
|
||
}
|
||
|
||
export async function apiClient<T>(
|
||
endpoint: string,
|
||
options: FetchOptions = {}
|
||
): Promise<T> {
|
||
const { requireAuth = true, ...fetchOptions } = options;
|
||
|
||
const headers: HeadersInit = {
|
||
'Content-Type': 'application/json',
|
||
...fetchOptions.headers,
|
||
};
|
||
|
||
if (requireAuth) {
|
||
const { data: { session } } = await supabase.auth.getSession();
|
||
if (!session) throw new Error('Unauthorized');
|
||
headers['Authorization'] = `Bearer ${session.access_token}`;
|
||
}
|
||
|
||
const res = await fetch(`${process.env.NEXT_PUBLIC_SITE_URL}${endpoint}`, {
|
||
...fetchOptions,
|
||
headers,
|
||
});
|
||
|
||
if (!res.ok) {
|
||
const errorData = await res.json().catch(() => ({ error: { message: 'Unknown error' } }));
|
||
throw new Error(errorData.error?.message || `Request failed: ${res.status}`);
|
||
}
|
||
|
||
return res.json();
|
||
}
|
||
```
|
||
|
||
#### Exemplo de Serviço
|
||
|
||
```typescript
|
||
// services/whatsapp.service.ts
|
||
import { apiClient } from '@/lib/api-client';
|
||
import { WhatsAppInstance } from '@/types/whatsapp';
|
||
|
||
export const whatsappService = {
|
||
async getInstances(): Promise<WhatsAppInstance[]> {
|
||
return apiClient<WhatsAppInstance[]>('/api/whatsapp/instances');
|
||
},
|
||
|
||
async generateQRCode(instanceName: string): Promise<{ qrCode: string }> {
|
||
return apiClient<{ qrCode: string }>(`/api/whatsapp/instances/${instanceName}/qrcode`, {
|
||
method: 'POST',
|
||
});
|
||
},
|
||
|
||
async disconnectInstance(instanceName: string): Promise<{ success: boolean }> {
|
||
return apiClient(`/api/whatsapp/instances/${instanceName}/disconnect`, {
|
||
method: 'POST',
|
||
});
|
||
},
|
||
};
|
||
```
|
||
|
||
---
|
||
|
||
## Arquitetura Backend
|
||
|
||
### Arquitetura de Serviços (Serverless)
|
||
|
||
#### Organização de Funções
|
||
|
||
```
|
||
app/api/
|
||
├── whatsapp/
|
||
│ └── instances/
|
||
│ ├── route.ts
|
||
│ └── [name]/
|
||
│ ├── qrcode/route.ts
|
||
│ └── disconnect/route.ts
|
||
└── google-calendar/
|
||
├── status/route.ts
|
||
└── callback/route.ts
|
||
```
|
||
|
||
#### Template de Função
|
||
|
||
```typescript
|
||
// app/api/whatsapp/instances/route.ts
|
||
import { NextRequest, NextResponse } from 'next/server';
|
||
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs';
|
||
import { cookies } from 'next/headers';
|
||
import { evolutionAPI } from '@/lib/evolutionapi';
|
||
|
||
export async function GET(request: NextRequest) {
|
||
try {
|
||
// 1. Authenticate
|
||
const supabase = createRouteHandlerClient({ cookies });
|
||
const { data: { session } } = await supabase.auth.getSession();
|
||
|
||
if (!session) {
|
||
return NextResponse.json(
|
||
{ error: { code: 'UNAUTHORIZED', message: 'Not authenticated' } },
|
||
{ status: 401 }
|
||
);
|
||
}
|
||
|
||
// 2. Parse instance names
|
||
const instanceNames = process.env.EVOLUTION_INSTANCE_NAMES?.split(',') || [];
|
||
|
||
// 3. Fetch status from EvolutionAPI
|
||
const instances = await Promise.all(
|
||
instanceNames.map(async (name) => {
|
||
try {
|
||
const status = await evolutionAPI.getInstanceStatus(name.trim());
|
||
return { instanceName: name.trim(), status };
|
||
} catch (error) {
|
||
console.error(`Failed to get status for ${name}:`, error);
|
||
return { instanceName: name.trim(), status: 'disconnected' as const };
|
||
}
|
||
})
|
||
);
|
||
|
||
return NextResponse.json(instances);
|
||
} catch (error) {
|
||
console.error('GET /api/whatsapp/instances error:', error);
|
||
return NextResponse.json(
|
||
{ error: { code: 'INTERNAL_ERROR', message: 'Failed to fetch instances' } },
|
||
{ status: 500 }
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
### Arquitetura de Banco de Dados
|
||
|
||
#### Design do Schema
|
||
|
||
*Veja seção "Schema do Banco de Dados" acima*
|
||
|
||
#### Camada de Acesso a Dados
|
||
|
||
```typescript
|
||
// lib/db/google-calendar.repository.ts
|
||
import { createClient } from '@supabase/supabase-js';
|
||
import { GoogleCalendarIntegration } from '@/types/google-calendar';
|
||
|
||
const supabase = createClient(
|
||
process.env.SUPABASE_URL!,
|
||
process.env.SUPABASE_SERVICE_ROLE_KEY!
|
||
);
|
||
|
||
export const googleCalendarRepository = {
|
||
async getByUserId(userId: string): Promise<GoogleCalendarIntegration | null> {
|
||
const { data, error } = await supabase
|
||
.from('portal.google_calendar_integration')
|
||
.select('*')
|
||
.eq('user_id', userId)
|
||
.single();
|
||
|
||
if (error && error.code !== 'PGRST116') throw error;
|
||
return data;
|
||
},
|
||
|
||
async upsert(userId: string, email: string): Promise<GoogleCalendarIntegration> {
|
||
const { data, error } = await supabase
|
||
.from('portal.google_calendar_integration')
|
||
.upsert({
|
||
user_id: userId,
|
||
is_connected: true,
|
||
connected_email: email,
|
||
connected_at: new Date().toISOString(),
|
||
})
|
||
.select()
|
||
.single();
|
||
|
||
if (error) throw error;
|
||
return data;
|
||
},
|
||
};
|
||
```
|
||
|
||
### Autenticação e Autorização
|
||
|
||
#### Fluxo de Auth
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
actor User
|
||
participant Frontend
|
||
participant SupabaseAuth
|
||
participant APIRoute
|
||
participant DB
|
||
|
||
User->>Frontend: Login
|
||
Frontend->>SupabaseAuth: signInWithPassword()
|
||
SupabaseAuth->>Frontend: JWT token
|
||
Frontend->>Frontend: Armazena token
|
||
User->>Frontend: Acessa /dashboard
|
||
Frontend->>APIRoute: GET /api/... (Bearer JWT)
|
||
APIRoute->>SupabaseAuth: Valida JWT
|
||
SupabaseAuth->>APIRoute: Valid session
|
||
APIRoute->>DB: Query com RLS
|
||
DB->>APIRoute: Filtered data
|
||
APIRoute->>Frontend: Response
|
||
```
|
||
|
||
#### Middleware/Guards
|
||
|
||
```typescript
|
||
// lib/auth/auth-middleware.ts
|
||
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs';
|
||
import { cookies } from 'next/headers';
|
||
import { NextRequest, NextResponse } from 'next/server';
|
||
|
||
export async function requireAuth(request: NextRequest) {
|
||
const supabase = createRouteHandlerClient({ cookies });
|
||
const { data: { session } } = await supabase.auth.getSession();
|
||
|
||
if (!session) {
|
||
return {
|
||
error: NextResponse.json(
|
||
{ error: { code: 'UNAUTHORIZED', message: 'Authentication required' } },
|
||
{ status: 401 }
|
||
),
|
||
session: null,
|
||
};
|
||
}
|
||
|
||
return { error: null, session };
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Estrutura Unificada do Projeto
|
||
|
||
```plaintext
|
||
dashboard-promova/
|
||
├── app/
|
||
│ ├── (auth)/
|
||
│ │ ├── login/page.tsx
|
||
│ │ └── reset-password/page.tsx
|
||
│ ├── (dashboard)/
|
||
│ │ ├── layout.tsx
|
||
│ │ └── dashboard/page.tsx
|
||
│ ├── api/
|
||
│ │ ├── health/ # Health check para K8s probes
|
||
│ │ │ └── route.ts
|
||
│ │ ├── whatsapp/
|
||
│ │ │ └── instances/
|
||
│ │ │ ├── route.ts
|
||
│ │ │ └── [name]/
|
||
│ │ │ ├── qrcode/route.ts
|
||
│ │ │ └── disconnect/route.ts
|
||
│ │ └── google-calendar/
|
||
│ │ ├── status/route.ts
|
||
│ │ └── callback/route.ts
|
||
│ └── layout.tsx
|
||
├── components/
|
||
│ ├── ui/
|
||
│ ├── layout/
|
||
│ ├── whatsapp/
|
||
│ └── google-calendar/
|
||
├── lib/
|
||
│ ├── supabase.ts
|
||
│ ├── evolutionapi.ts
|
||
│ ├── api-client.ts
|
||
│ ├── auth/
|
||
│ └── db/
|
||
├── types/
|
||
│ ├── whatsapp.ts
|
||
│ ├── google-calendar.ts
|
||
│ └── api.ts
|
||
├── hooks/
|
||
│ ├── useAuth.ts
|
||
│ └── useWhatsAppInstances.ts
|
||
├── services/
|
||
│ ├── whatsapp.service.ts
|
||
│ └── google-calendar.service.ts
|
||
├── styles/
|
||
│ └── globals.css
|
||
├── public/
|
||
├── docs/
|
||
│ ├── prd.md
|
||
│ ├── architecture.md
|
||
│ └── prd/ # PRD fragmentado (shards)
|
||
├── k8s/ # Kubernetes manifests
|
||
│ ├── namespace.yaml
|
||
│ ├── configmap.yaml
|
||
│ ├── secret.yaml # (gitignored ou encrypted)
|
||
│ ├── deployment.yaml
|
||
│ ├── service.yaml
|
||
│ ├── ingress.yaml
|
||
│ ├── hpa.yaml # (opcional)
|
||
│ └── kustomization.yaml # (opcional, para overlays dev/staging/prod)
|
||
├── argocd/ # ArgoCD Application definitions
|
||
│ └── application.yaml
|
||
├── .github/
|
||
│ └── workflows/
|
||
│ ├── ci.yaml # CI: lint, test, build
|
||
│ └── docker-build.yaml # Build e push Docker image
|
||
├── .env.local.example
|
||
├── .dockerignore
|
||
├── Dockerfile # Multi-stage Docker build
|
||
├── next.config.js
|
||
├── tailwind.config.js
|
||
├── tsconfig.json
|
||
├── package.json
|
||
└── README.md
|
||
```
|
||
|
||
**Novos Arquivos/Pastas:**
|
||
- **`app/api/health/route.ts`:** Endpoint de health check para liveness/readiness probes K8s
|
||
- **`k8s/`:** Manifests Kubernetes (namespace, deployment, service, ingress, etc.)
|
||
- **`argocd/`:** ArgoCD Application definition para GitOps
|
||
- **`.github/workflows/`:** CI/CD pipelines (GitHub Actions para CI, ArgoCD para CD)
|
||
- **`Dockerfile`:** Multi-stage build para produção
|
||
- **`.dockerignore`:** Excluir arquivos desnecessários do build context
|
||
|
||
---
|
||
|
||
## Workflow de Desenvolvimento
|
||
|
||
### Setup de Desenvolvimento Local
|
||
|
||
#### Pré-requisitos
|
||
|
||
```bash
|
||
# Node.js 18+ e npm
|
||
node --version # >= 18
|
||
npm --version # >= 9
|
||
|
||
# Docker (para build de imagens e teste local)
|
||
docker --version # >= 24
|
||
|
||
# kubectl (para interagir com cluster K8s)
|
||
kubectl version --client # >= 1.28
|
||
|
||
# (Opcional) k9s para gerenciamento visual do cluster
|
||
k9s version
|
||
```
|
||
|
||
#### Setup Inicial
|
||
|
||
```bash
|
||
# Clone repository
|
||
git clone https://github.com/automatizase/dashboard-promova.git
|
||
cd dashboard-promova
|
||
|
||
# Install dependencies
|
||
npm install
|
||
|
||
# Copy environment template
|
||
cp .env.local.example .env.local
|
||
|
||
# Edit .env.local com credenciais
|
||
# nano .env.local
|
||
|
||
# Start development (local, sem Docker)
|
||
npm run dev
|
||
```
|
||
|
||
#### Comandos de Desenvolvimento
|
||
|
||
**Desenvolvimento Local (sem Docker):**
|
||
```bash
|
||
# Start dev server
|
||
npm run dev
|
||
|
||
# Run tests
|
||
npm run test
|
||
|
||
# Run E2E tests
|
||
npm run test:e2e
|
||
|
||
# Lint
|
||
npm run lint
|
||
|
||
# Build production
|
||
npm run build
|
||
|
||
# Start production
|
||
npm run start
|
||
```
|
||
|
||
**Desenvolvimento com Docker (simula produção):**
|
||
```bash
|
||
# Build Docker image
|
||
docker build -t portal:dev .
|
||
|
||
# Run container local
|
||
docker run -p 3000:3000 \
|
||
--env-file .env.local \
|
||
portal:dev
|
||
|
||
# Acessar: http://localhost:3000
|
||
|
||
# Stop container
|
||
docker ps # Ver CONTAINER ID
|
||
docker stop <CONTAINER_ID>
|
||
```
|
||
|
||
**Build e Push para Registry (CI/CD):**
|
||
```bash
|
||
# Build com tag de versão
|
||
docker build -t registry.automatizase.com/portal:1.0.0 .
|
||
|
||
# Login no registry privado
|
||
docker login registry.automatizase.com
|
||
|
||
# Push para registry
|
||
docker push registry.automatizase.com/portal:1.0.0
|
||
|
||
# Tag como latest
|
||
docker tag registry.automatizase.com/portal:1.0.0 registry.automatizase.com/portal:latest
|
||
docker push registry.automatizase.com/portal:latest
|
||
```
|
||
|
||
**Deploy para Kubernetes (via kubectl manual, ou ArgoCD automatizado):**
|
||
```bash
|
||
# Aplicar todos manifests K8s
|
||
kubectl apply -f k8s/
|
||
|
||
# Verificar pods
|
||
kubectl get pods -n automatizase-portal
|
||
|
||
# Ver logs
|
||
kubectl logs -f deployment/portal-deployment -n automatizase-portal
|
||
|
||
# Port-forward para testar local
|
||
kubectl port-forward svc/portal-service 3000:80 -n automatizase-portal
|
||
|
||
# Rollback (se algo der errado)
|
||
kubectl rollout undo deployment/portal-deployment -n automatizase-portal
|
||
|
||
# Verificar status do Ingress
|
||
kubectl get ingress -n automatizase-portal
|
||
```
|
||
|
||
### Configuração de Ambiente
|
||
|
||
#### Variáveis de Ambiente Necessárias
|
||
|
||
```bash
|
||
# Frontend (.env.local)
|
||
NEXT_PUBLIC_SITE_URL=http://localhost:3000
|
||
NEXT_PUBLIC_SUPABASE_URL=https://xxxxx.supabase.co
|
||
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGci...
|
||
|
||
# Backend
|
||
SUPABASE_SERVICE_ROLE_KEY=eyJhbGci...
|
||
|
||
EVOLUTION_API_URL=https://evolution.automatizase.com
|
||
EVOLUTION_API_KEY=your-key
|
||
EVOLUTION_INSTANCE_NAMES=instance1,instance2,instance3
|
||
|
||
N8N_OAUTH_URL=https://n8n.automatizase.com/webhook/google-oauth
|
||
```
|
||
|
||
---
|
||
|
||
## Arquitetura de Deploy
|
||
|
||
### Estratégia de Deploy
|
||
|
||
**Containerização:**
|
||
- **Build Tool:** Docker multi-stage build
|
||
- **Base Image:** node:18-alpine (otimizado para produção)
|
||
- **Output:** Container image com NextJS standalone build
|
||
- **Registry:** Registry privado (registry.automatizase.com) ou Docker Hub
|
||
|
||
**Orquestração Kubernetes:**
|
||
- **Platform:** Kubernetes cluster (self-hosted ou cloud)
|
||
- **Namespace:** `automatizase-portal`
|
||
- **Replicas:** 2+ pods (alta disponibilidade via HPA)
|
||
- **Service:** ClusterIP (internal)
|
||
- **Ingress:** Nginx Ingress Controller (HTTPS via Cert-Manager)
|
||
|
||
**GitOps Deployment:**
|
||
- **Tool:** ArgoCD
|
||
- **Source:** Git repository (`k8s/` folder)
|
||
- **Sync:** Automatizado (self-heal + prune)
|
||
- **Rollback:** Via ArgoCD UI ou kubectl
|
||
|
||
### Pipeline CI/CD
|
||
|
||
**CI (Continuous Integration) - GitHub Actions:**
|
||
|
||
```yaml
|
||
# .github/workflows/ci.yaml
|
||
name: CI
|
||
|
||
on:
|
||
push:
|
||
branches: [main, develop]
|
||
pull_request:
|
||
branches: [main]
|
||
|
||
jobs:
|
||
test:
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- uses: actions/checkout@v3
|
||
|
||
- uses: actions/setup-node@v3
|
||
with:
|
||
node-version: '18'
|
||
cache: 'npm'
|
||
|
||
- name: Install dependencies
|
||
run: npm ci
|
||
|
||
- name: Lint
|
||
run: npm run lint
|
||
|
||
- name: Type check
|
||
run: npx tsc --noEmit
|
||
|
||
- name: Run unit tests
|
||
run: npm run test
|
||
|
||
- name: Build application
|
||
run: npm run build
|
||
env:
|
||
# Env vars necessárias para build (apenas públicas)
|
||
NEXT_PUBLIC_SITE_URL: https://portal.automatizase.com
|
||
NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}
|
||
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }}
|
||
```
|
||
|
||
**CD (Continuous Delivery) - Docker Build & Push:**
|
||
|
||
```yaml
|
||
# .github/workflows/docker-build.yaml
|
||
name: Docker Build and Push
|
||
|
||
on:
|
||
push:
|
||
branches: [main]
|
||
tags:
|
||
- 'v*' # Trigger on version tags (v1.0.0)
|
||
|
||
env:
|
||
REGISTRY: registry.automatizase.com
|
||
IMAGE_NAME: portal
|
||
|
||
jobs:
|
||
build-and-push:
|
||
runs-on: ubuntu-latest
|
||
permissions:
|
||
contents: read
|
||
packages: write
|
||
|
||
steps:
|
||
- uses: actions/checkout@v3
|
||
|
||
- name: Set up Docker Buildx
|
||
uses: docker/setup-buildx-action@v2
|
||
|
||
- name: Log in to Docker Registry
|
||
uses: docker/login-action@v2
|
||
with:
|
||
registry: ${{ env.REGISTRY }}
|
||
username: ${{ secrets.DOCKER_REGISTRY_USERNAME }}
|
||
password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
|
||
|
||
- name: Extract metadata (tags, labels)
|
||
id: meta
|
||
uses: docker/metadata-action@v4
|
||
with:
|
||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||
tags: |
|
||
type=ref,event=branch
|
||
type=semver,pattern={{version}}
|
||
type=semver,pattern={{major}}.{{minor}}
|
||
type=sha,prefix={{branch}}-
|
||
|
||
- name: Build and push Docker image
|
||
uses: docker/build-push-action@v4
|
||
with:
|
||
context: .
|
||
push: true
|
||
tags: ${{ steps.meta.outputs.tags }}
|
||
labels: ${{ steps.meta.outputs.labels }}
|
||
cache-from: type=gha
|
||
cache-to: type=gha,mode=max
|
||
build-args: |
|
||
NEXT_PUBLIC_SITE_URL=https://portal.automatizase.com
|
||
|
||
- name: Output image tag
|
||
run: echo "Image pushed: ${{ steps.meta.outputs.tags }}"
|
||
```
|
||
|
||
**CD (GitOps) - ArgoCD:**
|
||
|
||
ArgoCD monitora automaticamente o repositório Git (`k8s/` folder) e sincroniza mudanças para o cluster. Quando novo commit é pushado em `main`:
|
||
|
||
1. GitHub Actions executa CI (testes, lint, build)
|
||
2. Se CI passa, GitHub Actions builda e pusha Docker image para registry
|
||
3. Developer atualiza `k8s/deployment.yaml` com nova tag de imagem:
|
||
```yaml
|
||
image: registry.automatizase.com/portal:v1.0.1 # Atualizar aqui
|
||
```
|
||
4. Commit e push para `main`
|
||
5. ArgoCD detecta mudança no Git, atualiza deployment no cluster automaticamente
|
||
6. Kubernetes faz rolling update (zero downtime)
|
||
|
||
**Fluxo Completo:**
|
||
```
|
||
Developer push → CI tests → Build Docker → Push to registry →
|
||
Update k8s/deployment.yaml tag → Push to Git → ArgoCD sync → K8s rolling update
|
||
```
|
||
|
||
### Ambientes
|
||
|
||
| Ambiente | Frontend URL | Backend URL | K8s Namespace | ArgoCD App | Propósito |
|
||
|-------------|----------------------------------------|-----------------------------------------------|------------------------|-----------------|----------------------|
|
||
| Development | http://localhost:3000 | http://localhost:3000/api | - | - | Dev local (npm/docker)|
|
||
| Staging | https://staging.portal.automatizase.com| https://staging.portal.automatizase.com/api | automatizase-portal-staging | portal-staging | Pre-production K8s |
|
||
| Production | https://portal.automatizase.com | https://portal.automatizase.com/api | automatizase-portal | portal-prod | Live K8s |
|
||
|
||
**Notas:**
|
||
- **Development:** Roda localmente (npm dev ou Docker), sem K8s
|
||
- **Staging (opcional):** Cluster K8s separado ou namespace separado, mesma infra
|
||
- **Production:** Cluster K8s produção, Ingress com TLS, 2+ replicas, HPA ativo
|
||
|
||
---
|
||
|
||
## Segurança e Performance
|
||
|
||
### Requisitos de Segurança
|
||
|
||
**Segurança Frontend:**
|
||
- **CSP Headers:** Configurado em `next.config.js`
|
||
- **XSS Prevention:** React auto-escapes, validação de input
|
||
- **Secure Storage:** Tokens em httpOnly cookies
|
||
|
||
**Segurança Backend:**
|
||
- **Input Validation:** Validar params, sanitize input
|
||
- **Rate Limiting:** Vercel built-in ou Upstash
|
||
- **CORS Policy:** Next.js default same-origin
|
||
|
||
**Segurança de Autenticação:**
|
||
- **Token Storage:** httpOnly cookies via Supabase Auth Helpers
|
||
- **Session Management:** Supabase gerencia refresh automático
|
||
- **Password Policy:** Min 8 caracteres (configurar Supabase)
|
||
|
||
### Otimização de Performance
|
||
|
||
**Performance Frontend:**
|
||
- **Bundle Size Target:** < 200KB initial bundle
|
||
- **Loading Strategy:** SSR para pages, lazy load modals
|
||
- **Caching Strategy:** Vercel Edge Cache, SWR para hooks
|
||
|
||
**Performance Backend:**
|
||
- **Response Time Target:** < 500ms P95
|
||
- **Database Optimization:** Indexes em `user_id`
|
||
- **Caching Strategy:** Redis futuro para cache de status
|
||
|
||
---
|
||
|
||
## Estratégia de Testes
|
||
|
||
### Pirâmide de Testes
|
||
|
||
```plaintext
|
||
E2E (Playwright)
|
||
/ \
|
||
/ Integration \
|
||
/ \
|
||
/____________________\
|
||
Frontend Unit Backend Unit
|
||
(Vitest+RTL) (Vitest)
|
||
```
|
||
|
||
### Organização de Testes
|
||
|
||
#### Testes Frontend
|
||
|
||
```plaintext
|
||
tests/
|
||
├── unit/
|
||
│ ├── components/
|
||
│ ├── hooks/
|
||
│ └── services/
|
||
└── integration/
|
||
```
|
||
|
||
#### Testes Backend
|
||
|
||
```plaintext
|
||
tests/
|
||
├── api/
|
||
│ ├── whatsapp/
|
||
│ └── google-calendar/
|
||
└── lib/
|
||
```
|
||
|
||
#### Testes E2E
|
||
|
||
```plaintext
|
||
e2e/
|
||
├── auth.spec.ts
|
||
├── whatsapp-management.spec.ts
|
||
└── google-calendar-oauth.spec.ts
|
||
```
|
||
|
||
### Exemplos de Testes
|
||
|
||
#### Teste de Componente Frontend
|
||
|
||
```typescript
|
||
// tests/unit/components/WhatsAppInstanceCard.test.tsx
|
||
import { render, screen, fireEvent } from '@testing-library/react';
|
||
import { describe, it, expect, vi } from 'vitest';
|
||
import { WhatsAppInstanceCard } from '@/components/whatsapp/WhatsAppInstanceCard';
|
||
|
||
describe('WhatsAppInstanceCard', () => {
|
||
it('renders instance name and status', () => {
|
||
const instance = { instanceName: 'test', status: 'connected' as const };
|
||
render(
|
||
<WhatsAppInstanceCard
|
||
instance={instance}
|
||
onGenerateQRCode={vi.fn()}
|
||
onDisconnect={vi.fn()}
|
||
/>
|
||
);
|
||
|
||
expect(screen.getByText('test')).toBeInTheDocument();
|
||
expect(screen.getByText('Conectado')).toBeInTheDocument();
|
||
});
|
||
});
|
||
```
|
||
|
||
#### Teste de API Backend
|
||
|
||
```typescript
|
||
// tests/api/whatsapp/instances.test.ts
|
||
import { describe, it, expect, vi } from 'vitest';
|
||
import { GET } from '@/app/api/whatsapp/instances/route';
|
||
|
||
describe('GET /api/whatsapp/instances', () => {
|
||
it('returns instances with status', async () => {
|
||
const response = await GET(new Request('http://localhost/api/whatsapp/instances'));
|
||
const data = await response.json();
|
||
|
||
expect(response.status).toBe(200);
|
||
expect(Array.isArray(data)).toBe(true);
|
||
});
|
||
});
|
||
```
|
||
|
||
#### Teste E2E
|
||
|
||
```typescript
|
||
// e2e/auth.spec.ts
|
||
import { test, expect } from '@playwright/test';
|
||
|
||
test('user can login', async ({ page }) => {
|
||
await page.goto('/login');
|
||
await page.fill('input[type="email"]', 'user@example.com');
|
||
await page.fill('input[type="password"]', 'password');
|
||
await page.click('button:has-text("Entrar")');
|
||
await expect(page).toHaveURL('/dashboard');
|
||
});
|
||
```
|
||
|
||
---
|
||
|
||
## Padrões de Código
|
||
|
||
### Regras Críticas Fullstack
|
||
|
||
- **Type Sharing:** Definir types em `/types` e importar
|
||
- **API Calls:** Usar service layer, não fetch direto
|
||
- **Environment Variables:** Acessar via `process.env`, nunca hardcode
|
||
- **Error Handling:** Todos API Routes com try/catch
|
||
- **State Updates:** Nunca mutar state diretamente
|
||
- **Auth Middleware:** Sempre validar auth em API Routes
|
||
- **Tailwind Classes:** Priorizar utility classes
|
||
|
||
### Convenções de Nomenclatura
|
||
|
||
| Elemento | Frontend | Backend | Exemplo |
|
||
|-----------------|--------------------------|----------------------|---------------------------------|
|
||
| Components | PascalCase | - | `WhatsAppInstanceCard.tsx` |
|
||
| Hooks | camelCase com 'use' | - | `useAuth.ts` |
|
||
| API Routes | - | kebab-case | `/api/whatsapp/instances` |
|
||
| Database Tables | - | snake_case | `user_settings` |
|
||
| Types | PascalCase | PascalCase | `WhatsAppInstance` |
|
||
| Functions | camelCase | camelCase | `generateQRCode()` |
|
||
|
||
---
|
||
|
||
## Estratégia de Tratamento de Erros
|
||
|
||
### Fluxo de Erros
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
actor User
|
||
participant Frontend
|
||
participant API
|
||
participant External
|
||
|
||
User->>Frontend: Ação
|
||
Frontend->>API: Request
|
||
API->>External: Request externo
|
||
|
||
alt Sucesso
|
||
External->>API: Success
|
||
API->>Frontend: Success
|
||
Frontend->>User: Feedback sucesso
|
||
else Erro
|
||
External->>API: Error
|
||
API->>API: Catch + Log
|
||
API->>Frontend: Erro formatado
|
||
Frontend->>User: Toast erro
|
||
end
|
||
```
|
||
|
||
### Formato de Resposta de Erro
|
||
|
||
```typescript
|
||
interface ApiError {
|
||
error: {
|
||
code: string;
|
||
message: string;
|
||
details?: Record<string, any>;
|
||
timestamp: string;
|
||
requestId: string;
|
||
};
|
||
}
|
||
```
|
||
|
||
### Tratamento de Erros Frontend
|
||
|
||
```typescript
|
||
// lib/error-handler.ts
|
||
export function handleApiError(error: unknown): string {
|
||
if (error instanceof ApiClientError) {
|
||
const messages: Record<string, string> = {
|
||
'UNAUTHORIZED': 'Sessão expirada. Faça login novamente.',
|
||
'INSTANCE_NOT_FOUND': 'Instância não encontrada.',
|
||
};
|
||
return messages[error.code] || error.message;
|
||
}
|
||
return 'Erro inesperado. Tente novamente.';
|
||
}
|
||
```
|
||
|
||
### Tratamento de Erros Backend
|
||
|
||
```typescript
|
||
// lib/error-handler.ts
|
||
export function handleRouteError(error: unknown, requestId: string): NextResponse {
|
||
console.error('[API Error]', { error, requestId });
|
||
|
||
if (error instanceof AppError) {
|
||
return NextResponse.json(
|
||
{
|
||
error: {
|
||
code: error.code,
|
||
message: error.message,
|
||
timestamp: new Date().toISOString(),
|
||
requestId,
|
||
},
|
||
},
|
||
{ status: error.statusCode }
|
||
);
|
||
}
|
||
|
||
return NextResponse.json(
|
||
{
|
||
error: {
|
||
code: 'INTERNAL_ERROR',
|
||
message: 'Erro interno do servidor.',
|
||
timestamp: new Date().toISOString(),
|
||
requestId,
|
||
},
|
||
},
|
||
{ status: 500 }
|
||
);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Monitoramento (POC Simplificada)
|
||
|
||
### Logs Básicos
|
||
|
||
**kubectl logs:**
|
||
```bash
|
||
# Ver logs do pod
|
||
kubectl logs -f deployment/portal -n automatizase-portal
|
||
|
||
# Logs das últimas 100 linhas
|
||
kubectl logs --tail=100 deployment/portal -n automatizase-portal
|
||
|
||
# Logs com timestamp
|
||
kubectl logs --timestamps deployment/portal -n automatizase-portal
|
||
```
|
||
|
||
**console.log no NextJS:**
|
||
- Logs vão para stdout do container
|
||
- Acessíveis via `kubectl logs`
|
||
- Suficiente para POC
|
||
|
||
**Métricas Básicas:**
|
||
- Status do pod: `kubectl get pods -n automatizase-portal`
|
||
- Recursos usados: `kubectl top pods -n automatizase-portal`
|
||
- Eventos: `kubectl get events -n automatizase-portal`
|
||
|
||
---
|
||
|
||
## Relatório de Resultados do Checklist
|
||
|
||
### Resumo Executivo
|
||
|
||
**Completude da Arquitetura:** 95% ✅
|
||
|
||
**Prontidão para Desenvolvimento:** **PRONTA** - Arquitetura completa, detalhada e pronta para desenvolvimento orientado por IA.
|
||
|
||
**Gaps Críticos:**
|
||
- EvolutionAPI endpoints exatos não confirmados - **REQUER INVESTIGAÇÃO**
|
||
- n8n OAuth callback contract não documentado - **VERIFICAR COM EQUIPE**
|
||
|
||
### Recomendações
|
||
|
||
**Ações Imediatas:**
|
||
|
||
1. **Investigar EvolutionAPI Endpoints**
|
||
- Consultar documentação oficial
|
||
- Confirmar rotas exatas
|
||
- Testar com Postman
|
||
|
||
2. **Documentar n8n OAuth Callback**
|
||
- Confirmar query params de callback
|
||
- Testar fluxo OAuth end-to-end
|
||
|
||
3. **Setup Ambiente de Desenvolvimento**
|
||
- Configurar `.env.local`
|
||
- Executar migrations do schema `portal`
|
||
- Verificar conectividade
|
||
|
||
**Próximos Passos:**
|
||
|
||
1. ✅ Completar investigações de dependências externas
|
||
2. ✅ Setup ambiente de desenvolvimento
|
||
3. ✅ Passar para **Dev Agent** para implementação
|
||
4. ✅ Seguir estrutura de projeto definida
|
||
|
||
### Decisão Final
|
||
|
||
✅ **PRONTA PARA DESENVOLVIMENTO**
|
||
|
||
A arquitetura está completa e detalhada. Gaps identificadas são investigáveis rapidamente e não bloqueiam início do trabalho.
|
||
|
||
**Pontos Fortes (POC Simplificada):**
|
||
- **Simples e rápida:** Docker + K8s básico, deploy em poucas horas
|
||
- **Stack moderna:** Next.js 14 + Supabase + Docker + K8s
|
||
- **Mínimo viável:** Apenas o necessário para rodar em K8s
|
||
- **NextJS + Supabase:** Frontend e backend conforme PRD
|
||
- **Nginx Ingress:** Roteamento HTTP simples
|
||
|
||
**Checklist Rápido:**
|
||
1. ✅ Dockerfile simples (sem multi-stage)
|
||
2. ✅ K8s manifests mínimos (namespace, deployment, service, ingress)
|
||
3. ✅ ConfigMap e Secret para env vars
|
||
4. ✅ 1 replica POC (não precisa HA)
|
||
5. ✅ Nginx Ingress porta 80
|
||
6. ✅ Deploy via `kubectl apply -f k8s/`
|
||
7. ⚠️ Confirmar endpoints EvolutionAPI
|
||
8. ⚠️ Testar flow OAuth com n8n
|
||
9. ⚠️ Criar secrets com valores reais
|
||
|
||
---
|
||
|
||
**Pronto para desenvolvimento! 🚀**
|