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: