Pular para conteúdo

Auditoria de Pontos de Melhoria

Data: 2026-06-02

Escopo: Auditoria READ-ONLY do branch develop, sistema em producao (cliente Highlas).

Stack auditada: Django 5.2 + DRF + Celery + Next.js 15 + PostgreSQL + Redis + Channels.

Foram encontrados 30 achados distribuidos pelas categorias. Itens ja mapeados (HMAC, cooldown, opt-out, codigos Meta, quality rating, expiracao de surveys) nao foram repetidos — vide roadmap-gaps.md e diretrizes-meta.md.

Legenda: 🔴 ALTA · 🟡 MEDIA · 🟢 BAIXA


Resumo Executivo

Achados por severidade: - 🔴 ALTA: 11 (S-01, S-02, S-03, S-04, P-01, P-02, P-03, C-01, C-02, C-03, O-01, O-02) - 🟡 MEDIA: 16 - 🟢 BAIXA: 7

Top 5 prioridades para o cliente em producao:

  1. S-02 — Tornar MessageLog read-only via API (15min, evita destruicao de auditoria)
  2. P-03 + C-01 — Implementar idempotencia via wa_message_id no webhook (mensagens duplicadas e o bug que mais frustra cliente final)
  3. S-04 — Adicionar autorizacao ao MediaProxyView + remover JWT da querystring
  4. C-02 + P-02 — Mover envio Meta para Celery com retry/backoff (combina performance + confiabilidade)
  5. O-01 + O-02 — Health check + metricas/Sentry (visibilidade antes que o cliente descubra problemas)

Esforco total estimado para resolver os 11 achados ALTA: ~4–5 dias de trabalho focado.

Arquivos criticos com maior densidade de problemas:

Arquivo Achados
backend/whatsapp/views.py S-01, S-02, S-06, P-02, P-03, C-01
backend/whatsapp/services/meta_graph_service.py (945 linhas) C-02, M-01
backend/whatsapp/services/survey_service.py (721 linhas) C-01, M-01
backend/chat/views.pyMediaProxyView S-04
backend/app/settings.py S-05, S-09, S-10
backend/responses/serializers.py P-01
backend/responses/tasks.py P-06, C-02
backend/contacts/serializers.py S-07

1. SEGURANCA

S-01 🔴 Default inseguro do META_VERIFY_TOKEN permite verificacao de webhook por terceiros

  • Local: backend/whatsapp/views.py:269
  • Descricao: verify_token = getattr(settings, 'META_VERIFY_TOKEN', 'my_verify_token_12345'). Se o .env ficar sem META_VERIFY_TOKEN (deploy mal configurado, rotacao esquecida), o webhook fica aberto a verificacao com um token publico conhecido.
  • Impacto real: Atacante consegue assumir o webhook na Meta apontando para seu proprio dominio, e/ou enviar payloads forjados se descobrir a URL.
  • Sugestao: Trocar para getattr(settings, 'META_VERIFY_TOKEN', None) e retornar 500/403 explicitamente se for vazio. Falhar fechado, nao aberto.
  • Esforco: 15min

S-02 🔴 MessageLogViewSet permite POST/PUT/PATCH/DELETE sobre logs de auditoria

  • Local: backend/whatsapp/views.py:97–120 (http_method_names inclui post/put/patch/delete)
  • Descricao: Logs de auditoria sao editaveis e deletaveis via API. O proprio docstring admite "Recomenda-se restringir edicoes a administradores" — mas isso nunca foi feito.
  • Impacto real: Operador (ou conta comprometida) pode apagar/alterar evidencia de conversas com cliente — destruindo o valor LGPD/disputa que motiva o log existir.
  • Sugestao: Trocar ModelViewSet por ReadOnlyModelViewSet (ou pelo menos restringir http_method_names = ['get', 'head', 'options']). Se precisar editar manualmente, fazer pelo Django Admin com log de auditoria nativa.
  • Esforco: 15min

S-03 🔴 Webhook Meta nao valida assinatura HMAC e aceita qualquer payload externo

  • Local: backend/whatsapp/views.py:254–450
  • Descricao: Ja mapeado como pendente, mas vale ressaltar o impacto agravado: Como o endpoint cria MessageLog, dispara Conversation, abre WebSocket broadcast e roteia para SurveyService / ChatService, qualquer terceiro que descubra a URL pode injetar mensagens falsas, abrir conversas ficticias, alterar status de mensagens (statuses) e poluir auditoria.
  • Sugestao: Implementar X-Hub-Signature-256 HMAC validation antes de qualquer side-effect.
  • Esforco: 2h

S-04 🔴 MediaProxyView expoe download de qualquer midia da Meta para qualquer usuario autenticado

  • Local: backend/chat/views.py:279–364
  • Descricao: O endpoint /api/v1/chat/media/<media_id>/ autentica o usuario, mas nao verifica autorizacao ao media_id. Qualquer usuario com JWT valido pode baixar qualquer midia desde que descubra/adivinhe um media_id. Alem disso, autenticacao via querystring ?token= faz o JWT vazar para logs do nginx (access_log), historico de browser, Referer e CDN.
  • Impacto real: Vazamento de midia confidencial (anexos LGPD-sensiveis) + vazamento de JWT em logs.
  • Sugestao: (1) Validar que o media_id aparece em pelo menos um MessageLog que o operador tenha permissao de ver. (2) Encurtar o token: gerar uma URL assinada de curta duracao ao inves de aceitar JWT por querystring. (3) Configurar nginx para mascarar o querystring token= nos logs.
  • Esforco: 4h

S-05 🟡 JWT access token com vida util de 24h e alto demais para um console operacional

  • Local: backend/app/settings.py:215–221
  • Descricao: ACCESS_TOKEN_LIFETIME=24h. Combinado com localStorage (XSS-rouvavel) e ausencia de rotacao fina, qualquer XSS rouba um token valido por ate 24h sem possibilidade de revogacao (nao ha blacklist do access — so do refresh).
  • Impacto real: Em caso de XSS ou comprometimento de maquina, atacante tem 24h de janela.
  • Sugestao: Reduzir ACCESS_TOKEN_LIFETIME para 15-60min e confiar na rotacao do refresh (ja configurada). Considerar mover refresh para httpOnly cookie para mitigar XSS.
  • Esforco: 1h

S-06 🟡 search_fields = ['phone', 'payload'] permite varredura full-text do payload JSON

  • Local: backend/whatsapp/views.py:117
  • Descricao: Pesquisar pelo campo payload (JSONField) faz ILIKE %term% sobre o JSON serializado. Sem indice (nao ha GIN/jsonb_path_ops), com a tabela MessageLog crescendo rapido isso vira sequential scan custoso e expoe interno de payload (ex. operador pode buscar tokens/IDs internos sem precisar listar mensagens).
  • Impacto real: (1) Performance que degrada exponencialmente; (2) Operador pode "garimpar" message_ids da Meta, run_ids internos, IDs de templates, etc.
  • Sugestao: Remover payload de search_fields. Se precisar, criar campos extraidos (ex: message_text) e indexar.
  • Esforco: 30min

S-07 🟡 ContactImportSerializer valida apenas extensao, nao MIME type real do arquivo

  • Local: backend/contacts/serializers.py:65–75
  • Descricao: Validacao por sufixo do nome (.csv, .xlsx). Atacante pode upload payload.exe renomeado para data.csv. Se openpyxl falhar abrindo, levanta exception generica.
  • Impacto real: Risco moderado em contexto de import autenticado, mas viola o proprio padrao do CLAUDE.md ("Uploads: validar MIME type, nao apenas extensao"). Tambem pode causar DoS se enviar Excel com formula bomb ou XML expansion.
  • Sugestao: Usar python-magic ou magic.from_buffer(file.read(2048)) para checar MIME real. Limitar tamanho do arquivo (atualmente sem limite). Configurar MAX_UPLOAD_SIZE.
  • Esforco: 1h

S-08 🟡 Upload de midia em SendMediaView confia no content_type declarado pelo cliente

  • Local: backend/chat/views.py:259 (mime_type=file.content_type or 'application/octet-stream') e backend/chat/services.py:101
  • Descricao: O validador (if mime_type not in type_config['mimes']) usa o MIME enviado pelo browser, que e trivialmente forjavel. Possivel enviar binario arbitrario com Content-Type: image/jpeg e a Meta aceita/cobra/banca de risco fica no nosso lado.
  • Sugestao: Detectar MIME real via python-magic antes de upload para Meta. Bonus: scan de antivirus para document/video (sandbox).
  • Esforco: 1h

S-09 🟢 Falta de CSP, Permissions-Policy, Referrer-Policy

  • Local: backend/app/settings.py:319–327
  • Descricao: Apenas HSTS, nosniff, e XFO (via middleware padrao). Sem CSP, Referrer-Policy, Permissions-Policy, Cross-Origin-Opener-Policy. O CLAUDE.md cita CSP como expectativa.
  • Impacto real: Sem CSP, XSS no frontend executa scripts de qualquer origem.
  • Sugestao: Adicionar django-csp ou middleware custom com CSP estrito ao proprio dominio + dev tools de Next. Adicionar Referrer-Policy: strict-origin-when-cross-origin, Permissions-Policy: camera=(), microphone=(self) (precisa de mic para audio).
  • Esforco: 2h

S-10 🟡 Rate limiting global muito frouxo, AnonRateThrottle de 100/h em endpoints publicos

  • Local: backend/app/settings.py:133–141
  • Descricao: O webhook e csrf_exempt + @require_http_methods + sem throttle (nao passa pela DRF stack). TokenObtainPairView (login) tambem nao tem throttle especifico — qualquer um pode tentar 100 logins/hora por IP. Endpoints autenticados tambem: 1000/h por user. Operador comprometido pode disparar runs/import em volume.
  • Sugestao: Adicionar ScopedRateThrottle em endpoints criticos: login (5/min), start_survey (30/min), send-media (10/min). Para webhook, throttle por IP em middleware (uma vez validado HMAC, pode-se ser mais frouxo).
  • Esforco: 2h

2. PERFORMANCE

P-01 🔴 N+1 em SurveyRunSerializer.get_progress_percentage / get_total_questions no detail

  • Local: backend/responses/serializers.py:114–124 e responses/views.py:182
  • Descricao: No retrieve, o prefetch_related('answers__question', 'survey__questions') e feito, mas get_progress_percentage chama obj.survey.questions.count() que dispara nova query (nao usa o prefetch — count() reconta no DB). Mesma coisa para obj.answers.count(). Em listas com muitos runs, isso e amplificado.
  • Impacto real: Em lista de 20 runs paginados, 20 queries extras de count. Para analytics_overview, idem.
  • Sugestao: Anotar _total_questions=Count('survey__questions') no get_queryset do list, ou usar len(prefetched_questions) em vez de .count().
  • Esforco: 30min

P-02 🔴 evolution_webhook e meta_webhook fazem trabalho pesado SINCRONO antes de responder 200

  • Local: backend/whatsapp/views.py:280–440 e 455–520
  • Descricao: O webhook deve responder em <3s (proprio CLAUDE.md). Atualmente: chama SurveyService.process_incoming_message (varias queries + logica), MetaGraphService.send_message (HTTP outbound!), send_question_message etc — tudo sincrono dentro do handler. Se a Meta API estiver lenta (acontece) o webhook timeout = retry, criando duplicacao.
  • Impacto real: Sob latencia da Meta, ha (1) timeout da Meta -> retry -> mensagens duplicadas; (2) usuario recebe mensagem com atraso; (3) workers HTTP travados.
  • Sugestao: Webhook deve apenas: validar HMAC, persistir MessageLog (in), e disparar tarefa Celery para processar. Resposta 200 imediata. (Combinar com S-03.)
  • Esforco: 4h

P-03 🔴 Webhook NAO e idempotente — mesma message.id da Meta cria multiplos MessageLog e potencialmente respostas duplicadas

  • Local: backend/whatsapp/views.py:399–435
  • Descricao: A Meta faz retry se webhook responder !=2xx em 3s ou se confirmacao demorar. O codigo cria MessageLog sem deduplicacao por wa_message_id (campo existe no model MessageLog.wa_message_id mas nao e preenchido para mensagens in). Isso permite: registrar 2x a resposta "SIM" de um usuario, processar 2x = enviar pergunta 2 duas vezes, salvar SurveyAnswer 2x (este ultimo e mitigado por unique_together('run','question') + update_or_create, mas o efeito do _proceed_to_next_question ocorre 2x).
  • Impacto real: Cliente recebe a mesma pergunta 2-3 vezes -> frustracao + custo Meta + parece bug. Idempotencia e requisito explicito.
  • Sugestao: Capturar message.id da Meta na criacao do MessageLog (em wa_message_id). Antes de processar, fazer MessageLog.objects.filter(wa_message_id=mid).exists() -> se sim, retornar 200 sem processar.
  • Esforco: 1h

