Skip to content

docs: add lgpd_data_audit.md (Análise LGPD dos dados armazenados: anonimização e pontos de vazamento) - #7

Open
GuilhermeAlmeidadaLuz wants to merge 42 commits into
mainfrom
develop
Open

GuilhermeAlmeidadaLuz wants to merge 42 commits into
mainfrom
develop

Conversation

@GuilhermeAlmeidadaLuz

Copy link
Copy Markdown
Collaborator

Resumo
Este Pull Request entrega o relatório da "Análise LGPD dos dados armazenados: anonimização e pontos de vazamento", documentando a auditoria completa do schema de dados do backend do SinalACS sob a ótica de privacidade e proteção de dados.

O que foi adicionado
Criação do documento spec/lgpd_data_audit.md, estruturado nos seguintes eixos:

Inventário e Classificação: Mapeamento campo a campo das tabelas do domínio e logs operacionais gerados pelo framework, com classificação de sensibilidade baseada na LGPD.

Avaliação Criptográfica: Diagnóstico de vulnerabilidades e entropia na geração de hashes atuais (como o cpfHash e o locationHash), além da identificação de dados de saúde não estruturados armazenados em texto claro.

Matriz de Vazamentos: Levantamento de riscos de exposição indireta em tabelas internas do Serverpod e na arquitetura de mensageria do piloto free-tier.

Ciclo de Vida: Análise sobre a retenção de dados (ausência de expurgo) e validação da segurança dos seeds de desenvolvimento.

Plano de Ação: Recomendações priorizadas para adequação do sistema.

hbgit and others added 30 commits September 11, 2026 17:04
O commit b402cee reconciliou a documentação com a migração para o
Serverpod, mas a varredura deixou passar dois trechos que continuavam
afirmando o contrário.

No PRD, o exemplo da política ABAC atribuía sua natureza ilustrativa a
"o backend não usa Serverpod/ORM" — falso hoje. O motivo real é outro:
não existe camada ABAC genérica, a checagem de papel e microárea é
feita inline no caso de uso (red_alert_service.dart) e o RBAC
institucional segue não implementado.

No plano de teste, o bullet de contratos apoiava sua análise de risco
numa premissa invertida: alegava ausência de ORM/codegen para concluir
que não há quebra de compilação em falhas de contrato. Com o
serverpod generate produzindo o cliente tipado em sinalacs_client, o
oposto é verdade para os endpoints RPC. A conclusão que importa
permanece — o risco está na mensageria assíncrona, que não passa por
codegen, e a validação AsyncAPI segue como mitigação.

AGENTS.md não foi tocado porque já estava correto desde b402cee. As
entradas M1.x do PROGRESS.md também não, por serem registro histórico
deliberado, conforme a nota de leitura no topo daquele documento.

Signed-off-by: Herbert Rocha <herberthb12@gmail.com>
….env

Os dois apps eram protótipos de UI autocontidos: nenhuma dependência de rede,
`MqttSecureClient` nunca instanciado, login navegando incondicionalmente e risco
de triagem calculado por comparação de string no cliente — uma segunda regra de
risco convivendo com o motor determinístico do servidor (INV-02).

Apps
- Ambos passam a consumir o `sinalacs_client` gerado, por dependência de path.
- `lib/core/network/` em cada app: interface `PatientBackend`/`AcsBackend` com a
  implementação real por trás, para que a UI não dependa do `Client` gerado e os
  testes de widget sigam herméticos.
- A triagem do paciente perde a regra local e passa a chamar `triage.evaluate`,
  com as seis perguntas que o motor do servidor realmente recebe. O botão de
  pânico chama `alerts.createRedAlert` com chave de idempotência estável por
  tentativa — um retry não vira um segundo alerta vermelho.
- O painel do ACS deixa de ter três cartões fixos: é alimentado pelo broker,
  ordenado por risco e, no mesmo risco, por antiguidade. Descarta alerta de
  outra microárea e deduplica reentrega (QoS 1 é at-least-once).

MQTT e infraestrutura
- ACL do broker liberava `sinalacs/v1/microareas/area-12/alerts`, mas o backend
  publica no UUID da microárea do seed: o ACS era negado exatamente no tópico do
  alerta vermelho.
- Certificado do broker não tinha subjectAltName, então a verificação de
  hostname falhava em 10.0.2.2. Passa a trazer SAN e a ser renovado por
  expiração, em vez de "só criar se o arquivo não existir" — a validade de 7
  dias nunca era renovada.
- `SecureMqttConfig` tinha `useWebSocket: true` por padrão, o que fazia
  `connect()` lançar `UnsupportedError` na configuração padrão. Agora TCP/TLS na
  8883, o único listener publicado, com a CA vinda de asset em bytes.
- Sessão persistente com clientId estável: o broker reentrega o que chegou
  enquanto o aparelho estava sem sinal.
- Numa falha de conexão o cliente ficava reconectando para sempre, fora do
  alcance de `disconnect()`, porque a referência só era guardada após o sucesso.
- Cleartext HTTP liberado apenas em builds de debug e apenas para 10.0.2.2 e
  loopback; os manifests de release ficam intocados.

Sincronização de visitas
- Novo endpoint `visits.sync`. A tabela `visits` já tinha `localId` único e
  `version`, então só faltava a superfície RPC: conflito devolve a versão do
  servidor sem sobrescrever, e reenvio do mesmo estado é idempotente.
- A fila offline era em memória e reinstanciada a cada gravação, então a visita
  era descartada no retorno do callback. Passa a ser única, persistida e a
  falhar honestamente: `sync()` sem sincronizador não carimba "SINCRONIZADO".

Configuração e segredos
- Não havia `.env` algum, e o repositório é público: oito valores secretos
  distintos estavam versionados em texto claro. O pior deles é o que o
  `.gitignore` deveria proteger — o `database` de `config/passwords.yaml`, que é
  idêntico nos blocos development, test, staging e production e estava literal
  no CI e no compose do Serverpod.
- `JWT_SECRET` nunca era passado pelo compose, então a stack assinava tokens com
  um fallback público. Como o token carrega papel e microárea, isso permitia
  forjar um acesso de ACS a qualquer território.
- `.env` gerado por máquina com `scripts/dev/bootstrap_env.sh`, que também cria
  o `config/passwords.yaml` ausente em qualquer clone limpo — sem ele o
  Serverpod chama `exit(1)` sem flush e a suíte morre sem uma linha de log.
- Todo segredo no compose é `${VAR:?}`: faltando, o Compose falha nomeando a
  variável em vez de subir com senha embutida.
- Compose-template do Serverpod removido (quatro segredos, referenciado por
  nada), substituído pelo serviço `postgres-test` sob profile. CI passa a usar
  um valor efêmero declarado uma vez. `DATABASE_URL`, escrito e nunca lido, foi
  apagado junto com o DSN que carregava.
- Fora de `development`, o servidor recusa subir com `JWT_SECRET` ausente,
  vazio, em branco ou igual ao fallback de desenvolvimento.
- O histórico do git não foi reescrito: os valores antigos seguem visíveis em
  commits passados e por isso foram rotacionados. Verificado que a senha antiga
  do broker passa a ser recusada com `notAuthorized`.

Permanecem dois defaults de desenvolvimento intencionais e documentados: o
segredo JWT de dev (que o próprio código recusa fora de development) e a
credencial MQTT do app do ACS, que `String.fromEnvironment` compila no binário —
a correção real é credencial por dispositivo/mTLS, lacuna já registrada.

Testes: backend 25 -> 48, ACS 28, paciente 7. Validado ponta a ponta contra a
stack local a partir de um clone limpo, incluindo o ciclo completo do alerta
vermelho pelo broker.

Signed-off-by: Herbert Rocha <herberthb12@gmail.com>
O caminho ACS → servidor estava completo em componentes e desligado na prática.
O app montava a `OfflineVisitQueue` sem sincronizador, então `sync()` caía no
ramo sem remetente e devolvia erro: as visitas registradas em campo nunca subiam
ao backend, e a retenção — `save()` reescreve a tabela a partir dos pendentes,
logo o que o servidor confirma sai do disco — nunca disparava, embora estivesse
implementada e coberta por teste.

Nenhum teste podia ver isso. A UI só é testável com a fila injetada, então a
montagem real nunca era exercitada.

Signed-off-by: Herbert Rocha <herberthb12@gmail.com>
…broker

Dois defeitos com a mesma forma: o app sabia o que estava errado e não contava.

Um slot de banner para dois estados
- `feedError: _feedError ?? _storageError` sempre mostrava o do broker, porque
  em campo os dois caem juntos. O ACS lia "Sem conexão com a central de alertas."
  e nunca descobria que as visitas do dia não estavam sendo salvas. O subtítulo
  era fixo ("Novos alertas podem não estar chegando."), então quando o banner
  exibido era o de armazenamento ele afirmava algo factualmente errado.
- Agora são dois banners independentes, `feed_error` e `storage_error`, cada um
  com seu texto, alertas primeiro — risco de vida na frente.
- O campo `_storageError` foi apagado. Era calculado uma vez no `initState`, de
  modo que toda falha posterior de `add()`/`_persist()` ficava invisível; virou
  getter lido a cada build. Guardar a resposta num campo *era* o defeito.
- A tela de visita ganhou o mesmo aviso inline — é onde a pessoa acabou de
  gravar, e obrigá-la a voltar à Fila para descobrir que não salvou é o mesmo
  defeito de outra forma. O contador de pendentes ganha o sufixo "(em memória)",
  e a mensagem de erro de sync parou de prometer que a visita está no aparelho.
- Avisos de infraestrutura passam a `AcsColors.accent`: por `docs/telas-acs.md`
  a cor é gravidade clínica, e um cartão vermelho de "sem conexão" competia com
  o alerta vermelho de paciente logo abaixo. O mesmo vale para os dois avisos
  operacionais amarelos da tela de visita. O chip `broker_status` e o
  `login_error` não mudam: indicador de status compacto e convenção de formulário.

O app nunca conectava ao broker local
- `mqttPassword` tinha `defaultValue: 'development-acs-password'`, mas o broker
  cria `acs-area-12` com `MQTT_ACS_PASSWORD` — hex aleatório por máquina, gerado
  pelo `bootstrap_env.sh`. Nenhum valor embutido em código poderia acertá-lo, e
  a documentação mandava rodar `flutter run` sem `--dart-define` nenhum. O
  default sai: vazio é estado detectável, e a tela diz que faltou a senha.
- `scripts/dev/run_acs.sh` (novo) roda o `sync_dev_ca.sh` e preenche os quatro
  dart-defines. Lê só `MQTT_ACS_PASSWORD` do `.env` com `grep | cut` em vez de
  `source .env`, para não empurrar `JWT_SECRET` e `POSTGRES_PASSWORD` ao Gradle,
  e nunca imprime a senha. Não cobre o `apps/patient`, que só usa
  `SINALACS_HOST` e cujo default já funciona no emulador.
- `_connectFeed` descartava o erro e escrevia sempre a mesma frase, então senha
  errada, CA ausente, host errado e broker fora do ar eram indistinguíveis —
  inclusive para quem depura. Agora o feed falha com `AlertFeedFailure` tipada
  (`missingPassword`, `missingCaAsset`, `refused`, `unreachable`) e a causa vai
  para `dart:developer`; ela nunca chega à tela.
- O `mqtt_client` já gravava o motivo da recusa em `connectionStatus.returnCode`;
  o código só jogava fora. `mqttRefusalReason` traduz os quatro códigos, lendo o
  `returnCode` ANTES do `disconnect()`, que limpa o status.
- `live_check` usava a senha como fallback: sem guarda, remover o default
  trocaria uma mensagem clara por um timeout mudo. Agora sai com código 2 e o
  comando que falta.

Verificação
- `flutter analyze` limpo; 59 testes herméticos no ACS (eram 50) e 6 no paciente;
  `scripts/qa/e2e.sh --emulator` com 7 + 11 testes de integração.
- No emulador, os quatro caminhos de MQTT, cada um com seu aviso: sem
  `--dart-define`, pelo script (sem banner), com senha errada, e com o broker
  parado. Antes os quatro produziam a mesma frase.
- Testes que reprovariam o código antigo: "os dois avisos coexistem quando
  broker e armazenamento caem juntos" e "o aviso acende depois de uma gravação
  que falha". `alert_feed_test.dart` guarda contra reintroduzir o `defaultValue`.
- `setUpAll` do `red_alert_cycle_test` troca um timeout incompreensível pelo
  comando de compilação que falta.
- Higiene: a senha não aparece no logcat, no diff nem em arquivo versionado.
  `client.logging` continua desligado — ele imprime o CONNECT com a senha.

Limite conhecido, fora de escopo: a falha na conexão *inicial* do broker nunca é
reagendada — `autoReconnect` só age depois de uma conexão bem-sucedida e
`_connectFeed()` roda uma vez no `initState`. Quem abre o app sem sinal precisa
reiniciá-lo quando o sinal volta.

Signed-off-by: Herbert Rocha <herberthb12@gmail.com>
`_connectFeed()` rodava uma vez, no `initState`. Se falhasse, o app nunca mais
tentava — o banner ficava na tela até alguém fechar e reabrir o aplicativo. O
`autoReconnect` do `mqtt_client` não cobre esse caso: ele só age depois de uma
conexão bem-sucedida, e as duas rotas de erro do cliente chamam `disconnect()`,
que o desliga de propósito para não ficar tentando para sempre um cliente órfão.

Em campo isso significava um ACS que abre o app na zona rural sem sinal ficar
sem alerta pelo resto do turno, mesmo com o sinal voltando minutos depois — e
como a sessão MQTT é persistente, o broker estava guardando esses alertas com
QoS 1 o tempo todo, só esperando uma conexão que nunca vinha. Colide com o
invariante de que alerta vermelho nunca some em silêncio.

Retentativa, só quando vale a pena
- `reconnect_schedule.dart` (novo): backoff puro de 2s a 60s, mesma escala do
  `MqttAlertDispatcher` do backend, sem `Timer` próprio — testável sem relógio
  falso.
- Só falha **transitória** é retentada: broker inalcançável, ou
  `brokerUnavailable` do CONNACK. Senha ausente, CA ausente e credencial ou
  identificador recusados não são — não mudam sozinhos, e insistir neles só
  gastaria bateria sem chance de sucesso.
- `mqtt_secure_client.dart` ganhou `MqttConnectionRefused.transient` e
  `mqttRefusalIsTransient()`, lendo o `returnCode` do CONNACK antes do
  `disconnect()`, que o zera. `AlertFeedFailure` ganhou o mesmo campo.
- `AcsHomeShell` agenda a retentativa, cancela a anterior antes de agendar outra,
  e para o timer quando o app vai para segundo plano — um `Timer` em background
  no Android não é confiável e só gastaria bateria à toa.
- O banner ganhou "Tentar agora" para quem já vê o sinal voltar e não quer
  esperar o backoff; ausente nas falhas permanentes, para não oferecer um botão
  que nunca vai funcionar.
- Voltar do segundo plano dispara uma tentativa imediata — o gatilho que mais
  importa na prática, porque o sinal costuma voltar com a tela apagada.

Defeito vizinho, a outra metade do mesmo problema
- O chip do cabeçalho também era escrito uma única vez, dentro de
  `_connectFeed`. Uma queda **depois** de uma conexão bem-sucedida nunca chegava
  a ele, que continuava dizendo "em linha" para sempre enquanto o
  `autoReconnect` trabalhava por baixo em silêncio.
- `AlertFeed.onConnectionChanged` subiu para a interface como campo mutável, em
  vez de só um parâmetro do construtor do `MqttAlertFeed` — o shell passou a
  assiná-lo depois de montar o feed, sem mudar a assinatura de `feedBuilder`
  usada pelos 20+ call sites de teste.
- `alert_feed.dart`.`start()` passou a chamar `stop()` na primeira linha: sem
  isso, uma segunda tentativa sobrescreveria `_client` e deixaria o cliente
  MQTT anterior órfão, fora do alcance de `stop()`/`dispose()` — inofensivo com
  uma única tentativa, um vazamento real a cada retentativa.

Verificação
- `flutter analyze` limpo; 68 testes herméticos no ACS (eram 59), 6 no paciente.
- 9 testes novos: backoff dobrando e voltando ao início; reconexão que dá certo
  na segunda tentativa (o teste que reprovaria o código antigo); atraso
  dobrando entre falhas; "Tentar agora" pulando a fila; senha ausente não
  agenda nada em 70s simulados; retorno do segundo plano reconecta na hora;
  chip refletindo uma queda pós-sucesso sem abrir banner novo; e
  `mqttRefusalIsTransient` para os quatro códigos de CONNACK.
- No emulador, com a stack local: broker parado → banner com "Tentar agora";
  broker volta **sem tocar no app** → reconectou sozinho, banner sumiu; um
  alerta criado nesse meio-tempo chegou pela fila, provando que a sessão
  persistente do broker guardava mesmo o que chegasse; APK sem
  `--dart-define` → falha permanente, sem botão, zero tentativas em 70s; app
  para segundo plano e volta com o broker fora do ar → reconectou poucos
  segundos depois de o broker subir, muito antes do teto de 60s.
- `./scripts/qa/e2e.sh --emulator`: 22 testes de integração no dispositivo mais
  os live checks na VM.

Signed-off-by: Herbert Rocha <herberthb12@gmail.com>
…mpilar em silêncio

Os dois defeitos originalmente relatados — `feedError ?? _storageError`
escondendo a falha de disco, e `SINALACS_MQTT_PASSWORD` com default — já
estavam corrigidos em 601d5f4. A verificação achou uma terceira lacuna que
aquele commit deixou de fora de propósito: `flutter build apk` puro continuava
compilando com sucesso um APK que nunca conecta ao broker. O defeito só se
denunciava em tempo de execução, pelo banner "compilado sem a senha" — tarde
demais para quem já distribuiu o APK, e alerta que não chega é o pior modo de
falha deste produto.

A guarda
- `apps/acs/android/app/build.gradle.kts` ganhou uma checagem em `doFirst` das
  tarefas `compileFlutterBuild*`: decodifica a propriedade `dart-defines` (o
  Flutter Gradle Plugin lê a mesma) e falha, com o comando certo, se
  `SINALACS_MQTT_PASSWORD` não estiver lá.
- Precisou ser `doFirst` de TAREFA, não bloco de configuração: na configuração
  dispararia em todo `gradlew`, inclusive o sync do Android Studio, que não
  passa define nenhum — tornaria o projeto impossível de abrir na IDE.
- Escotilha explícita para quem quer de propósito um APK sem senha (por
  exemplo, para reproduzir o banner): `-Psinalacs.allowMissingMqttPassword=true`,
  no molde constrangedor-de-digitar de `allowUnencryptedForTesting`.
- A mensagem nunca imprime `dart-defines` decodificado: a lista carrega a
  própria senha.
- `apps/patient` não usa a senha; a guarda não vai lá.

A senha sai do argv
- `scripts/dev/run_acs.sh` trocou `--dart-define` por `--dart-define-from-file`,
  tirando a senha da linha de comando do `flutter` — visível em `ps` para
  outros usuários da mesma máquina, risco que o próprio cabeçalho do script já
  registrava como aceito.
- O arquivo é um JSON com só as quatro chaves do app, nunca o `.env` inteiro:
  `--dart-define-from-file` também aceita formato `.env`, e apontar para o real
  assaria `JWT_SECRET` e `POSTGRES_PASSWORD` dentro do APK.
- `mktemp` (0600, fora da árvore do repositório) mais um `trap` de limpeza.
- O `exec` saiu das duas chamadas ao `flutter`: `exec` substitui o processo do
  shell, e o `trap` nunca rodaria — o arquivo com a senha ficaria esquecido em
  `/tmp`, trocando uma exposição temporária por uma persistente.
- `scripts/qa/e2e.sh` continua passando a senha por `argv`, sem mudança: aquele
  caminho roda `flutter test`/`dart run`, não `flutter build`, e não passa pela
  guarda.

Verificação
- `flutter build apk` sem defines → falha (saída 1), mensagem limpa, sem
  vazar a senha; com a escotilha → compila com aviso.
- `./scripts/dev/run_acs.sh --build` → instala e conecta: "Alertas em tempo
  real", sem banner.
- Senha fora do argv confirmado com uma cópia instrumentada do script: arquivo
  0600, JSON correto, removido pelo trap ao final — nada sobrou em `ps` nem em
  `/tmp`.
- `./gradlew :app:tasks` (equivalente ao sync da IDE) passa sem senha nenhuma —
  a guarda não quebra a IDE.
- `flutter analyze`/`flutter test` limpos nos dois apps (68 + 6); `flutter
  build apk` do paciente, sem guarda, continua passando.
- `./scripts/qa/e2e.sh --emulator --keep`: 22 testes de integração no
  dispositivo, verdes.

Signed-off-by: Herbert Rocha <herberthb12@gmail.com>
Signed-off-by: Herbert Rocha <herberthb12@gmail.com>
Corrige o fluxo de observações de visita no app ACS.

O problema era real: a tela de registro coletava texto em `notes`, mas esse valor era descartado no momento do salvamento e também no payload de sincronização (`notes: {}`), fazendo com que a observação não passasse pela fila offline nem chegasse ao backend.

Ajustes feitos:
- adiciona `notes` ao modelo `OfflineVisitRecord`
- persiste a coluna `notes` no banco local SQLCipher
- inclui a migração de schema v2 → v3 para criar a coluna quando necessário
- grava o texto do campo na visita ao salvar
- envia as observações no `VisitSyncEntry` durante `visits.sync`
- ajusta o helperText da UI para refletir o comportamento real
- adiciona teste de regressão cobrindo o ciclo offline + sincronização

Impacto:
- visitas offline preservam observações de campo
- sincronização não perde texto de acompanhamento
- helperText e comportamento ficam consistentes com a implementação

Verificação:
- `flutter test test/login_flow_test.dart --plain-name "deve preservar observações no ciclo offline e ao sincronizar"`
- resultado: `All tests passed`

Signed-off-by: Herbert Rocha <herberthb12@gmail.com>
- implementa o mapa operacional na tela do ACS com:
  - marcadores baseados na fila de alertas priorizados;
  - destaque da localização atual do ACS;
  - resumo do alerta selecionado no mapa;
  - legenda clínica por risco (vermelho/amarelo/verde);
  - estado real do mapa conectado ao queue em vez de placeholder estático.

- corrige a reprodução de notas no fluxo offline de visitas:
  - inclui anotações no modelo de visita persistida;
  - preserva o campo de observações ao serializar/sincronizar com o backend;
  - mantém o comportamento consistente com a fila local e o envio ao servidor.

- ajusta o texto de ajuda da UI para refletir o comportamento real:
  - o helper deixa claro que as anotações são enviadas e persistidas;
  - remove a inconsistência com o fluxo de sincronização.

- adiciona teste de regressão para o mapa:
  - valida renderização com dados reais da fila;
  - protege a integração da localização atual e dos marcadores.

- mantém o alinhamento com as regras do projeto:
  - prioridade por risco;
  - microárea e fluxo ACS;
  - operação offline-first;
  - sem regressão na análise estática do Flutter.

- `flutter test test/map_screen_test.dart` ✅
- `flutter analyze` ✅

Signed-off-by: Herbert Rocha <herberthb12@gmail.com>
Implementa o fluxo operacional de deslocamento do ACS até o local do
paciente, conectando mapa, GPS, geofencing e registro offline de visitas.

Principais mudanças:
- adiciona o RouteService para calcular distância, ETA, pontos da rota e
  estado de chegada;
- introduz os estados de geofencing indisponível, a caminho e chegado;
- integra o mapa com a fila real de alertas e permite selecionar o alerta
  prioritário;
- adiciona handoff do mapa e do geofencing para o registro da visita;
- substitui a tela demonstrativa de geofencing por uma tela baseada em GPS,
  fila de alertas e raio configurável;
- exige confirmação de chegada antes de habilitar o registro da visita;
- libera automaticamente a confirmação quando o GPS está dentro do raio;
- preserva UUID, observações, status e resultado na fila offline;
- mantém a visita pendente em falhas de persistência, rede ou sincronização;
- melhora mensagens de estado e evita SnackBars sobrepostos;
- adiciona cobertura para rota ativa, chegada, GPS indisponível, geofencing e
  handoff para visita.

Validação:
- flutter test test/map_screen_test.dart test/login_flow_test.dart
  test/route_service_test.dart
- 45 testes aprovados;
- flutter analyze sem problemas.
```

Signed-off-by: Herbert Rocha <herberthb12@gmail.com>
Adiciona uma suíte de integração executada no Android emulator para validar
o fluxo operacional do mapa do ACS sem depender de backend ou dados reais.

Cenários cobertos:
- criação de uma fila com alerta sintético da microárea correta;
- renderização da tela de mapa no fallback operacional sem chave da Google Maps;
- traçado de rota ativa quando o ACS ainda está distante do destino;
- detecção de rota concluída quando o ACS está no destino;
- liberação do botão de registro da visita após a chegada;
- exibição do estado de geofencing como local alcançado;
- encaminhamento do alerta correto para o registro da visita;
- descarte da fila ao final de cada teste para evitar vazamento de estado.

A implementação usa somente UUIDs sintéticos e não acessa dados de pacientes
reais, backend ou broker MQTT. O teste mantém a validação hermética no
dispositivo e isola a funcionalidade de mapa das integrações de infraestrutura.

Validação executada:
- dispositivo: `emulator-5554`;
- comando:
  `flutter test integration_test/map_flow_test.dart -d emulator-5554
   --dart-define=SINALACS_MQTT_PASSWORD=integration-test`;
- resultado: `+2: All tests passed!`;
- suíte E2E completa com `./scripts/qa/e2e.sh --emulator --keep`;
- resultado final do ACS no emulator: `+13: All tests passed!`;
- `flutter analyze`: `No issues found!`.

Observação:
- como o ambiente não possui `GOOGLE_MAPS_API_KEY` configurada no Android,
  os testes validam o fallback operacional, as rotas, o geofencing e o
  handoff para visita. A renderização real dos tiles da Google Maps requer
  uma chave válida configurada no aplicativo.

Signed-off-by: Herbert Rocha <herberthb12@gmail.com>
Completa o fluxo operacional do alerta vermelho permitindo que o ACS
inicie o acompanhamento presencial depois de acessar a tela de
escalonamento, sem substituir ou reduzir a prioridade do acionamento
do SAMU.

Alterações principais:
- adiciona callback de visita à EscalationScreen;
- conecta o fluxo de escalonamento ao destino de registro de visita;
- preserva o alerta selecionado e seu alertId durante a navegação;
- mantém “Ligar para o SAMU (192)” como ação primária;
- adiciona “Iniciar rota de visita” como ação secundária;
- exibe orientação explícita de que a visita não substitui o SAMU;
- mantém a validação de chegada/geofencing antes de permitir o salvamento;
- adiciona teste widget para validar a ação e a preservação do alertId;
- amplia os testes de integração no emulator para rota, geofencing e
  escalonamento de alerta vermelho.

Validação:
- testes focados ACS: 46 testes aprovados;
- integração no emulator-5554:
  - rota ativa;
  - rota concluída;
  - chegada por geofencing;
  - encaminhamento para visita;
  - escalonamento de alerta vermelho;
  - SAMU mantido como ação primária;
- suíte E2E completa com:
  `./scripts/qa/e2e.sh --emulator --keep`
- resultado no dispositivo: `+14: All tests passed!`;
- backend, PostgreSQL, MQTT/TLS e sincronização de visitas validados;
- `flutter analyze`: `No issues found!`.

Observação:
- os testes de mapa usam o fallback operacional porque não há
  GOOGLE_MAPS_API_KEY configurada no Android; a renderização real dos tiles
  da Google Maps permanece dependente de uma chave válida.

Signed-off-by: Herbert Rocha <herberthb12@gmail.com>
Adiciona o caminho de configuração da GOOGLE_MAPS_API_KEY para permitir que o
app ACS use o Google Maps Android SDK sem armazenar credenciais reais no
código-fonte ou no .env.example.

Alterações principais:
- documenta GOOGLE_MAPS_API_KEY no .env.example;
- adiciona o slot local da variável no .env, sem valor real;
- atualiza scripts/dev/run_acs.sh para ler a chave do ambiente;
- injeta a chave via dart-define-from-file durante o build;
- extrai o valor dos dart-defines no Gradle;
- configura o placeholder GOOGLE_MAPS_API_KEY no AndroidManifest;
- registra a chave no metadata com.google.android.geo.API_KEY;
- mantém o fallback operacional quando a chave está vazia;
- evita expor ou versionar uma chave real no projeto.

Segurança:
- a chave deve ser criada no Google Cloud Console;
- Maps SDK for Android deve estar habilitado;
- a chave deve ser restrita ao package
  com.example.sinalacs_acs;
- configurar as restrições por SHA-1 para debug e release;
- habilitar somente as APIs necessárias;
- a chave real deve permanecer apenas no .env local ou no secret manager
  utilizado pelo pipeline de build.

Validação:
- bash -n scripts/dev/run_acs.sh;
- scripts/dev/run_acs.sh --help;
- flutter analyze;
- build do APK debug via scripts/dev/run_acs.sh --build;
- git diff --check;
- diagnósticos dos arquivos alterados sem erros.

Observação:
- nenhuma chave real foi criada ou adicionada automaticamente;
- enquanto GOOGLE_MAPS_API_KEY permanecer vazia, o app continuará usando o
  fallback sem tiles reais do Google Maps.

Signed-off-by: Herbert Rocha <herberthb12@gmail.com>
Atualiza a identidade nativa do aplicativo ACS para o namespace oficial
br.com.prismrr.sinalacs.acs.

Alterações realizadas:
- altera o namespace Android no build.gradle.kts;
- altera o applicationId para br.com.prismrr.sinalacs.acs;
- move MainActivity.kt para o diretório correspondente ao novo namespace;
- atualiza a declaração package da MainActivity;
- remove as referências nativas ao package com.example.sinalacs_acs;
- preserva o nome do pacote Dart sinalacs_acs, evitando alterações
  desnecessárias nos imports Flutter.

Validação:
- build do APK debug executado com scripts/dev/run_acs.sh --build;
- flutter analyze concluído sem problemas;
- APK reporta o package:
  br.com.prismrr.sinalacs.acs;
- APK instalado com sucesso no emulator-5554;
- Android confirmou o registro em:
  /data/app/.../br.com.prismrr.sinalacs.acs.../base.apk;
- nenhum erro de diagnóstico encontrado.

Ação necessária após o merge:
- atualizar no Google Cloud Console as restrições da GOOGLE_MAPS_API_KEY;
- adicionar o novo package name br.com.prismrr.sinalacs.acs;
- revisar os SHA-1 dos certificados debug e release;
- manter a chave restrita ao Maps SDK for Android.

Signed-off-by: Herbert Rocha <herberthb12@gmail.com>
Atualiza o teste de integração do fluxo de mapa para consumir
GOOGLE_MAPS_API_KEY via dart-define, permitindo validar tanto o fallback
sem chave quanto a renderização nativa do GoogleMap.

Alterações:
- lê GOOGLE_MAPS_API_KEY com String.fromEnvironment;
- injeta a chave no MapScreen durante os testes;
- mantém a asserção do fallback quando a chave não está configurada;
- valida a presença do widget GoogleMap quando a chave está disponível;
- mantém os testes de rota ativa, rota concluída, geofencing e escalonamento;
- não imprime nem armazena o valor da chave no código-fonte.

Validação:
- emulator: emulator-5554;
- teste executado com a chave local via --dart-define;
- resultado: +3: All tests passed!;
- branch nativo GoogleMap validado com sucesso;
- flutter analyze: No issues found!;
- diagnósticos do teste: nenhum erro.

Requisito operacional:
- a chave deve permanecer fora do versionamento;
- a restrição no Google Cloud deve incluir o package
  br.com.prismrr.sinalacs.acs;
- configurar os SHA-1 correspondentes aos builds debug e release.

Signed-off-by: Herbert Rocha <herberthb12@gmail.com>
…rio na sincronização

`alerts.createRedAlert` é o único produtor de alertas do sistema, e crava
sempre `riskLevel: 'red'` — emergência, com SAMU. Com `patientId` obrigatório
no registro de visita (mudança anterior) e nenhum alerta não-vermelho jamais
publicado, a aba "Visita" ficou inalcançável fora do caminho reativo: o PRD
mede engajamento do ACS em ≥ 8 visitas/dia, e visita de rotina — o padrão real
de uso — não tinha de onde partir. O caminho reativo em si (cartão vermelho →
"Acionar SAMU" → escalonamento → "Iniciar rota de visita") já existia de uma
mudança anterior não registrada; verificado que continua funcionando ponta a
ponta, sem alteração.

Diretório de pacientes da microárea (backend)
- `patients.listMicroArea` (endpoint novo): a microárea vem sempre do token,
  nunca de um parâmetro — aceitar um da chamada seria oferecer ao dispositivo a
  chance de pedir outro território.
- `OrmPatientDirectoryStore` faz um JOIN em duas etapas porque `Patient` não
  guarda microárea — ela vive em `users`, e `Patient.id` É o UUID do usuário.
- O payload é só nome e condições crônicas: o que `spec/lgpd_design.md`
  autoriza para visita de rotina, nada além. `emergencyContact` fica de fora —
  não ajuda a escolher quem visitar.

Seletor na aba Visita (app)
- Sem alerta selecionado, `VisitRegistrationScreen` mostra busca por nome mais
  a lista (`Key('patient_picker')`); escolher libera o formulário exatamente
  como um alerta faria.
- O nome vive só em memória, para o rótulo. `OfflineVisitRecord` continua
  carregando apenas o UUID — mesma disciplina já estabelecida para o caminho
  por alerta; nada muda no que é gravado no banco cifrado.
- Falha ao carregar a lista não trava a tela: mostra o motivo com um jeito de
  tentar de novo, e o caminho por alerta continua funcionando sem rede nenhuma.

Dois furos adjacentes, achados ao implementar o diretório
- **Territorialização do sync (INV-01).** `VisitSyncService` validava que o
  ACS é territorializado, mas nunca que o PACIENTE pertence ao mesmo
  território — qualquer UUID de paciente existente era aceito, de qualquer
  microárea. `VisitStore.microAreaOfPatient` fecha isso; a recusa é por visita
  (`SyncStatus.error`), não descarta o resto do lote, e a mensagem não cita a
  microárea alheia nem o nome do paciente.
- **`audit_logs` era tabela morta.** Existia desde a migração-base, documentada
  como "trilha de auditoria de acesso a dados sensíveis, append-only", e
  nenhuma linha de código escrevia nela — este PR introduzia a primeira leitura
  em massa de PHI do sistema. `AuditTrail` liga os dois pontos que ele cria: a
  leitura da lista (evento, sem enumerar quem foi lido — listar recriaria o
  prontuário dentro do próprio log) e a recusa por território (com o UUID do
  paciente envolvido). `ipHash` nunca é IP em claro — SHA-256 sobre
  `request.remoteInfo`, que o Serverpod já resolve corretamente atrás do
  Traefik. Escrita best-effort: uma trilha fora do ar não pode impedir o ACS de
  trabalhar. A assinatura em hash chain que a spec descreve (LGPD-RT03)
  continua não implementada, e os demais endpoints sensíveis ainda não
  escrevem na trilha — registrado como dívida.

Seed ganhou cinco pacientes sintéticos (nomes obviamente fictícios) na
microárea do ACS, mais um sexto fora dela, para o seletor ser demonstrável e
para provar territorialização sem precisar de outra stack de teste.

Verificação
- Backend: 62 testes (unit + integração contra Postgres real, incluindo o JOIN
  de verdade, o `inSet` e a escrita real em `audit_logs`).
- App: 83 testes herméticos, 14 no dispositivo.
- No emulador, contra a stack real: escolhi um paciente no seletor, salvei,
  sincronizei — `synced` no Postgres. O nome não aparece no arquivo do banco
  cifrado nem no logcat depois de gravar e sincronizar pelo seletor. Uma visita
  para paciente de outra microárea voltou como `error` e gravou a linha
  `denied_territory`, sem tocar `visits`. O caminho por alerta vermelho
  (SAMU → escalonamento → visita) segue idêntico ao de antes desta mudança.

Documentação: CLAUDE.md, PROGRESS.md e spec/lgpd_design.md atualizados,
incluindo a correção de uma linha já desatualizada ("apps não consomem o
cliente gerado" — consomem, desde a integração com o backend real).

Signed-off-by: Herbert Rocha <herberthb12@gmail.com>
…s, alertas, auditoria)

Cinco telas no shell responsivo do backoffice: login local (padrão dev,
mesmo fluxo dos outros apps), painel de indicadores por RiskLevel,
listagem somente leitura de microáreas/ACS, consulta filtrável de
alertas (sem reclassificação de risco) e log de auditoria com
self-audit do próprio admin.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WyMMKXSksyUUNXPieFpSfX
…tient-app

A issue citava Flutter 3.24.0 para este job, mas o ci.yml real fixa
3.44.8 nos jobs patient-app/acs-app -- usei a versão que já roda no
pipeline, não a citada na issue.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WyMMKXSksyUUNXPieFpSfX
Screenshots reais ficam pendentes nesta entrega -- ver descrição do PR.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WyMMKXSksyUUNXPieFpSfX
… teste

_InfoRow (painel de indicadores) e os DropdownButtonFormField de filtro
de alertas estouravam a largura em telas <640px -- pego pelos próprios
testes de responsividade (flutter test acusou RenderFlex overflow).
Corrigido com Expanded/isExpanded. Também faltava o import de
flutter/material.dart em audit_log_screen_test.dart (flutter analyze
acusou).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WyMMKXSksyUUNXPieFpSfX
…orms=web)

Faltava o scaffolding de plataforma -- flutter run/build web falhava
com 'This application is not configured to build on the web.'.
Adiciona web/, .metadata, .gitignore próprio do app e README.md no
padrão de apps/patient e apps/acs. O test/widget_test.dart gerado
pelo comando (teste padrão do contador, referenciando uma classe
MyApp inexistente) foi removido -- os testes reais já estão em
test/*_test.dart.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WyMMKXSksyUUNXPieFpSfX
O banner 'ambiente de desenvolvimento' usava AdminColors.yellow na
borda e no ícone, e o chip de status do ACS na tela de microáreas
usava AdminColors.green para Ativo -- exatamente o padrão que a issue
proíbe explicitamente (amarelo para aviso, verde para status, não
para RiskLevel). Banner agora usa AdminColors.accent (índigo,
neutro); o chip de status usa ícone (check/remove) em vez de cor
para diferenciar ativo/inativo.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WyMMKXSksyUUNXPieFpSfX
Investiguei ligar de verdade (apps/patient e apps/acs já fazem isso, via
sinalacs_client + BackendClient) antes de decidir manter local. Motivo:
backend/sinalacs_server/lib/src/endpoints/auth_endpoint.dart só aceita
role patient ou acs -- não existe usuário fixo de dev para admin, a
chamada real falharia com AlertValidationException. Ligar isso exige
mudança no backend, fora do escopo desta issue (app apps/admin). Ver
descrição do PR para o achado completo.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WyMMKXSksyUUNXPieFpSfX
A frase antiga dizia que paciente/ACS também não chamam o backend hoje
-- ficou desatualizada depois que os dois passaram a autenticar de
verdade contra auth.developmentLogin. Corrige e explica por que o
admin continua local (mesmo motivo do commit anterior: endpoint só
aceita role patient/acs).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WyMMKXSksyUUNXPieFpSfX
Capturadas rodando flutter run -d web-server e abrindo localhost
manualmente no navegador, dados fictícios. Remove a nota de pendência
em telas-admin.md, que já referenciava esses arquivos pelo nome.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WyMMKXSksyUUNXPieFpSfX
hbgit and others added 2 commits September 14, 2026 12:11
…sada

Duas dívidas registradas explicitamente no PR anterior (82bc5fa), fechadas
agora — independentes entre si, mas no mesmo commit por serem pequenas.

Cadeia de hash em audit_logs (LGPD-RT03)
- A trilha ganhara escritores no PR anterior, mas só fazia `insertRow`:
  qualquer um com acesso de escrita ao Postgres editava ou apagava uma linha
  sem deixar rastro — não serve ao não-repúdio que a spec promete.
- `audit_logs` ganha `sequence` (contígua, índice único), `previousHash` (o
  `entryHash` da linha anterior, ou 64 zeros na gênese) e `entryHash`
  (HMAC-SHA256 do conteúdo da linha). `AuditChain`
  (`application/audit/audit_chain.dart`) calcula os dois lados — escrita e
  verificação — com o MESMO código, para uma segunda implementação do cálculo
  não virar a forma mais provável de a verificação mentir.
- A chave é um segredo PRÓPRIO, `AUDIT_CHAIN_SECRET`, nunca derivado do
  `JWT_SECRET`: os dois precisam rotacionar de forma independente, e SHA-256
  sem chave não detectaria uma reescrita completa por quem tem acesso de
  escrita ao banco — exatamente o adversário da §458 de spec/lgpd_design.md.
  Plumbing completo: `.env.example`, `bootstrap_env.sh`, `docker-compose.yml`,
  `AppConfig` (`_resolveSecret` generaliza o que antes era só de
  `JWT_SECRET`).
- `OrmAuditTrail.record` lê a última linha e insere a próxima dentro da MESMA
  transação, sob `pg_advisory_xact_lock` — sem isso, duas gravações
  concorrentes leriam a mesma linha anterior e bifurcariam a cadeia. O índice
  único em `sequence` é o segundo cinto: uma falha do lock aparece como erro,
  não como corrupção silenciosa.
- `AuditChainVerifier` (três checagens: sequência contígua, elo
  previousHash/entryHash, hash recalculado) e `bin/audit_chain_check.dart`
  tornam a cadeia verificável sob demanda, sem subir o servidor HTTP.
- Fora de escopo, registrado na spec: o append-only em si não é imposto pelo
  banco (sem trigger/`REVOKE`) — a cadeia *detecta* a violação, não a impede.
  O harness de teste do Serverpod só aplica migrações, então um trigger via
  seed não existiria no banco de teste; seria garantia não testável.

Estado terminal para recusa definitiva no sync de visitas
- `VisitSyncService._syncOne` colapsava seis motivos de falha distintos no
  mesmo `SyncStatus.error`. No aparelho, `OfflineVisitQueue._applyOutcomes`
  reenfileirava `error` incondicionalmente — para a recusa territorial isso
  era retentativa eterna GARANTIDA: a checagem de território roda antes do
  lookup por `localId`, então o reenvio falha de forma idêntica para sempre, e
  nada no aparelho explicava o motivo.
- `SyncStatus` ganha `rejected`, terminal (sem migração — `syncStatus` é
  `text` puro no banco). Critério: "esta visita pode dar certo numa próxima
  tentativa?" — localId vazio, UUID malformado, território incompatível e
  visita de outro agente viram `rejected`; "paciente não encontrado" continua
  `error`, porque o paciente pode ser cadastrado depois. `SyncFsm` ganha o
  estado espelhado, sem transição de saída (`networkUp`/`syncStart` não o
  alcançam).
- App: `OfflineVisitRecord` ganha `rejectionReason` (não mexe em `status`, que
  é dado clínico enviado ao servidor). A fila ganha uma quarta lista
  (`_rejected`) que sai da retentativa mas continua no disco —
  `VisitStore.save` passou a persistir pendentes MAIS recusadas, senão a
  recusada sumiria do aparelho no instante da recusa, antes de o ACS decidir
  qualquer coisa. `discardRejected()` é a única saída, e a tela só a oferece
  atrás de uma confirmação (`Key('discard_rejected')`, `AlertDialog`) — nada
  remove o registro sozinho.
- `encrypted_database.dart` vai de v3 a v4 com `ALTER TABLE ... ADD COLUMN
  rejection_reason` — a primeira migração aditiva desde que o schema existe;
  as anteriores (v1 → v2) recriavam a tabela porque o app ainda não tinha
  tido release.
- Precedência de retorno da fila: rejected > conflict > error > synced —
  recusa definitiva é trabalho de campo que NUNCA vai ao servidor, o que mais
  precisa chegar aos olhos do ACS. `sync_visits` desabilita sozinho quando só
  restam recusadas, porque depende de `pendingCount`, que a recusada já não
  integra.

Verificação
- Backend: 83 testes (69 unit + 14 integração contra Postgres real,
  incluindo duas escritas reais encadeadas e a checagem do
  `pg_advisory_xact_lock`; cobertura própria da cadeia em
  test/unit/audit_chain_test.dart — edição, remoção, renumeração e segredo
  errado, todos detectados).
- App: 89 testes herméticos (6 novos: recusa saindo da fila sem reenviar,
  precedência sobre conflito, `discardRejected`, persistência da recusada em
  disco, migração v3 → v4 preservando linhas, fluxo de descarte com
  confirmação na tela).
- Contra a stack real, via chamada HTTP de verdade (login → listMicroArea →
  visits.sync para paciente de outra microárea): duas linhas gravadas em
  audit_logs, encadeadas corretamente; `bin/audit_chain_check.dart` aprovou a
  cadeia; um `UPDATE` direto no Postgres alterando o `result` da segunda linha
  fez o verificador falhar exatamente naquela `sequence`, com exit code 1;
  desfeito o `UPDATE`, a cadeia voltou a validar. O sync territorial devolveu
  `rejected` (não mais `error`) pela API real, e a tabela `visits` continuou
  vazia para aquele paciente.
- Não verificado em dispositivo físico/emulador (adb indisponível neste
  ambiente): o fluxo de UI do descarte com confirmação está coberto apenas
  pelos testes herméticos de widget, não por uma checagem visual real.

Documentação: spec/lgpd_design.md (LGPD-RT03 deixa de dizer "não implementada"
e passa a listar o que ainda falta — append-only imposto pelo banco),
spec/PRD_system.md (ER diagram e tabela de criptografia), CLAUDE.md (contagem
de testes, AUDIT_CHAIN_SECRET, SyncStatus.rejected, schema v4) e PROGRESS.md.

Signed-off-by: Herbert Rocha <herberthb12@gmail.com>
@gitguardian

gitguardian Bot commented Sep 14, 2026

Copy link
Copy Markdown

⚠️ GitGuardian has uncovered 1 secret following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

🔎 Detected hardcoded secret in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
37197951 Triggered Username Password 0447345 docker-compose.yml View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secret safely. Learn here the best practices.
  3. Revoke and rotate this secret.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@GuilhermeAlmeidadaLuz GuilhermeAlmeidadaLuz changed the title Develop (Em resposta a Issue #3) - Análise LGPD dos dados armazenados: anonimização e pontos de vazamento docs: add lgpd_data_audit.md (Análise LGPD dos dados armazenados: anonimização e pontos de vazamento) Sep 14, 2026
vinimartinsufrr and others added 10 commits September 15, 2026 08:58
Corrige os 9 pontos da review em pull/5#pullrequestreview-5199741445:

- Indicadores, microáreas, alertas e auditoria: FutureBuilder só checava
  hasData, então um erro real ficava com spinner infinito (indicadores) ou
  era mostrado como se a lista estivesse vazia (alertas, auditoria) --
  escondendo, por exemplo, uma falha ao carregar alertas vermelhos. Todas
  as quatro telas agora tratam snapshot.hasError com um estado de erro
  próprio e botão de retry (_AsyncError, cor neutra, não usa
  vermelho/amarelo/verde -- essas cores continuam reservadas a RiskLevel).
- Microáreas e alertas: recordAccess() era chamado sem await no initState
  (fire-and-forget), então uma falha no registro de auditoria não impedia
  a tela de mostrar dado sensível, e virava exceção assíncrona não
  tratada. Agora o acesso é registrado antes dos dados serem buscados/
  exibidos, no mesmo espírito do que AuditLogScreen já fazia certo.
- Dropdown de filtro (microárea e status): com value null, o
  DropdownButton mostra o hint, não o child do item -- sem hint definido,
  o campo ficava em branco ao selecionar Todas/Todos em vez de mostrar o
  rótulo. Adicionado hint explícito nos dois.
- Login: campos de matrícula/senha vinham pré-preenchidos
  (admin.dev/123456), diferente do padrão atual do ACS (que remove esse
  preenchimento). Removido; o botão continua avançando independente do
  que for digitado, já documentado no próprio LoginScreen.
- web/manifest.json e web/index.html: description, theme/background
  color e orientation ainda eram o padrão gerado pelo flutter create
  (azul do Flutter, A new Flutter project., portrait-primary), não o
  tema índigo/escuro e o layout desktop-first do backoffice.

Durante a correção, os próprios testes novos (próximo commit) pegaram
mais um bug: setState(() => _future = fetch()) retorna a Future da
atribuição (regra de que x = y vale y em Dart), o que o Flutter rejeita.
Corrigido para corpo de bloco nos dois retries que tinham esse padrão
(Indicadores e Microáreas).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WyMMKXSksyUUNXPieFpSfX
Cobre os quatro FutureBuilder corrigidos (indicadores, microáreas,
alertas, auditoria): erro não pode aparecer como vazio nem travar em
spinner, e o botão Tentar novamente precisa recuperar de verdade. Mais
um teste de regressão do hint dos dropdowns (Todas/Todos visíveis
fechados). Duplo de teste novo em test/support/failing_admin_data_source.dart,
que permite forçar falha em qualquer método uma única vez.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WyMMKXSksyUUNXPieFpSfX
Os outros 2 comentários inline (pull/5#discussion_r4006859849 e
r4006859916) já tinham sido corrigidos sem querer no commit anterior
(mesmo bug do recordAccess sem await e do hasError da auditoria, só que
relatado como comentário de linha em vez de no resumo da review).
Restavam estes dois:

- Login sem nenhum controle de acesso: o botão Entrar ignorava
  matrícula/senha e não tinha gate nenhum, então qualquer pessoa
  clicava e via alertas/auditoria, mesmo numa build fora de dev. O
  banner é só aviso, não bloqueia nada. Adiciona devLoginEnabled
  (default kDebugMode, false em build profile/release) que desabilita
  o botão de verdade fora de modo debug -- não resolve a falta de auth
  institucional (fora de escopo, já documentado), mas fecha o buraco de
  qualquer um entrar em qualquer build.

- Filtro de microárea em Alertas: os itens do dropdown eram três
  strings fixas no código (as do seed mock). Trocando o
  AdminDataSource por uma fonte real, ou surgindo uma microárea nova,
  os alertas dela continuariam retornando mas sem opção de filtro
  correspondente. As opções agora vêm de fetchMicroAreas().

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WyMMKXSksyUUNXPieFpSfX
…nâmico)

login_dev_gate_test.dart: com devLoginEnabled false o botão fica
desabilitado e não navega; com true funciona normal.
alerts_filter_dynamic_test.dart: um AdminDataSource com uma microárea
fora do antigo seed mock aparece como opção no filtro, provando que
as opções vêm de fetchMicroAreas() e não de uma lista fixa no código.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WyMMKXSksyUUNXPieFpSfX
docs: add UX and accessibility assessment report
…G nos apps

O relatório spec/ux_accessibility_assessment.md media contraste manualmente no
WebAIM contra o fundo do Scaffold, mas texto de risco/status é renderizado
dentro de Card — a superfície errada. Isso produziu um falso positivo (achado
"Alta": Colors.white54, na verdade 5.36:1) e deixou passar duas falhas piores,
nunca detectadas: red/accent de PREENCHIMENTO reaproveitados como cor de TEXTO
caem para 3.04:1 e 2.84:1 sobre o card. A correção que o relatório sugeria
(#EF4444) também não resolvia (3.90:1 sobre card).

Contraste (acs_theme.dart, patient_theme.dart)
- Novos tokens redOnSurface/accentOnSurface (ACS) e
  dangerOnSurface/accentOnSurface (paciente): variante de TEXTO para cada cor
  que só era segura como preenchimento. red/accent/danger continuam com a cor
  original nos botões (pânico, SAMU, confirmar recebimento) — só a leitura
  como texto muda.
- helper acsOnSurface() converte fill -> texto no ponto único onde um switch
  de risco alimentaria os dois (record `_risk` de _AlertCard, agora
  (fill, textColor, label); mesma correção em _AlertMapSummary).
- ~10 ocorrências de accent/red como TextStyle/Icon trocadas nos dois apps.

Alvo de toque (WCAG 2.5.5)
- "Ligar para o SAMU (192)" não tinha minimumSize (40dp de altura visual, não
  48x52 como o relatório media) -> 64x60dp, o padrão de emergência do PRD.
- save_visit, discard_rejected, "Encaminhar para UBS Central" e
  escalation_visit também sem minimumSize -> 48x52dp.

Semântica (WCAG 4.1.2 e 4.1.3, este último ausente da baseline anterior)
- _AlertCard: o bloco paciente/risco/confirmação/horário vira uma frase única
  via Semantics(excludeSemantics: true) em vez de 4 nós soltos; botões
  continuam como nós próprios.
- Semantics(liveRegion: true) em 7 pontos de status dinâmico: login_error
  (ACS e paciente), estado do alerta de emergência, feed_error/storage_error,
  visit_storage_error, patient_directory_error, rejected_visits_count.
- Bug adjacente: GeofencingScreen/EscalationScreen mostravam o riskLevel cru
  do servidor ("red") em vez do rótulo traduzido; nova _riskLabelPt() central.

Testes
- apps/{acs,patient}/test/support/contrast.dart: luminância relativa e
  composição de alfa (WCAG 1.4.3), sem depender de medição manual.
- apps/{acs,patient}/test/contrast_tokens_test.dart: matriz determinística
  token x superfície real, incluindo os casos que documentam por que o fill
  puro e o #EF4444 sugerido falham como texto.
- meetsGuideline(textContrastGuideline/androidTapTargetGuideline/
  labeledTapTargetGuideline) e SemanticsFlags.isLiveRegion adicionados a
  login_flow_test.dart e patient_app_mvp_test.dart.

Verificação
- flutter analyze e flutter test limpos nos dois apps (as 12 falhas
  pré-existentes de encrypted_database_test.dart são falta de libsqlite3.so
  neste Linux, gap de ambiente documentado no CLAUDE.md, não relacionado).
- Validado ponta a ponta no emulador Android contra backend/broker reais: um
  alerta disparado pelo app Paciente chegou ao ACS via MQTT/TLS real com o
  novo contraste, risco traduzido e botão do SAMU no tamanho corrigido. Os 14
  testes de apps/acs/integration_test/ (inclusive a prova de que o banco
  segue criptografado) passam sobre o código revisado.

spec/ux_accessibility_assessment.md e PROGRESS.md reescritos com os números
corretos, a nova metodologia e o critério 4.1.3 incorporado à baseline.

Signed-off-by: Herbert Rocha <herberthb12@gmail.com>
…n.md

spec/ui_design.md faz quatro afirmações transversais (container responsivo
centralizado, dark mode nativo, foco suave, microinterações de clique) e três
específicas por app (baixa fricção + hierarquia do botão de emergência no
paciente; cor restrita ao risco clínico + fila dinâmica + offline-first +
container mais largo no ACS), mas nenhuma delas tinha um plano de verificação
associado — só a descrição de intenção.

spec/ux_ui_test_plan.md cruza cada afirmação com o estado atual do código
(apps/acs/lib/app/app.dart, apps/patient/lib/app/app.dart) para que cada
atividade comece de uma hipótese concreta, não de uma exploração às cegas.
Três achados já confirmados nesta auditoria, ainda sem correção:

- Container responsivo: só a tela de login aplica ConstrainedBox+Center
  (420dp paciente, 600dp ACS). Todas as telas pós-login — o helper _page()
  do ACS e as telas de Triagem/Status/Perfil do paciente — esticam sem
  limite em tela larga, contrariando "adapta-se centralizado em desktop" e,
  no caso do ACS, o "max-w-2xl para acomodar dados de triagem" citado
  nominalmente no documento de design.
- Cor clínica: AcsColors.green é usado fora do mapeamento de RiskLevel no
  fluxo de geofencing ("local alcançado") — não é decorativo, mas também
  não é risco; decisão de produto em aberto (exceção documentada vs.
  violação da regra "cores de emergência só ao ranqueamento clínico").
- Microinterações: o protótipo HTML usa active:scale (Tailwind); os widgets
  Flutter reais usam o ripple M3 nativo. Equivalência intencional, não
  defeito — falta só registrar em ui_design.md para não ser lida como
  pendência de implementação.

O documento é complementar a spec/ux_accessibility_assessment.md (WCAG 2.1
AA: contraste, alvo de toque, semântica, status messages) — não duplica
nenhum item de lá, e aponta explicitamente para ele nos casos de sobreposição
(contraste do botão de pânico, foco visível).

Cada uma das 9 atividades tem método, critério de aceite e prioridade, mais
uma matriz de viewports (telefone compacto/phablet/tablet/desktop) e o
mapeamento de qual ferramenta usa cada uma (emulador real para percepção
visual/tempo/tátil, flutter test com tester.view.physicalSize para os itens
determinísticos de container, scripts/qa/e2e.sh --emulator como baseline de
regressão de fluxo, UXtweak para os cenários que precisam de usuário real).

Rastreado em #8, atribuída a
sarahw3b.

Signed-off-by: Herbert Rocha <herberthb12@gmail.com>
feat(admin): implementar backoffice administrativo (issue #4)
CLAUDE.md e AGENTS.md não eram atualizados desde 8c5215a. Desde então,
develop recebeu a branch feat/admin-backoffice (merge #5) e uma
reauditoria de acessibilidade (fix(a11y)), e nenhum dos dois ficou
refletido na documentação de orientação para IA — o guia ainda descrevia
apps/admin como "pubspec skeleton, no implementation yet" e o CI com
quatro jobs.

apps/admin (backoffice)
- Documentada a arquitetura real do app: AdminHomeShell com 4 telas
  somente leitura (Indicadores, Microáreas, Alertas, Auditoria), a
  interface AdminDataSource/MockAdminDataSource (mesmo padrão de DI de
  PatientBackend/AcsBackend, ainda sem consumir sinalacs_client), o
  registro de acesso via recordAccess() antes de renderizar dado
  sensível (PRD §4.2.2), o motivo do login não chamar
  auth.developmentLogin (backend só aceita role 'patient'/'acs'), o
  layout desktop-first (NavigationRail/NavigationBar) e o suporte a
  Flutter Web, exclusivo deste app entre os três.

CI
- CLAUDE.md e AGENTS.md ainda diziam "quatro jobs" / "jobs separados
  para backend e apps"; agora refletem os cinco jobs reais
  (serverpod-backend, backend-docker-build, patient-app, acs-app,
  admin-app).

Acessibilidade (WCAG 2.2 AA)
- Documentados os tokens redOnSurface/accentOnSurface (ACS) e
  dangerOnSurface/accentOnSurface (paciente) introduzidos pela
  reauditoria de contraste: cores clínicas de preenchimento só atingem
  4.5:1 (WCAG 1.4.3) como fill, não como texto sobre Card/surfaceRaised.
  Adicionada uma regra de trabalho explícita nos dois arquivos: medir
  contraste contra a superfície onde o texto realmente renderiza, não
  contra o fundo do Scaffold — o erro que produziu o falso positivo
  original em spec/ux_accessibility_assessment.md.

Leitura obrigatória
- Adicionados os três documentos novos em spec/ (lgpd_data_audit.md,
  ux_accessibility_assessment.md, ux_ui_test_plan.md) às listas de
  leitura obrigatória de ambos os arquivos.

Signed-off-by: Herbert Rocha <herberthb12@gmail.com>
@hbgit

hbgit commented Sep 15, 2026

Copy link
Copy Markdown
Owner

Olá @GuilhermeAlmeidadaLuz , agradeço a contribuição.

Visando atualizar o repo, favor, implementar as sugestões apresentadas no relatório e descrever na atualização da sua PR.

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.

4 participants