Skip to content

feat: update default company email prompt in install command and fix … - #74

Open
luizhakan wants to merge 34 commits into
mazer-dev:mainfrom
luizhakan:main
Open

feat: update default company email prompt in install command and fix …#74
luizhakan wants to merge 34 commits into
mazer-dev:mainfrom
luizhakan:main

Conversation

@luizhakan

@luizhakan luizhakan commented Jun 23, 2026

Copy link
Copy Markdown

…package name in lockfile

Summary by CodeRabbit

  • Novos Recursos
    • Assistente de IA com chat no web e WhatsApp, anexos com transcrição/extração e criação de faturas mediante confirmação.
    • WhatsApp: sincronização, envio e edição de mensagens, além de sugestões automáticas (tarefas/notas) com IA.
    • Planner: geração do plano do dia (capacidade, status) e envio via WhatsApp.
  • Melhorias
    • Novas telas e ações no painel para gerenciar o “plano do dia” e reforçar o uso de IA em tarefas e análise de clientes.
    • Atualizações em configurações e webhooks/validações de anexos.
  • Documentação
    • Guia completo de plano/anexos e deploy; expansão do .env.example.

@luizhakan
luizhakan marked this pull request as draft June 23, 2026 15:29
@luizhakan
luizhakan marked this pull request as ready for review June 23, 2026 15:30
luizhakan and others added 25 commits June 23, 2026 13:21
- Implement SchemaSnapshotServiceTest to validate schema snapshot generation and caching behavior.
- Create CliWhatsappMessageSuggesterEnrichedContextTest to ensure AI suggestions for WhatsApp messages are accurate based on tasks and client analysis.
- Add ClientAiAnalysisUiTest to verify UI behavior for client analysis generation and display.
- Develop TaskAiActionsTest to test AI-driven actions for task summaries and subtasks creation.
- Introduce TaskDraftMessageActionTest to validate draft message generation and handling in task context.
- Implement TaskSuggestDescriptionActionTest to ensure AI suggestions populate task descriptions correctly.
- Create WhatsappDictateMessageTest to validate AI message generation based on user instructions in WhatsApp conversations.
- Add GenerateClientAnalysisTest to ensure client analysis generation and notification functionality works as expected.
- Implement TaskWaitingClientTagTest to validate task tagging behavior based on client waiting status.
- Create WhatsappConversationTokenServiceTest to verify token counting for WhatsApp conversations.
- Add ConversationHistoryRendererTest to ensure proper rendering of conversation history with attachments.
- Implement ReadOnlySelectValidatorTest to validate SQL query restrictions for read-only operations.
- Create PejotaHelperOrDefaultTest to ensure default settings are applied when no user is authenticated.
- Added functionality to handle AI-generated suggestions in the EvolutionWebhookHandler.
- Introduced a new database migration for the `whatsapp_suggestions` table to store AI suggestions.
- Updated the `WhatsappConversationSyncService` to disable suggestion dispatching during bulk imports.
- Enhanced the UI to display pending AI suggestions in the WhatsApp conversation resource.
- Created tests for the new suggestion service and actions related to accepting and dismissing suggestions.
- Implemented job handling for analyzing WhatsApp conversations and dispatching suggestions with a delay.
- storeMessage persisted the whole sync batch payload on every message,
  exhausting PHP memory on the chat poll (blank 500 modal); each message
  now keeps only its own record, without inline media base64
- the 10s chat poll now syncs without media downloads/AI enrichment
  (withMedia: false); full sync stays on the manual action
- context token refresh runs once per conversation instead of per message
- fromMe messages no longer rename conversations to the account owner's
  pushName ("Luiz Fernando"); candidate discovery keeps existing names
- messages chat: edit/delete own messages on WhatsApp via Evolution API
  (updateMessage/deleteMessageForEveryone), respecting WhatsApp windows
- messages chat: fix leading whitespace from Blade indentation inside
  whitespace-pre-wrap bubbles; add wire:key to the message loop
- assistant schema snapshot documents units (duration in minutes, money
  in cents) so the data assistant stops misreading worked hours

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The data assistant gains its single write capability: proposing an
invoice draft ({"invoice": {...}}) that the server validates (client,
project, items with product/unit, mandatory due_date), stores as a
pending action and answers with a data summary plus a one-word
passphrase generated in PHP. The invoice (status Sent) is only created
when the user's next message matches the passphrase exactly
(case-sensitive), compared deterministically before any AI runs; the
pending draft expires in 15 minutes and any other message just
continues the conversation.

The model can never create, update or delete anything by itself: SQL
stays SELECT-only on the read-only connection, unknown write-like
actions are ignored, and tests pin all of it down.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…do suporte a conversas e conversão de timestamps para o fuso horário local
…ling

- Implement `AssistantWhatsappWebhookHandlerTest` to cover various scenarios for handling WhatsApp messages, including session management, command processing, and message types.
- Introduce `MakesAttachmentFixtures` trait for creating realistic file uploads in tests.
- Create `ProcessAssistantWhatsappMessageTest` to validate the processing of incoming messages and responses, including audio transcription and error handling.
- Add `WhatsappMarkdownConverterTest` to ensure proper conversion of markdown to WhatsApp format.
- Implement `WhatsappJidNormalizerTest` to validate normalization of WhatsApp JIDs and number handling.
- Enhance `EvolutionApiClientTest` with tests for sending text messages and configuring webhooks for specific instances.
- Update existing tests to reflect changes in message handling logic and ensure proper assertions.
…ion feature

- Added a nullable 'name' column to the whatsapp_conversations table and populated it based on existing data.
- Implemented a modal in the messages chat view for users to ask questions to an AI, including validation for input.
- Created tests for the new AI question feature and ensured it does not interfere with existing conversation data.
- Updated existing tests to reflect changes in conversation handling and ensure proper functionality.
…incluindo campos e lógica para gerenciamento de grupos
…tifications

- Implement DailyPlanGeneratorTest to validate daily plan generation logic.
- Create DailyPlanResponseParserTest to ensure correct parsing of responses.
- Add DailyPlanWhatsappNotifierTest to verify WhatsApp notifications for daily plans.
- Introduce GenerateDailyPlansCommandTest to test command for generating daily plans.
- Implement PlanOfTheDayPageTest for Livewire component handling of daily plans.
- Add PlannerCapacityTest to validate planner capacity calculations.
- Create SendDailyPlansCommandTest to test sending of daily plans via command.
- Implement SyncRecentWhatsappConversationsCommandTest to verify synchronization of recent WhatsApp conversations.
@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Este PR introduz um assistente de dados baseado em IA (chat web e via WhatsApp, com suporte a anexos), uma integração completa com a Evolution API para conversas de WhatsApp, um planejador diário automático gerado por IA e enviado via WhatsApp, análises de relacionamento de clientes e ações de IA em tarefas, além de configuração, migrações e um script Python independente de exportação de e-mails.

Changes

Plataforma de IA, WhatsApp e Planner