P-04 🟡 mediaproxy cacheia midia inteira em Redis — risco de OOM

  • Local: backend/chat/views.py:328–349
  • Descricao: Midias podem ter ate 100MB (PDFs). Cacheado em Redis (maxmemory 512mb, allkeys-lru). Algumas midias grandes podem evicar todo o cache da aplicacao (que serve sessoes!). Pior: serializado/deserializado JSON ou pickle a cada hit.
  • Impacto real: Eviction massiva -> sessoes dropam, performance cai. Sessoes estao em default cache (SESSION_CACHE_ALIAS = 'default').
  • Sugestao: Usar disk-cache para midia (volume Docker) ou stream direto da Meta sem armazenar. Separar cache de sessoes em outro Redis DB.
  • Esforco: 2h

P-05 🟡 Signal broadcast_message_log e sincrono dentro do post_save da request HTTP

  • Local: backend/whatsapp/signals.py:13–62
  • Descricao: Toda criacao de MessageLog dispara async_to_sync(channel_layer.group_send) — uma RTT a Redis (channels), serializacao, etc — dentro do hot path do webhook. Sob carga, isso vira gargalo.
  • Sugestao: Disparar o broadcast em uma task Celery send_realtime_update.delay(message_id).
  • Esforco: 1h

P-06 🟡 expire_stale_runs faz 5 querysets .count() + 5 .update() separados — sem transaction.atomic

  • Local: backend/responses/tasks.py:94–208
  • Descricao: Entre o count() e o update() outra worker pode mover registros. Nao e catastrofico (no pior caso conta menos do que atualiza), mas o relatorio no log fica inconsistente. Tambem: 8 queries onde 1 update com Q() por regra resolveria.
  • Sugestao: Envolver em transaction.atomic e usar unico UPDATE com Q(...) | Q(...) | Q(...). Para o relatorio, fazer um .count() antes do update unico.
  • Esforco: 1h

P-07 🟡 Conversation.unread_count += 1 em handle_incoming_message e race-condition-prone

  • Local: backend/chat/services.py:163–170
  • Descricao: Padrao classico "read-modify-write" sem F(). Duas mensagens simultaneas -> contador incrementa so 1 vez. Em alta concorrencia, conta perde.
  • Sugestao: Conversation.objects.filter(pk=...).update(unread_count=F('unread_count')+1) ou select_for_update.
  • Esforco: 15min

P-08 🟢 MessageLog.payload (JSONField) sem indice GIN apesar de ser pesquisado

  • Local: backend/whatsapp/models.py:57 e indexes ao redor
  • Descricao: Filtros usam payload__message_id=... (linha 307 de views.py), payload__type__in=[...] (linha 149). Sem GIN, full scan.
  • Sugestao: Para Postgres, adicionar GinIndex(fields=['payload']) via migration. Considerar promover message_id (do payload) para coluna propria ja que e consultado.
  • Esforco: 1h

P-09 🟢 RunNewClient.tsx usa useState + useEffect para data fetching ao inves de TanStack Query

  • Local: frontend/src/components/runs/RunNewClient.tsx:58–84
  • Descricao: Viola padrao explicito do CLAUDE.md ("nunca useState + useEffect manual"). Nao ha cache, refetch, dedup. Header.tsx, SurveyNewClient.tsx, AnalyticsClient.tsx provavelmente repetem o padrao.
  • Sugestao: Migrar para useQuery (ja existem hooks em hooks/queries/).
  • Esforco: 2h por componente

3. CONFIABILIDADE

C-01 🔴 try/except Exception muito amplo em handlers criticos — 22 ocorrencias encontradas

  • Local: Varios, exemplos: backend/whatsapp/views.py:446, whatsapp/services/survey_service.py:72,86,111,124, chat/views.py:319
  • Descricao: Captura generica de Exception em pontos criticos. No webhook, qualquer erro vira JsonResponse({'error': '...'}, status=500), mas a Meta nunca vai retentar porque o handler retornou. Resultado: mensagem perdida, log diz "erro generico", nada actionable. Em process_incoming_message, retornar None silencia falhas.
  • Impacto real: Falhas silenciosas em producao. Cliente envia "SIM" e nada acontece — ninguem sabe.
  • Sugestao: Capturar excecoes especificas (requests.Timeout, requests.ConnectionError, Contact.DoesNotExist). Para o resto, deixar propagar para Sentry/similar. Webhook deve retornar 500 para a Meta retentar quando falha e transitoria (network, DB lock), mas idempotencia (P-03) e pre-requisito.
  • Esforco: 4h

C-02 🔴 Sem retry/backoff para Meta API — falha = mensagem perdida

  • Local: backend/whatsapp/services/meta_graph_service.py:36–88 e demais metodos
  • Descricao: requests.post(..., timeout=30). Se Meta retornar 5xx ou timeout, (False, None) e retornado e a mensagem e perdida sem retry. Nao ha circuit breaker — sob outage da Meta, continuamos batendo (e gastando rate limit).
  • Impacto real: Pesquisa fica travada (cliente respondeu "9" para NPS mas a proxima pergunta nao chega), sem alerta. Idem perda silenciosa.
  • Sugestao: Usar requests com urllib3.Retry (status_forcelist=[502,503,504], backoff_factor=1, total=3) ou tenacity. Para envios criticos, mover para Celery task com autoretry_for=(RequestException,) e retry_backoff=True.
  • Esforco: 4h

C-03 🔴 start_survey cria multiplas runs sequencialmente sem transacao — falha parcial deixa estado inconsistente

  • Local: backend/surveys/views.py:408–481
  • Descricao: Para cada telefone: Contact.get_or_create -> cria SurveyRun -> meta_service.send_initial_survey_message (HTTP!) -> se falhar, survey_run.delete(). Mas se o processo for interrompido entre create e a tentativa de envio (ex: timeout HTTP, OOM, container kill), a run fica como pending sem sent_at, esperando expiracao de 24h.
  • Impacto real: Em bulk de 100 contatos, falha do worker no meio deixa metade enviadas, metade pendentes silenciosas.
  • Sugestao: Mover envio para Celery tasks, criar SurveyRun(status='pending') em transacao atomica, e a task lida com retry/envio. Permite tambem respeitar rate-limit da Meta com eta espacado.
  • Esforco: 4h

C-04 🟡 Conversation.get_or_create (chat/services.py:228) em alta concorrencia pode violar unique

  • Local: backend/chat/services.py:225–237
  • Descricao: Duas mensagens simultaneas do mesmo telefone (entrega delivered + read em paralelo) podem disparar 2 get_or_create concorrentes — Postgres impoe unique, uma vai estourar IntegrityError nao tratado.
  • Sugestao: try/except IntegrityError + re-fetch, ou select_for_update com transaction.atomic.
  • Esforco: 30min

C-05 🟡 Cobertura de testes baixa em fluxos criticos

  • Local: backend/whatsapp/tests/ esta vazio (so __init__.py)
  • Descricao: Webhook, SurveyService, MetaGraphService nao tem testes. Tem testes em chat/, responses/, surveys/, contacts/, mas o coracao do produto (webhook + survey logic) nao.
  • Impacto real: Qualquer refator (ex: implementar HMAC) tem alto risco de regredir o sistema em producao.
  • Sugestao: Minimo: testes para process_incoming_message (SIM/NAO/PARAR/resposta-numerica), _validate_answer (cada kind), _evaluate_conditions, webhook GET (verificacao), webhook POST (parsing Meta payload).
  • Esforco: 1 dia

C-06 🟡 Logs em strings concatenadas com f-string em logger.error(f'...{e}') perdem contexto estruturado

  • Local: Toda a base — exemplos em survey_service.py, meta_graph_service.py, views.py
  • Descricao: F-strings perdem o lazy evaluation do logging. Nao da para filtrar/agregar por phone, survey_id, run_id em ferramenta de log central. Conflita com requirement de "logs estruturados".
  • Sugestao: Padronizar logger.info("...", extra={'phone': phone, 'run_id': run.id}) ou adotar structlog. Pelo menos parar de usar f-strings em logs.
  • Esforco: 4h (gradual)

4. MANUTENIBILIDADE

M-01 🟡 Arquivos enormes — meta_graph_service.py (945 linhas), responses/views.py (956), survey_service.py (721)

  • Local: varios
  • Descricao: MetaGraphService mistura: envio de texto, midia, botoes, lista, template, parsing de contato. SurveyService faz parsing + validacao + fluxo + condicional + completion. Dificil testar isoladamente.
  • Sugestao: Quebrar: MetaGraphClient (HTTP puro) + MetaMessageBuilder (formata payloads) + WebhookParser. SurveyService -> SurveyFlowEngine + AnswerValidator.
  • Esforco: 1 dia

M-02 🟡 SurveyCreateSerializer.update e create duplicam ~50 linhas de logica

  • Local: backend/surveys/serializers.py:188–346
  • Descricao: O bloco "primeiro passo cria perguntas, segundo passo resolve FKs e cria conditions" e literalmente repetido entre create e update.
  • Sugestao: Extrair _create_questions_for_survey(survey, questions_data) helper.
  • Esforco: 30min

M-03 🟡 Magic strings espalhadas: status, kinds, types de payload

  • Local: varios — 'pending', 'in_progress', 'survey_accept', 'initial_invite', 'completion', 'chat', 'survey', etc
  • Descricao: Strings comparadas em multiplos arquivos sem constantes centralizadas. Renomear um status quebra silenciosamente o codigo.
  • Sugestao: TextChoices Django no model + import em todos os usos. Aplicar para payload.type, source, button_id ids.
  • Esforco: 4h

M-04 🟡 Import dentro de funcao em hot path (lazy imports)

  • Local: Webhook tem ~6 imports locais dentro do handler (from chat.services import ChatService, from channels.layers import get_channel_layer, etc.). Idem em signals, services.
  • Descricao: Faz sentido em alguns casos (circular import), mas multiplos imports sao feitos a cada request — overhead pequeno mas mensuravel + acopla manutencao. Em services/survey_service.py ha from whatsapp.models import MessageLog dentro de cada metodo.
  • Sugestao: Mover para top do arquivo onde nao ha circular. Para circular, refatorar (M-01).
  • Esforco: 1h

M-05 🟢 TypeScript any espalhado, especialmente em formularios e graficos

  • Local: Lista completa em busca anterior — RunNewClient.tsx, AnalyticsClient.tsx, SurveyFormWithQuestions.tsx, etc.
  • Descricao: ~30 ocorrencias de : any em codigo de producao. Recharts callbacks ((entry: any)) e aceitavel (lib mal tipada). Mas payload: any, result: any, fail: any em fluxos de dominio e evitavel.
  • Sugestao: Tipar pelo menos as fronteiras de API (ex: tipos de respostas de start_survey). Adicionar noImplicitAny: true no tsconfig (se nao estiver).
  • Esforco: 4h

M-06 🟢 ~22 console.log deixados em codigo de producao

  • Local: frontend/src/components/forms/SurveyFormWithQuestions.tsx (varios, com emoji), RunNewClient.tsx, Header.tsx, etc.
  • Descricao: Logs de debug (com emojis) deixados apos desenvolvimento. Poluem o console em producao.
  • Sugestao: Remover ou trocar por um util debug() que checa process.env.NODE_ENV === 'development'. Adicionar regra no-console no eslint (ja tem eslint configurado).
  • Esforco: 30min

M-07 🟢 Arquivo RunsClient.tsx.tmp esquecido no repo

  • Local: frontend/src/components/runs/RunsClient.tsx.tmp
  • Descricao: Backup acidental.
  • Sugestao: Deletar.
  • Esforco: 5min

M-08 🟢 Tipos de hint inconsistentes — Python 3 union syntax e Optional misturados

  • Local: Em varios services, alguns retornos usam tuple[bool, str | None] (Python 3.10+) e outros Optional[str]
  • Descricao: OK funcionalmente, mas inconsistente. Nao ha mypy configurado.
  • Sugestao: Configurar mypy --strict para whatsapp/services/, responses/, e padronizar gradualmente.
  • Esforco: 1 dia

5. UX / UI

U-01 🟡 Tokens JWT em localStorage — vulneravel a XSS

  • Local: frontend/src/lib/api.ts:41–67
  • Descricao: XSS injetado le o token. localStorage e o padrao facil mas inseguro. Combinado com S-05 (24h TTL), uma unica vulnerabilidade XSS da controle prolongado.
  • Sugestao: Mover refresh para httpOnly cookie + access em memoria (Redux/Context). Ou pelo menos isolar via Service Worker.
  • Esforco: 1 dia (refator de auth)

U-02 🟡 Mensagens de erro tecnicas vazando para usuario

  • Local: frontend/src/components/runs/RunNewClient.tsx:231–260 (catch any => fail.error_code), responses/views.py:518–520 ('Erro interno do servidor'), chat/views.py:191 (return Response({'error': error_msg}) retorna mensagem do ValueError direto).
  • Descricao: Strings como "Falha ao enviar convite da pesquisa", "Erro ao processar webhook Meta", "ffmpeg erro: " podem chegar ao usuario/operador sem traducao amigavel.
  • Sugestao: Mapear error_code no backend e traduzir no frontend. Nunca expor stack/stderr.
  • Esforco: 2h

U-03 🟡 Acessibilidade limitada — so ~12 aria-* no projeto inteiro

  • Local: frontend/src/components
  • Descricao: Para um console operacional com formularios complexos (SurveyFormWithQuestions com steps), 12 aria labels e pouco. Sem aria-label, leitor de tela pode pular informacao chave (status de mensagens, botoes de acao em tabelas).
  • Sugestao: Auditar com axe-core ou Lighthouse. Foco: botoes com icones sem label, modais sem aria-modal + aria-labelledby, tabelas sem <caption>.
  • Esforco: 1 dia

U-04 🟢 loading.tsx global do Next sem skeleton — so spinner

  • Local: frontend/src/app/loading.tsx
  • Descricao: Spinner generico bloqueia toda a tela em rota. Skeleton por pagina da percepcao de velocidade.
  • Sugestao: Skeletons especificos por tipo de pagina.
  • Esforco: 2h

6. OBSERVABILIDADE

O-01 🔴 Sem health check endpoint no backend

  • Local: backend/app/urls.py — nao ha /healthz ou /readyz
  • Descricao: docker-compose nao tem healthcheck no backend (tem para db, redis, docs). Sem /healthz apropriado, o orchestrator nao detecta backend degradado (ex: DB caiu mas WSGI ainda responde).
  • Sugestao: Endpoint /api/healthz/ que verifica: DB connection, Redis ping, Celery beat heartbeat (timestamp recent). Adicionar healthcheck: no docker-compose.
  • Esforco: 2h

O-02 🔴 Sem metricas de negocio expostas / monitoradas

  • Local: Sistema todo — nao ha prometheus_client, statsd, nem Sentry
  • Descricao: Nao tem como saber em real-time: taxa de resposta SIM vs NAO, mensagens falhadas por hora, latencia da Meta API, runs travadas. Tudo e "olho no log" depois.
  • Sugestao: Minimo: middleware Sentry para erros + um endpoint /metrics Prometheus com contadores basicos (survey_invites_sent_total, survey_responses_received_total{status="sim|nao|other"}, meta_api_latency_seconds). Dashboard Grafana.
  • Esforco: 1 dia

O-03 🟡 Nao ha alerting configurado para Celery tasks falhando

  • Local: responses/tasks.py usa self.retry(exc=exc, countdown=...), mas apos 3 tentativas a tarefa morre silenciosa
  • Descricao: Em producao, send_scheduled_surveys falhando 3x = pesquisas agendadas nao saem, e ninguem sabe.
  • Sugestao: on_failure callback que envia email/webhook. Ou Sentry com integracao Celery.
  • Esforco: 1h

O-04 🟡 Log rotation 10MB x 5 = so 50MB de historia — muito curto para 1 sistema em producao

  • Local: backend/app/settings.py:355–376
  • Descricao: Em alta carga (50k mensagens/dia), 50MB ≈ poucas horas. Investigar incidente de ontem fica inviavel.
  • Sugestao: Aumentar maxBytes para 50MB, backupCount para 30. Idealmente shipar para log aggregator (Loki, ELK, CloudWatch).
  • Esforco: 30min

7. DEBT TECNICO

D-01 🟡 Codigo deprecated da Evolution API ainda presente

  • Local: backend/app/settings.py:428–431 (config comentada como DEPRECATED mas mantida), backend/whatsapp/services/evolution_service.py (existe mas docker-compose desativa o servico), backend/whatsapp/views.py:455–520 (evolution_webhook view ativa em URL)
  • Descricao: A rota /api/v1/webhook/evolution/ ainda esta ativa e exposta. Sem assinatura HMAC, sem cleanup, mas accessible. Um caminho a menos para auditar = um caminho a mais para atacante.
  • Sugestao: Se Evolution API foi de fato abandonada, remover a view, URL, service e settings. Se nao, documentar o porque de manter.
  • Esforco: 1h

D-02 🟡 Backups JSON commitados no repo

  • Local: /backup_completo.json, /backup_contacts.json, /backup_surveys.json, /backend/backup_surveys.json
  • Descricao: 4 arquivos JSON na raiz do repo — alguns parecem dados de contatos reais. Risco LGPD (dados de clientes em git history mesmo apos delete).
  • Sugestao: Verificar conteudo. Se forem dados reais: remover, rotacionar git history (git filter-repo), e mover backups para storage offline (S3 + criptografia).
  • Esforco: 2h

D-03 🟡 .env real esta no diretorio (nao no git, mas presente)

  • Local: /backend/.env (confere com .gitignore que protege)
  • Descricao: .env esta na maquina com credenciais reais. Como o repo esta em /home/ubuntu/projetos/, qualquer comprometimento do user ubuntu expoe tudo. Nao ha mecanismo de secrets externos (Vault, Doppler, etc.).
  • Sugestao: Mover para secrets store. Restringir permissoes: chmod 600 .env.
  • Esforco: 30min (chmod) | 1 dia (migrar para secrets manager)

D-04 🟢 Diretorio __pycache__, .pytest_cache, staticfiles versionados ou no working dir

  • Local: backend/.pytest_cache, backend/staticfiles
  • Descricao: staticfiles existe na arvore. Nao esta no git (gitignore inclui), mas a pasta foi criada com 30 jan e pode estar stale.
  • Sugestao: Limpar e ter pipeline que recria a cada deploy.
  • Esforco: 15min

D-05 🟢 Permission.objects.all() sem paginacao retorna ~150+ permissoes

  • Local: backend/users/views.py:204–216pagination_class = None
  • Descricao: Conforme tabela cresce (mais models), o endpoint vai retornar payload maior. Hoje aceitavel (~150).
  • Sugestao: Manter sem paginacao mas adicionar OPTIONS/cache HTTP (Cache-Control: private, max-age=3600) — permissoes mudam raramente.
  • Esforco: 30min

Sugestao de Ordem de Implementacao

Considerando dependencias entre achados, a ordem otima e:

Sprint 1 — Quick wins de seguranca (1 dia)

  1. S-02 (15min) — MessageLog read-only
  2. S-01 (15min) — META_VERIFY_TOKEN falhar fechado
  3. S-06 (30min) — remover payload de search_fields
  4. M-07 (5min) — deletar .tmp esquecido
  5. D-03 (30min) — chmod 600 .env
  6. D-04 (15min) — limpar staticfiles/.pytest_cache
  7. C-04 (30min) — IntegrityError handling
  8. P-07 (15min) — F() no unread_count
  9. M-06 (30min) — remover console.log

Sprint 2 — Idempotencia + Async (2-3 dias)

  1. P-03 (1h) — wa_message_id no MessageLog in
  2. C-01 (4h) — refinar except clauses
  3. C-02 (4h) — retry/backoff Meta
  4. P-02 (4h) — webhook async via Celery
  5. C-03 (4h) — start_survey em Celery + transacional

Sprint 3 — Seguranca avancada (2 dias)

  1. S-03 (2h) — HMAC do webhook (depende de App Secret)
  2. S-04 (4h) — MediaProxyView authorization
  3. S-05 + U-01 (1 dia) — JWT TTL + httpOnly cookie
  4. S-07 + S-08 (2h) — MIME real em uploads
  5. S-09 (2h) — CSP + Permissions-Policy
  6. S-10 (2h) — Rate limiting fino

Sprint 4 — Observabilidade (1-2 dias)

  1. O-01 (2h) — health check + docker healthcheck
  2. O-02 (1 dia) — Sentry + Prometheus
  3. O-03 (1h) — alertas Celery
  4. O-04 (30min) — log rotation maior
  5. C-06 (4h) — logs estruturados

Sprint 5 — Performance (1 dia)

  1. P-01 (30min) — anotacoes em SurveyRun list
  2. P-04 (2h) — cache de midia em disco
  3. P-05 (1h) — broadcast async
  4. P-06 (1h) — atomicidade expire_stale_runs
  5. P-08 (1h) — indice GIN no payload

Sprint 6 — Manutenibilidade (2-3 dias, opcional)

  1. M-01 (1 dia) — quebrar arquivos gigantes
  2. M-02 (30min) — DRY no SurveyCreateSerializer
  3. M-03 (4h) — constantes para magic strings
  4. M-04 (1h) — imports no topo
  5. M-05 (4h) — eliminar TypeScript any
  6. C-05 (1 dia) — testes para fluxos criticos
  7. U-02 (2h) — error_code mapping
  8. U-03 (1 dia) — acessibilidade

Sprint 7 — Limpeza de debt (1 dia)

  1. D-01 (1h) — Evolution API cleanup
  2. D-02 (2h) — sanear backups JSON
  3. U-04 (2h) — skeletons
  4. M-08 (1 dia) — mypy strict
  5. P-09 (2h por componente) — migrar useState para TanStack Query
  6. D-05 (30min) — cache HTTP em permissions

Total estimado para o roadmap completo: ~15-20 dias de trabalho focado.

Para mais detalhes sobre cada gap arquitetural ja conhecido, vide roadmap-gaps.md e diretrizes-meta.md.