diff --git a/README.md b/README.md
index 34fad7b..a0fb2c2 100644
--- a/README.md
+++ b/README.md
@@ -148,6 +148,7 @@ flowchart LR
- [Catálogo inicial de automações e agentes](docs/automacoes-sugeridas.md)
- [Dados do GLPI para Analytics e ML](docs/glpi-dados-analytics-ml.md)
- [Autonomia com LangGraph em ambiente monitorado](docs/langgraph-autonomia-ambiente-monitorado.md)
+- [Evolucao NOC, ITSM e operacao padronizada](docs/evolucao-noc-operacao.md)
- [Portas e conectividade](docs/portas-e-conectividade.md)
- [Roadmap de implantação](docs/roadmap.md)
- [Guia do backend MVP](backend/README.md)
diff --git a/backend/.env.example b/backend/.env.example
index d4a64fa..f6a533a 100644
--- a/backend/.env.example
+++ b/backend/.env.example
@@ -54,6 +54,8 @@ HELPDESK_EVOLUTION_BASE_URL=http://localhost:8080
HELPDESK_EVOLUTION_API_KEY=
HELPDESK_EVOLUTION_INSTANCE_NAME=helpdeskAutomacao
HELPDESK_EVOLUTION_WEBHOOK_SECRET=
+# Nao habilite em producao: segredo em query string pode vazar em logs/proxies.
+HELPDESK_EVOLUTION_WEBHOOK_ALLOW_QUERY_SECRET=false
# Algumas versões da Evolution entregam o remetente como @lid em vez de telefone.
# Cadastre apenas mapeamentos confirmados: {"LID":"telefone cadastrado no GLPI"}
HELPDESK_EVOLUTION_LID_PHONE_MAP=
diff --git a/backend/app/api/routes/helpdesk.py b/backend/app/api/routes/helpdesk.py
index d7a820c..4c1b49e 100644
--- a/backend/app/api/routes/helpdesk.py
+++ b/backend/app/api/routes/helpdesk.py
@@ -37,6 +37,9 @@
LLMGenerateResponse,
IdentityLookupResponse,
LLMStatusResponse,
+ NOCAlertReviewRequest,
+ NOCAlertReviewResponse,
+ NOCOperationalReportResponse,
NormalizedWhatsAppMessage,
RuntimeAuditOverviewResponse,
RuntimeAutomationRunnerStatusResponse,
@@ -142,6 +145,30 @@ async def get_ticket_operations_summary(
return await orchestrator.get_ticket_operations_summary()
+@router.get(
+ "/helpdesk/noc/report",
+ response_model=NOCOperationalReportResponse,
+ dependencies=[Depends(require_audit_access), Depends(require_automation_read_access)],
+)
+async def get_noc_operational_report(
+ period_label: str = Query(default="periodo atual", min_length=3, max_length=120),
+ orchestrator: HelpdeskOrchestrator = Depends(get_helpdesk_orchestrator),
+) -> NOCOperationalReportResponse:
+ return await orchestrator.get_noc_operational_report(period_label=period_label)
+
+
+@router.post(
+ "/helpdesk/noc/alerts/review",
+ response_model=NOCAlertReviewResponse,
+ dependencies=[Depends(require_audit_access)],
+)
+async def review_noc_alerts(
+ payload: NOCAlertReviewRequest,
+ orchestrator: HelpdeskOrchestrator = Depends(get_helpdesk_orchestrator),
+) -> NOCAlertReviewResponse:
+ return await orchestrator.review_noc_alerts(payload)
+
+
@router.get(
"/helpdesk/runtime/overview",
response_model=RuntimeOverviewResponse,
@@ -520,7 +547,9 @@ async def receive_evolution_whatsapp_webhook(
)
raw_body = await request.body()
- provided_secret = secret_header or secret_query
+ provided_secret = secret_header
+ if provided_secret is None and settings.evolution_webhook_allow_query_secret:
+ provided_secret = secret_query
if not whatsapp_client.validate_evolution_webhook_secret(provided_secret):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
diff --git a/backend/app/api/runtime_dashboard.py b/backend/app/api/runtime_dashboard.py
index 50a5564..f6ef3f7 100644
--- a/backend/app/api/runtime_dashboard.py
+++ b/backend/app/api/runtime_dashboard.py
@@ -3,7 +3,9 @@
def build_runtime_dashboard_html(*, api_prefix: str) -> str:
overview_path = f"{api_prefix}/helpdesk/runtime/overview"
+ agent_path = f"{api_prefix}/helpdesk/agent/investigate"
overview_path_json = json.dumps(overview_path)
+ agent_path_json = json.dumps(agent_path)
template = """
@@ -361,6 +363,41 @@ def build_runtime_dashboard_html(*, api_prefix: str) -> str:
+
+ Agente LangGraph
+ Investigacao shadow/read-only com GLPI, Zabbix, auditoria, conhecimento local e memoria operacional.
+
+
+ Endpoint:
+ Aguardando investigacao.
+
+
+
+
Containers Docker
Sem leitura dos containers.
@@ -384,13 +421,17 @@ def build_runtime_dashboard_html(*, api_prefix: str) -> str:
"""
- return template.replace("__OVERVIEW_PATH__", overview_path_json)
\ No newline at end of file
+ return template.replace("__OVERVIEW_PATH__", overview_path_json).replace("__AGENT_PATH__", agent_path_json)
diff --git a/backend/app/core/config.py b/backend/app/core/config.py
index ea49a39..7093191 100644
--- a/backend/app/core/config.py
+++ b/backend/app/core/config.py
@@ -55,6 +55,7 @@ class Settings(BaseSettings):
evolution_api_key: str | None = None
evolution_instance_name: str | None = None
evolution_webhook_secret: str | None = None
+ evolution_webhook_allow_query_secret: bool = False
evolution_lid_phone_map: dict[str, str] = {}
operational_postgres_dsn: str | None = None
diff --git a/backend/app/orchestration/helpdesk.py b/backend/app/orchestration/helpdesk.py
index 4f73ede..d2054b7 100644
--- a/backend/app/orchestration/helpdesk.py
+++ b/backend/app/orchestration/helpdesk.py
@@ -1,5 +1,6 @@
from dataclasses import dataclass, field
from datetime import datetime, timedelta, timezone
+import hashlib
from app.schemas.helpdesk import (
AutomationJobCreateRequest,
@@ -13,6 +14,12 @@
CorrelationResponse,
IdentityLookupResponse,
MassIncidentCandidateResponse,
+ NOCActionItemResponse,
+ NOCAlertReviewItemRequest,
+ NOCAlertClassificationResponse,
+ NOCAlertReviewRequest,
+ NOCAlertReviewResponse,
+ NOCOperationalReportResponse,
NormalizedWhatsAppMessage,
OperationalAssistantResponse,
RequesterIdentity,
@@ -78,6 +85,26 @@
"inicio": "help",
"start": "help",
}
+STATUS_QUERY_ALIASES = {"atual", "consulta", "consultar", "current", "status", "ver"}
+OPERATOR_GREETING_MESSAGES = {
+ "bom dia",
+ "boa noite",
+ "boa tarde",
+ "hello",
+ "hi",
+ "ola",
+ "olá",
+ "oi",
+}
+LLM_OPERATOR_REPLY_FORBIDDEN_MARKERS = {
+ "aqui está",
+ "mensagem base",
+ "mensagem reescrita",
+ "prompt",
+ "reescreva",
+ "reescrevi",
+ "reescrita",
+}
OPERATIONAL_ROLES = {
UserRole.TECHNICIAN,
@@ -1000,6 +1027,385 @@ async def get_ticket_operations_summary(self) -> TicketOperationsSummaryResponse
notes=summary.notes,
)
+ async def get_noc_operational_report(
+ self,
+ period_label: str = "periodo atual",
+ ) -> NOCOperationalReportResponse:
+ ticket_summary = await self.get_ticket_operations_summary()
+ automation_summary = await self.get_automation_summary()
+ action_items = self._build_noc_action_items(ticket_summary, automation_summary)
+ operational_risks = self._build_noc_operational_risks(
+ ticket_summary,
+ automation_summary,
+ )
+ agenda = self._build_incident_meeting_agenda(ticket_summary, action_items)
+
+ executive_summary = (
+ f"NOC com {ticket_summary.unresolved_backlog_count} ticket(s) em backlog, "
+ f"{ticket_summary.high_priority_backlog_count} de alta criticidade, "
+ f"{ticket_summary.mass_incident_candidate_count} agrupamento(s) candidato(s) "
+ f"a incidente em massa e {automation_summary.total_jobs} job(s) de automacao "
+ "no periodo analisado."
+ )
+ if operational_risks:
+ executive_summary = f"{executive_summary} Riscos principais: {operational_risks[0]}"
+
+ return NOCOperationalReportResponse(
+ generated_at=datetime.now(timezone.utc).isoformat(),
+ period_label=period_label.strip() or "periodo atual",
+ executive_summary=executive_summary,
+ ticket_operations=ticket_summary,
+ automation=automation_summary,
+ action_items=action_items,
+ incident_meeting_agenda=agenda,
+ operational_risks=operational_risks,
+ notes=[
+ *ticket_summary.notes,
+ *automation_summary.notes,
+ "Relatorio consolidado gerado a partir de snapshots analiticos, auditoria operacional e fila de automacao.",
+ ],
+ )
+
+ async def review_noc_alerts(
+ self,
+ payload: NOCAlertReviewRequest,
+ ) -> NOCAlertReviewResponse:
+ classifications = [
+ self._classify_noc_alert(alert)
+ for alert in payload.alerts
+ ]
+ classification_counts: dict[str, int] = {}
+ for item in classifications:
+ classification_counts[item.classification] = (
+ classification_counts.get(item.classification, 0) + 1
+ )
+
+ average_assertiveness = 0.0
+ if classifications:
+ average_assertiveness = round(
+ sum(item.assertiveness_percent for item in classifications)
+ / len(classifications),
+ 2,
+ )
+
+ action_items = self._build_alert_review_action_items(classifications)
+ notes = ["Revisao heuristica executada com base nos metadados enviados."]
+ if not classifications:
+ notes.append("Nenhum alerta informado para classificacao.")
+
+ return NOCAlertReviewResponse(
+ generated_at=datetime.now(timezone.utc).isoformat(),
+ period_label=payload.period_label,
+ total_alerts=len(classifications),
+ classification_counts=classification_counts,
+ average_assertiveness_percent=average_assertiveness,
+ alerts=classifications,
+ action_items=action_items,
+ notes=notes,
+ )
+
+ def _build_noc_action_items(
+ self,
+ ticket_summary: TicketOperationsSummaryResponse,
+ automation_summary: AutomationSummaryResponse,
+ ) -> list[NOCActionItemResponse]:
+ action_items: list[NOCActionItemResponse] = []
+
+ if ticket_summary.mass_incident_candidate_count > 0:
+ candidate = ticket_summary.mass_incident_candidates[0]
+ action_items.append(
+ NOCActionItemResponse(
+ area="gestao_incidentes",
+ priority=TicketPriority.CRITICAL,
+ title="Avaliar incidente em massa candidato",
+ recommendation=(
+ "Abrir ou vincular incidente pai no ITSM e conduzir pauta na reuniao de Gestao de Incidentes."
+ ),
+ evidence=(
+ f"{candidate.ticket_count} ticket(s) agrupados por {candidate.scope} em {candidate.label}."
+ ),
+ owner_hint=candidate.routed_to or "Supervisor NOC",
+ due_hint="hoje",
+ )
+ )
+
+ if ticket_summary.unassigned_backlog_count > 0:
+ action_items.append(
+ NOCActionItemResponse(
+ area="helpdesk",
+ priority=TicketPriority.HIGH,
+ title="Distribuir backlog sem responsavel",
+ recommendation=(
+ "Atribuir tickets sem dono e registrar primeira tratativa para reduzir aging operacional."
+ ),
+ evidence=f"{ticket_summary.unassigned_backlog_count} ticket(s) em backlog sem atribuicao.",
+ owner_hint="Supervisor NOC",
+ due_hint="proximo ciclo operacional",
+ )
+ )
+
+ if ticket_summary.high_priority_backlog_count > 0:
+ action_items.append(
+ NOCActionItemResponse(
+ area="sla",
+ priority=TicketPriority.HIGH,
+ title="Revisar tickets de alta criticidade",
+ recommendation=(
+ "Validar severidade, comunicacao, escalonamento e risco de SLA para tickets high/critical."
+ ),
+ evidence=(
+ f"{ticket_summary.high_priority_backlog_count} ticket(s) high/critical ainda nao resolvido(s)."
+ ),
+ owner_hint="Gestao de Incidentes",
+ due_hint="8 x 5",
+ )
+ )
+
+ if (
+ ticket_summary.unresolved_backlog_count > 0
+ and ticket_summary.backlog_assignment_coverage_percent < 80
+ ):
+ action_items.append(
+ NOCActionItemResponse(
+ area="processos",
+ priority=TicketPriority.MEDIUM,
+ title="Melhorar cobertura de atribuicao da fila",
+ recommendation="Revisar roteamento, grupos responsaveis e regra de escalonamento inicial.",
+ evidence=(
+ f"Cobertura de atribuicao em {ticket_summary.backlog_assignment_coverage_percent}%."
+ ),
+ owner_hint="Dono do processo ITSM",
+ )
+ )
+
+ if (
+ ticket_summary.total_tickets > 0
+ and ticket_summary.average_correlation_event_count < 1
+ ):
+ action_items.append(
+ NOCActionItemResponse(
+ area="monitoracao",
+ priority=TicketPriority.MEDIUM,
+ title="Aumentar correlacao entre tickets e eventos",
+ recommendation=(
+ "Revisar tags, servicos, ativos e regra de integracao entre monitoracao e ITSM."
+ ),
+ evidence=(
+ "Media de eventos correlacionados por ticket abaixo de 1.0 "
+ f"({ticket_summary.average_correlation_event_count})."
+ ),
+ owner_hint="Monitoracao",
+ )
+ )
+
+ if automation_summary.dead_letter_queue_depth > 0:
+ action_items.append(
+ NOCActionItemResponse(
+ area="automacao",
+ priority=TicketPriority.HIGH,
+ title="Tratar automacoes em dead-letter",
+ recommendation=(
+ "Analisar erros, corrigir pre-condicoes e decidir reprocessamento ou cancelamento."
+ ),
+ evidence=f"{automation_summary.dead_letter_queue_depth} job(s) na dead-letter queue.",
+ owner_hint="Administrador da plataforma",
+ due_hint="hoje",
+ )
+ )
+
+ if automation_summary.queue_depth > 10:
+ action_items.append(
+ NOCActionItemResponse(
+ area="automacao",
+ priority=TicketPriority.MEDIUM,
+ title="Verificar profundidade da fila de automacao",
+ recommendation=(
+ "Avaliar capacidade do worker, retries e aprovacao pendente antes de ampliar uso operacional."
+ ),
+ evidence=f"{automation_summary.queue_depth} job(s) aguardando processamento.",
+ owner_hint="Administrador da plataforma",
+ )
+ )
+
+ return action_items
+
+ def _build_noc_operational_risks(
+ self,
+ ticket_summary: TicketOperationsSummaryResponse,
+ automation_summary: AutomationSummaryResponse,
+ ) -> list[str]:
+ risks: list[str] = []
+ if ticket_summary.mass_incident_candidate_count > 0:
+ risks.append("ha agrupamentos de tickets que podem indicar incidente em massa")
+ if ticket_summary.high_priority_backlog_count > 0:
+ risks.append("existem tickets high/critical ainda nao resolvidos")
+ if ticket_summary.unassigned_backlog_count > 0:
+ risks.append("ha backlog sem responsavel atribuido")
+ if automation_summary.dead_letter_queue_depth > 0:
+ risks.append("ha automacoes em dead-letter que exigem decisao operacional")
+ if not risks:
+ risks.append("nenhum risco operacional critico foi detectado pelas heuristicas atuais")
+ return risks
+
+ def _build_incident_meeting_agenda(
+ self,
+ ticket_summary: TicketOperationsSummaryResponse,
+ action_items: list[NOCActionItemResponse],
+ ) -> list[str]:
+ agenda = [
+ "Revisar incidentes high/critical abertos e em risco de SLA.",
+ "Validar tickets sem atribuicao e responsaveis por fila.",
+ "Revisar recorrencias por servico, ativo, categoria e causa raiz.",
+ "Confirmar comunicacoes realizadas para equipes impactadas.",
+ "Definir acoes preventivas, demandas e mudancas relacionadas.",
+ ]
+ if ticket_summary.mass_incident_candidate_count > 0:
+ agenda.insert(0, "Decidir abertura ou vinculacao de incidente pai para agrupamentos candidatos.")
+ if any(item.area == "monitoracao" for item in action_items):
+ agenda.append("Priorizar revisao de alertas, tags e correlacao monitoracao-ITSM.")
+ return agenda
+
+ def _classify_noc_alert(
+ self,
+ alert: NOCAlertReviewItemRequest,
+ ) -> NOCAlertClassificationResponse:
+ missing_fields = self._missing_alert_fields(alert)
+ evidence: list[str] = []
+ if missing_fields:
+ evidence.append(f"Campos obrigatorios ausentes: {', '.join(missing_fields)}.")
+
+ if alert.duplicate_of:
+ classification = "redundante"
+ recommended_action = "Consolidar com o alerta principal ou desativar com aprovacao."
+ evidence.append(f"Marcado como duplicado de {alert.duplicate_of}.")
+ elif missing_fields:
+ classification = "incompleto"
+ recommended_action = "Completar metadados, dono, runbook, recuperacao e regra ITSM."
+ elif alert.event_count > 0 and alert.incident_count == 0:
+ classification = "ruidoso"
+ recommended_action = "Ajustar expressao, janela, dependencia, severidade ou supressao."
+ evidence.append("Alerta possui eventos sem incidentes relacionados.")
+ elif self._false_positive_rate(alert) >= 60:
+ classification = "ruidoso"
+ recommended_action = "Revisar threshold e dependencia; validar com historico de incidentes."
+ evidence.append(f"Taxa de falso positivo em {self._false_positive_rate(alert)}%.")
+ else:
+ classification = "assertivo"
+ recommended_action = "Manter ativo, garantir POP/runbook e acompanhar tendencia."
+ evidence.append("Metadados minimos presentes e relacao operacional aceitavel.")
+
+ assertiveness = self._alert_assertiveness_percent(alert, classification, missing_fields)
+
+ return NOCAlertClassificationResponse(
+ alert_name=alert.alert_name,
+ service_name=alert.service_name,
+ asset_name=alert.asset_name,
+ severity=alert.severity,
+ classification=classification,
+ assertiveness_percent=assertiveness,
+ recommended_action=recommended_action,
+ missing_fields=missing_fields,
+ evidence=evidence,
+ )
+
+ def _missing_alert_fields(
+ self,
+ alert: NOCAlertReviewItemRequest,
+ ) -> list[str]:
+ missing: list[str] = []
+ required_values = {
+ "service_name_or_asset_name": alert.service_name or alert.asset_name,
+ "severity": alert.severity,
+ "responsible_group": alert.responsible_group,
+ "runbook": alert.runbook,
+ "trigger_expression": alert.trigger_expression,
+ "recovery_criteria": alert.recovery_criteria,
+ }
+ for field_name, value in required_values.items():
+ if not str(value or "").strip():
+ missing.append(field_name)
+ if not alert.has_itsm_rule:
+ missing.append("itsm_rule")
+ return missing
+
+ def _false_positive_rate(
+ self,
+ alert: NOCAlertReviewItemRequest,
+ ) -> float:
+ if alert.event_count <= 0:
+ return 0.0
+ return round((alert.false_positive_count / alert.event_count) * 100, 2)
+
+ def _alert_assertiveness_percent(
+ self,
+ alert: NOCAlertReviewItemRequest,
+ classification: str,
+ missing_fields: list[str],
+ ) -> float:
+ if alert.event_count <= 0:
+ base = 50.0
+ else:
+ valid_events = max(0, alert.event_count - alert.false_positive_count)
+ base = round((valid_events / alert.event_count) * 100, 2)
+
+ base -= len(missing_fields) * 8
+ if classification == "redundante":
+ base -= 25
+ elif classification == "incompleto":
+ base -= 15
+ elif classification == "ruidoso":
+ base -= 10
+
+ return round(min(100.0, max(0.0, base)), 2)
+
+ def _build_alert_review_action_items(
+ self,
+ classifications: list[NOCAlertClassificationResponse],
+ ) -> list[NOCActionItemResponse]:
+ counts: dict[str, int] = {}
+ for item in classifications:
+ counts[item.classification] = counts.get(item.classification, 0) + 1
+
+ action_items: list[NOCActionItemResponse] = []
+ if counts.get("ruidoso", 0) > 0:
+ action_items.append(
+ NOCActionItemResponse(
+ area="monitoracao",
+ priority=TicketPriority.HIGH,
+ title="Reduzir alertas ruidosos",
+ recommendation=(
+ "Revisar thresholds, dependencias, janelas e severidades dos alertas classificados como ruidosos."
+ ),
+ evidence=f"{counts['ruidoso']} alerta(s) com baixa assertividade operacional.",
+ owner_hint="Monitoracao",
+ )
+ )
+ if counts.get("redundante", 0) > 0:
+ action_items.append(
+ NOCActionItemResponse(
+ area="monitoracao",
+ priority=TicketPriority.MEDIUM,
+ title="Consolidar alertas redundantes",
+ recommendation="Desativar ou agrupar duplicidades apos aprovacao e registro de mudanca.",
+ evidence=f"{counts['redundante']} alerta(s) redundante(s).",
+ owner_hint="Monitoracao",
+ )
+ )
+ if counts.get("incompleto", 0) > 0:
+ action_items.append(
+ NOCActionItemResponse(
+ area="processos",
+ priority=TicketPriority.MEDIUM,
+ title="Completar governanca dos alertas",
+ recommendation=(
+ "Preencher dono, runbook, criterio de recuperacao, tags e regra ITSM antes de considerar produtivo."
+ ),
+ evidence=f"{counts['incompleto']} alerta(s) sem campos obrigatorios.",
+ owner_hint="Dono do processo NOC",
+ )
+ )
+ return action_items
+
async def process_whatsapp_webhook_messages(
self,
messages: list[NormalizedWhatsAppMessage],
@@ -1011,13 +1417,17 @@ async def process_whatsapp_webhook_messages(
now = datetime.now(timezone.utc)
self._purge_processed_whatsapp_message_ids(now)
for message in messages:
- dedup_key = self._whatsapp_message_dedup_key(message)
- if dedup_key and dedup_key in PROCESSED_WHATSAPP_MESSAGE_IDS:
+ dedup_keys = self._whatsapp_message_dedup_keys(message)
+ matched_dedup_key = next(
+ (key for key in dedup_keys if key in PROCESSED_WHATSAPP_MESSAGE_IDS),
+ None,
+ )
+ if matched_dedup_key:
ignored_events.append(
- f"Mensagem duplicada ignorada: {message.external_message_id}."
+ self._build_whatsapp_duplicate_note(message, matched_dedup_key)
)
continue
- if dedup_key:
+ for dedup_key in dedup_keys:
PROCESSED_WHATSAPP_MESSAGE_IDS[dedup_key] = now
try:
response = await self.process_whatsapp_message(message)
@@ -1027,7 +1437,7 @@ async def process_whatsapp_webhook_messages(
)
continue
except Exception:
- if dedup_key:
+ for dedup_key in dedup_keys:
PROCESSED_WHATSAPP_MESSAGE_IDS.pop(dedup_key, None)
raise
interactions.append(response)
@@ -1054,10 +1464,25 @@ async def process_meta_webhook_messages(
) -> WhatsAppWebhookProcessingResponse:
return await self.process_whatsapp_webhook_messages(messages, ignored_events)
- def _whatsapp_message_dedup_key(self, message: NormalizedWhatsAppMessage) -> str | None:
- if not message.external_message_id:
- return None
- return f"{message.sender_phone}:{message.external_message_id}"
+ def _whatsapp_message_dedup_keys(self, message: NormalizedWhatsAppMessage) -> list[str]:
+ keys: list[str] = []
+ if message.external_message_id:
+ keys.append(f"id:{message.sender_phone}:{message.external_message_id}")
+
+ normalized_text = " ".join(message.text.casefold().split())
+ if normalized_text:
+ digest = hashlib.sha256(normalized_text.encode("utf-8")).hexdigest()[:20]
+ keys.append(f"content:{message.sender_phone}:{digest}")
+ return keys
+
+ def _build_whatsapp_duplicate_note(
+ self,
+ message: NormalizedWhatsAppMessage,
+ dedup_key: str,
+ ) -> str:
+ if dedup_key.startswith("content:"):
+ return "Mensagem duplicada ignorada: conteúdo recente já processado."
+ return f"Mensagem duplicada ignorada: {message.external_message_id}."
def _purge_processed_whatsapp_message_ids(self, now: datetime) -> None:
expired_before = now - WHATSAPP_MESSAGE_DEDUP_TTL
@@ -1155,6 +1580,29 @@ async def _run_operator_assistant(
message: NormalizedWhatsAppMessage,
requester: RequesterIdentity,
) -> OperationalAssistantResponse:
+ available_commands = self._available_command_docs(requester.role)
+ if self._is_greeting_only(message.text):
+ return OperationalAssistantResponse(
+ role=requester.role,
+ flow_name=self._flow_name_for_role(requester.role),
+ reply_text=self._build_operator_greeting_reply(requester),
+ triage=TicketTriageResponse(
+ suggested_priority=TicketPriority.MEDIUM,
+ resolved_priority=TicketPriority.MEDIUM,
+ suggested_queue="NOC-Operacional",
+ confidence="low",
+ summary="Saudacao operacional sem incidente informado.",
+ next_steps=["Use /help para comandos ou /open para abrir chamado."],
+ mode="local",
+ notes=["Saudacao simples respondida sem acionar LLM ou abrir chamado."],
+ ),
+ available_commands=available_commands,
+ notes=[
+ f"Fluxo operacional aplicado para o papel {requester.role.value}.",
+ "Saudacao simples respondida sem triagem operacional detalhada.",
+ ],
+ )
+
triage = await self.triage_ticket(
TicketTriageRequest(
subject=self._build_subject_from_text(message.text, prefix="Operacional WhatsApp"),
@@ -1171,7 +1619,6 @@ async def _run_operator_assistant(
requester_team=requester.team,
)
)
- available_commands = self._available_command_docs(requester.role)
reply_text, reply_notes = await self._compose_operator_assistant_reply(
role=requester.role,
triage=triage,
@@ -1211,6 +1658,8 @@ async def _compose_operator_assistant_reply(
"Reescreva a mensagem base abaixo para WhatsApp com tom humano, cordial e profissional. "
"Preserve exatamente comandos com '/', filas, status e fatos tecnicos. "
"Nao invente informacoes, nao prometa execucao automatica e nao remova alertas de seguranca. "
+ "Responda apenas com a mensagem final ao usuario. "
+ "Nao diga que reescreveu, revisou, melhorou ou formatou a mensagem. "
"Responda com no maximo 6 linhas.\n\n"
f"Papel operacional: {role.value}\n"
f"Resumo da triagem: {triage.summary}\n"
@@ -1226,7 +1675,8 @@ async def _compose_operator_assistant_reply(
user_prompt=prompt,
system_prompt=(
"Voce melhora a redacao de um assistente virtual de helpdesk. "
- "Seja natural, claro e confiavel sem perder objetividade."
+ "Seja natural, claro e confiavel sem perder objetividade. "
+ "Nunca inclua prefacios como 'aqui esta' ou 'mensagem reescrita'."
),
max_tokens=220,
temperature=0.3,
@@ -1234,11 +1684,54 @@ async def _compose_operator_assistant_reply(
except IntegrationError:
return base_reply, []
- refined_reply = result.content.strip()
+ refined_reply = self._sanitize_operator_llm_reply(result.content)
if not refined_reply:
- return base_reply, []
+ return base_reply, [
+ "IA retornou resposta operacional fora do contrato; mantida mensagem padrao."
+ ]
return refined_reply, [f"Resposta operacional refinada pela IA ({result.provider})."]
+ def _is_greeting_only(self, text: str) -> bool:
+ normalized = text.strip().casefold().strip(" !?.;,:\n\t")
+ normalized = " ".join(normalized.split())
+ return normalized in OPERATOR_GREETING_MESSAGES
+
+ def _build_operator_greeting_reply(self, requester: RequesterIdentity) -> str:
+ first_name = (requester.display_name or requester.external_id or "").strip().split(" ")[0]
+ greeting_target = f", {first_name}" if first_name else ""
+ return (
+ f"Olá{greeting_target}. Modo operacional ativo para "
+ f"{self._role_display_name(requester.role)}.\n"
+ "Use /help para ver os comandos disponíveis.\n"
+ "Para abrir chamado: /open ."
+ )
+
+ def _sanitize_operator_llm_reply(self, content: str) -> str | None:
+ text = content.strip()
+ if not text:
+ return None
+
+ if text.startswith("```"):
+ text = text.strip("`").strip()
+
+ lines = [line.strip() for line in text.splitlines()]
+ while lines and self._is_operator_llm_meta_line(lines[0]):
+ lines.pop(0)
+ text = "\n".join(line for line in lines if line).strip().strip('"').strip("'")
+
+ lowered = text.casefold()
+ if not text or len(text) > 1200:
+ return None
+ if any(marker in lowered for marker in LLM_OPERATOR_REPLY_FORBIDDEN_MARKERS):
+ return None
+ return text
+
+ def _is_operator_llm_meta_line(self, line: str) -> bool:
+ normalized = line.strip().casefold().strip(" :.-")
+ if not normalized:
+ return True
+ return any(marker in normalized for marker in LLM_OPERATOR_REPLY_FORBIDDEN_MARKERS)
+
async def _run_operator_command(
self,
message: NormalizedWhatsAppMessage,
@@ -1522,16 +2015,37 @@ async def _run_operator_command(
if command_name == "status":
status_parts = text.split(maxsplit=2)
- if len(status_parts) < 3:
+ if len(status_parts) < 2:
return TechnicianCommandResponse(
command_name="status",
status="invalid",
operation_mode="local",
- reply_text="Uso: /status ",
- notes=["Comando status recebido sem ticket ou status alvo."],
+ reply_text="Uso: /status [new|processing|planned|waiting|solved|closed]",
+ notes=["Comando status recebido sem ticket."],
)
ticket_id = status_parts[1].strip()
+ if len(status_parts) == 2 or status_parts[2].strip().lower() in STATUS_QUERY_ALIASES:
+ try:
+ ticket = await self.get_ticket(ticket_id)
+ except ResourceNotFoundError:
+ return TechnicianCommandResponse(
+ command_name="status",
+ status="not-found",
+ operation_mode="local",
+ reply_text=f"Ticket {ticket_id} não encontrado para consulta de status.",
+ notes=["Consulta de status não executada porque o ticket não existe."],
+ )
+
+ return TechnicianCommandResponse(
+ command_name="status",
+ status="completed",
+ operation_mode=ticket.integration_mode,
+ reply_text=self._build_operator_ticket_status_reply(ticket),
+ ticket=ticket,
+ notes=["Status do ticket consultado sem alterar o chamado."],
+ )
+
status_name = status_parts[2].strip().lower()
if status_name not in PRIVILEGED_ALLOWED_STATUSES:
return TechnicianCommandResponse(
@@ -2092,13 +2606,28 @@ def _build_operator_assistant_reply(
f"Melhor próximo passo agora: {next_step}",
]
if triage.resolution_hints:
- parts.append(f"Tentativa segura sugerida: {triage.resolution_hints[0]}")
+ parts.append(f"Sugestao de resolucao: {triage.resolution_hints[0]}")
if triage.similar_incidents:
- parts.append(f"Referencia parecida: {triage.similar_incidents[0]}")
+ parts.append(f"Caso semelhante: {triage.similar_incidents[0]}")
parts.append("Se quiser registrar um novo chamado, use /open .")
parts.append("Se preferir, envie /help para ver os comandos disponíveis.")
return "\n".join(parts)
+ def _build_operator_ticket_status_reply(self, ticket: TicketDetailsResponse) -> str:
+ updated_at = ticket.updated_at or "sem data de atualização"
+ lines = [
+ f"Status atual do ticket {ticket.ticket_id}: {ticket.status}.",
+ f"Assunto: {ticket.subject}",
+ f"Prioridade: {ticket.priority}. Atualizado em: {updated_at}.",
+ ]
+ if ticket.assigned_glpi_user_id:
+ lines.append(f"Responsável GLPI: {ticket.assigned_glpi_user_id}.")
+ lines.append(
+ "Para alterar: /status "
+ f"{ticket.ticket_id} ."
+ )
+ return "\n".join(lines)
+
def _to_ticket_resolution_entry_response(self, entry: object) -> TicketResolutionEntryResponse:
return TicketResolutionEntryResponse(
source=str(getattr(entry, "source", "unknown")),
@@ -2453,7 +2982,7 @@ def _available_command_docs(self, role: UserRole) -> list[str]:
"help": "/help",
"me": "/me",
"open": "/open ",
- "status": "/status ",
+ "status": "/status [status]",
"ticket": "/ticket ",
}
available_commands = sorted(ALLOWED_OPERATOR_COMMANDS.get(role, set()))
diff --git a/backend/app/schemas/helpdesk.py b/backend/app/schemas/helpdesk.py
index 9d31def..1e9cda5 100644
--- a/backend/app/schemas/helpdesk.py
+++ b/backend/app/schemas/helpdesk.py
@@ -461,6 +461,72 @@ class TicketOperationsSummaryResponse(BaseModel):
notes: list[str] = Field(default_factory=list)
+class NOCActionItemResponse(BaseModel):
+ area: str
+ priority: TicketPriority
+ title: str
+ recommendation: str
+ evidence: str
+ owner_hint: str | None = None
+ due_hint: str | None = None
+
+
+class NOCOperationalReportResponse(BaseModel):
+ generated_at: str
+ period_label: str
+ executive_summary: str
+ ticket_operations: TicketOperationsSummaryResponse
+ automation: AutomationSummaryResponse
+ action_items: list[NOCActionItemResponse] = Field(default_factory=list)
+ incident_meeting_agenda: list[str] = Field(default_factory=list)
+ operational_risks: list[str] = Field(default_factory=list)
+ notes: list[str] = Field(default_factory=list)
+
+
+class NOCAlertReviewItemRequest(BaseModel):
+ alert_name: str = Field(..., min_length=3, max_length=200)
+ service_name: str | None = Field(default=None, max_length=120)
+ asset_name: str | None = Field(default=None, max_length=120)
+ severity: str | None = Field(default=None, max_length=40)
+ responsible_group: str | None = Field(default=None, max_length=120)
+ runbook: str | None = Field(default=None, max_length=240)
+ trigger_expression: str | None = Field(default=None, max_length=500)
+ recovery_criteria: str | None = Field(default=None, max_length=240)
+ has_itsm_rule: bool = False
+ event_count: int = Field(default=0, ge=0)
+ incident_count: int = Field(default=0, ge=0)
+ false_positive_count: int = Field(default=0, ge=0)
+ duplicate_of: str | None = Field(default=None, max_length=200)
+
+
+class NOCAlertReviewRequest(BaseModel):
+ period_label: str = Field(default="periodo atual", min_length=3, max_length=120)
+ alerts: list[NOCAlertReviewItemRequest] = Field(default_factory=list, max_length=200)
+
+
+class NOCAlertClassificationResponse(BaseModel):
+ alert_name: str
+ service_name: str | None = None
+ asset_name: str | None = None
+ severity: str | None = None
+ classification: str
+ assertiveness_percent: float
+ recommended_action: str
+ missing_fields: list[str] = Field(default_factory=list)
+ evidence: list[str] = Field(default_factory=list)
+
+
+class NOCAlertReviewResponse(BaseModel):
+ generated_at: str
+ period_label: str
+ total_alerts: int = 0
+ classification_counts: dict[str, int] = Field(default_factory=dict)
+ average_assertiveness_percent: float = 0.0
+ alerts: list[NOCAlertClassificationResponse] = Field(default_factory=list)
+ action_items: list[NOCActionItemResponse] = Field(default_factory=list)
+ notes: list[str] = Field(default_factory=list)
+
+
class RuntimeHealthResponse(BaseModel):
status: str
service: str
diff --git a/backend/pyproject.toml b/backend/pyproject.toml
index 03bb87a..7f41277 100644
--- a/backend/pyproject.toml
+++ b/backend/pyproject.toml
@@ -14,18 +14,23 @@ dependencies = [
"asyncpg>=0.30,<1.0",
"fastapi>=0.116,<1.0",
"httpx>=0.28,<1.0",
+ "idna>=3.15,<4.0",
"langchain>=1.0,<2.0",
+ "langchain-core>=1.3.3,<2.0",
"langgraph>=1.0,<2.0",
"langgraph-checkpoint-postgres>=3.0,<4.0",
+ "langsmith>=0.8.0,<1.0",
"pydantic-settings>=2.10,<3.0",
"psycopg[binary,pool]>=3.2,<4.0",
"redis>=5.2,<6.0",
+ "starlette>=1.0.1,<2.0",
+ "urllib3>=2.7.0,<3.0",
"uvicorn[standard]>=0.35,<1.0",
]
[project.optional-dependencies]
dev = [
- "pytest>=8.3,<9.0",
+ "pytest>=9.0.3,<10.0",
]
[tool.pytest.ini_options]
diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py
index c0998c1..616f8de 100644
--- a/backend/tests/conftest.py
+++ b/backend/tests/conftest.py
@@ -47,6 +47,7 @@ def isolate_test_settings() -> None:
"evolution_api_key": settings.evolution_api_key,
"evolution_instance_name": settings.evolution_instance_name,
"evolution_webhook_secret": settings.evolution_webhook_secret,
+ "evolution_webhook_allow_query_secret": settings.evolution_webhook_allow_query_secret,
"operational_postgres_dsn": settings.operational_postgres_dsn,
"operational_postgres_schema": settings.operational_postgres_schema,
"operational_audit_retention_days": settings.operational_audit_retention_days,
@@ -101,6 +102,7 @@ def isolate_test_settings() -> None:
settings.evolution_api_key = None
settings.evolution_instance_name = None
settings.evolution_webhook_secret = None
+ settings.evolution_webhook_allow_query_secret = False
settings.operational_postgres_dsn = None
settings.operational_postgres_schema = "helpdesk_platform"
settings.operational_audit_retention_days = 30
diff --git a/backend/tests/test_health.py b/backend/tests/test_health.py
index 0cc9a17..1a88126 100644
--- a/backend/tests/test_health.py
+++ b/backend/tests/test_health.py
@@ -11,6 +11,7 @@
from app.core.config import get_settings
from app.core.dependencies import get_glpi_client, get_zabbix_client
from app.main import app
+from app.orchestration.helpdesk import PROCESSED_WHATSAPP_MESSAGE_IDS
from app.services.docker_runtime import (
DockerApplicationRecord,
DockerContainerRecord,
@@ -277,6 +278,127 @@ def test_ticket_operations_summary_route_returns_mass_incident_candidates() -> N
assert candidate["notes"]
+def test_noc_operational_report_requires_audit_and_automation_read_scope() -> None:
+ response = client.get(
+ "/api/v1/helpdesk/noc/report",
+ headers=AUDIT_HEADERS,
+ )
+
+ assert response.status_code == 401
+ assert "leitura administrativa de automação" in response.json()["detail"].lower()
+
+
+def test_noc_operational_report_returns_actions_and_meeting_agenda() -> None:
+ store = TicketAnalyticsStore(get_settings())
+ now = datetime.now(timezone.utc)
+
+ asyncio.run(
+ store.upsert_snapshot(
+ TicketAnalyticsSnapshotRecord(
+ ticket_id="701",
+ subject="VPN indisponivel para diretoria",
+ description="Incidente critico reportado pela operacao.",
+ status="new",
+ priority="critical",
+ requester_glpi_user_id=31,
+ assigned_glpi_user_id=None,
+ external_id="helpdesk-api-701",
+ request_type_id=1,
+ request_type_name="Direct",
+ category_id=9,
+ category_name="Rede",
+ asset_name="vpn-edge-01",
+ service_name="vpn-corporativa",
+ source_channel="api",
+ routed_to="NOC-Critico",
+ correlation_event_count=0,
+ source_updated_at=now - timedelta(minutes=20),
+ )
+ )
+ )
+
+ response = client.get(
+ "/api/v1/helpdesk/noc/report?period_label=semana%20atual",
+ headers=RUNTIME_OVERVIEW_HEADERS,
+ )
+
+ assert response.status_code == 200
+ body = response.json()
+ assert body["period_label"] == "semana atual"
+ assert "NOC com" in body["executive_summary"]
+ assert body["ticket_operations"]["unresolved_backlog_count"] >= 1
+ assert body["action_items"]
+ assert any(item["area"] in {"helpdesk", "sla", "monitoracao"} for item in body["action_items"])
+ assert body["incident_meeting_agenda"]
+ assert body["operational_risks"]
+
+
+def test_noc_alert_review_classifies_alert_quality() -> None:
+ response = client.post(
+ "/api/v1/helpdesk/noc/alerts/review",
+ headers=AUDIT_HEADERS,
+ json={
+ "period_label": "maio 2026",
+ "alerts": [
+ {
+ "alert_name": "ERP HTTP 5xx alto",
+ "service_name": "erp",
+ "severity": "high",
+ "responsible_group": "NOC-Critico",
+ "runbook": "POP-ERP-HTTP",
+ "trigger_expression": "5xx > 5%",
+ "recovery_criteria": "5xx < 1%",
+ "has_itsm_rule": True,
+ "event_count": 10,
+ "incident_count": 8,
+ "false_positive_count": 1,
+ },
+ {
+ "alert_name": "CPU alta transitoria",
+ "asset_name": "app-01",
+ "severity": "warning",
+ "responsible_group": "Monitoracao",
+ "runbook": "IT-CPU",
+ "trigger_expression": "cpu > 80",
+ "recovery_criteria": "cpu < 70",
+ "has_itsm_rule": True,
+ "event_count": 20,
+ "incident_count": 0,
+ "false_positive_count": 15,
+ },
+ {
+ "alert_name": "Disco cheio duplicado",
+ "asset_name": "db-01",
+ "severity": "high",
+ "responsible_group": "Banco",
+ "runbook": "POP-DISCO",
+ "trigger_expression": "disk > 90",
+ "recovery_criteria": "disk < 80",
+ "has_itsm_rule": True,
+ "event_count": 4,
+ "incident_count": 4,
+ "false_positive_count": 0,
+ "duplicate_of": "Disco cheio principal",
+ },
+ {
+ "alert_name": "Alerta sem governanca",
+ "event_count": 1,
+ },
+ ],
+ },
+ )
+
+ assert response.status_code == 200
+ body = response.json()
+ assert body["total_alerts"] == 4
+ assert body["classification_counts"]["assertivo"] == 1
+ assert body["classification_counts"]["ruidoso"] == 1
+ assert body["classification_counts"]["redundante"] == 1
+ assert body["classification_counts"]["incompleto"] == 1
+ assert body["average_assertiveness_percent"] > 0
+ assert len(body["action_items"]) == 3
+
+
def test_runtime_overview_route_requires_automation_read_scope_alongside_audit_scope() -> None:
response = client.get(
"/api/v1/helpdesk/runtime/overview",
@@ -739,7 +861,13 @@ def test_ops_dashboard_page_is_available_for_browser_access() -> None:
assert response.status_code == 200
assert "Painel operacional do orquestrador" in response.text
assert "/api/v1/helpdesk/runtime/overview" in response.text
+ assert "/api/v1/helpdesk/agent/investigate" in response.text
+ assert "Agente LangGraph" in response.text
+ assert "Investigar" in response.text
assert "Containers Docker" in response.text
+ assert "function escapeHtml" in response.text
+ assert "sessionStorage.setItem(storageKeys.audit" not in response.text
+ assert "sessionStorage.setItem(storageKeys.automation" not in response.text
def test_automation_route_rejects_api_token_without_dedicated_automation_token() -> None:
@@ -2663,6 +2791,28 @@ def test_admin_freeform_message_uses_admin_operational_flow() -> None:
assert body["assistant_result"]["flow_name"] == "admin_operations"
+def test_admin_greeting_uses_clean_operational_reply() -> None:
+ payload = {
+ "sender_phone": "+5511900019999",
+ "sender_name": "Ricardo Santana",
+ "text": "Olá",
+ "requester_role": "user",
+ }
+
+ response = client.post("/api/v1/webhooks/whatsapp/messages", json=payload)
+
+ assert response.status_code == 202
+ body = response.json()
+ reply_text = body["assistant_result"]["reply_text"]
+ assert body["outcome_type"] == "assistant"
+ assert body["requester_role"] == "admin"
+ assert "Olá, Ricardo" in reply_text
+ assert "Modo operacional ativo para administrador" in reply_text
+ assert "mensagem reescrita" not in reply_text.casefold()
+ assert "aqui está" not in reply_text.casefold()
+ assert "confirm" not in reply_text.casefold()
+
+
def test_technician_command_returns_operational_result() -> None:
payload = {
"sender_phone": "+5511912345678",
@@ -2749,6 +2899,61 @@ def test_evolution_duplicate_entrar_message_is_ignored_after_first_reply() -> No
assert any("duplicada" in event for event in second_body["ignored_events"])
+def test_evolution_duplicate_content_is_ignored_even_with_new_message_id() -> None:
+ settings = get_settings()
+ original_secret = settings.evolution_webhook_secret
+ settings.evolution_webhook_secret = "segredo-evolution"
+ PROCESSED_WHATSAPP_MESSAGE_IDS.clear()
+
+ payload = {
+ "event": "MESSAGES_UPSERT",
+ "data": {
+ "key": {
+ "remoteJid": "5521972008679@s.whatsapp.net",
+ "fromMe": False,
+ "id": "EVO-DUP-CONTENT-001",
+ },
+ "pushName": "Paula Almeida",
+ "message": {"conversation": "/help"},
+ "messageType": "conversation",
+ },
+ }
+ duplicate_payload = {
+ **payload,
+ "data": {
+ **payload["data"],
+ "key": {
+ **payload["data"]["key"],
+ "id": "EVO-DUP-CONTENT-002",
+ },
+ },
+ }
+
+ try:
+ first_response = client.post(
+ "/api/v1/webhooks/whatsapp/evolution",
+ json=payload,
+ headers={"X-Evolution-Webhook-Secret": "segredo-evolution"},
+ )
+ second_response = client.post(
+ "/api/v1/webhooks/whatsapp/evolution",
+ json=duplicate_payload,
+ headers={"X-Evolution-Webhook-Secret": "segredo-evolution"},
+ )
+ finally:
+ settings.evolution_webhook_secret = original_secret
+ PROCESSED_WHATSAPP_MESSAGE_IDS.clear()
+
+ assert first_response.status_code == 202
+ assert len(first_response.json()["interactions"]) == 1
+
+ assert second_response.status_code == 202
+ second_body = second_response.json()
+ assert second_body["interactions"] == []
+ assert second_body["integration_mode"] == "noop"
+ assert any("conteúdo recente" in event for event in second_body["ignored_events"])
+
+
def test_technician_ticket_command_reads_existing_ticket() -> None:
create_payload = {
"subject": "Servidor de banco sem resposta",
@@ -2891,6 +3096,58 @@ def test_technician_status_command_updates_allowed_status() -> None:
assert "Sugestao:" in body["command_result"]["reply_text"]
+def test_technician_status_command_reads_status_when_no_new_status_is_sent() -> None:
+ create_payload = {
+ "subject": "Banco de dados lento",
+ "description": "Consultas do ERP estão demorando mais de trinta segundos.",
+ "category": "infra",
+ "asset_name": "db-prod-02",
+ "service_name": "postgresql",
+ "priority": "high",
+ "requester": {
+ "external_id": "user-201",
+ "display_name": "Marina Lopes",
+ "phone_number": "+5511933332222",
+ "role": "user",
+ },
+ }
+ ticket_id = client.post("/api/v1/helpdesk/tickets/open", json=create_payload).json()["ticket_id"]
+
+ response = client.post(
+ "/api/v1/webhooks/whatsapp/messages",
+ json={
+ "sender_phone": "+5511912345678",
+ "sender_name": "Ana Souza",
+ "text": f"/status {ticket_id}",
+ "requester_role": "user",
+ },
+ )
+
+ assert response.status_code == 202
+ body = response.json()
+ assert body["command_result"]["command_name"] == "status"
+ assert body["command_result"]["status"] == "completed"
+ assert body["command_result"]["ticket"]["ticket_id"] == ticket_id
+ assert body["command_result"]["ticket"]["status"] == MOCK_TICKET_STORE[ticket_id].status
+ assert "Status atual do ticket" in body["command_result"]["reply_text"]
+ assert "Uso:" not in body["command_result"]["reply_text"]
+
+ alias_response = client.post(
+ "/api/v1/webhooks/whatsapp/messages",
+ json={
+ "sender_phone": "+5511912345678",
+ "sender_name": "Ana Souza",
+ "text": f"/status {ticket_id} status",
+ "requester_role": "user",
+ },
+ )
+
+ assert alias_response.status_code == 202
+ alias_body = alias_response.json()
+ assert alias_body["command_result"]["ticket"]["status"] == MOCK_TICKET_STORE[ticket_id].status
+ assert "Status inválido" not in alias_body["command_result"]["reply_text"]
+
+
def test_technician_status_solved_records_solution_and_notifies_requester() -> None:
create_payload = {
"subject": "ERP indisponivel para o financeiro",
diff --git a/backend/tests/test_whatsapp_client.py b/backend/tests/test_whatsapp_client.py
index 44797aa..0e6fc5e 100644
--- a/backend/tests/test_whatsapp_client.py
+++ b/backend/tests/test_whatsapp_client.py
@@ -239,7 +239,7 @@ def test_receive_evolution_webhook_rejects_invalid_secret() -> None:
assert "evolution" in response.json()["detail"].lower()
-def test_receive_evolution_webhook_accepts_secret_query_fallback() -> None:
+def test_receive_evolution_webhook_rejects_secret_query_by_default() -> None:
test_client = TestClient(app)
class FakeOrchestrator:
@@ -255,7 +255,9 @@ async def process_whatsapp_webhook_messages(self, messages, ignored_events):
settings = get_settings()
original_secret = settings.evolution_webhook_secret
+ original_allow_query = settings.evolution_webhook_allow_query_secret
settings.evolution_webhook_secret = "segredo-evolution"
+ settings.evolution_webhook_allow_query_secret = False
try:
response = test_client.post(
@@ -276,6 +278,52 @@ async def process_whatsapp_webhook_messages(self, messages, ignored_events):
)
finally:
settings.evolution_webhook_secret = original_secret
+ settings.evolution_webhook_allow_query_secret = original_allow_query
+ app.dependency_overrides.pop(get_helpdesk_orchestrator, None)
+
+ assert response.status_code == 403
+
+
+def test_receive_evolution_webhook_accepts_secret_query_when_explicitly_enabled() -> None:
+ test_client = TestClient(app)
+
+ class FakeOrchestrator:
+ async def process_whatsapp_webhook_messages(self, messages, ignored_events):
+ return WhatsAppWebhookProcessingResponse(
+ processed_messages=len(messages),
+ interactions=[],
+ ignored_events=ignored_events,
+ integration_mode="mock",
+ )
+
+ app.dependency_overrides[get_helpdesk_orchestrator] = lambda: FakeOrchestrator()
+
+ settings = get_settings()
+ original_secret = settings.evolution_webhook_secret
+ original_allow_query = settings.evolution_webhook_allow_query_secret
+ settings.evolution_webhook_secret = "segredo-evolution"
+ settings.evolution_webhook_allow_query_secret = True
+
+ try:
+ response = test_client.post(
+ "/api/v1/webhooks/whatsapp/evolution?secret=segredo-evolution",
+ json={
+ "event": "MESSAGES_UPSERT",
+ "data": {
+ "key": {
+ "remoteJid": "5521972008679@s.whatsapp.net",
+ "fromMe": False,
+ "id": "EVO-QUERY-555",
+ },
+ "pushName": "Paula Almeida",
+ "message": {"conversation": "/me"},
+ "messageType": "conversation",
+ },
+ },
+ )
+ finally:
+ settings.evolution_webhook_secret = original_secret
+ settings.evolution_webhook_allow_query_secret = original_allow_query
app.dependency_overrides.pop(get_helpdesk_orchestrator, None)
assert response.status_code == 202
@@ -302,7 +350,7 @@ async def find_user_by_phone(self, phone_number: str):
try:
response = test_client.post(
- "/api/v1/webhooks/whatsapp/evolution?secret=segredo-evolution",
+ "/api/v1/webhooks/whatsapp/evolution",
json={
"event": "MESSAGES_UPSERT",
"data": {
@@ -316,6 +364,7 @@ async def find_user_by_phone(self, phone_number: str):
"messageType": "conversation",
},
},
+ headers={"X-Evolution-Webhook-Secret": "segredo-evolution"},
)
finally:
settings.evolution_webhook_secret = original_secret
diff --git a/docs/ativacao-whatsapp.md b/docs/ativacao-whatsapp.md
index a3c273f..490909f 100644
--- a/docs/ativacao-whatsapp.md
+++ b/docs/ativacao-whatsapp.md
@@ -146,7 +146,8 @@ Se ainda nao existir, primeiro crie ou conecte a instancia dedicada do helpdesk
### Observacoes praticas
- o backend exige `HELPDESK_EVOLUTION_WEBHOOK_SECRET` e valida o header `X-Evolution-Webhook-Secret`;
-- se a versao da Evolution nao repassar headers customizados, o backend tambem aceita o mesmo segredo em `?secret=...` no webhook;
+- por seguranca, segredo em query string fica desabilitado por padrao, pois URLs aparecem em logs e proxies;
+- se uma versao da Evolution nao repassar headers customizados, habilite temporariamente `HELPDESK_EVOLUTION_WEBHOOK_ALLOW_QUERY_SECRET=true` e use `?secret=...` apenas ate corrigir o caminho de headers.
- em `auto`, se a Evolution estiver configurada, ela sera usada para envio antes da Meta;
- se a Evolution entregar o remetente como `220095666237694@lid`, cadastre um mapa explicito em `HELPDESK_EVOLUTION_LID_PHONE_MAP`, por exemplo `{"220095666237694":"+5521972008679"}`;
- `@lid` sem mapa explicito continua ignorado, para manter a regra de que apenas telefones cadastrados no GLPI podem acionar o assistente;
diff --git a/docs/checklist-go-live.md b/docs/checklist-go-live.md
index eeb9a92..0efa27c 100644
--- a/docs/checklist-go-live.md
+++ b/docs/checklist-go-live.md
@@ -73,6 +73,45 @@ Marcar o que entra nesta liberacao:
- Time sabe que o telefone do usuario precisa existir no `GLPI`.
- Procedimento de contingencia comunicado.
+## Checklist de prontidao operacional do NOC
+
+- POPs criticos revisados, aprovados e versionados.
+- Instrucoes Tecnicas criticas revisadas, aprovadas e versionadas.
+- Matriz de responsaveis por fila, sistema, severidade e horario definida.
+- Procedimento de abertura, triagem, escalonamento, solucao e fechamento validado.
+- Criterios de registro de tratativas no historico do ITSM comunicados.
+- Processo de comunicacao com equipes impactadas definido.
+- Fluxo de incidentes e problemas validado no ITSM adotado pelo ambiente.
+- Pauta da reuniao semanal de Gestao de Incidentes publicada.
+- Relatorio semanal do NOC com modelo aprovado.
+- Relatorio executivo mensal com modelo aprovado.
+- Trilhas de capacitacao de Help Desk, incidentes, problemas e monitoracao definidas.
+- Evidencias de treinamento dos operadores do piloto registradas.
+
+## Checklist de monitoracao e alertas
+
+- Alertas criticos revisados quanto a assertividade.
+- Alertas ruidosos, redundantes ou sem dono mapeados.
+- Triggers do Zabbix com severidade, tags, grupo responsavel e acao esperada.
+- Runbook ou POP associado aos alertas criticos.
+- Dashboards tecnicos do Zabbix ou Grafana revisados.
+- Dashboard executivo publicado ou planejado com data e responsavel.
+- Monitoracoes redundantes revisadas para evitar falsos positivos.
+- Lacunas de monitoracao identificadas a partir de incidentes recorrentes.
+- Regras de correlacao entre eventos e tickets validadas.
+- Escopo de acompanhamento 8 x 5 definido para incidentes em tempo real.
+
+## Checklist de demandas, mudancas e auditoria
+
+- Fluxo de demandas no Azure DevOps definido.
+- Criterio de precificacao por esforco, risco, urgencia, beneficio e recorrencia definido.
+- Mudancas com impacto no NOC possuem aprovador, janela e rollback.
+- Comunicacao de mudanca para equipes impactadas validada.
+- Configuracoes criticas de Zabbix, Grafana, backend e playbooks versionadas.
+- Backup de configuracoes criticas validado.
+- Auditoria de alteracoes criticas habilitada ou documentada.
+- Rotina de restauracao ou reversao testada para configuracoes principais.
+
## Criticos a acompanhar nas primeiras horas
- falhas 401, 403, 404, 500 e 502 no backend
@@ -98,6 +137,10 @@ Horario real de ativacao:
- consulta de ticket respondeu corretamente
- logs ficaram estaveis na primeira hora
- nenhuma autenticacao critica falhou
+- incidente critico possui POP, responsavel, comunicacao e registro no ITSM
+- alerta critico possui dono, severidade, runbook e criterio de escalonamento
+- relatorio operacional inicial do NOC foi gerado ou possui responsavel e data
+- mudancas da janela ficaram registradas com aprovador, evidencias e rollback
## Condicoes de rollback
diff --git a/docs/evolucao-noc-operacao.md b/docs/evolucao-noc-operacao.md
new file mode 100644
index 0000000..ebda341
--- /dev/null
+++ b/docs/evolucao-noc-operacao.md
@@ -0,0 +1,244 @@
+# Evolucao NOC, ITSM e Operacao Padronizada
+
+## Objetivo
+
+Evoluir a plataforma para apoiar a operacao do NOC com processos padronizados, gestao de incidentes e problemas, monitoracao assertiva, indicadores executivos, controle de mudancas, governanca de demandas e melhoria continua.
+
+Esta frente complementa a automacao tecnica ja prevista no projeto. O foco deixa de ser apenas integrar GLPI, Zabbix, WhatsApp e backend, e passa a organizar a rotina operacional do NOC como um sistema auditavel, mensuravel e treinavel.
+
+## Escopo funcional
+
+### 1. POPs, instrucoes tecnicas e capacitacao
+
+Entregas esperadas:
+
+- inventario de Procedimentos Operacionais Padrao por atividade, fila, sistema, severidade e horario;
+- inventario de Instrucoes Tecnicas por ferramenta, servico, painel e rotina critica;
+- revisao, otimizacao e versionamento dos procedimentos existentes;
+- matriz de aderencia entre POP, IT, fila operacional e categoria ITSM;
+- trilha de treinamento para Help Desk, Gestao de Incidentes, Gestao de Problemas e NOC;
+- registro de capacitacao por colaborador, turma, data, conteudo e evidencias;
+- avaliacao periodica de aderencia operacional por amostragem de tickets e incidentes.
+
+Requisitos de controle:
+
+- todo procedimento precisa ter dono, aprovador, data de revisao e versao;
+- toda instrucao critica precisa ter criterio de acionamento, pre-condicoes, rollback e registro esperado no ITSM;
+- mudancas em POP ou IT devem gerar historico de aprovacao e comunicacao para equipes impactadas.
+
+### 2. Help Desk, incidentes, problemas e ITSM
+
+Entregas esperadas:
+
+- fluxo padronizado para abertura, triagem, atendimento, escalonamento, solucao e fechamento;
+- acompanhamento e fechamento de incidentes e problemas em ITSM, GLPI, Ivanti ou Pratica, conforme ambiente do cliente;
+- vinculacao entre incidente, problema, causa raiz, impacto, workaround e solucao definitiva;
+- relatorio de incidente com linha do tempo, impacto, causa raiz, acoes tomadas e prevencao de recorrencia;
+- participacao, elaboracao e apresentacao nas reunioes semanais de Gestao de Incidentes;
+- processo de comunicacao com equipes impactadas e registro de tratativas no historico do NOC.
+
+Indicadores minimos:
+
+- backlog por fila, status, prioridade e responsavel;
+- aging por severidade;
+- SLA violado e em risco;
+- taxa de reabertura;
+- recorrencia por servico, ativo, categoria e causa raiz;
+- tempo ate reconhecimento, tempo ate contencao e tempo ate resolucao;
+- tickets sem categoria, sem atribuicao, sem historico ou sem causa raiz quando obrigatoria.
+
+### 3. Monitoracao, alertas e correlacao
+
+Entregas esperadas:
+
+- revisao de todos os alertas mal configurados ou com baixa assertividade para o NOC;
+- revisao periodica das monitoracoes existentes para reduzir redundancias e falsos positivos;
+- revisao e padronizacao das configuracoes do Zabbix implementadas nas monitoracoes;
+- revisao da criacao de dashboards e paineis inteligentes em Grafana e Zabbix com visao executiva e tecnica;
+- revisao da implementacao de novas metricas e gatilhos com base em incidentes, problemas e tendencias;
+- acompanhamento de incidentes em tempo real, com correlacao entre eventos e sistemas em regime 8 x 5;
+- integracao de ferramentas de monitoracao com ITSM para abertura, atualizacao e correlacao de tickets.
+
+Classificacao dos alertas:
+
+| Classe | Descricao | Acao esperada |
+| --- | --- | --- |
+| assertivo | Alerta indica falha real ou risco claro | manter, documentar runbook e SLA |
+| ruidoso | Alerta aciona sem impacto operacional frequente | ajustar trigger, janela, dependencia ou severidade |
+| redundante | Alerta duplica outro melhor ou de camada superior | consolidar ou desativar com aprovacao |
+| incompleto | Alerta nao tem contexto, dono ou acao clara | enriquecer mensagem, tags e procedimento |
+| ausente | Incidente recorrente sem monitoracao equivalente | criar metrica, item, trigger ou correlacao |
+
+Campos obrigatorios para alertas produtivos:
+
+- servico afetado;
+- ativo, host ou componente;
+- severidade;
+- janela de validacao;
+- criterio de recuperacao;
+- grupo responsavel;
+- runbook ou POP associado;
+- regra de abertura ou correlacao no ITSM;
+- acao esperada do NOC;
+- criterio para escalonamento.
+
+### 4. Gestao de demandas, precificacao e Azure DevOps
+
+Entregas esperadas:
+
+- fluxo de entrada, classificacao, priorizacao e aceite de demandas no Azure DevOps;
+- avaliacao de precificacao por esforco, complexidade, risco, dependencia, urgencia e recorrencia;
+- separacao entre demanda operacional, melhoria, projeto estrategico, problema recorrente e mudanca;
+- rastreabilidade entre demanda, incidente motivador, problema, mudanca, release e evidencia de conclusao;
+- relatorio consolidado de demandas por status, area, prioridade, custo estimado e beneficio esperado.
+
+Modelo inicial de precificacao:
+
+| Dimensao | Escala sugerida | Evidencia |
+| --- | --- | --- |
+| esforco | P, M, G, GG | horas, perfis envolvidos e complexidade tecnica |
+| risco | baixo, medio, alto | impacto potencial, rollback e dependencia |
+| urgencia | baixa, normal, alta, critica | SLA, regulatorio, cliente ou operacao |
+| beneficio | operacional, financeiro, risco, experiencia | indicador esperado antes e depois |
+| recorrencia | pontual, recorrente, estrutural | volume historico e tendencia |
+
+### 5. Gestao de mudancas no NOC
+
+Entregas esperadas:
+
+- alinhamento de mudancas que afetem monitoracao, ITSM, comunicacao ou rotina operacional;
+- criterio para mudanca padrao, normal e emergencial;
+- checklist de impacto no NOC antes da execucao;
+- comunicacao previa para equipes impactadas;
+- registro de janela, aprovador, responsavel tecnico, rollback e validacao pos-mudanca;
+- atualizacao de POP, IT, dashboard, alerta e base de conhecimento quando aplicavel.
+
+### 6. Pessoas, processos e indicadores
+
+Entregas esperadas:
+
+- geracao de informacao para gestao das atividades operacionais e taticas dos colaboradores do NOC;
+- matriz de responsabilidades por processo, fila, sistema e severidade;
+- indicadores de produtividade, qualidade, aderencia a processo e necessidade de capacitacao;
+- planejamento estrategico da estruturacao da equipe de monitoracao;
+- visao de capacidade por turno, perfil, demanda, criticidade e backlog.
+
+Indicadores de gestao:
+
+- volumetria por colaborador, fila e categoria;
+- tempo medio de primeira acao;
+- tempo medio de resolucao;
+- tickets reabertos por colaborador, categoria e causa;
+- tickets sem registro adequado de tratativa;
+- treinamentos concluidos e pendentes;
+- aderencia ao procedimento por amostragem;
+- carga operacional por turno e por fila.
+
+### 7. Relatorios e apresentacoes consolidadas do NOC
+
+Entregas esperadas:
+
+- relatorio semanal de incidentes, problemas, alertas, demandas e mudancas;
+- relatorio mensal executivo com tendencias, riscos, ganhos e plano de melhoria;
+- apresentacao consolidada do NOC para gestao;
+- painel tecnico para acompanhamento diario;
+- painel executivo para disponibilidade, SLA, recorrencia, risco e capacidade.
+
+Estrutura recomendada para relatorio consolidado:
+
+1. resumo executivo;
+2. incidentes relevantes;
+3. problemas e causas raiz;
+4. alertas revisados, ajustados e pendentes;
+5. demandas e mudancas;
+6. indicadores operacionais;
+7. riscos e bloqueios;
+8. acoes concluidas;
+9. proximas acoes;
+10. decisoes solicitadas.
+
+### 8. Controle de versoes, backup e auditoria
+
+Entregas esperadas:
+
+- controle de versao para configuracoes criticas de Zabbix, Grafana, backend, playbooks, POPs e ITs;
+- backup periodico das configuracoes e dashboards;
+- trilha de auditoria para alteracoes em monitoracao, integracoes, alertas, paineis e procedimentos;
+- rotina de restauracao testada;
+- evidencia de quem alterou, quando alterou, por que alterou e qual mudanca aprovou.
+
+## Backlog priorizado
+
+### Onda 1: governanca operacional minima
+
+- Criar catalogo de POPs e ITs.
+- Criar matriz de processos do NOC.
+- Definir modelo de relatorio semanal e mensal.
+- Definir campos obrigatorios de registro de tratativa no ITSM.
+- Criar checklist de mudanca com impacto no NOC.
+
+### Onda 2: assertividade da monitoracao
+
+- Inventariar alertas produtivos.
+- Classificar alertas por assertivo, ruidoso, redundante, incompleto e ausente.
+- Padronizar templates, severidades, tags e grupos no Zabbix.
+- Revisar dashboards tecnicos e executivos.
+- Criar rotina mensal de revisao de falsos positivos e lacunas de monitoracao.
+
+### Onda 3: indicadores, relatorios e reunioes
+
+- Consolidar indicadores de incidentes, problemas, demandas e mudancas.
+- Criar relatorio operacional semanal do NOC.
+- Criar apresentacao executiva mensal.
+- Criar pauta fixa para reuniao semanal de Gestao de Incidentes.
+- Criar painel de acompanhamento 8 x 5 para incidentes em tempo real.
+
+### Onda 4: demandas, precificacao e Azure DevOps
+
+- Criar taxonomia de work items.
+- Definir criterio de precificacao e priorizacao.
+- Integrar demandas com incidentes, problemas, mudancas e projetos.
+- Criar relatorio de demandas por custo, risco, urgencia e beneficio.
+- Criar fluxo de aceite e encerramento com evidencias.
+
+### Onda 5: integracao estrategica e melhoria continua
+
+- Integrar ferramentas de monitoracao com ITSM.
+- Automatizar correlacao entre eventos, incidentes, problemas e mudancas.
+- Implementar pos-incidente semi-automatico.
+- Gerar recomendacoes de novas metricas e gatilhos com base em tendencias.
+- Usar historico para priorizar reducao de recorrencia e falsos positivos.
+
+## Criterios de aceite da evolucao
+
+- POPs e ITs criticos inventariados, versionados e com donos definidos.
+- Fluxo de incidente e problema padronizado e evidenciado no ITSM.
+- Alertas criticos revisados, classificados e associados a runbook ou POP.
+- Dashboards tecnicos e executivos publicados e validados pelo NOC.
+- Relatorio semanal e mensal do NOC gerados com dados rastreaveis.
+- Reuniao semanal de Gestao de Incidentes com pauta, evidencias e acoes.
+- Demandas controladas no Azure DevOps com criterio de precificacao.
+- Mudancas com impacto no NOC registradas, aprovadas e comunicadas.
+- Configuracoes criticas com backup, versao e auditoria.
+- Indicadores de pessoas, processos e operacao disponiveis para gestao.
+
+## Modelos operacionais incluidos
+
+- [Modelo de POP ou Instrucao Tecnica](templates/pop-instrucao-tecnica.md)
+- [Modelo de Relatorio de Incidente](templates/relatorio-incidente.md)
+- [Modelo de Revisao de Alertas do NOC](templates/revisao-alertas-noc.md)
+- [Modelo de Relatorio Consolidado do NOC](templates/relatorio-consolidado-noc.md)
+- [Modelo de Precificacao de Demanda no Azure DevOps](templates/precificacao-demanda-azure-devops.md)
+
+## Relacao com a plataforma tecnica
+
+Esta evolucao deve ser implementada sem substituir as ferramentas oficiais do cliente. O backend de orquestracao deve atuar como camada de integracao, auditoria, correlacao e apoio inteligente.
+
+Fontes oficiais recomendadas:
+
+- ITSM, GLPI, Ivanti ou Pratica para chamados, incidentes, problemas, mudancas e historico;
+- Zabbix para eventos, triggers, hosts e metricas;
+- Grafana para paineis tecnicos e executivos;
+- Azure DevOps para demandas, projetos, backlog e evidencias de entrega;
+- repositorio Git para versionamento de configuracoes, POPs, ITs, dashboards e automacoes;
+- banco operacional do backend para auditoria, correlacao, snapshots analiticos e recomendacoes.
diff --git a/docs/fase-5-operacao-avancada.md b/docs/fase-5-operacao-avancada.md
index 5444955..72f70b0 100644
--- a/docs/fase-5-operacao-avancada.md
+++ b/docs/fase-5-operacao-avancada.md
@@ -205,6 +205,14 @@ Nao ha ainda base de conhecimento indexada, `RAG`, indice vetorial nem servico p
2. Implementar `RAG` para consulta controlada.
3. Gerar pos-mortem semi-automatico com base em ticket, auditoria e solution.
+### Bloco 5: preparar governanca operacional do NOC
+
+1. Mapear POPs, Instrucoes Tecnicas, filas, categorias, servicos e responsaveis.
+2. Criar base para relatorios consolidados do NOC, incluindo incidentes, problemas, demandas, mudancas, alertas e indicadores.
+3. Criar modelo de avaliacao de assertividade de alertas e qualidade de monitoracao.
+4. Definir integracao entre incidentes recorrentes, problemas, demandas e mudancas.
+5. Preparar os dados para indicadores de pessoas, processos, produtividade, capacitacao e aderencia operacional.
+
## Criterios de saida da Fase 5
Considere a Fase 5 fechada quando estes pontos estiverem juntos:
@@ -221,3 +229,7 @@ Considere a Fase 5 fechada quando estes pontos estiverem juntos:
- dados operacionais: ja sustentam a fase;
- inteligencia operacional agregada: ainda incompleta;
- fase atual: pronta para continuar, mas nao pronta para ser considerada concluida.
+
+## Proxima frente relacionada
+
+Depois de consolidar a Fase 5, a evolucao natural e a frente de governanca operacional descrita em [Evolucao NOC, ITSM e operacao padronizada](evolucao-noc-operacao.md).
diff --git a/docs/roadmap.md b/docs/roadmap.md
index 958af15..5ec2e9f 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -113,16 +113,42 @@ Situação observada hoje:
O detalhamento técnico desta frente fica em [Fase 5: Operacao Avancada](fase-5-operacao-avancada.md).
+## Fase 6: Governanca operacional do NOC
+
+Status atual: planejada.
+
+Objetivo:
+Transformar a plataforma em apoio direto para gestao do NOC, conectando processos, pessoas, monitoracao, ITSM, demandas, mudancas, relatorios e melhoria continua.
+
+Entregas:
+
+- revisao, otimizacao e controle de POPs e Instrucoes Tecnicas;
+- capacitacao e trilhas de treinamento para Help Desk, incidentes, problemas e NOC;
+- relatorios e apresentacoes consolidadas do NOC;
+- gestao de demandas e precificacao no Azure DevOps;
+- gestao de mudancas com impacto no NOC;
+- revisao de alertas com baixa assertividade, redundancia e falsos positivos;
+- padronizacao de Zabbix, dashboards Grafana e paineis executivos;
+- integracao entre monitoracao e ITSM;
+- acompanhamento 8 x 5 de incidentes com correlacao entre eventos e sistemas;
+- controle de versoes, backup e auditoria de configuracoes;
+- indicadores para gestao operacional e tatica dos colaboradores.
+
+Critério de saída:
+NOC com processos versionados, indicadores rastreaveis, alertas revisados, dashboards validados, demandas controladas, mudancas governadas e relatorios executivos recorrentes.
+
+O detalhamento desta frente fica em [Evolucao NOC, ITSM e operacao padronizada](evolucao-noc-operacao.md).
+
## Backlog priorizado
-1. Estruturar backend principal de integração.
-2. Modelar usuários, técnicos, filas e permissões.
-3. Implementar fluxo de abertura de chamado por WhatsApp.
-4. Implementar notificação técnica.
-5. Implementar consulta a alertas do Zabbix.
-6. Adicionar correlação com tickets do GLPI.
-7. Adicionar RAG com base de conhecimento.
-8. Liberar primeira automação homologada de baixo risco.
+1. Fechar lacunas da Fase 5: relatorios completos, incidente em massa, pos-mortem e conhecimento.
+2. Criar catalogo versionado de POPs e Instrucoes Tecnicas.
+3. Criar rotina de revisao de alertas, Zabbix, dashboards e falsos positivos.
+4. Criar relatorios semanais e mensais consolidados do NOC.
+5. Implantar gestao de demandas, priorizacao e precificacao no Azure DevOps.
+6. Padronizar gestao de mudancas com impacto no NOC.
+7. Integrar monitoracao com ITSM para correlacao e registro automatico.
+8. Criar indicadores de pessoas, processos, produtividade, qualidade e capacitacao.
## Critérios de qualidade da próxima etapa
diff --git a/docs/templates/pop-instrucao-tecnica.md b/docs/templates/pop-instrucao-tecnica.md
new file mode 100644
index 0000000..5cf5322
--- /dev/null
+++ b/docs/templates/pop-instrucao-tecnica.md
@@ -0,0 +1,89 @@
+# Modelo de POP ou Instrucao Tecnica
+
+## Identificacao
+
+- titulo:
+- tipo: POP | Instrucao Tecnica
+- processo:
+- fila ou equipe:
+- servico ou sistema:
+- criticidade:
+- versao:
+- dono:
+- aprovador:
+- data de criacao:
+- data da ultima revisao:
+- proxima revisao:
+
+## Objetivo
+
+Descrever o resultado esperado do procedimento.
+
+## Quando usar
+
+- gatilho operacional:
+- alerta relacionado:
+- categoria ITSM:
+- horario aplicavel:
+- pre-condicoes:
+
+## Quando nao usar
+
+- restricoes:
+- riscos:
+- casos que exigem escalonamento:
+
+## Responsabilidades
+
+| Papel | Responsabilidade |
+| --- | --- |
+| NOC | |
+| Help Desk | |
+| N2/N3 | |
+| Supervisor | |
+| Gestor de mudanca | |
+
+## Procedimento
+
+1.
+2.
+3.
+
+## Evidencias obrigatorias
+
+- ticket ITSM:
+- print ou log:
+- horario de inicio:
+- horario de fim:
+- acao executada:
+- resultado:
+- responsavel:
+
+## Comunicacao
+
+- publico impactado:
+- canal:
+- mensagem padrao:
+- responsavel pela comunicacao:
+
+## Escalonamento
+
+| Condicao | Acionar | SLA |
+| --- | --- | --- |
+| | | |
+
+## Rollback ou contingencia
+
+1.
+2.
+3.
+
+## Criterio de conclusao
+
+-
+
+## Historico de versoes
+
+| Versao | Data | Alteracao | Responsavel | Aprovador |
+| --- | --- | --- | --- | --- |
+| 0.1 | | Criacao inicial | | |
diff --git a/docs/templates/precificacao-demanda-azure-devops.md b/docs/templates/precificacao-demanda-azure-devops.md
new file mode 100644
index 0000000..cc5ddaf
--- /dev/null
+++ b/docs/templates/precificacao-demanda-azure-devops.md
@@ -0,0 +1,63 @@
+# Modelo de Precificacao de Demanda no Azure DevOps
+
+## Identificacao
+
+- titulo:
+- work item:
+- solicitante:
+- area:
+- servico:
+- origem: incidente | problema | melhoria | projeto | mudanca
+- prioridade:
+- data de entrada:
+- responsavel pela avaliacao:
+
+## Contexto
+
+- descricao da demanda:
+- problema que resolve:
+- incidente ou problema relacionado:
+- impacto atual:
+- consequencia de nao fazer:
+
+## Avaliacao
+
+| Dimensao | Classificacao | Justificativa |
+| --- | --- | --- |
+| Esforco | P | |
+| Complexidade | baixa | |
+| Risco | baixo | |
+| Urgencia | normal | |
+| Beneficio | operacional | |
+| Recorrencia | pontual | |
+| Dependencias | baixa | |
+
+## Estimativa
+
+- horas estimadas:
+- perfis necessarios:
+- custo estimado:
+- janela necessaria:
+- ambientes afetados:
+
+## Criterios de aceite
+
+-
+-
+-
+
+## Evidencias de conclusao
+
+- pull request:
+- mudanca:
+- dashboard:
+- ticket ITSM:
+- teste:
+- comunicacao:
+
+## Decisao
+
+- aprovado:
+- aprovado por:
+- data:
+- observacoes:
diff --git a/docs/templates/relatorio-consolidado-noc.md b/docs/templates/relatorio-consolidado-noc.md
new file mode 100644
index 0000000..e4ec7e0
--- /dev/null
+++ b/docs/templates/relatorio-consolidado-noc.md
@@ -0,0 +1,68 @@
+# Modelo de Relatorio Consolidado do NOC
+
+## Periodo
+
+- inicio:
+- fim:
+- responsavel:
+- publico:
+
+## Resumo executivo
+
+- principais ocorrencias:
+- riscos atuais:
+- ganhos obtidos:
+- decisoes necessarias:
+
+## Indicadores operacionais
+
+| Indicador | Valor atual | Periodo anterior | Tendencia | Observacao |
+| --- | --- | --- | --- | --- |
+| Backlog total | | | | |
+| SLA em risco | | | | |
+| SLA violado | | | | |
+| MTTA | | | | |
+| MTTR | | | | |
+| Reabertura | | | | |
+| Falsos positivos | | | | |
+
+## Incidentes relevantes
+
+| Incidente | Servico | Impacto | Causa raiz | Status |
+| --- | --- | --- | --- | --- |
+| | | | | |
+
+## Problemas e recorrencias
+
+| Problema | Recorrencia | Servico | Acao proposta | Dono |
+| --- | --- | --- | --- | --- |
+| | | | | |
+
+## Alertas e monitoracao
+
+- alertas revisados:
+- alertas ajustados:
+- alertas desativados:
+- novas monitoracoes:
+- lacunas pendentes:
+
+## Demandas e mudancas
+
+| Item | Tipo | Status | Risco | Beneficio | Dono |
+| --- | --- | --- | --- | --- | --- |
+| | demanda | | | | |
+| | mudanca | | | | |
+
+## Pessoas e processos
+
+- aderencia a POP:
+- treinamentos concluidos:
+- treinamentos pendentes:
+- filas com sobrecarga:
+- pontos de atencao:
+
+## Plano de acao
+
+| Acao | Tipo | Prioridade | Dono | Prazo | Status |
+| --- | --- | --- | --- | --- | --- |
+| | | | | | |
diff --git a/docs/templates/relatorio-incidente.md b/docs/templates/relatorio-incidente.md
new file mode 100644
index 0000000..cba66a7
--- /dev/null
+++ b/docs/templates/relatorio-incidente.md
@@ -0,0 +1,70 @@
+# Modelo de Relatorio de Incidente
+
+## Identificacao
+
+- incidente:
+- ticket ITSM:
+- servico afetado:
+- ativos envolvidos:
+- severidade:
+- inicio:
+- deteccao:
+- contencao:
+- resolucao:
+- responsavel tecnico:
+- gestor responsavel:
+
+## Resumo executivo
+
+Descrever o que ocorreu, impacto, duracao e situacao atual.
+
+## Impacto
+
+- usuarios impactados:
+- sistemas impactados:
+- areas impactadas:
+- impacto financeiro ou operacional:
+- SLA afetado:
+
+## Linha do tempo
+
+| Horario | Evento | Fonte | Responsavel |
+| --- | --- | --- | --- |
+| | | | |
+
+## Causa raiz
+
+- causa raiz identificada:
+- evidencia:
+- fator contribuinte:
+- recorrencia conhecida:
+
+## Acoes tomadas
+
+| Acao | Tipo | Responsavel | Resultado |
+| --- | --- | --- | --- |
+| | contencao | | |
+| | correcao | | |
+| | prevencao | | |
+
+## Comunicacao
+
+- equipes comunicadas:
+- clientes ou usuarios comunicados:
+- canal:
+- horarios:
+- pendencias de comunicacao:
+
+## Relacao com problemas e mudancas
+
+- problema relacionado:
+- mudanca relacionada:
+- demanda Azure DevOps:
+- necessidade de revisao de POP ou IT:
+- necessidade de nova metrica, trigger ou dashboard:
+
+## Decisoes e pendencias
+
+| Item | Dono | Prazo | Status |
+| --- | --- | --- | --- |
+| | | | |
diff --git a/docs/templates/revisao-alertas-noc.md b/docs/templates/revisao-alertas-noc.md
new file mode 100644
index 0000000..0897ff5
--- /dev/null
+++ b/docs/templates/revisao-alertas-noc.md
@@ -0,0 +1,58 @@
+# Modelo de Revisao de Alertas do NOC
+
+## Identificacao
+
+- periodo analisado:
+- ferramenta: Zabbix | Grafana | Outra
+- responsavel:
+- aprovador:
+- data:
+
+## Inventario de alertas
+
+| Alerta | Servico | Ativo | Severidade | Grupo | Classe | Acao |
+| --- | --- | --- | --- | --- | --- | --- |
+| | | | | | assertivo | manter |
+| | | | | | ruidoso | ajustar |
+| | | | | | redundante | consolidar |
+| | | | | | incompleto | enriquecer |
+| | | | | | ausente | criar |
+
+## Analise de falsos positivos
+
+- volume total de alertas:
+- volume sem impacto real:
+- principais causas:
+- janelas com maior ruido:
+- servicos mais afetados:
+
+## Ajustes propostos
+
+| Alerta | Ajuste | Motivo | Risco | Dono | Prazo |
+| --- | --- | --- | --- | --- | --- |
+| | | | | | |
+
+## Padronizacao obrigatoria
+
+- tags:
+- severidade:
+- dependencia:
+- mensagem:
+- criterio de recuperacao:
+- grupo responsavel:
+- runbook ou POP:
+- regra ITSM:
+
+## Resultado esperado
+
+- reducao estimada de falsos positivos:
+- incidentes melhor detectados:
+- lacunas fechadas:
+- dashboards impactados:
+
+## Aprovacao
+
+- aprovado por:
+- data:
+- mudanca relacionada:
+- rollback:
diff --git a/infra/evolution/README.md b/infra/evolution/README.md
index db08438..bea42bc 100644
--- a/infra/evolution/README.md
+++ b/infra/evolution/README.md
@@ -90,7 +90,8 @@ Para ligar a instância ao backend, o webhook fica configurado com:
- evento: `MESSAGES_UPSERT`
- `base64`: `false`
- URL: endpoint público do backend apontando para `/api/v1/webhooks/whatsapp/evolution`
-- header opcional: `X-Evolution-Webhook-Secret: ` quando `HELPDESK_EVOLUTION_WEBHOOK_SECRET` estiver configurado no backend
+- header obrigatorio recomendado: `X-Evolution-Webhook-Secret: ` quando `HELPDESK_EVOLUTION_WEBHOOK_SECRET` estiver configurado no backend
+- query string `?secret=...` deve ficar desabilitada em producao; use apenas com `HELPDESK_EVOLUTION_WEBHOOK_ALLOW_QUERY_SECRET=true` em contingencia controlada
Se preferir, grave a URL no `.env` local e rode sem argumentos: