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
Open
GuilhermeAlmeidadaLuz wants to merge 42 commits into
GuilhermeAlmeidadaLuz wants to merge 42 commits into
Conversation
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>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WyMMKXSksyUUNXPieFpSfX
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WyMMKXSksyUUNXPieFpSfX
…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
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
… apps 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
…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 id | GitGuardian status | Secret | Commit | Filename | |
|---|---|---|---|---|---|
| 37197951 | Triggered | Username Password | 0447345 | docker-compose.yml | View secret |
🛠 Guidelines to remediate hardcoded secrets
- Understand the implications of revoking this secret by investigating where it is used in your code.
- Replace and store your secret safely. Learn here the best practices.
- Revoke and rotate this secret.
- 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
- following these best practices for managing and storing secrets including API keys and other credentials
- install secret detection on pre-commit to catch secret before it leaves your machine and ease remediation.
🦉 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.
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>
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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.