Skip to content

feat: resolução de conflito de visita no ACS (#79) - #91

Merged
hbgit merged 11 commits into
developfrom
feat/issue-79-conflito-visita
Oct 17, 2026
Merged

hbgit merged 11 commits into
developfrom
feat/issue-79-conflito-visita

Conversation

@hbgit

@hbgit hbgit commented Oct 17, 2026

Copy link
Copy Markdown
Owner

Closes #79

Resumo

O ACS passa a ver cada visita em conflito de sincronização, comparar lado a lado a versão do aparelho com a do servidor e decidir qual prevalece — ou editar e enviar uma nova versão (PRD §2.3.2, risco R1). Antes, a visita em conflito voltava à fila, era reenviada a cada "Sincronizar agora" (o contador crescia sem limite), não sobrevivia a um reinício do app e o ACS não tinha com o que comparar.

Plano: docs/superpowers/plans/2026-10-16-issue-79-resolucao-de-conflito.md.

Backend

  • VisitSyncResult.serverVisit: no conflict, o autor recebe a cópia do servidor (nunca no syncLegacy); trilha read/visit_conflict sem conteúdo clínico. Sem migração de banco.
  • visits.resolveConflict(accessToken, visit, expectedServerVersion): compare-and-set explícito — versão vista igual à atual grava com version + 1 e audita write/visit/conflict_resolved_local; vencida devolve conflito novo sem gravar. Mesmas recusas do sync, por um _checkTarget compartilhado.
  • Correção de território (achada na revisão final): sync/syncDeferred/resolveConflict recusam um localId existente enviado com outro patientId (denied_patient_mismatch, mesma resposta da recusa territorial). Sem isso, um ACS movido de microárea leria as notas decifradas de uma visita antiga.

App ACS

  • Conflito sai da retentativa e fica persistido no SQLCipher (schema v8, migração só aditiva) com a cópia do servidor, até o ACS decidir; continua bloqueando "Limpar este aparelho" e entra nas contagens.
  • OfflineVisitQueue.resolveKeepServer (local, com confirmação destrutiva) e resolveKeepLocal (manter a minha / editar e enviar, via resolveConflict), seguras com sync em voo.
  • Telas novas VisitConflictListScreen e VisitConflictCompareScreen (visit_conflict_screen.dart): lado a lado ≥ 600 dp, empilhadas no celular, marca "Diferente" com ícone + texto, alvos 48×52, liveRegion, testadas a 130%/200% de fonte.
  • O envio diferido não envia nem apaga visita em conflito. O diálogo de "Sair" separa as visitas em conflito ("aguardam a sua decisão").

Testes

  • Backend: dart test test/unit 933 passando; integração (visit_conflict_resolution_test.dart novo + legado + território) 25 passando com -j 1.
  • App ACS: flutter test 606 passando, flutter analyze limpo.
  • Emulador (emulator-5554, banco sinalacs_e2e): scripts/qa/acs_full_e2e.sh saiu 0, 6/6 testWidgets; o banco confirma versão 4 para "manter a minha", versão 3 para "manter a do servidor", uma linha conflict_resolved_local e cadeia de auditoria íntegra. Log em docs/superpowers/plans/2026-10-16-issue-79-e2e.log. O e2e rodou antes da correção de território (coberta por testes de unidade e integração).
  • O teste "fila por dono" do e2e passou a tocar pelo centro do botão (_tocarNoMeio), porque a barra inferior cobria o alvo.

Em aberto (registrado no PROGRESS.md)

  • resolveConflict lê e depois grava sem trava: dois aparelhos do mesmo ACS resolvendo ao mesmo tempo, o segundo vence sem aviso (o sync tem a mesma lacuna).
  • "Manter a do servidor" não deixa linha na trilha do servidor; conflito de visita legada segue sem tela; conflito só aparece na tela de registro de visita; mensagem de wipe bloqueado não cita "conflito"; capturas de tela pendentes.

hbgit added 11 commits October 16, 2026 19:52
…são vista (#79)

O autor resolve o conflito de visita explicitamente: igual à versão vista
grava o conteúdo do aparelho em existing.version + 1 e audita
write/visit/conflict_resolved_local; versão vencida devolve um conflito novo
com a cópia do servidor (read/visit_conflict), sem gravar. As checagens de
identidade, território e autoria saem de _syncOne para _checkTarget,
compartilhado com o sync, para as recusas serem as mesmas.
…servidor (#79)

Conflito sai da retentativa (D3) e vive em _conflicts, persistido com a
cópia do servidor; restore separa pendente/conflito/recusada; conflito sem
versão é erro retentável (D7); forceConflict removido.
resolveKeepServer (só local) e resolveKeepLocal (visits.resolveConflict com
compare-and-set, espera sync em voo). BackendVisitSynchronizer também é o
VisitConflictResolver; AcsBackend.resolveVisitConflict com a mesma checagem
de dono de syncVisits. Envio diferido não manda nem apaga conflito (D6).
Cópia do servidor ilegível no disco vira null em vez de derrubar o load.
O app conta conflitos como visitas no aparelho (wipe, fila só em RAM, Sair).
…sita (#79)

- VisitConflictListScreen e VisitConflictCompareScreen: painéis "Neste
  aparelho" / "No servidor (versão N)", marca "Diferente" (ícone + texto,
  accentOnSurface), "Manter a minha versão", "Editar e enviar" e "Manter a
  do servidor" com confirmação destrutiva; resultado em liveRegion e ações
  travadas com a chamada em voo.
- VisitRegistrationScreen: botão "Resolver conflito(s)" e texto novo do
  resultado de conflito.
- Contagens do "Sair"/"Limpar este aparelho" passam a incluir conflitos só
  em RAM, e conflito no disco conta como revisão (depende do ACS dono).
- resolveKeepLocal recusado não recoloca nas recusadas a visita descartada
  durante a chamada.
…ste (#79)

Novo testWidgets antes do da MFA: duas visitas registradas com o envio fora
do ar, levadas à v3 por um 'outro aparelho' do mesmo ACS; pela tela, mantém
a do aparelho numa (v4 no servidor) e a do servidor noutra (v3 intacta).
O script confere versões, status, autoria, a trilha conflict_resolved_local
e visit_conflict, e a cadeia de auditoria (audit_chain_check).

Toques no fim da tela passam a centralizar o alvo (_tocarNoMeio): a barra
de navegação e o SnackBar cobriam o botão encostado na borda, inclusive o
wipe_button do teste da fila por dono.
…nflict (#79)

O território era conferido contra o patientId declarado e nunca contra o da
linha existente: um ACS movido de microárea recebia a cópia da área de origem
no conflito ou a sobrescrevia pelo resolveConflict. Agora audita
denied_patient_mismatch e responde como a recusa territorial, como no ramo
legado. O teste da visita criada pelo resolveConflict passa a conferir a
linha conflict_resolved_local.
…onta descartada (#79)

O diálogo de Sair ganha a linha logout_conflict_visits e o "sobem sozinhas"
deixa de contar as visitas em conflito, que o envio diferido não envia. O ramo
synced de resolveKeepLocal não recoloca em enviadas a visita descartada
durante a chamada, como o ramo rejected.
@hbgit
hbgit merged commit 8b436d5 into develop Oct 17, 2026
18 of 19 checks passed
@hbgit
hbgit deleted the feat/issue-79-conflito-visita branch October 17, 2026 12:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant