fix(atendimento): o plantão para de se desligar sozinho 15 min depois de ser ligado - #720
Conversation
…ligar O cron `attendant-heartbeat` gravava `is_available = false` em quem nao emitisse sinal de vida ha mais de `HEARTBEAT_TIMEOUT_MINUTES` (15), rodando de 5 em 5 minutos. **O sinal nunca existiu**: varredura do repositorio inteiro nao acha emissor nenhum — nem hook, nem `setInterval`, nem `beforeunload`. O unico escritor de `last_heartbeat_at` e o proprio botao, em `attendants/availability/[user_id]/route.ts:99`, no clique. Entao a varredura derrubava TODO atendente ~15 min depois de ele se declarar de plantao, em toda instalacao, sempre — e nada o religava: a jornada publicada so sabe RESTRINGIR (`isWithinSchedule`), nunca ACENDER. O cabecalho da rota dizia que ela era "defesa contra aba fechada sem beforeunload": ela defendia contra a ausencia de um sinal que nunca foi construido. CAUSA RAIZ, de modelagem: `is_available` carregava duas coisas na mesma coluna — "eu me declarei de plantao" (decisao, que dura) e "meu navegador esta vivo agora" (presenca, que expira). A varredura de presenca escrevia por cima da decisao. A REGRA, enunciada pelo dono do produto: | chave | jornada | agora | |-----------|---------|--------------------------------| | desligada | qualquer| off — decisao de gente vence | | ligada | nenhuma | on 24/7 | | ligada | tem | on dentro, off fora, e RELIGA | `estaDePlantao` e DERIVADA, nunca gravada — e e isso que faz o "religa sozinho" existir: nao ha o que religar porque nada foi desligado, a conta so muda de resposta quando o relogio entra na janela. Gravar exigiria uma rodada periodica para reacender, que e o anti-pattern 5 da doutrina e era exatamente o que estava ali. A tela para de ler presenca e passa a fazer a MESMA conta do roteador (sem a capacidade): antes a tela lia uma coisa e o motor outra sobre a mesma pessoa. "Fora do horario" ganhou selo proprio — confundi-lo com "Desligado" fazia o operador procurar defeito onde so havia jornada terminada. Sai: a rota do cron, a linha do crontab do scheduler, `isHeartbeatStale` e `HEARTBEAT_TIMEOUT_MINUTES`. FICA a coluna `last_heartbeat_at`: remover pede migration, ela e inofensiva, e serviria a um emissor de presenca de verdade se alguem construir um. Sabotagem conferida: `estaDePlantao` ignorando a jornada derruba 3 de 15 — as tres previstas (off fora da janela, religa sozinho, paridade com o roteador). Gates: typecheck 0, lint 0 erros, testes da area 83/83 + 28/28. NAO MEDI: a suite inteira nesta branch. E `pnpm test:shell` tem 1 vermelho — `.env continua 600` —, provado ambiental: `chmod 600` nao pega neste disco (o arquivo sai 644 depois do comando), entao `find -perm 600` nao casa. No CI (Linux) passa. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
@paulolimajr77 is attempting to deploy a commit to the rafael-maudibrasil's projects Team on Vercel. A member of the Team first needs to authorize it. |
ECC Tools / Security EvidenceCommit: Security evidence gate passed (success) No security-sensitive scanner-evidence gap detected. Mode: enforce Scanned 8 changed file(s). No missing scanner-evidence signal was detected. Check publication was denied or unavailable. An app owner must enable Checks: read and write, and the installation owner must approve the updated permission. |
ECC Tools / PR Risk TaxonomyCommit: PR taxonomy review recommended (neutral) Detected 1 PR taxonomy bucket(s): CI/CD Recommendation. Scanned 8 changed file(s). Roadmap taxonomy buckets: CI/CD RecommendationCI, dependency, coverage, and contract signals should be routed into follow-up checks or verification work. Signals:
Paths:
Check publication was denied or unavailable. An app owner must enable Checks: read and write, and the installation owner must approve the updated permission. |
ECC Tools / Reference Set ReadinessCommit: Reference set readiness gaps detected (neutral) Reference evidence present for 0/7 areas (0%) across 8 changed file(s). This check is based on files changed in this PR. Repository-level readiness is still reported by
Check publication was denied or unavailable. An app owner must enable Checks: read and write, and the installation owner must approve the updated permission. |
|
Recebido, @paulolimajr77 — obrigado por isto. Duas coisas que vão parecer erro seu e não são:
Um mantenedor vai revisar de verdade — rodando os gates e reproduzindo o comportamento, não só Esta mensagem é automática e não diz nada sobre o seu PR: ela é sobre o processo. O que vem |
ECC Tools / Hosted Promotion ReadinessCommit: Hosted promotion readiness passed (success) No hosted promotion evidence gaps detected across 8 changed file(s); 0 corpus scenarios had matching evidence. This check compares PR file changes against the evaluator/RAG promotion corpus in No evaluator corpus scenarios matched this PR. Check publication was denied or unavailable. An app owner must enable Checks: read and write, and the installation owner must approve the updated permission. |
|
Recebido — obrigado. Duas coisas que vão parecer erro seu e não são:
Um aviso útil: a Vou revisar de verdade — rodando os gates e reproduzindo o comportamento, não só lendo o diff — e volto com o resultado. Se eu achar algo, venho com a medição junto, nunca com um "acho que". |
A divida que este commit paga: o conserto do plantao mudou o que a coluna Status MOSTRA, e nenhuma spec cobria essa coluna. A unica prova era alguem lembrar de ligar a chave e voltar meia hora depois. Cobra os tres estados pelo TEXTO que a pessoa le: desligado -> "Desligado" ligado + jornada contem agora -> "De plantao" ligado + jornada nao contem -> "Fora do horario" (chave SEGUE ligada) ligado + jornada vazia -> "De plantao" (24/7)⚠️ O caso 3 assere tambem que a CHAVE continua marcada. E o coracao da mudanca: sair da janela nao pode mexer na decisao da pessoa — era justamente isso que a varredura antiga fazia, gravando `is_available=false` e deixando o sistema sem como religar. SEM RELOGIO MOCKADO: a janela e calculada A PARTIR DE AGORA, no fuso da jornada — uma que contem este instante e outra que nao. Deterministico a qualquer hora, e exercita o mesmo `Intl` que o produto usa. Montagem por API (a MESMA rota que o dialogo usa), asserção pela tela: o que mudou foi o selo, e o editor de janelas nao foi tocado. A chave, essa sim, e clicada — ela e a entrada do estado que o selo le. O caso vive na spec que ja mora nesta tela, e nao num arquivo novo: mesma tela, mesmo login, e um arquivo a parte custaria mais uma sessao num job que ja vive perto do teto de logins por IP. NAO MEDI: a corrida local do `pnpm test:e2e` — precisa de Supabase local e app buildado, que nao tenho de pe nesta maquina. A spec esta em SPECS_PARTE_* do `e2e.yml`, entao o CI a roda. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ECC Tools / Security EvidenceCommit: Security evidence gate passed (success) No security-sensitive scanner-evidence gap detected. Mode: enforce Scanned 9 changed file(s). No missing scanner-evidence signal was detected. Check publication was denied or unavailable. An app owner must enable Checks: read and write, and the installation owner must approve the updated permission. |
ECC Tools / PR Risk TaxonomyCommit: PR taxonomy review recommended (neutral) Detected 1 PR taxonomy bucket(s): CI/CD Recommendation. Scanned 9 changed file(s). Roadmap taxonomy buckets: CI/CD RecommendationCI, dependency, coverage, and contract signals should be routed into follow-up checks or verification work. Signals:
Paths:
Check publication was denied or unavailable. An app owner must enable Checks: read and write, and the installation owner must approve the updated permission. |
ECC Tools / Reference Set ReadinessCommit: Reference set readiness gaps detected (neutral) Reference evidence present for 0/7 areas (0%) across 9 changed file(s). This check is based on files changed in this PR. Repository-level readiness is still reported by
Check publication was denied or unavailable. An app owner must enable Checks: read and write, and the installation owner must approve the updated permission. |
ECC Tools / Hosted Promotion ReadinessCommit: Hosted promotion readiness passed (success) No hosted promotion evidence gaps detected across 9 changed file(s); 0 corpus scenarios had matching evidence. This check compares PR file changes against the evaluator/RAG promotion corpus in No evaluator corpus scenarios matched this PR. Check publication was denied or unavailable. An app owner must enable Checks: read and write, and the installation owner must approve the updated permission. |
|
Acrescentei a prova de tela que faltava — o PR original não tinha nenhuma spec cobrindo a coluna Status, que é justamente o que a mudança altera.
Sem relógio mockado: a janela é calculada a partir de agora, no fuso da jornada — uma que contém este instante e outra que não. Determinístico a qualquer hora, e exercita o mesmo Montagem pela API (a mesma rota que o diálogo envia), asserção pela tela. O editor de janelas não foi tocado por esta mudança, então dirigi-lo seria fricção sem prova a mais. A chave, essa sim, é clicada. O caso vive na spec que já mora nessa tela, e não num arquivo novo — mesma tela, mesmo login, e um arquivo à parte custaria mais uma sessão num job que já vive perto do teto de logins por IP. Não medi: a corrida local do |
…egue Divida declarada quando o cron de auto-offline saiu, e paga agora. A coluna se chama "ultimo sinal de vida" e registrava, na verdade, o CLIQUE na chave — nunca houve emissor de presenca nenhum no produto. Com o cron fora, sobrou uma cadeia inteira sem leitor: coluna -> SELECT -> `ultimoSinalDeVida` -> campo da API -> tipo do hook -> ninguem O selo de Status parou de le-la quando virou `estaDePlantao`. Campo no fio sem leitor do outro lado e a mesma familia do "evento sem consumer" da doutrina. SAI: a escrita (`[user_id]/route.ts`), a coluna dos tres SELECT, a projecao, o campo da resposta e o campo do tipo do hook. FICA: a COLUNA no banco. Remover pediria migration, e fecharia a porta enquanto a decisao esta aberta — o PR do plantao propoe explicitamente as duas saidas, e a outra e construir o emissor que falta. Se alguem construir, este e o lugar; e agora ela esta limpa, sem carimbos de clique se fingindo de batida. O invariante `gov-4-routing` continua exigindo a coluna, com o comentario corrigido: ela e RESERVA declarada, para que remove-la seja decisao e nao faxina. Sabotagem natural: `attendants-availability-route` prende a lista exata de campos da resposta. Antes de eu atualiza-lo, ele foi o UNICO vermelho — 1 de 24 —, que e a prova de que a remocao pegou onde devia. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ECC Tools / Security EvidenceCommit: Security scanner evidence required (action_required) Detected 1 security-sensitive predictive risk signal(s) without scanner evidence. Mode: enforce Findings:
Touched security-sensitive paths:
Expected evidence:
Check publication was denied or unavailable. An app owner must enable Checks: read and write, and the installation owner must approve the updated permission. |
ECC Tools / PR Risk TaxonomyCommit: PR taxonomy review recommended (neutral) Detected 2 PR taxonomy bucket(s): Security Evidence, CI/CD Recommendation. Scanned 15 changed file(s). Roadmap taxonomy buckets: Security EvidenceSecurity-sensitive changes should carry explicit scanner, code-scanning, or focused regression evidence. Signals:
Paths:
CI/CD RecommendationCI, dependency, coverage, and contract signals should be routed into follow-up checks or verification work. Signals:
Paths:
Check publication was denied or unavailable. An app owner must enable Checks: read and write, and the installation owner must approve the updated permission. |
ECC Tools / Reference Set ReadinessCommit: Reference set readiness gaps detected (neutral) Reference evidence present for 0/7 areas (0%) across 15 changed file(s). This check is based on files changed in this PR. Repository-level readiness is still reported by
Check publication was denied or unavailable. An app owner must enable Checks: read and write, and the installation owner must approve the updated permission. |
ECC Tools / Hosted Promotion ReadinessCommit: Hosted promotion readiness passed (success) No hosted promotion evidence gaps detected across 15 changed file(s); 0 corpus scenarios had matching evidence. This check compares PR file changes against the evaluator/RAG promotion corpus in No evaluator corpus scenarios matched this PR. Check publication was denied or unavailable. An app owner must enable Checks: read and write, and the installation owner must approve the updated permission. |
Em Equipe › Atendimento, ligar a chave de um atendente dura cerca de quinze minutos. Depois disso o sistema a desliga sozinho, e nada a religa: a pessoa some do roteamento e dos horários da Agenda sem ter feito nada.
Não é intermitente e não depende de configuração: acontece em toda instalação, sempre.
A causa
app/api/v1/cron/attendant-heartbeatgravais_available = falseem quem não emite sinal de vida há mais deHEARTBEAT_TIMEOUT_MINUTES(15), e roda de 5 em 5 minutos.O sinal nunca existiu. Varredura do repositório inteiro: não há emissor em lugar nenhum — nem hook, nem
setInterval, nembeforeunload. O único escritor delast_heartbeat_até o próprio botão, no clique (attendants/availability/[user_id]/route.ts:99).O cabeçalho da rota diz que ela é "defesa contra aba fechada sem
beforeunload". Ela defende contra a ausência de um sinal que nunca foi construído — e o efeito é derrubar todo mundo.E nada religa: a jornada publicada só sabe restringir (
isWithinSchedule), nunca acender.Causa raiz
is_availablecarrega duas coisas na mesma coluna: "eu me declarei de plantão" (decisão, que dura) e "meu navegador está vivo agora" (presença, que expira). A varredura de presença escreve por cima da decisão.A regra que fica
estaDePlantaoé derivada, nunca gravada, e é isso que faz o "religa sozinho" existir: não há o que religar porque nada foi desligado — a conta muda de resposta quando o relógio entra na janela. Gravar exigiria uma rodada periódica para reacender, que é o anti-pattern 5 doCLAUDE.md(campo sincronizado por cron que devia ser derivado) — e era exatamente o que estava ali.A tela para de ler presença e passa a fazer a mesma conta do roteador (sem a capacidade). Antes a tela lia uma coisa e o motor outra sobre a mesma pessoa. "Fora do horário" ganhou selo próprio: confundi-lo com "Desligado" faz o operador procurar defeito onde só há jornada terminada.
Há duas saídas defensáveis para o mesmo diagnóstico:
Escolhi a primeira porque a jornada publicada já é a fonte de verdade do produto para "quando esta pessoa atende", e porque um emissor de presença não resolve o caso de quem fecha o navegador e continua atendendo pelo celular. Mas se você preferir a segunda, o diagnóstico e os testes deste PR servem igual — e eu abro o outro.
O que sai
A rota do cron, a linha do crontab do
scheduler,isHeartbeatStaleeHEARTBEAT_TIMEOUT_MINUTES.Fica a coluna
last_heartbeat_at: remover pede migration, ela é inofensiva, e serviria a um emissor de verdade se alguém construir um.O que medi
lib/routing/eligibility.test.ts— a regra inteira, com clock injetado, incluindo o caso "RELIGA SOZINHO: a mesma linha do banco, dois instantes, duas respostas" (nada escreve no meio; só o relógio anda) e um caso de paridade tela × roteador, que prende as duas na mesma resposta.Sabotagem conferida, previsão antes de rodar:
estaDePlantaoignorando a jornada derruba 3 de 15 — as três previstas.tests/unit/cron-routes-scheduled.test.ts(a cerca rota × crontab) ecron-audita-so-quando-ha-efeito.test.tsseguem verdes com a rota fora.Gates na prévia do merge:
typecheck0,lint0 erros, testes da área 83/83 + 28/28.O que NÃO medi
pnpm test:unitinteiro nesta branchpnpm test:shelltem 1 vermelho, e é ambiental, não desta mudança:.env continua 600usafind -perm 600, echmod 600não pega no disco onde rodei (o arquivo sai644depois do comando). Medido nos dois sentidos. No CI (Linux) passa.🤖 Generated with Claude Code