60 KiB
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
portalseparado do schemapublicexistente 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)
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:
- Usuário acessa
portal.automatizase.com - Nginx Ingress roteia para Service K8s (porta 80)
- Service encaminha para Pod NextJS (porta 3000)
- 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 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 startroda servidor production
Build Rápido:
# 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
# k8s/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: automatizase-portal
2. ConfigMap
# 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
# 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:
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
# 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
# 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)
# 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:
# 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):
# 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 contaupdated_at: timestamp - Última atualização
Interface 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 paraauth.users.idcreated_at: timestamp - Data de criaçãoupdated_at: timestamp - Última atualização
Interface 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 keyuser_id: UUID - FK paraauth.users.idis_connected: boolean - Status de conexãoconnected_email: string | null - Email da conta Google autorizadaconnected_at: timestamp | null - Data da última autorizaçãocreated_at: timestamp - Data de criaçãoupdated_at: timestamp - Última atualização
Interface 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ânciastatus: 'connected' | 'disconnected' - Status de conexãoqrCode: string | null - Base64 do QR code (quando gerado)
Interface 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
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
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
- 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_NAMESexistem
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
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
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
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
-- 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
// 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
// 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:
useStatepara UI state - Server state: Custom hooks com
fetche polling - Form state:
useStateou 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
// 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
// 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
// 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
// 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
// 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
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
// 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
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 K8sk8s/: 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
# 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
# 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):
# 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):
# 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):
# 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):
# 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
# 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:
# .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:
# .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:
- GitHub Actions executa CI (testes, lint, build)
- Se CI passa, GitHub Actions builda e pusha Docker image para registry
- Developer atualiza
k8s/deployment.yamlcom nova tag de imagem:image: registry.automatizase.com/portal:v1.0.1 # Atualizar aqui - Commit e push para
main - ArgoCD detecta mudança no Git, atualiza deployment no cluster automaticamente
- 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
E2E (Playwright)
/ \
/ Integration \
/ \
/____________________\
Frontend Unit Backend Unit
(Vitest+RTL) (Vitest)
Organização de Testes
Testes Frontend
tests/
├── unit/
│ ├── components/
│ ├── hooks/
│ └── services/
└── integration/
Testes Backend
tests/
├── api/
│ ├── whatsapp/
│ └── google-calendar/
└── lib/
Testes E2E
e2e/
├── auth.spec.ts
├── whatsapp-management.spec.ts
└── google-calendar-oauth.spec.ts
Exemplos de Testes
Teste de Componente Frontend
// 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
// 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
// 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
/typese 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
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
interface ApiError {
error: {
code: string;
message: string;
details?: Record<string, any>;
timestamp: string;
requestId: string;
};
}
Tratamento de Erros Frontend
// 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
// 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:
# 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:
-
Investigar EvolutionAPI Endpoints
- Consultar documentação oficial
- Confirmar rotas exatas
- Testar com Postman
-
Documentar n8n OAuth Callback
- Confirmar query params de callback
- Testar fluxo OAuth end-to-end
-
Setup Ambiente de Desenvolvimento
- Configurar
.env.local - Executar migrations do schema
portal - Verificar conectividade
- Configurar
Próximos Passos:
- ✅ Completar investigações de dependências externas
- ✅ Setup ambiente de desenvolvimento
- ✅ Passar para Dev Agent para implementação
- ✅ 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:
- ✅ Dockerfile simples (sem multi-stage)
- ✅ K8s manifests mínimos (namespace, deployment, service, ingress)
- ✅ ConfigMap e Secret para env vars
- ✅ 1 replica POC (não precisa HA)
- ✅ Nginx Ingress porta 80
- ✅ Deploy via
kubectl apply -f k8s/ - ⚠️ Confirmar endpoints EvolutionAPI
- ⚠️ Testar flow OAuth com n8n
- ⚠️ Criar secrets com valores reais
Pronto para desenvolvimento! 🚀