Layer / File(s) Summary
Infraestrutura, configuração e migrações
.env.example, config/*, database/migrations/*, routes/*, bootstrap/app.php, lang/*.json, phpunit.xml, PejotaHelper.php
Novas variáveis de ambiente, configs de OpenAI/AI CLI/assistant/planner/evolution, conexão SQLite somente-leitura, migrações para todas as novas tabelas, rotas de webhook/anexos e traduções.
Motor de contexto e execução de prompts
app/Services/Ai/AiCliRunner.php, app/Services/Ai/Context/*, app/Services/Ai/SchemaSnapshotService.php, app/Services/Ai/ReadOnlySelectValidator.php, app/Services/Ai/Support/AiJsonExtractor.php, app/Services/Ai/OpenAi*
Execução de CLIs de IA (Codex/AGY), construtores de contexto de conversa/tarefa/cliente e transcrição de áudio via OpenAI.
Assistente de dados: chat, anexos e canal WhatsApp
app/Livewire/AssistantChat.php, app/Services/Ai/AssistantChatService.php, app/Services/Ai/AssistantInvoiceService.php, app/Services/Ai/AssistantAttachment*, app/Jobs/ProcessAssistant*, app/Models/Assistant*, app/Services/Evolution/AssistantWhatsappWebhookHandler.php
Chat com IA (agentic) capaz de consultar dados, criar faturas com confirmação e processar até três anexos, com canal dedicado no WhatsApp.
Integração WhatsApp cliente-facing
app/Services/Evolution/EvolutionApiClient.php, EvolutionWebhookHandler.php, WhatsappConversation*, app/Filament/App/Resources/WhatsappConversationResource*
Cliente da Evolution API, ingestão de mensagens/anexos, sugestões de IA sobre conversas e UI de gerenciamento de conversas do WhatsApp.
Planejamento diário automático
app/Services/Planner/*, app/Models/DailyPlan*, app/Jobs/GenerateDailyPlan.php, app/Filament/App/Pages/PlanOfTheDay.php, comandos pj:daily-plan*
Geração diária de um plano priorizado via IA, com validação/whitelisting da resposta, job de fila com resolução de timezone e envio automático via WhatsApp.
Análises de IA para clientes e tarefas
app/Services/Ai/ClientAnalysisService.php, app/Services/Ai/TaskAiService.php, ClientResource.php, TaskResource.php, AiBriefing.php
Geração de análise de relacionamento por cliente e ações de IA em tarefas (resumo, rascunho, subtarefas), mais briefing diário administrativo.
Ajustes diversos de UI
ProjectResource.php, InvoiceResource.php, WorkSession*, PriorityEnum.php
Pequenas correções independentes de comportamento e UI no Filament.

Estimated code review effort: 5 (Critical) | ~180 minutes

Script utilitário de exportação de e-mails

Layer / File(s) Summary
Exportação de e-mails via Grafana
export_emails.py
Script CLI Python que consulta logs de e-mail em uma fonte Grafana/MySQL e exporta em HTML ou CSV.

Sequence Diagram(s)

sequenceDiagram
  participant Usuario
  participant Chat as AssistantChat (Livewire)
  participant Job as ProcessAssistantMessage
  participant Servico as AssistantChatService
  participant CLI as AiCliRunner

  Usuario->>Chat: envia mensagem/anexo
  Chat->>Job: dispatch(conversation, user)
  Job->>Servico: respond(conversation)
  Servico->>CLI: complete(prompt)
  CLI-->>Servico: resposta JSON (say/query/invoice)
  Servico-->>Job: texto final
  Job-->>Chat: mensagem do assistente persistida
Loading
sequenceDiagram
  participant Scheduler
  participant Command as pj:daily-plan
  participant Job as GenerateDailyPlan
  participant Generator as DailyPlanGenerator
  participant CLI as AiCliRunner
  participant Notifier as DailyPlanWhatsappNotifier
  participant Cliente

  Scheduler->>Command: executa diariamente às 07h
  Command->>Job: dispatch(company, date, mode)
  Job->>Generator: generate(company, date, mode)
  Generator->>CLI: complete(prompt do planner)
  CLI-->>Generator: itens priorizados
  Generator-->>Job: DailyPlan READY
  Command->>Notifier: send(plan)
  Notifier->>Cliente: envia plano via WhatsApp
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.54% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed O título menciona corretamente a alteração do prompt padrão no comando de instalação, embora não resuma o restante do grande conjunto de mudanças.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…horário do usuário e criar testes correspondentes

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

🟠 Major comments (32)
export_emails.py-35-52 (1)

35-52: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

urlopen sem timeout.

A chamada de rede em run_query não define timeout, podendo travar o script indefinidamente se o Grafana ficar sem resposta.

🕒 Proposta de correção
     try:
-        with urllib.request.urlopen(req) as response:
+        with urllib.request.urlopen(req, timeout=30) as response:
             data = json.loads(response.read().decode('utf-8'))

Quanto aos alertas de SSRF (Ruff S310 / ast-grep urlopen-unsanitized-data) para essas linhas: GRAFANA_URL é uma constante fixa no arquivo, não input externo, então considero falso positivo nesse contexto.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@export_emails.py` around lines 35 - 52, Atualize a chamada de rede em
run_query, especificamente urllib.request.urlopen(req), para fornecer um timeout
finito usando a configuração ou constante de timeout existente; preserve o
processamento bem-sucedido e o tratamento atual de exceções.

Source: Linters/SAST tools

export_emails.py-109-115 (1)

109-115: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

SQL injection via interpolação de datas não validadas em três queries rawSql. start_date/end_date vêm diretamente de args.inicio/args.fim (CLI) sem validação de formato, e são inseridas por f-string na rawSql enviada ao datasource MySQL do Grafana nos três modos.

  • export_emails.py#L109-L115: validar/parsear start_date/end_date como datas antes de montar a query (ex.: datetime.strptime), ou usar placeholders ($__timeFrom()/$__timeTo() do Grafana) em vez de interpolação de string.
  • export_emails.py#L149-L154: aplicar a mesma validação/parametrização usada no modo "templates".
  • export_emails.py#L190-L195: aplicar a mesma validação/parametrização usada no modo "templates".
🛡️ Proposta de correção (validação mínima antes de montar as queries)
+def _validate_date(value, label):
+    for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d"):
+        try:
+            datetime.strptime(value, fmt)
+            return value
+        except ValueError:
+            continue
+    raise SystemExit(f"Data inválida para {label}: {value!r}")
+
     start_date = args.inicio
     end_date = args.fim if args.fim else datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+    start_date = _validate_date(start_date, "--inicio")
+    end_date = _validate_date(end_date, "--fim")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@export_emails.py` around lines 109 - 115, Validate and parse the CLI-derived
start_date and end_date before constructing rawSql, using one consistent safe
approach across the three query-building sites in export_emails.py: lines
109-115, 149-154, and 190-195. Prefer parameterization or strict date parsing
such as datetime.strptime, and ensure all three modes use the same validated
values without direct untrusted f-string interpolation.

Source: Linters/SAST tools

bootstrap/app.php-14-16 (1)

14-16: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Não exponha o webhook quando os segredos estiverem vazios.

Com EVOLUTION_WEBHOOK_TOKEN e EVOLUTION_API_KEY vazios, EvolutionWebhookController::authorizeWebhook() não rejeita a requisição. Esta exceção de CSRF deixa qualquer cliente capaz de disparar o processamento do webhook. Exija ao menos um segredo configurado antes de aceitar eventos, ou falhe a inicialização quando a verificação estiver habilitada.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bootstrap/app.php` around lines 14 - 16, Update
EvolutionWebhookController::authorizeWebhook() and the webhook configuration so
requests are rejected when both EVOLUTION_WEBHOOK_TOKEN and EVOLUTION_API_KEY
are empty; require at least one configured secret before authorizing events, or
fail application initialization when webhook verification is enabled. Keep the
CSRF exception only for a webhook endpoint protected by this validation.
database/migrations/2026_07_13_000000_add_name_to_whatsapp_conversations_table.php-16-34 (1)

16-34: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Remova o N+1 do backfill. Cada conversa dispara uma consulta em clients e um UPDATE individual. Em bases grandes, isso alonga a migração e aumenta o tempo de bloqueio; carregue os clientes em lote e aplique os updates em chunks.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@database/migrations/2026_07_13_000000_add_name_to_whatsapp_conversations_table.php`
around lines 16 - 34, Remove the N+1 pattern in the migration backfill: replace
the per-conversation client lookup inside the whatsapp_conversations iteration
with batched client loading, and apply name updates in chunks rather than
issuing one UPDATE per record. Preserve the existing name-priority fallback
using push_name, client name/tradename, phone_number, and remote_jid.
config/database.php-51-58 (1)

51-58: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Propague DB_URL para sqlite_readonly também. A conexão principal já usa url, mas a variante somente leitura não; em ambientes que dependem só de DB_URL, o assistente pode abrir o arquivo padrão ou apontar para o banco errado.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@config/database.php` around lines 51 - 58, Atualize a configuração
`sqlite_readonly` para propagar o valor de `DB_URL` por meio da mesma chave
`url` usada pela conexão principal. Preserve a configuração atual de `database`,
`prefix` e das opções somente leitura, garantindo que ambientes configurados
exclusivamente com `DB_URL` usem a URL fornecida.
app/Services/Ai/AiCliRunner.php-35-40 (1)

35-40: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Não trate esse fluxo como somente leitura em app/Services/Ai/AiCliRunner.php:131-133,185-197.
Quando services.ai_cli.agy_skip_permissions liga --dangerously-skip-permissions e services.ai_cli.use_sudo executa o AGY como root, um prompt/anexo malicioso pode virar execução com privilégios totais no host. Desative essa combinação ou use um usuário dedicado sem privilégios com sudoers mínimo.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Services/Ai/AiCliRunner.php` around lines 35 - 40, Update the AGY
execution flow in AiCliRunner, including the agy_skip_permissions and use_sudo
handling, so they cannot be enabled together; reject or disable this combination
before launching AGY. Preserve normal permission-skipping and sudo behavior when
used independently, and ensure AGY never runs as root with permissions skipped.
app/Services/Ai/ConversationContextBuilder.php-11-27 (1)

11-27: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Histórico de conversa (WhatsApp) não é envolvido pelo PromptGuard.

conversationContext (linha 14, usado na linha 23) é o histórico armazenado da conversa — exatamente o tipo de conteúdo que PromptGuard.php foi criado para proteger contra prompt injection, mas ele é interpolado sem PromptGuard::wrap().

Será consolidado com o achado equivalente em NotesContextSection.php.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Services/Ai/ConversationContextBuilder.php` around lines 11 - 27,
Atualize o método build da classe ConversationContextBuilder para envolver
conversationContext com PromptGuard::wrap() antes de passá-lo à seção “Histórico
completo armazenado desta conversa”. Preserve o comportamento atual para valores
nulos ou vazios e reutilize a API existente do PromptGuard, alinhando o
tratamento ao de NotesContextSection.
app/Services/Ai/Context/NotesContextSection.php-22-58 (1)

22-58: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Conteúdo de notas não é envolvido pelo PromptGuard.

O PromptGuard.php (novo, nesta mesma camada) documenta que Any content originating from a client (WhatsApp messages, audio transcriptions, image descriptions, notes, etc.) must be wrapped with wrap() before being interpolated into a prompt, citando "notas" explicitamente. Aqui, o excerto de cada nota é interpolado diretamente na string final sem PromptGuard::wrap(), deixando esse trecho vulnerável a prompt injection via conteúdo de nota malicioso.

Será consolidado com o achado equivalente em ConversationContextBuilder.php.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Services/Ai/Context/NotesContextSection.php` around lines 22 - 58,
Atualize o método build da classe NotesContextSection para envolver o conteúdo
variável de cada nota, especialmente o excerpt gerado por excerpt(), com
PromptGuard::wrap() antes de interpolá-lo no prompt final. Preserve o título, a
data e o formato atual das linhas, aplicando a proteção somente ao conteúdo da
nota conforme o padrão documentado por PromptGuard.
app/Services/Ai/Context/PromptGuard.php-16-28 (1)

16-28: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

wrap() não sanitiza ocorrências dos próprios delimitadores no conteúdo.

Os delimitadores START/END são strings estáticas e previsíveis. Se o conteúdo do cliente (mensagem de WhatsApp, nota, transcrição) contiver literalmente <<<FIM_DADOS>>>, o atacante pode forjar o fechamento da seção de dados e fazer o restante do texto ser interpretado como instrução real pelo modelo — justamente o ataque que esta classe deveria prevenir.

🔒️ Correção sugerida: remover ocorrências dos delimitadores antes de envolver o conteúdo
     public static function wrap(string $content): string
     {
-        return self::START."\n".$content."\n".self::END;
+        $sanitized = str_replace([self::START, self::END], '', $content);
+
+        return self::START."\n".$sanitized."\n".self::END;
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Services/Ai/Context/PromptGuard.php` around lines 16 - 28, Atualize o
método PromptGuard::wrap para remover ou neutralizar todas as ocorrências de
PromptGuard::START e PromptGuard::END no conteúdo recebido antes de adicionar os
delimitadores externos. Preserve a estrutura atual de quebra de linhas e o
contrato de retorno, garantindo que o conteúdo não consiga forjar o fechamento
da seção.
app/Services/Ai/SchemaSnapshotService.php-49-62 (1)

49-62: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Mascarar colunas sensíveis no snapshot do schema
EXCLUDED_TABLES só remove tabelas inteiras; o snapshot ainda enumera todas as colunas das tabelas incluídas, então users.password e users.remember_token continuam indo para o prompt da IA. Vale aplicar exclusão/mascaramento por coluna para campos sensíveis, em vez de depender só da exclusão por tabela.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Services/Ai/SchemaSnapshotService.php` around lines 49 - 62, Atualize o
fluxo de geração do snapshot em `SchemaSnapshotService` para excluir ou mascarar
colunas sensíveis, incluindo `users.password` e `users.remember_token`, antes de
montar os dados enviados ao prompt da IA. Preserve `EXCLUDED_TABLES` para
tabelas inteiras e centralize a regra de colunas sensíveis em uma configuração
ou método reutilizável.
app/Filament/App/Resources/TaskResource.php-259-277 (1)

259-277: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Chamadas de IA síncronas sem tratamento de erro em toda a UI de tarefas. Todas essas ações executam o CLI de IA no thread da requisição (o job GenerateClientAnalysis reserva 420s para o mesmo tipo de chamada) e nenhuma trata exceção, então falha/lentidão do CLI vira erro na tela.

  • app/Filament/App/Resources/TaskResource.php#L259-L277: envolva suggestDescription() em try/catch com notificação de erro.
  • app/Filament/App/Resources/TaskResource.php#L1184-L1186: proteja summaryForClient() no fillForm e retorne texto vazio com aviso em caso de falha.
  • app/Filament/App/Resources/TaskResource.php#L1204-L1207: proteja draftClientMessage() da mesma forma.
  • app/Filament/App/Resources/TaskResource.php#L1242-L1249: proteja suggestSubtasks() e trate lista vazia.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Filament/App/Resources/TaskResource.php` around lines 259 - 277, Protect
every synchronous AI call in TaskResource.php: in lines 259-277, wrap
suggestDescription() with exception handling and show an error notification; in
lines 1184-1186, wrap summaryForClient() in fillForm, return empty text on
failure, and notify the user; in lines 1204-1207, apply the same handling to
draftClientMessage(); and in lines 1242-1249, protect suggestSubtasks(), notify
on failure, and handle an empty returned list. Use the existing action,
fillForm, and notification patterns without changing successful results.
app/Filament/App/Resources/TaskResource.php-1094-1101 (1)

1094-1101: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

status_id sem required() permite gravar nulo.

Submeter o modal sem selecionar nada chama update(['status_id' => null]), que quebra a FK/coluna obrigatória.

🛡️ Correção sugerida
                                             ->form([
                                                 Select::make('status_id')
                                                     ->label('Status')
+                                                    ->required()
                                                     ->options(fn (): array => Status::all()->pluck('name', 'id')->toArray()),
                                             ]),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Filament/App/Resources/TaskResource.php` around lines 1094 - 1101,
Atualize o Select::make('status_id') no formulário da ação para torná-lo
obrigatório usando required(), impedindo o envio do modal sem um status
selecionado e evitando que o callback de update grave status_id como null.
app/Filament/App/Resources/TaskResource.php-824-847 (1)

824-847: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Extração de índice frágil e sem validação da chave.

O laço pega o primeiro segmento numérico do state path; se o path ganhar qualquer prefixo numérico (ação montada, infolist aninhada de subtarefa na Linha 898), o índice resolvido aponta para o item errado e sobrescreve outro checklist. Além disso, $checklist[$index] é gravado sem checar se a chave existe, criando entradas espúrias em estado dessincronizado.

🐛 Correção sugerida
-                                                            $parts = explode('.', $statePath);
-                                                            $index = null;
-                                                            foreach ($parts as $part) {
-                                                                if (is_numeric($part)) {
-                                                                    $index = (int) $part;
-                                                                    break;
-                                                                }
-                                                            }
-
-                                                            if ($index !== null) {
-                                                                $checklist = $record->checklist;
+                                                            $numericParts = array_filter(explode('.', $statePath), 'is_numeric');
+                                                            $index = $numericParts === [] ? null : (int) end($numericParts);
+
+                                                            $checklist = $record->checklist ?? [];
+
+                                                            if ($index !== null && array_key_exists($index, $checklist)) {
                                                                 $checklist[$index]['completed'] = ! ($checklist[$index]['completed'] ?? false);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Filament/App/Resources/TaskResource.php` around lines 824 - 847, Atualize
a closure de action do checklist para resolver o índice do item a partir do
segmento correto do state path, sem assumir que o primeiro segmento numérico
representa o checklist. Antes de alterar $checklist[$index], valide que a chave
existe; para índices ausentes ou inválidos, não grave alterações nem crie
entradas espúrias.
app/Jobs/GenerateClientAnalysis.php-42-56 (1)

42-56: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Restaure o Auth no fim do job

Auth::onceUsingId() deixa o usuário preso no guard do worker. Sem um finally para limpar o estado, o próximo job no mesmo processo pode herdar esse contexto e operar no tenant errado. Use Auth::forgetUser() (ou Auth::setUser(null)) ao final.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Jobs/GenerateClientAnalysis.php` around lines 42 - 56, Atualize o job em
torno de Auth::onceUsingId() para sempre limpar o usuário autenticado ao final
da execução, incluindo quando service->generate() lançar uma exceção. Use um
bloco finally com Auth::forgetUser() (ou Auth::setUser(null)), preservando o
relatório, a notificação e o retorno existentes no catch.
app/Console/Commands/AiBriefing.php-55-79 (1)

55-79: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

briefCompany() precisa limpar o guard entre empresas

Auth::onceUsingId() permanece no guard durante o mesmo processo; quando uma empresa sem user_id vem depois de outra com user_id, ela herda o usuário anterior e o PejotaHelper usa timezone/formato/locale errados. Limpe o guard antes de resolver o owner e adicione um teste cobrindo duas empresas no mesmo Company::all().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Console/Commands/AiBriefing.php` around lines 55 - 79, Atualize
briefCompany() para limpar o guard de autenticação antes de resolver o owner da
empresa, evitando que empresas sem user_id herdem o usuário anterior; mantenha o
login via Auth::onceUsingId() apenas quando houver owner. Adicione um teste que
processe duas empresas no mesmo Company::all(), sendo a segunda sem user_id, e
valide que o contexto da segunda usa as configurações corretas.
app/Services/Ai/AssistantWhatsappMediaIngestor.php-101-134 (1)

101-134: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Catch restrito demais em ingest() para uma chamada disparada por webhook externo.

InvalidArgumentException e RuntimeException são capturadas. Qualquer outro Throwable de persistAttachment()/storeTemporaryAudio() (ex.: falha de disco, erro inesperado do uploader) propaga sem tratamento até o chamador do webhook, que — pelo trecho de AssistantWhatsappWebhookHandler::handleMessage fornecido como contexto — não envolve essa chamada em try/catch. Isso pode interromper o processamento do webhook com a mensagem do usuário já persistida e sem resposta. AssistantAttachmentProcessor::processAll, na mesma camada, já captura Throwable de forma ampla — vale alinhar este método ao mesmo padrão.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Services/Ai/AssistantWhatsappMediaIngestor.php` around lines 101 - 134,
Amplie o tratamento de exceções em AssistantWhatsappMediaIngestor::ingest para
capturar qualquer Throwable lançado por storeTemporaryAudio ou
persistAttachment, mantendo o retorno atual com kind, audio_path nulo e a
mensagem da exceção no campo error; alinhe o método ao padrão amplo usado por
AssistantAttachmentProcessor::processAll.
app/Services/Documents/AttachmentTextExtractor.php-43-81 (1)

43-81: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

docx()/xlsx() carregam conteúdo do ZIP sem limite de tamanho descomprimido ("zip bomb").

ZipArchive::getFromName('word/document.xml'), getFromName('xl/sharedStrings.xml') e a leitura de cada xl/worksheets/sheetN.xml carregam o conteúdo descomprimido inteiro em memória sem nenhum teto. Um DOCX/XLSX malicioso dentro do limite de 25MB (comprimido) pode se expandir para muito mais que isso, o que é especialmente arriscado no VPS de uma CPU e ~4GB de RAM sem swap descrito no plano de deploy. Considere checar statName($entry)['size'] (ou usar getStream() com leitura limitada) antes de getFromName() e rejeitar/truncar entradas anormalmente grandes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Services/Documents/AttachmentTextExtractor.php` around lines 43 - 81,
Adicione um limite explícito para o tamanho descomprimido das entradas ZIP antes
de carregá-las em memória nos métodos docx() e xlsx(). Use
ZipArchive::statName() para validar word/document.xml, xl/sharedStrings.xml e
cada xl/worksheets/sheetN.xml, rejeitando ou truncando entradas acima do limite
definido; preserve o processamento normal das entradas dentro do limite e
garanta o fechamento do ZIP em todos os caminhos.
app/Http/Controllers/AttachmentsController.php-25-41 (1)

25-41: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Endpoint de anexos do WhatsApp sem X-Content-Type-Options: nosniff.

getAssistantAttachment protege contra MIME sniffing e usa Content-Disposition seguro, mas getWhatsappAttachment não. O mime_type de um WhatsappAttachment vem de metadados reportados pelo remetente/Evolution API (fonte externa), então é o caso mais exposto a MIME spoofing (ex.: um contato mal-intencionado enviando conteúdo HTML com mime_type de imagem, sendo então "sniffado" e renderizado pelo navegador). Alinhe este método ao padrão já usado no endpoint do assistente.

🛡️ Sugestão de correção
         return response()->file($disk->path($attachment->path), [
             'Accept-Ranges' => 'bytes',
             'Content-Type' => $this->contentType($attachment),
+            'X-Content-Type-Options' => 'nosniff',
         ]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Http/Controllers/AttachmentsController.php` around lines 25 - 41, Update
getWhatsappAttachment to match the security headers and safe Content-Disposition
behavior used by getAssistantAttachment: add X-Content-Type-Options: nosniff and
use the established safe attachment filename/disposition handling while
preserving the existing authorization and file validation.
app/Services/Ai/AssistantQuickAnswers.php-110-120 (1)

110-120: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Tarefas com vencimento hoje aparecem como "atrasadas por 0 dia(s)".

$task->due_date é uma data cast na timezone da aplicação (00:00), enquanto $today é meia-noite na timezone do usuário; quando as duas diferem (ex.: America/Sao_Paulo vs UTC), lt() é verdadeiro para a data de hoje e a linha cai no ramo de atraso com diffInDays = 0. Comparar apenas as datas civis elimina a dependência de fuso.

🐛 Sugestão
-            if ($task->due_date->lt($today)) {
-                $days = (int) $task->due_date->diffInDays($today);
+            if ($task->due_date->toDateString() < $today->toDateString()) {
+                $days = (int) $task->due_date->startOfDay()->diffInDays($today->startOfDay());
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Services/Ai/AssistantQuickAnswers.php` around lines 110 - 120, Atualize o
callback de mapeamento em AssistantQuickAnswers para comparar apenas as datas
civis de due_date e today, sem considerar horário ou timezone, antes de decidir
entre os ramos de atraso e vencimento hoje. Preserve o cálculo de dias para
tarefas realmente atrasadas e faça com que a data de hoje siga sempre o texto
“due today”.
app/Services/Evolution/AssistantWhatsappWebhookHandler.php-378-392 (1)

378-392: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Evite o fallback implícito para a primeira empresa
Company::query()->value('id') pega a primeira empresa da tabela quando EVOLUTION_DEFAULT_COMPANY_ID está vazio; em ambiente com mais de um tenant, isso pode atribuir mensagens ao tenant errado. Se a configuração for obrigatória, falhe ou registre erro explícito em vez de escolher uma empresa arbitrária.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Services/Evolution/AssistantWhatsappWebhookHandler.php` around lines 378
- 392, Update companyId() to remove the implicit Company::query()->value('id')
fallback when services.evolution.default_company_id is missing; treat the
configuration as required and fail explicitly or log an explicit error instead
of selecting an arbitrary company, while preserving the configured ID path.
app/Services/Evolution/EvolutionWebhookHandler.php-159-176 (1)

159-176: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Documentos do WhatsApp enviados com legenda nunca geram anexo.

messageType() classifica documentWithCaptionMessage como document_with_caption (via o loop genérico de sufixo *Message), mas isMediaType() só aceita ['audio', 'image', 'video', 'document', 'sticker']. Ao mesmo tempo, mediaInfo() só inspeciona audioMessage/imageMessage/videoMessage/documentMessage/stickerMessage diretamente sob message, nunca desembrulhando documentWithCaptionMessage.message.documentMessage. Assim, em storeAttachment() a condição $media === null && ! $this->isMediaType($message->message_type) é verdadeira para esse tipo de mensagem e a função retorna sem criar nenhum anexo — o arquivo é perdido, apenas a legenda é salva como texto.

A própria messageText() já desembrulha esse caminho aninhado (message.documentWithCaptionMessage.message.documentMessage.caption), confirmando que esse formato ocorre de fato nos payloads reais do Evolution/Baileys.

base64() e messagePayload() têm a mesma lacuna: nenhuma delas verifica message.documentWithCaptionMessage.message.documentMessage.base64, então, se esse base64 aparecer ali, ele não seria extraído para um arquivo nem removido do payload persistido.

🐛 Correção proposta para mediaInfo() (aplicar o mesmo padrão em base64() e messagePayload())
     private function mediaInfo(array $messageData): ?array
     {
         $message = data_get($messageData, 'message', []);
         if (! is_array($message)) {
             return null;
         }
 
+        // "documentWithCaptionMessage" aninha o documentMessage real um
+        // nível mais profundo; messageText() já desembrulha isso para a
+        // legenda, mas este método nunca fez o mesmo, então documentos com
+        // legenda eram silenciosamente tratados como "sem mídia" abaixo.
+        $captioned = data_get($message, 'documentWithCaptionMessage.message.documentMessage');
+        if (is_array($captioned)) {
+            $mime = data_get($captioned, 'mimetype');
+            $filename = data_get($captioned, 'fileName');
+
+            return [
+                'type' => 'document',
+                'mime_type' => is_string($mime) ? $mime : null,
+                'filename' => is_string($filename) ? $filename : null,
+                'extension' => is_string($filename) ? pathinfo($filename, PATHINFO_EXTENSION) : $this->extensionFromMime($mime),
+            ];
+        }
+
         foreach (['audioMessage', 'imageMessage', 'videoMessage', 'documentMessage', 'stickerMessage'] as $key) {
             $media = data_get($message, $key);
             ...
     private function base64(array $messageData): ?array
     {
         $raw = data_get($messageData, 'message.base64')
             ?: data_get($messageData, 'base64')
             ?: data_get($messageData, 'message.audioMessage.base64')
             ?: data_get($messageData, 'message.imageMessage.base64')
             ?: data_get($messageData, 'message.videoMessage.base64')
-            ?: data_get($messageData, 'message.documentMessage.base64');
+            ?: data_get($messageData, 'message.documentMessage.base64')
+            ?: data_get($messageData, 'message.documentWithCaptionMessage.message.documentMessage.base64');

Also applies to: 431-450, 452-472, 474-497, 499-509, 549-552

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Services/Evolution/EvolutionWebhookHandler.php` around lines 159 - 176,
Extend the document-media handling in mediaInfo(), base64(), and
messagePayload() to unwrap
message.documentWithCaptionMessage.message.documentMessage and process its media
fields exactly like the existing direct documentMessage path. Ensure mediaInfo()
returns the nested document metadata, base64() extracts its base64 content, and
messagePayload() removes nested base64 before persisting the payload, while
preserving current handling for other message types.
app/Services/Evolution/EvolutionWebhookHandler.php-32-68 (1)

32-68: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Trabalho pesado e bloqueante executado de forma síncrona na requisição do webhook do Evolution. O controller (ver snippet de EvolutionWebhookController) chama EvolutionWebhookHandler::handle() e EvolutionWebhookForwarder::forward() sequencialmente, ambos de forma síncrona, na mesma requisição HTTP que responde ao webhook do Evolution API — sem despachar nada para fila.

  • app/Services/Evolution/EvolutionWebhookHandler.php#L32-L68: handle() roda com $withMedia=true por padrão (download de mídia + CLIs de IA para transcrição/descrição, que podem levar minutos conforme documentado em ProcessAssistantMessage.php), mesmo sendo o próprio docblock (linhas 24-31) que alerta contra isso em "caminhos vinculados a requisições"; considere despachar um job para o processamento de mídia/enriquecimento e responder ao webhook rapidamente com $withMedia=false.
  • app/Services/Evolution/EvolutionWebhookForwarder.php#L10-L25: forward() adiciona até 8s de chamada HTTP bloqueante na mesma requisição; considere despachar via job/queue em vez de chamar Http::post() de forma síncrona no fluxo do webhook.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Services/Evolution/EvolutionWebhookHandler.php` around lines 32 - 68,
Make EvolutionWebhookHandler::handle() return quickly from the webhook path by
processing the initial message with $withMedia=false and dispatching
media/enrichment work to a queue job instead of performing it synchronously. In
app/Services/Evolution/EvolutionWebhookHandler.php lines 32-68, preserve message
persistence and token behavior while moving heavy processing out of the request.
In app/Services/Evolution/EvolutionWebhookForwarder.php lines 10-25, replace the
synchronous Http::post() in forward() with queued dispatch so forwarding no
longer blocks the webhook response.
app/Services/Evolution/EvolutionApiClient.php-23-43 (1)

23-43: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Tratar ConnectionException nos clientes HTTP da Evolution API
Todos os métodos desta classe capturam só RequestException. Timeout/DNS/conexão caem em ConnectionException, então fetchAllGroups() ainda pode estourar a tela em vez de retornar [], e os demais métodos deixam de retornar a RuntimeException padronizada. Inclua ConnectionException no catch e trate a mensagem conforme o tipo da exceção.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Services/Evolution/EvolutionApiClient.php` around lines 23 - 43, Atualize
os métodos da classe EvolutionApiClient, incluindo sendTextToNumber e
fetchAllGroups, para capturar também ConnectionException além de
RequestException. Preserve a mensagem do corpo da resposta para RequestException
e use a mensagem da exceção de conexão para ConnectionException, mantendo o
retorno [] de fetchAllGroups e a RuntimeException padronizada nos demais
métodos.
app/Services/Evolution/WhatsappConversationSyncService.php-113-116 (1)

113-116: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

syncAll() perdeu o filtro is_array() que sync() aplica.

Em sync() (linhas 49-53) os registros passam por ->filter(fn ($record) => is_array($record)) antes do sortBy(fn (array $record) ...). Aqui não: qualquer item não-array vindo de findMessagesPage() (string, null) provoca TypeError no callback tipado — e também em messageTimestamp(array $record) — abortando o backfill completo no meio da paginação.

🐛 Correção proposta
                     $records = collect($page['records'])
+                        ->filter(fn ($record) => is_array($record))
                         ->sortBy(fn (array $record): int => $this->messageTimestamp($record))
                         ->values()
                         ->all();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Services/Evolution/WhatsappConversationSyncService.php` around lines 113
- 116, Adicione ao fluxo de `syncAll()` no processamento de `$page['records']` o
mesmo filtro `is_array()` usado por `sync()` antes de `sortBy` e de
`messageTimestamp`. Preserve apenas registros array, mantendo a ordenação e a
conversão final para lista existentes, para que itens inválidos não alcancem os
callbacks tipados nem interrompam o backfill.
app/Filament/App/Resources/WhatsappConversationResource/RelationManagers/MessagesRelationManager.php-96-97 (1)

96-97: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Tabela sem paginação carrega o histórico inteiro.

paginated(false) combinado com with('attachments') traz todas as mensagens da conversa em cada render/poll. Como SyncWhatsappConversationHistory importa o histórico completo, conversas antigas podem render centenas/milhares de registros por requisição. Considere limitar a janela (ex.: últimas N mensagens via modifyQueryUsing) ou usar paginação simples.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@app/Filament/App/Resources/WhatsappConversationResource/RelationManagers/MessagesRelationManager.php`
around lines 96 - 97, Atualize a configuração da tabela em
MessagesRelationManager para evitar carregar todo o histórico a cada
renderização: remova paginated(false) e habilite paginação simples, ou aplique
em modifyQueryUsing uma janela limitada às últimas N mensagens. Preserve o eager
loading de attachments e garanta que o limite/paginação seja aplicado à consulta
de mensagens.
app/Filament/App/Resources/WhatsappConversationResource/RelationManagers/MessagesRelationManager.php-449-471 (1)

449-471: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Chamadas de IA síncronas no request Livewire.

CliWhatsappMessageSuggester/CliWhatsappConversationQuestionAnswerer executam CLI de IA no thread da requisição. Este mesmo PR assume que essas chamadas podem levar minutos (AnalyzeWhatsappConversation::$timeout = 420), o que tende a estourar max_execution_time/timeout do proxy e devolver 504 ao usuário. Considere despachar em job e devolver o resultado via notificação/polling.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@app/Filament/App/Resources/WhatsappConversationResource/RelationManagers/MessagesRelationManager.php`
around lines 449 - 471, Altere generateAiSuggestion para não executar
CliWhatsappMessageSuggester::suggest de forma síncrona no request Livewire:
despache um job assíncrono com a conversa e composerMessage, retorne
imediatamente e informe o usuário de que o processamento foi iniciado. Preserve
a atualização de composerMessage/aiSuggestion e as notificações de sucesso ou
falha no fluxo assíncrono, usando polling ou outro mecanismo existente para
disponibilizar o resultado.
app/Http/Controllers/Webhooks/EvolutionWebhookController.php-53-62 (1)

53-62: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Verificação de apikey pode ser burlada omitindo o campo.

Com webhook_verify_api_key ativo e api_key configurada, um payload sem apikey (ou com string vazia) passa direto, porque a comparação só ocorre quando $payloadKey é string não vazia. Se webhook_token não estiver configurado, o endpoint fica sem autenticação alguma. Quando a verificação está habilitada e a chave está configurada, a ausência do campo deveria ser rejeitada.

🔒️ Correção proposta
         $apiKey = config('services.evolution.api_key');
         $payloadKey = $request->input('apikey');
 
-        if (is_string($apiKey) && $apiKey !== '' && is_string($payloadKey) && $payloadKey !== '' && ! hash_equals($apiKey, $payloadKey)) {
+        if (! is_string($apiKey) || $apiKey === '') {
+            return;
+        }
+
+        if (! is_string($payloadKey) || ! hash_equals($apiKey, $payloadKey)) {
             abort(Response::HTTP_UNAUTHORIZED);
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Http/Controllers/Webhooks/EvolutionWebhookController.php` around lines 53
- 62, Update the apikey validation in EvolutionWebhookController so that when
webhook_verify_api_key is enabled and services.evolution.api_key is configured,
a missing or empty request apikey is rejected with HTTP_UNAUTHORIZED. Preserve
hash_equals validation for non-empty payload keys and the existing early return
when verification is disabled.

Source: Linters/SAST tools

app/Console/Commands/SendDailyPlans.php-54-58 (1)

54-58: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Mesmo problema de vazamento de autenticação entre empresas do arquivo GenerateDailyPlans.php.

Auth::onceUsingId só é chamado se $company->user_id existir; caso contrário, o usuário autenticado da iteração anterior permanece resolvido para esta empresa (mesmo processo foreach). Isso afeta PejotaHelper::getUserTimeZoneOrDefault() na linha 60, podendo calcular "hoje" com o fuso de outra empresa.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Console/Commands/SendDailyPlans.php` around lines 54 - 58, Atualize
sendForCompany para limpar o estado de autenticação antes de processar cada
empresa sem user_id, evitando reutilizar o usuário da iteração anterior.
Preserve Auth::onceUsingId para empresas com user_id e garanta que
PejotaHelper::getUserTimeZoneOrDefault() execute sem usuário autenticado quando
não houver vínculo.
app/Services/Planner/DailyPlanResponseParser.php-83-135 (1)

83-135: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Itens do tipo contract não têm verificação anti-hallucination equivalente à de task/invoice/habit.

normalizeItem() rejeita itens TASK sem task_id válido (linhas 105-107), INVOICE sem invoice_id e sem client_id (linhas 109-111), e HABIT sem task_id (linhas 113-115). Não há verificação equivalente para DailyPlanItemTypeEnum::CONTRACT: um item desse tipo é aceito mesmo com contract_id nulo (e sem fallback por client_id, como acontece com invoice). Isso contraria o objetivo documentado da classe ("ids que nunca apareceram no contexto são removidos... anti-hallucination") e permite que a IA "invente" uma renovação de contrato sem nenhum contrato real referenciado.

🔧 Correção sugerida
         if ($type === DailyPlanItemTypeEnum::HABIT && $taskId === null) {
             return null;
         }
+
+        if ($type === DailyPlanItemTypeEnum::CONTRACT && $contractId === null && $clientId === null) {
+            return null;
+        }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Services/Planner/DailyPlanResponseParser.php` around lines 83 - 135,
Update normalizeItem() to reject DailyPlanItemTypeEnum::CONTRACT items when
contractId is null, matching the existing validation for TASK, INVOICE, and
HABIT. Keep valid contract-linked items unchanged and continue using the
whitelisted contract ID from context->validContractIds.
app/Console/Commands/GenerateDailyPlans.php-56-68 (1)

56-68: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Estado de autenticação pode "vazar" de uma empresa para a próxima no mesmo processo.

Auth::onceUsingId($company->user_id) só é chamado quando $company->user_id existe. Se a empresa anterior no foreach (Linha 49) tinha user_id e a próxima não tem, auth()->user() continua resolvendo o usuário da empresa anterior durante todo o restante de planCompany() para a nova empresa — inclusive em PejotaHelper::getUserTimeZoneOrDefault() (linhas 71-72), que decide o "hoje" usado para plan_date. Isso pode gerar planos com data/fuso da empresa errada. Esse comportamento de "vazamento" de auth entre iterações no mesmo processo PHP é um problema documentado do Laravel quando Auth::setUser()/onceUsingId() é chamado condicionalmente dentro de loops longos.

🔧 Correção sugerida
-        if ($company->user_id) {
-            Auth::onceUsingId($company->user_id);
-        }
+        if ($company->user_id) {
+            Auth::onceUsingId($company->user_id);
+        } else {
+            Auth::forgetGuards();
+        }

Ligado ao mesmo problema em app/Console/Commands/SendDailyPlans.php (linhas 56-58) — ver comentário consolidado.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Console/Commands/GenerateDailyPlans.php` around lines 56 - 68, Atualize o
método planCompany para limpar o estado de autenticação antes de processar cada
empresa, garantindo que empresas sem user_id não reutilizem o usuário
autenticado pela iteração anterior. Preserve a autenticação via
Auth::onceUsingId quando user_id existir e assegure que auth()->user() e
PejotaHelper::getUserTimeZoneOrDefault() reflitam somente a empresa atual.
app/Jobs/GenerateDailyPlan.php-56-66 (1)

56-66: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Mensagem de exceção crua exposta ao usuário final.

$exception?->getMessage() é persistido em failure_reason e exibido diretamente na página "Plan of the day" (ver PlanOfTheDayPageTest::test_failed_plan_shows_the_failure_reason, que faz assertSee('CLI indisponível')). Exceções de chamadas de CLI/API externas podem conter detalhes internos sensíveis (tokens, caminhos, respostas de terceiros) que não deveriam ir para a UI sem sanitização; o detalhe completo deveria ser logado (report()/Log::error()) e um texto genérico exibido ao usuário.

🔒 Correção sugerida
     public function failed(?Throwable $exception): void
     {
+        if ($exception) {
+            report($exception);
+        }
+
         DailyPlan::allTenants()
             ->where('company_id', $this->company->id)
             ->whereDate('plan_date', $this->date)
             ->where('status', DailyPlanStatusEnum::GENERATING->value)
             ->update([
                 'status' => DailyPlanStatusEnum::FAILED->value,
-                'failure_reason' => $exception?->getMessage() ?? __('The plan generation job failed.'),
+                'failure_reason' => __('The plan generation job failed.'),
             ]);
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Jobs/GenerateDailyPlan.php` around lines 56 - 66, Update
GenerateDailyPlan::failed so failure_reason never persists the raw exception
message: report or log the full Throwable for diagnostics, then store a generic
user-facing failure message instead. Preserve the existing fallback wording and
ensure the failed plan UI no longer receives sensitive CLI/API exception
details.
app/Filament/App/Pages/PlanOfTheDay.php-147-150 (1)

147-150: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Timezone helper inconsistente com o resto da feature de Daily Plan.

today() usa PejotaHelper::getUserTimeZone(), enquanto DailyPlanOverview::todayPlan() e o comando SendDailyPlans usam PejotaHelper::getUserTimeZoneOrDefault() para o mesmo cálculo de "hoje". Se o usuário não tiver timezone configurado, essa divergência pode lançar exceção aqui e quebrar toda a página (computed plan() e workedTodayMinutes() dependem de today()), enquanto o widget e o comando continuam funcionando com o fallback.

🛠️ Correção sugerida
     private function today(): CarbonImmutable
     {
-        return CarbonImmutable::now(PejotaHelper::getUserTimeZone())->startOfDay();
+        return CarbonImmutable::now(PejotaHelper::getUserTimeZoneOrDefault())->startOfDay();
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Filament/App/Pages/PlanOfTheDay.php` around lines 147 - 150, Atualize o
método today() em PlanOfTheDay para usar
PejotaHelper::getUserTimeZoneOrDefault(), mantendo o cálculo com
CarbonImmutable::now(...)->startOfDay(). Alinhe o comportamento com
DailyPlanOverview::todayPlan() e SendDailyPlans, preservando o fallback quando o
usuário não tiver timezone configurado.

Comment on lines +80 to +108
public function score(string $left, string $right): int
{
$left = $this->digits($left);
$right = $this->digits($right);

if ($left === '' || $right === '') {
return 0;
}

if ($left === $right) {
return 100;
}

if (str_ends_with($left, $right) || str_ends_with($right, $left)) {
return 92;
}

if (substr($left, -9) === substr($right, -9)) {
return 88;
}

if (substr($left, -8) === substr($right, -8)) {
return 80;
}

similar_text($left, $right, $percent);

return (int) round($percent);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Heurística de sufixo casa números curtos/parciais e pode vincular a conversa ao cliente errado.

Sem exigência de comprimento mínimo, um telefone parcial casa com alta pontuação:

  • score('1490', '5554999371490')str_ends_with92
  • substr('1490', -9) === '1490' e substr('1490', -8) === '1490', logo até os ramos de 9/8 dígitos degeneram para comparação da string inteira quando um lado é mais curto.
  • similar_text sobre dígitos ultrapassa 70 com facilidade entre dois celulares do mesmo DDD.

Como linkClient()/linkConversation() gravam client_id via forceFill()->save() sem confirmação humana, um falso positivo reatribui a conversa e expõe o histórico de WhatsApp de um cliente a outro.

🔒️ Correção sugerida
     public function score(string $left, string $right): int
     {
         $left = $this->digits($left);
         $right = $this->digits($right);
 
         if ($left === '' || $right === '') {
             return 0;
         }
 
         if ($left === $right) {
             return 100;
         }
 
+        // Sufixos só são confiáveis com número suficientemente longo em ambos
+        // os lados; abaixo disso qualquer fragmento casaria com muitos números.
+        if (strlen($left) < 8 || strlen($right) < 8) {
+            return 0;
+        }
+
         if (str_ends_with($left, $right) || str_ends_with($right, $left)) {
             return 92;
         }
 
         if (substr($left, -9) === substr($right, -9)) {
             return 88;
         }
 
         if (substr($left, -8) === substr($right, -8)) {
             return 80;
         }
 
-        similar_text($left, $right, $percent);
-
-        return (int) round($percent);
+        // Semelhança textual entre dígitos não é evidência de mesmo número.
+        return 0;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public function score(string $left, string $right): int
{
$left = $this->digits($left);
$right = $this->digits($right);
if ($left === '' || $right === '') {
return 0;
}
if ($left === $right) {
return 100;
}
if (str_ends_with($left, $right) || str_ends_with($right, $left)) {
return 92;
}
if (substr($left, -9) === substr($right, -9)) {
return 88;
}
if (substr($left, -8) === substr($right, -8)) {
return 80;
}
similar_text($left, $right, $percent);
return (int) round($percent);
}
public function score(string $left, string $right): int
{
$left = $this->digits($left);
$right = $this->digits($right);
if ($left === '' || $right === '') {
return 0;
}
if ($left === $right) {
return 100;
}
// Sufixos só são confiáveis com número suficientemente longo em ambos
// os lados; abaixo disso qualquer fragmento casaria com muitos números.
if (strlen($left) < 8 || strlen($right) < 8) {
return 0;
}
if (str_ends_with($left, $right) || str_ends_with($right, $left)) {
return 92;
}
if (substr($left, -9) === substr($right, -9)) {
return 88;
}
if (substr($left, -8) === substr($right, -8)) {
return 80;
}
// Semelhança textual entre dígitos não é evidência de mesmo número.
return 0;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Services/Evolution/WhatsappConversationMatcher.php` around lines 80 -
108, Atualize o método WhatsappConversationMatcher::score para impedir
pontuações de correspondência de sufixo ou similaridade para números
curtos/parciais: exija um comprimento mínimo adequado antes dessas heurísticas e
retorne 0 quando qualquer número não atender ao limite. Preserve a pontuação de
igualdade exata apenas quando aplicável e mantenha linkClient/linkConversation
inalterados.

Comment thread export_emails.py
Comment on lines +11 to +14
GRAFANA_URL = "https://grafana.topmassagens.com.br/api/ds/query?ds_type=mysql"
DATASOURCE_UID = "dfpqllglhd88wf"
AUTH_USER = "timetop"
AUTH_PASS = "77308515"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Credenciais em texto puro no código-fonte.

AUTH_USER/AUTH_PASS (e a URL/UID de produção) estão hardcoded e serão versionados no git. Isso expõe credenciais reais do Grafana/Top Massagens a qualquer pessoa com acesso ao repositório.

🔒️ Proposta de correção usando variáveis de ambiente
-GRAFANA_URL = "https://grafana.topmassagens.com.br/api/ds/query?ds_type=mysql"
-DATASOURCE_UID = "dfpqllglhd88wf"
-AUTH_USER = "timetop"
-AUTH_PASS = "77308515"
+GRAFANA_URL = os.environ["GRAFANA_URL"]
+DATASOURCE_UID = os.environ["GRAFANA_DATASOURCE_UID"]
+AUTH_USER = os.environ["GRAFANA_AUTH_USER"]
+AUTH_PASS = os.environ["GRAFANA_AUTH_PASS"]

Recomendo também revogar/rotacionar essa senha, já que foi exposta.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
GRAFANA_URL = "https://grafana.topmassagens.com.br/api/ds/query?ds_type=mysql"
DATASOURCE_UID = "dfpqllglhd88wf"
AUTH_USER = "timetop"
AUTH_PASS = "77308515"
GRAFANA_URL = os.environ["GRAFANA_URL"]
DATASOURCE_UID = os.environ["GRAFANA_DATASOURCE_UID"]
AUTH_USER = os.environ["GRAFANA_AUTH_USER"]
AUTH_PASS = os.environ["GRAFANA_AUTH_PASS"]
🧰 Tools
🪛 Ruff (0.15.21)

[error] 14-14: Possible hardcoded password assigned to: "AUTH_PASS"

(S105)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@export_emails.py` around lines 11 - 14, Remova as credenciais e configurações
de produção hardcoded em GRAFANA_URL, DATASOURCE_UID, AUTH_USER e AUTH_PASS;
carregue esses valores por variáveis de ambiente, validando sua presença antes
de executar as requisições. Atualize o fluxo de exportação para usar os valores
carregados e revogue/rotacione a senha atualmente exposta.

Source: Linters/SAST tools

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/Feature/Planner/GenerateDailyPlanJobTest.php`:
- Around line 30-35: Atualize o matcher de generate no teste para também validar
que $mode é DailyPlanModeEnum::FULL e que $date representa 00:00:00, preservando
as verificações existentes de data e timezone; isso deve eliminar os parâmetros
não utilizados apontados pelo PHPMD.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e742b50e-28b3-4a60-b801-fffe02938543

📥 Commits

Reviewing files that changed from the base of the PR and between 32b7049 and b01d487.

📒 Files selected for processing (2)
  • app/Jobs/GenerateDailyPlan.php
  • tests/Feature/Planner/GenerateDailyPlanJobTest.php
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/Jobs/GenerateDailyPlan.php

Comment on lines +30 to +35
$generator->shouldReceive('generate')
->once()
->withArgs(function (Company $company, CarbonImmutable $date, DailyPlanModeEnum $mode): bool {
return $date->toDateString() === '2026-07-25'
&& $date->timezone->getName() === 'America/Sao_Paulo';
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Valide o modo e o início do dia no matcher

O teste verifica apenas a data e o timezone. Uma regressão que envie DailyPlanModeEnum::LIGHT ou uma data que não esteja em 00:00:00 continuaria passando, apesar de o job dever encaminhar FULL e aplicar startOfDay(). Isso também explica os parâmetros não utilizados apontados pelo PHPMD.

Correção sugerida
-            ->withArgs(function (Company $company, CarbonImmutable $date, DailyPlanModeEnum $mode): bool {
-                return $date->toDateString() === '2026-07-25'
-                    && $date->timezone->getName() === 'America/Sao_Paulo';
+            ->withArgs(function (Company $company, CarbonImmutable $date, DailyPlanModeEnum $mode): bool {
+                return $date->toDateString() === '2026-07-25'
+                    && $date->format('H:i:s') === '00:00:00'
+                    && $date->timezone->getName() === 'America/Sao_Paulo'
+                    && $mode === DailyPlanModeEnum::FULL;
             })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$generator->shouldReceive('generate')
->once()
->withArgs(function (Company $company, CarbonImmutable $date, DailyPlanModeEnum $mode): bool {
return $date->toDateString() === '2026-07-25'
&& $date->timezone->getName() === 'America/Sao_Paulo';
})
$generator->shouldReceive('generate')
->once()
->withArgs(function (Company $company, CarbonImmutable $date, DailyPlanModeEnum $mode): bool {
return $date->toDateString() === '2026-07-25'
&& $date->format('H:i:s') === '00:00:00'
&& $date->timezone->getName() === 'America/Sao_Paulo'
&& $mode === DailyPlanModeEnum::FULL;
})
🧰 Tools
🪛 PHPMD (2.15.0)

[warning] 32-32: Avoid unused parameters such as '$company'. (undefined)

(UnusedFormalParameter)


[warning] 32-32: Avoid unused parameters such as '$mode'. (undefined)

(UnusedFormalParameter)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Feature/Planner/GenerateDailyPlanJobTest.php` around lines 30 - 35,
Atualize o matcher de generate no teste para também validar que $mode é
DailyPlanModeEnum::FULL e que $date representa 00:00:00, preservando as
verificações existentes de data e timezone; isso deve eliminar os parâmetros não
utilizados apontados pelo PHPMD.

Source: Linters/SAST tools

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
app/Services/Planner/DailyPlanPromptBuilder.php (2)

40-40: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Valide no parser o reason obrigatório para follow-ups.

A regra exige um trecho da última mensagem, mas DailyPlanResponseParser apenas limita o campo e aceita null ou texto sem citação. Uma resposta inválida da IA ainda vira um item sem evidência; rejeite ou sinalize esse caso.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Services/Planner/DailyPlanPromptBuilder.php` at line 40, Atualize o
DailyPlanResponseParser para validar o reason obrigatório em itens de follow_up
e responder cliente, rejeitando ou sinalizando valores nulos, vazios ou sem um
trecho entre aspas da última mensagem relevante. Preserve a validação existente
de tamanho e permita a exceção descrita para anexos sem texto, desde que o
reason indique explicitamente essa situação.

44-46: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Não trate todo LIGHT como dia de folga.

DailyPlanGenerator também muda FULL para LIGHT quando a capacidade restante chega a zero em um dia útil. Esta regra pode fazer a IA afirmar que é folga e produzir um resumo incorreto. Diferencie “dia sem expediente” de “capacidade já esgotada”.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Services/Planner/DailyPlanPromptBuilder.php` around lines 44 - 46,
Atualize a regra adicionada em DailyPlanPromptBuilder para identificar
explicitamente quando o dia não tem expediente, em vez de aplicá-la a todo
DailyPlanModeEnum::LIGHT. Preserve um comportamento separado para dias úteis
cuja capacidade foi esgotada por DailyPlanGenerator, evitando que a IA os
descreva como folga ou gere um resumo incorreto.
app/Services/Planner/DailyPlanContextBuilder.php (2)

47-55: 🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Garanta que context_max_chars seja um limite efetivo.

Se as três versões ainda excederem o limite, build() retorna o último contexto mesmo assim. Isso pode ultrapassar limites de tokens do CLI e torna a degradação ineficaz. A etapa final deve remover/truncar seções de menor prioridade ou falhar explicitamente.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Services/Planner/DailyPlanContextBuilder.php` around lines 47 - 55,
Atualize o método build(), no fluxo após iterar por todas as degradações, para
garantir que o contexto retornado nunca exceda context_max_chars. Se nenhum
contexto couber, remova ou trunque as seções de menor prioridade na etapa final;
caso isso não seja possível, falhe explicitamente em vez de retornar o último
contexto excedente.

109-114: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Alinhe as whitelists aos IDs realmente presentes no contexto.

Os marcadores de tarefas exibem conversation_id vindo de todas as conversas, mas validConversationIds usa apenas o recorte recente; o parser pode remover um ID que o modelo acabou de receber. Além disso, validClientIds aceita qualquer cliente da empresa, mesmo sem aparecer no texto, permitindo itens sem evidência contextual. Gere as whitelists a partir dos registros renderizados.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Services/Planner/DailyPlanContextBuilder.php` around lines 109 - 114,
Atualize a construção de validConversationIds e validClientIds no contexto para
derivá-las dos registros efetivamente renderizados, mantendo apenas IDs
presentes no texto enviado ao modelo. Não use o recorte recente de
$conversations nem todos os clientes retornados por Client::allTenants() como
fonte das whitelists.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/Services/Planner/DailyPlanPromptBuilder.php`:
- Line 34: Unifique o limiar de silêncio entre o prompt e os marcadores: em
app/Services/Planner/DailyPlanPromptBuilder.php#L34-L34, derive a instrução do
mesmo limiar usado pelos marcadores; em
app/Services/Planner/DailyPlanContextBuilder.php#L228-L228, calcule esse limiar
a partir da configuração única escolhida; e em
app/Services/Planner/DailyPlanContextBuilder.php#L285-L288, aplique exatamente a
mesma fronteira ao gerar os marcadores.
- Line 33: Centralize the effective planning budget in the parser rather than
relying only on the prompt text: update the parser’s capacity validation to
reserve approximately 15% of the remaining capacity and reject plans exceeding
that budget, including follow_up and habit items currently allowed up to 120%.
Add or update tests covering the reserved-capacity boundary and over-budget
plans.

---

Outside diff comments:
In `@app/Services/Planner/DailyPlanContextBuilder.php`:
- Around line 47-55: Atualize o método build(), no fluxo após iterar por todas
as degradações, para garantir que o contexto retornado nunca exceda
context_max_chars. Se nenhum contexto couber, remova ou trunque as seções de
menor prioridade na etapa final; caso isso não seja possível, falhe
explicitamente em vez de retornar o último contexto excedente.
- Around line 109-114: Atualize a construção de validConversationIds e
validClientIds no contexto para derivá-las dos registros efetivamente
renderizados, mantendo apenas IDs presentes no texto enviado ao modelo. Não use
o recorte recente de $conversations nem todos os clientes retornados por
Client::allTenants() como fonte das whitelists.

In `@app/Services/Planner/DailyPlanPromptBuilder.php`:
- Line 40: Atualize o DailyPlanResponseParser para validar o reason obrigatório
em itens de follow_up e responder cliente, rejeitando ou sinalizando valores
nulos, vazios ou sem um trecho entre aspas da última mensagem relevante.
Preserve a validação existente de tamanho e permita a exceção descrita para
anexos sem texto, desde que o reason indique explicitamente essa situação.
- Around line 44-46: Atualize a regra adicionada em DailyPlanPromptBuilder para
identificar explicitamente quando o dia não tem expediente, em vez de aplicá-la
a todo DailyPlanModeEnum::LIGHT. Preserve um comportamento separado para dias
úteis cuja capacidade foi esgotada por DailyPlanGenerator, evitando que a IA os
descreva como folga ou gere um resumo incorreto.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9067b304-cdda-448e-b890-ab889bee54b3

📥 Commits

Reviewing files that changed from the base of the PR and between 3499575 and 495ed2d.

📒 Files selected for processing (4)
  • app/Services/Planner/DailyPlanContextBuilder.php
  • app/Services/Planner/DailyPlanGenerator.php
  • app/Services/Planner/DailyPlanPromptBuilder.php
  • tests/Feature/Planner/DailyPlanGeneratorTest.php
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/Services/Planner/DailyPlanGenerator.php
  • tests/Feature/Planner/DailyPlanGeneratorTest.php

];

$rules = [
"1. A soma de estimated_minutes NÃO pode passar da capacidade RESTANTE de hoje ({$capacity}), que já desconta o tempo trabalhado até agora; deixe ~15% de folga para imprevistos.",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Não deixe a política de capacidade apenas como instrução do prompt.

O parser aceita a capacidade total e ainda permite follow_up/habit até 120% dela; portanto, não reserva os ~15% solicitados aqui e pode gerar um plano acima do tempo restante. Centralize o orçamento efetivo no parser e cubra essa regra com testes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Services/Planner/DailyPlanPromptBuilder.php` at line 33, Centralize the
effective planning budget in the parser rather than relying only on the prompt
text: update the parser’s capacity validation to reserve approximately 15% of
the remaining capacity and reject plans exceeding that budget, including
follow_up and habit items currently allowed up to 120%. Add or update tests
covering the reserved-capacity boundary and over-budget plans.

Comment thread app/Services/Planner/DailyPlanPromptBuilder.php
luizhakan and others added 5 commits July 25, 2026 15:45
…que usuários especifiquem minutos adicionais e ajustem a geração do plano diário
…de conversas

O Codex/AGY rejeitam UTF-8 inválido na stdin e a mensagem de erro resultante
(que ecoava o prompt) quebrava a resposta JSON do Livewire. O prompt agora
passa por mb_scrub antes de chegar nos CLIs, e o max_execution_time da
requisição acompanha o timeout do processo.

Junto: botão "ver mensagens anteriores" no chat do WhatsApp, que carrega o
histórico de 50 em 50 até o início da conversa, e filtros da tela de tarefas
responsivos no celular.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PqRofErS4gVjLz9BZoadc5
Permite plugar qualquer agente que fale MCP por HTTP (Claude Code, codex, agy)
no PeJota para pegar contexto de um cliente: cadastro, contexto de IA, projetos,
tarefas, notas e o histórico de WhatsApp com transcrições.

O token é o cliente. Nenhuma tool recebe client_id, então não há caminho para
outro cliente: o escopo vem do token, não dos argumentos. Faturas e valores
não são expostos.

Três camadas garantem que nada escreve: só existem tools de leitura; toda
query nasce do cliente do token com company_id e client_id fixos; e a
requisição roda na conexão sqlite_readonly com um guard que barra qualquer
statement que não seja select.

Na interface, Settings > MCP Accesses mostra quantos clientes estão expostos,
a lista de clientes ganhou coluna e ação de MCP, e criar um acesso leva ao
manual de conexão com o token (exibido uma única vez), o comando do Claude
Code e o JSON para .mcp.json, além do botão de revogar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PqRofErS4gVjLz9BZoadc5
O pacote só existia no projeto como dependência do laravel/boost, que é de
desenvolvimento. Em produção o composer instala com --no-dev, então o
vendor/laravel/mcp não existia e a rota /mcp/client respondia 404. A versão
instalada é a mesma que já estava no lock (v0.5.9), apenas mudou de seção.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PqRofErS4gVjLz9BZoadc5
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
C Reliability Rating on New Code (required ≥ A)
D Security Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant