Flag de qualidade de rede e navegador nos pings do Avo

TLDR: Enriquece as listas “Room pings” e “Wait pings” no show de MeetingParticipant no Avo para exibir, em cada linha, uma flag de qualidade de rede (boa/média/ruim/desconhecida, baseada em network.rtt) e o navegador do participante, com um detalhe expansível mostrando o payload diagnostics completo.

Contexto

O PR #697 (meeting_ping_diagnostics) introduziu o modelo MeetingParticipantPing (kind: room/wait, pinged_at, diagnostics jsonb), criado a cada heartbeat via MeetingParticipants::CreatePingFlow, associado a MeetingParticipant via has_many :meeting_participant_pings.

No show do MeetingParticipant no Avo (app/avo/resources/meeting_participant.rb), as seções “Room pings” e “Wait pings” exibiam apenas uma lista de horários formatados (MeetingParticipants::FormatPingDates), lida a partir dos arrays legados room_pings/wait_pings (timestamps Unix). Essa lista não usava o MeetingParticipantPing nem o diagnostics associado — não havia nenhuma informação de qualidade de conexão ou dispositivo visível sem entrar no console.

Esta spec foi implementada em duas etapas na mesma branch/PR (#699): a primeira etapa cobriu apenas room_pings, com a decisão explícita de deixar wait_pings de fora do escopo inicial. A atualização de 04/08 estendeu o mesmo tratamento para wait_pings, já que o MeetingParticipants::CreatePing grava diagnostics para os dois kinds — o dado já existia no banco, só não era exibido para wait.

O payload diagnostics real (confirmado em produção e na factory spec/factories/meeting_participant_pings.rb) contém:

json { "device": { "user_agent": "Mozilla/5.0 ... Chrome/151.0.0.0 Safari/537.36" }, "network": { "rtt": 200, "downlink": 1.75, "effective_type": "4g", "save_data": false, "online": true }, "app_state": { "visibility_state": "visible" }, "media_devices": { "audioinput_count": 2, "videoinput_count": 1, "audiooutput_count": 2 }, "timestamp": "2026-08-03T14:50:32.112Z" }

Além disso, network.rtt, network.downlink e network.effective_type podem vir null — a Network Information API do navegador não é suportada em Safari/Firefox.

Objetivos

  • Classificar a qualidade de rede de cada ping em good / medium / bad / unknown, com base em diagnostics.network.rtt:
    • rtt < 100 → good
    • 100 <= rtt <= 300 → medium
    • rtt > 300 → bad
    • rtt ausente/null → unknown
  • Extrair navegador e versão (ex.: "Chrome 151") a partir de diagnostics.device.user_agent, via parser próprio baseado em regex (sem nova dependência de gem)
  • Atualizar as seções “Room pings” e “Wait pings” no show de MeetingParticipant para mostrar, em cada linha: horário, flag de qualidade (badge colorido) e navegador
  • Adicionar um <details>/”ver detalhes” expansível por linha, mostrando o diagnostics completo formatado (JSON legível)
  • Trocar a fonte de dados dos arrays legados para meeting_participant.meeting_participant_pings.room.order(:pinged_at) e .wait.order(:pinged_at), já que é isso que carrega o diagnostics

Fora de escopo

  • Remover ou alterar as colunas/arrays legados room_pings/wait_pings em MeetingParticipant — continuam sendo escritos normalmente pelo CreatePingFlow e usados em presence_status/serializer
  • Criar um Avo::Resources::MeetingParticipantPing dedicado ou tela de listagem própria — a exibição continua embutida no show do MeetingParticipant
  • Adicionar gem de parsing de user-agent — usa regex própria, cobrindo os navegadores que aparecem nos dados reais (Chrome, Safari, Firefox, Edge); navegadores não reconhecidos exibem “Desconhecido”
  • Diferenciar a classificação de qualidade ou os rótulos entre room e wait — mesmo formatter, mesmos thresholds, aplicado às duas relações

Mudanças

Arquivo Ação
app/use_cases/meeting_participant_pings/classify_network_quality.rb criado — recebe rtt (nullable), retorna :good/:medium/:bad/:unknown
app/use_cases/meeting_participant_pings/parse_browser.rb criado — recebe user_agent (nullable), retorna string tipo "Chrome 151" ou "Desconhecido" via regex
app/use_cases/meeting_participants/format_pings.rb criado (renomeado de format_room_pings.rb, sem dependência de kind) — recebe uma relação de MeetingParticipantPing de qualquer kind e monta a lista HTML: horário + qualidade + navegador + downlink
app/avo/resources/meeting_participant.rb atualizado — os campos room_pings e wait_pings passam a usar MeetingParticipants::FormatPings.call(@record.meeting_participant_pings.<kind>.order(:pinged_at))
spec/use_cases/meeting_participant_pings/classify_network_quality_spec.rb criado — casos: rtt < 100, entre 100-300, > 300, nil
spec/use_cases/meeting_participant_pings/parse_browser_spec.rb criado — casos: Chrome, Safari, Firefox, Edge, user_agent nil/vazio, não reconhecido
spec/use_cases/meeting_participants/format_pings_spec.rb criado (renomeado de format_room_pings_spec.rb), com um caso cobrindo ping kind: wait para provar que o formatter é agnóstico a kind
app/use_cases/meeting_participants/format_ping_dates.rb removido — ficou órfão depois que wait_pings passou a usar FormatPings (nunca teve spec própria)
config/locales/pt-BR/models/meeting_participant_ping.yml atualizado — escopo de tradução renomeado de format_room_pings para format_pings

Plano de implementação

  1. test + feat: MeetingParticipantPings::ClassifyNetworkQuality — thresholds de rtt
  2. test + feat: MeetingParticipantPings::ParseBrowser — regex para Chrome/Safari/Firefox/Edge; fallback “Desconhecido”
  3. test + feat: MeetingParticipants::FormatPings (antes FormatRoomPings) — monta a lista HTML por ping, usando os dois use cases acima
  4. feat: trocar a fonte do campo room_pings no resource (array → relação) e usar o novo formatter
  5. feat: renomear FormatRoomPings para FormatPings e atualizar o campo wait_pings no mesmo resource
  6. refactor: remover MeetingParticipants::FormatPingDates, sem referência após o passo 5

Como verificar

```bash make run.test path=spec/use_cases/meeting_participant_pings/classify_network_quality_spec.rb make run.test path=spec/use_cases/meeting_participant_pings/parse_browser_spec.rb make run.test path=spec/use_cases/meeting_participants/format_pings_spec.rb

console

MeetingParticipantPings::ClassifyNetworkQuality.call(rtt: 50) # => :good MeetingParticipantPings::ClassifyNetworkQuality.call(rtt: 200) # => :medium MeetingParticipantPings::ClassifyNetworkQuality.call(rtt: 400) # => :bad MeetingParticipantPings::ClassifyNetworkQuality.call(rtt: nil) # => :unknown

MeetingParticipantPings::ParseBrowser.call(user_agent: “Mozilla/5.0 … Chrome/151.0.0.0 Safari/537.36”) # => “Chrome 151” ```

Verificação visual: abrir no Avo um MeetingParticipant com pings room e wait reais e confirmar que cada linha, nas duas seções, mostra horário + badge de qualidade + navegador, e que o “ver detalhes” expande o JSON completo do diagnostics.

Documentação

O comportamento resultante está em room_ping_quality — thresholds de rtt, motivo de downlink/effective_type ficarem fora do cálculo, e a lista de navegadores reconhecidos.