From d88b5a9c85d4663495dc479941dacbb50e5ccf72 Mon Sep 17 00:00:00 2001 From: Herbert Rocha Date: Fri, 11 Sep 2026 17:04:35 -0400 Subject: [PATCH 01/43] =?UTF-8?q?docs(spec):=20corrige=20afirma=C3=A7?= =?UTF-8?q?=C3=B5es=20que=20ainda=20negavam=20o=20backend=20Serverpod?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- spec/PRD_system.md | 5 +++-- spec/test_plan.md | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/spec/PRD_system.md b/spec/PRD_system.md index 9e39819..b8ffd11 100644 --- a/spec/PRD_system.md +++ b/spec/PRD_system.md @@ -559,10 +559,11 @@ Cada registro possui um campo `version` (inteiro incremental). No momento da sin **Política ABAC (Atribute-Based Access Control):** -Exemplo ilustrativo da decisão original de stack (estilo Serverpod), sem correspondência com o código real do repositório: +Exemplo ilustrativo: não existe hoje uma camada de política ABAC genérica como esta. A checagem de papel e microárea é feita inline no caso de uso — ver `backend/sinalacs_server/lib/src/application/alerts/red_alert_service.dart` — e o RBAC institucional segue não implementado (ver RNF06 na seção 2.2). A API usada abaixo também é ilustrativa, não é a do ORM do Serverpod. ```dart -// Exemplo ilustrativo — não corresponde ao código real (o backend não usa Serverpod/ORM) +// Exemplo ilustrativo — não há camada ABAC genérica no código real; +// a regra equivalente vive inline em red_alert_service.dart Future canAccessPatient(Session session, String patientId) async { final user = await session.auth.getUser(); switch (user.role) { diff --git a/spec/test_plan.md b/spec/test_plan.md index bbe88cf..fd65a0e 100644 --- a/spec/test_plan.md +++ b/spec/test_plan.md @@ -15,7 +15,7 @@ Para otimizar o *Lead Time* e erradicar testes *flaky*, a pirâmide de testes tr * **Testes E2E (10%):** Focados apenas nos Caminhos Críticos (ex: Disparo do Botão de Alerta e Cache de Territorialização). -* **Contratos Orientados a Eventos:** A ausência de um ORM/codegen no backend (decisão original de usar Serverpod não foi implementada) significa que não há hoje quebra de compilação automática em falhas de contrato REST — isso é um risco a mitigar, não uma garantia existente. O risco real está na mensageria assíncrona. Implementaremos validação de contratos **AsyncAPI** para os *payloads* do *broker* Mosquitto, garantindo que o publicador (Paciente) e o consumidor (ACS) falem a mesma linguagem sem corromper o *buffer* de mensagens. +* **Contratos Orientados a Eventos:** Os contratos **síncronos** já têm garantia de compilação: o backend é um workspace Serverpod com ORM e codegen, e o `serverpod generate` produz o cliente Dart tipado em `sinalacs_client` a partir dos modelos `.spy.yaml` — uma quebra de contrato nos endpoints RPC falha na compilação. Essa garantia **não** se estende à mensageria assíncrona, onde está o risco real: os *payloads* do *broker* Mosquitto não passam por codegen. Implementaremos validação de contratos **AsyncAPI** para esses *payloads*, garantindo que o publicador (Paciente) e o consumidor (ACS) falem a mesma linguagem sem corromper o *buffer* de mensagens. From 0447345a756f2c8c2e5e8d901ef16756be6215b7 Mon Sep 17 00:00:00 2001 From: Herbert Rocha Date: Fri, 11 Sep 2026 18:55:34 -0400 Subject: [PATCH 02/43] =?UTF-8?q?feat:=20conecta=20os=20apps=20ao=20backen?= =?UTF-8?q?d=20real=20e=20centraliza=20a=20configura=C3=A7=C3=A3o=20em=20.?= =?UTF-8?q?env?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .env.example | 101 ++++ .github/workflows/ci.yml | 24 +- .gitignore | 7 + CLAUDE.md | 69 ++- README.md | 32 +- apps/acs/.flutter-plugins-dependencies | 2 +- .../android/app/src/debug/AndroidManifest.xml | 5 + .../debug/res/xml/network_security_config.xml | 19 + .../red_alert_cycle_test.dart | 193 ++++++++ apps/acs/lib/app/app.dart | 462 ++++++++++++++++-- apps/acs/lib/core/network/auth_session.dart | 84 ++++ apps/acs/lib/core/network/backend_client.dart | 158 ++++++ apps/acs/lib/core/network/backend_config.dart | 59 +++ apps/acs/lib/core/network/backend_scope.dart | 26 + apps/acs/lib/core/services/alert_feed.dart | 75 +++ apps/acs/lib/core/services/alert_queue.dart | 127 +++++ .../services/backend_visit_synchronizer.dart | 55 +++ .../lib/core/services/mqtt_secure_client.dart | 94 +++- .../core/services/offline_visit_queue.dart | 185 ++++++- apps/acs/pubspec.lock | 190 +++++-- apps/acs/pubspec.yaml | 16 +- apps/acs/test/login_flow_test.dart | 308 ++++++++++-- apps/acs/test/mqtt_secure_client_test.dart | 37 +- apps/acs/test/support/fakes.dart | 156 ++++++ apps/acs/test/widget_test.dart | 11 +- apps/acs/tool/live_check.dart | 143 ++++++ apps/patient/.flutter-plugins-dependencies | 2 +- .../android/app/src/debug/AndroidManifest.xml | 5 + .../debug/res/xml/network_security_config.xml | 19 + .../backend_connection_test.dart | 142 ++++++ apps/patient/lib/app/app.dart | 356 ++++++++++++-- .../lib/core/network/auth_session.dart | 84 ++++ .../lib/core/network/backend_client.dart | 198 ++++++++ .../lib/core/network/backend_config.dart | 19 + .../lib/core/network/backend_scope.dart | 26 + .../patient/lib/core/network/idempotency.dart | 21 + .../lib/core/privacy/location_hash.dart | 26 + apps/patient/pubspec.lock | 240 +++++---- apps/patient/pubspec.yaml | 14 +- apps/patient/test/patient_app_mvp_test.dart | 114 ++++- .../test/support/fake_patient_backend.dart | 100 ++++ apps/patient/tool/live_check.dart | 86 ++++ backend/DEPLOY.md | 11 + .../src/protocol/api/visit_sync_entry.dart | 196 ++++++++ .../src/protocol/api/visit_sync_result.dart | 117 +++++ .../lib/src/protocol/client.dart | 39 +- .../lib/src/protocol/protocol.dart | 327 +++++++------ backend/sinalacs_server/docker-compose.yaml | 44 -- .../visits/visit_sync_service.dart | 150 ++++++ .../lib/src/config/app_config.dart | 79 ++- .../lib/src/endpoints/visits_endpoint.dart | 48 ++ .../src/generated/api/visit_sync_entry.dart | 213 ++++++++ .../src/generated/api/visit_sync_result.dart | 129 +++++ .../lib/src/generated/endpoints.dart | 38 ++ .../lib/src/generated/protocol.dart | 360 ++++++++------ .../lib/src/generated/protocol.yaml | 2 + .../database/orm_visit_store.dart | 38 ++ .../src/models/api/visit_sync_entry.spy.yaml | 18 + .../src/models/api/visit_sync_result.spy.yaml | 14 + .../lib/src/runtime/alert_runtime.dart | 14 + .../integration/red_alert_cycle_test.dart | 1 - .../test_tools/serverpod_test_tools.dart | 56 +++ .../test/unit/app_config_test.dart | 109 +++++ .../test/unit/visit_sync_service_test.dart | 172 +++++++ docker-compose.yml | 66 ++- infra/docker/mosquitto/aclfile | 8 +- infra/docker/mosquitto/init.sh | 85 +++- scripts/dev/bootstrap_env.sh | 128 +++++ scripts/dev/sync_dev_ca.sh | 36 ++ scripts/qa/e2e.sh | 96 ++++ video/rpc_demo/watch_mqtt.sh | 33 +- 71 files changed, 6027 insertions(+), 690 deletions(-) create mode 100644 .env.example create mode 100644 apps/acs/android/app/src/debug/res/xml/network_security_config.xml create mode 100644 apps/acs/integration_test/red_alert_cycle_test.dart create mode 100644 apps/acs/lib/core/network/auth_session.dart create mode 100644 apps/acs/lib/core/network/backend_client.dart create mode 100644 apps/acs/lib/core/network/backend_config.dart create mode 100644 apps/acs/lib/core/network/backend_scope.dart create mode 100644 apps/acs/lib/core/services/alert_feed.dart create mode 100644 apps/acs/lib/core/services/alert_queue.dart create mode 100644 apps/acs/lib/core/services/backend_visit_synchronizer.dart create mode 100644 apps/acs/test/support/fakes.dart create mode 100644 apps/acs/tool/live_check.dart create mode 100644 apps/patient/android/app/src/debug/res/xml/network_security_config.xml create mode 100644 apps/patient/integration_test/backend_connection_test.dart create mode 100644 apps/patient/lib/core/network/auth_session.dart create mode 100644 apps/patient/lib/core/network/backend_client.dart create mode 100644 apps/patient/lib/core/network/backend_config.dart create mode 100644 apps/patient/lib/core/network/backend_scope.dart create mode 100644 apps/patient/lib/core/network/idempotency.dart create mode 100644 apps/patient/lib/core/privacy/location_hash.dart create mode 100644 apps/patient/test/support/fake_patient_backend.dart create mode 100644 apps/patient/tool/live_check.dart create mode 100644 backend/sinalacs_client/lib/src/protocol/api/visit_sync_entry.dart create mode 100644 backend/sinalacs_client/lib/src/protocol/api/visit_sync_result.dart delete mode 100644 backend/sinalacs_server/docker-compose.yaml create mode 100644 backend/sinalacs_server/lib/src/application/visits/visit_sync_service.dart create mode 100644 backend/sinalacs_server/lib/src/endpoints/visits_endpoint.dart create mode 100644 backend/sinalacs_server/lib/src/generated/api/visit_sync_entry.dart create mode 100644 backend/sinalacs_server/lib/src/generated/api/visit_sync_result.dart create mode 100644 backend/sinalacs_server/lib/src/infrastructure/database/orm_visit_store.dart create mode 100644 backend/sinalacs_server/lib/src/models/api/visit_sync_entry.spy.yaml create mode 100644 backend/sinalacs_server/lib/src/models/api/visit_sync_result.spy.yaml create mode 100644 backend/sinalacs_server/test/unit/app_config_test.dart create mode 100644 backend/sinalacs_server/test/unit/visit_sync_service_test.dart create mode 100755 scripts/dev/bootstrap_env.sh create mode 100755 scripts/dev/sync_dev_ca.sh create mode 100755 scripts/qa/e2e.sh diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..a664993 --- /dev/null +++ b/.env.example @@ -0,0 +1,101 @@ +# Configuração do SinalACS — modelo. +# +# NÃO edite este arquivo com valores reais: ele é versionado. Gere o seu `.env` +# com segredos aleatórios próprios da sua máquina: +# +# ./scripts/dev/bootstrap_env.sh +# +# O `.env` é lido automaticamente pelo `docker compose` para substituir as +# `${VARIÁVEIS}` do docker-compose.yml, e é ignorado pelo git (.gitignore). +# +# Todo segredo abaixo é declarado no compose como `${VAR:?...}`: se faltar, o +# `docker compose` falha dizendo qual variável está ausente, em vez de subir a +# stack com uma senha embutida. Era exatamente esse o problema anterior. +# +# ATENÇÃO ao trocar senhas de uma stack que já rodou: +# · POSTGRES_PASSWORD só vale na PRIMEIRA inicialização do volume. Para +# trocar, apague ./pg_data/ antes de subir. +# · As senhas MQTT são regravadas a cada boot pelo mosquitto-init, então +# trocá-las aqui basta. + +# --------------------------------------------------------------------------- +# Postgres da stack local · consumido por: postgres, serverpod, database-seed +# --------------------------------------------------------------------------- +POSTGRES_USER=sinalacs_user +POSTGRES_DB=sinalacs_db +# openssl rand -hex 32 +POSTGRES_PASSWORD= + +# --------------------------------------------------------------------------- +# Postgres do harness de teste do Serverpod +# +# Sobe com `docker compose --profile test up -d postgres-test`, na porta 9090, +# como backend/sinalacs_server/config/test.yaml espera. O bootstrap grava este +# mesmo valor em backend/sinalacs_server/config/passwords.yaml — os dois PRECISAM +# coincidir: se divergirem, o Serverpod falha ao carregar a config e chama +# exit(1), que não dá flush no stdout, e a suíte inteira morre sem uma linha de +# log sequer. +# --------------------------------------------------------------------------- +# openssl rand -hex 32 +TEST_DATABASE_PASSWORD= + +# --------------------------------------------------------------------------- +# Broker MQTT · consumido por: mosquitto-init (cria os usuários), serverpod +# +# Dois usuários, com ACLs distintas em infra/docker/mosquitto/aclfile: +# backend → readwrite em sinalacs/v1/# +# acs-area-12 → read no tópico da microárea, write nos ACKs +# --------------------------------------------------------------------------- +# openssl rand -hex 32 +MQTT_BACKEND_PASSWORD= +# openssl rand -hex 32 +MQTT_ACS_PASSWORD= + +# SANs adicionais no certificado do broker. Necessário para validar em aparelho +# físico na LAN, já que o certificado padrão só cobre mosquitto, localhost, +# 127.0.0.1 e 10.0.2.2 (o host visto do emulador Android). +# MQTT_CERT_SAN_EXTRA=IP:192.168.0.10 +MQTT_CERT_SAN_EXTRA= + +# --------------------------------------------------------------------------- +# Segredos da aplicação · consumido por: serverpod +# --------------------------------------------------------------------------- +# Chave HMAC que assina os tokens de auth.developmentLogin. O token carrega o +# papel e a microárea, então quem conhece este valor pode forjar um acesso de +# ACS para qualquer território. Nunca reaproveite entre ambientes. +# openssl rand -hex 32 +JWT_SECRET= + +# --------------------------------------------------------------------------- +# Comportamento · consumido por: serverpod +# --------------------------------------------------------------------------- +# development | staging | production. Fora de development, o servidor recusa +# subir com JWT_SECRET ausente, vazio ou igual ao fallback de desenvolvimento. +APP_ENV=development + +# Gate de auth.developmentLogin. Com false, a chamada falha como se a rota não +# existisse. É o único mecanismo de autenticação do protótipo — não é +# autenticação institucional. +ENABLE_DEV_LOGIN=true + +# --------------------------------------------------------------------------- +# Apps Flutter · NÃO são lidos do .env +# +# Os apps resolvem estes valores em tempo de COMPILAÇÃO, via --dart-define, com +# defaults em apps/*/lib/core/network/backend_config.dart. Ficam aqui como +# documentação, já que nenhum outro documento os cobria. +# +# flutter run \ +# --dart-define=SINALACS_HOST=http://10.0.2.2:8080/ \ +# --dart-define=SINALACS_MQTT_HOST=10.0.2.2 \ +# --dart-define=SINALACS_MQTT_USER=acs-area-12 \ +# --dart-define=SINALACS_MQTT_PASSWORD=... +# +# O default de 10.0.2.2 é o host da máquina visto de dentro do emulador Android. +# +# AVISO: SINALACS_MQTT_PASSWORD vira uma CONSTANTE NO BINÁRIO — String.fromEnvironment +# é resolvido na compilação, e o valor é extraível de qualquer APK. O default de +# desenvolvimento é uma escolha consciente para manter `flutter run` sem +# argumentos; em produção a credencial do broker não pode viajar no app. A +# correção é credencial por dispositivo / mTLS, registrada como lacuna conhecida. +# --------------------------------------------------------------------------- diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9a62fc1..3ea74e3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,21 @@ jobs: defaults: run: working-directory: backend + env: + # Senha do Postgres efêmero deste job. NÃO é um segredo: o banco existe só + # dentro do runner, some ao fim do job e nunca é alcançável de fora. + # + # O que importa é que este valor não seja compartilhado com mais nada. + # Antes era a MESMA senha do config/passwords.yaml local — que o + # .gitignore protege e que é idêntica nos blocos development, test, + # staging e production. O segredo protegido pelo .gitignore estava, na + # prática, publicado aqui. + # + # Declarado uma vez e referenciado nos dois lugares: o serviço abaixo e a + # geração do passwords.yaml. Eram dois literais mantidos iguais à mão, e + # divergir fazia a suíte morrer sem log nenhum. + CI_POSTGRES_PASSWORD: ci-ephemeral-not-a-secret + services: # Banco que o harness de teste do Serverpod espera, conforme # sinalacs_server/config/test.yaml. O harness aplica as migrações sozinho @@ -20,7 +35,7 @@ jobs: image: postgres:15-alpine env: POSTGRES_USER: postgres - POSTGRES_PASSWORD: ZGib82sxRnAaTto3iglbPkARA1qZUv0W + POSTGRES_PASSWORD: ${{ env.CI_POSTGRES_PASSWORD }} POSTGRES_DB: sinalacs_test ports: - 9090:5432 @@ -41,11 +56,12 @@ jobs: # Serverpod não carrega a senha do banco, falha ainda no carregamento de # config e chama exit(1); como o exit() do Dart não dá flush no stdout, a # mensagem de erro se perde e a falha fica totalmente silenciosa (exit 1, - # zero linhas de log). A senha tem que ser a mesma do serviço postgres - # acima: se as duas divergirem, o harness volta a falhar sem log nenhum. + # zero linhas de log). + # + # Localmente, quem gera este arquivo é scripts/dev/bootstrap_env.sh. - name: Cria config/passwords.yaml para o harness de teste run: | - printf "test:\n database: '%s'\n" 'ZGib82sxRnAaTto3iglbPkARA1qZUv0W' \ + printf "test:\n database: '%s'\n" "$CI_POSTGRES_PASSWORD" \ > sinalacs_server/config/passwords.yaml # Suíte completa: 16 testes herméticos em test/unit, que usam fakes das # interfaces AlertPublisher e AlertStore, mais 9 de integração que sobem o diff --git a/.gitignore b/.gitignore index c821156..80db99f 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,9 @@ coverage/ # Environment .env .env.* +# ...menos o modelo, que é documentação e precisa ser versionado. Sem esta +# exceção o padrão .env.* acima o engoliria. +!.env.example # Docker / local runtime pg_data/ @@ -25,3 +28,7 @@ infra/docker/mosquitto/runtime/ # Grafo de conhecimento (saída regenerável do graphify) graphify-out/ + +# Asset de desenvolvimento: CA do broker MQTT, copiada por +# scripts/dev/sync_dev_ca.sh a partir do runtime regerável do Mosquitto. +apps/acs/assets/certs/ diff --git a/CLAUDE.md b/CLAUDE.md index 79227ef..7e15017 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -28,6 +28,31 @@ Read these before making product/architecture decisions — when project docs co ## Commands +### Configuration — run this first + +```bash +./scripts/dev/bootstrap_env.sh # cria .env e config/passwords.yaml +``` + +There is no committed `.env`, and the stack will not start without one: every +secret in `docker-compose.yml` is declared as `${VAR:?...}`, so a missing value +fails the Compose interpolation by name instead of falling back to a password +baked into the versioned file. [.env.example](.env.example) is the full +reference for every variable. + +The script generates random per-machine values for `POSTGRES_PASSWORD`, +`TEST_DATABASE_PASSWORD`, `MQTT_BACKEND_PASSWORD`, `MQTT_ACS_PASSWORD` and +`JWT_SECRET`, writes `backend/sinalacs_server/config/passwords.yaml` with the +same test-database password, and `chmod 600` on both. It never overwrites +existing files without `--force`, and warns when `pg_data/` predates a rotation +(`POSTGRES_PASSWORD` only takes effect on the volume's first init — to adopt a +new one, `docker compose down && rm -rf pg_data/`). + +Rotating the MQTT passwords is enough on its own: `infra/docker/mosquitto/init.sh` +rewrites the broker's `passwordfile` on every boot. It used to only create it +when absent, so a changed password silently did nothing and the broker kept +rejecting the new one. + ### Local stack (Postgres + Mosquitto + backend + Traefik) ```bash docker compose up --build @@ -42,13 +67,23 @@ cd backend dart pub get dart analyze cd sinalacs_server -dart test # 25 tests: 16 unit + 9 integration +dart test # 48 tests: 37 unit + 11 integration dart test test/unit # hermetic only, no database needed dart test test/unit/red_alert_service_test.dart # single test file ``` Integration tests use Serverpod's `withServerpod` harness, which needs the test database from `sinalacs_server/config/test.yaml` — Postgres on `localhost:9090`, database `sinalacs_test`, user `postgres`, password from the `test:` block of `config/passwords.yaml` (gitignored). The harness applies migrations itself and rolls the database back after each case, so there is **no** manual schema or seed step. -Because `config/passwords.yaml` is gitignored, it is missing from any fresh checkout — including CI, where the workflow generates it before running the suite. Without it, Serverpod fails while loading config and calls `exit(1)`; since Dart's `exit()` does not flush stdout, the error message is lost and the whole suite dies with exit code 1 and **zero** output. If you ever see that signature, check this file first. +Start that database with the `test` profile of the root compose: + +```bash +docker compose --profile test up -d postgres-test +``` + +It replaced `backend/sinalacs_server/docker-compose.yaml`, the Serverpod template +compose that carried four passwords in cleartext in git and was referenced by +nothing. + +Because `config/passwords.yaml` is gitignored, it is missing from any fresh checkout. Locally `scripts/dev/bootstrap_env.sh` generates it; CI generates it in the job. Without it, Serverpod fails while loading config and calls `exit(1)`; since Dart's `exit()` does not flush stdout, the error message is lost and the whole suite dies with exit code 1 and **zero** output. If you ever see that signature, check this file first. The password in it **must** match `TEST_DATABASE_PASSWORD` in `.env` — the bootstrap script keeps them aligned and warns when they drift. After changing any `.spy.yaml` model or adding an endpoint, regenerate and create a migration (from `backend/sinalacs_server`): ```bash @@ -59,7 +94,9 @@ serverpod create-migration Run the server directly: `dart run backend/sinalacs_server/bin/main.dart`. Serverpod reads `config/*.yaml` plus `config/passwords.yaml`, and every setting can be overridden by environment variable: `SERVERPOD_DATABASE_HOST`/`_PORT`/`_NAME`/`_USER`/`_PASSWORD`/`_REQUIRE_SSL`, `SERVERPOD_APPLY_MIGRATIONS` (applies migrations at boot — this is what replaced the manual `psql` steps), `SERVERPOD_REDIS_ENABLED` (Redis is optional and off), and `SERVERPOD_INSIGHTS_SERVER_PORT` (remapped to 8083 in `docker-compose.yml`, because Serverpod's default 8081 is the Traefik dashboard here). -MQTT is not part of Serverpod and keeps its own env vars, read by `AppConfig.fromEnvironment()` in `backend/sinalacs_server/lib/src/config/app_config.dart`: `MQTT_BROKER`/`MQTT_USERNAME`/`MQTT_PASSWORD`/`MQTT_USE_TLS`/`MQTT_CA_CERT_PATH`, plus `JWT_SECRET`, `APP_ENV` (when `production`, boot fails fast if `JWT_SECRET` is missing) and `ENABLE_DEV_LOGIN` (default `false` — gates `auth.developmentLogin`, which fails as if the route did not exist when off). +MQTT is not part of Serverpod and keeps its own env vars, read by `AppConfig.fromEnvironment()` in `backend/sinalacs_server/lib/src/config/app_config.dart`: `MQTT_BROKER`/`MQTT_USERNAME`/`MQTT_PASSWORD`/`MQTT_USE_TLS`/`MQTT_CA_CERT_PATH`, plus `JWT_SECRET`, `APP_ENV` and `ENABLE_DEV_LOGIN` (default `false` — gates `auth.developmentLogin`, which fails as if the route did not exist when off). + +Outside `development`, boot fails fast when `JWT_SECRET` is absent, empty, blank, or equal to `AppConfig.developmentJwtSecret` — that fallback lives in versioned code, so it is public, and the token it signs carries the role and micro-area. The rules are in `_resolveJwtSecret` and covered by `test/unit/app_config_test.dart`; `AppConfig.fromMap()` exists so they are testable without touching `Platform.environment`. The dev seed (`sinalacs_server/lib/src/infrastructure/database/seeds/development.sql`) is not optional for a running stack: `auth.developmentLogin` issues tokens for fixed UUIDs, and `alerts.patientId` has a foreign key to `patients` — without the seed, `alerts.createRedAlert` fails with a foreign-key violation. In `docker-compose.yml` the `database-seed` service applies it after the server is healthy. @@ -75,6 +112,22 @@ flutter build apk --debug # debug APK, validated with compileSdk/targetSdk 36 ``` `apps/admin` exists only as a pubspec skeleton (backoffice), no implementation yet. +### Validating the real connection to the backend + +`flutter test` stays hermetic (it only runs `test/`, where the backend is a fake). Anything that needs the live stack lives outside it: + +```bash +./scripts/qa/e2e.sh # sobe a stack, valida na VM, derruba +./scripts/qa/e2e.sh --keep # mantém a stack de pé +./scripts/qa/e2e.sh --emulator # inclui integration_test em um emulador já aberto +``` + +The script brings up Docker Compose, waits for the healthcheck, applies the seed, runs `scripts/dev/sync_dev_ca.sh` (copies the broker CA into `apps/acs/assets/certs/`, which is gitignored and regenerated), and then runs each app's `tool/live_check.dart`. Those scripts run on the plain Dart VM — no emulator — using the apps' own network code: the patient one covers health/login/triage/alert/idempotency, and the ACS one covers the full cycle including the MQTT/TLS subscription and `visits.sync`. + +`integration_test/` in each app holds the on-device version, excluded from `flutter test` by construction. Run it against an already-running emulator with `--dart-define=SINALACS_HOST=http://10.0.2.2:8080/` (and `SINALACS_MQTT_HOST=10.0.2.2` for the ACS). CI's four jobs are unchanged and never need a backend. + +Debug builds of both apps carry `android/app/src/debug/res/xml/network_security_config.xml`, which permits cleartext only to `10.0.2.2` and loopback; release manifests are untouched. + CI (`.github/workflows/ci.yml`) runs four parallel jobs on push/PR to main: `serverpod-backend` (spins up the Postgres the test harness expects on port 9090, then `dart analyze` and the full 25-test suite), `backend-docker-build` (builds `backend/sinalacs_server/Dockerfile` to catch build breakage before deploy), `patient-app`, `acs-app` (each `flutter analyze && flutter test`, Flutter 3.44.8). Mirror this locally before pushing. ## Architecture @@ -93,14 +146,18 @@ Never hand-edit anything under `lib/src/generated/` or `migrations/` — run `se Key pattern: application services depend on abstract interfaces (`AlertPublisher`, `AlertStore`) defined alongside them in `application/`, implemented by `infrastructure/`. Follow this when adding new use cases — keep `application/` testable without real Postgres/MQTT (see how `test/unit/red_alert_service_test.dart` fakes both). -**Serverpod is RPC, not REST**, so there are no URL routes to match: the generated client calls methods. Endpoints: `health.check` (returns `{status, mqttConnected, dbConnected}`; answers as soon as the server is up, independent of MQTT/DB state), `auth.developmentLogin` (throws `EndpointDisabledException` unless `ENABLE_DEV_LOGIN=true`, preserving the old 404-not-403 semantics), `alerts.createRedAlert` (idempotency key is a method parameter, not a header; throws `AlertDispatchUnavailableException` if the MQTT dispatcher isn't connected), `alerts.acknowledge`, and `triage.evaluate`. Errors are typed exceptions declared in `.spy.yaml` and serialized to the client, replacing HTTP status codes. MQTT connects in the background after boot (non-blocking) with exponential-backoff auto-reconnect, so the server stays responsive even if the broker is unreachable — this matters on free-tier hosts that sleep/hibernate. +**Serverpod is RPC, not REST**, so there are no URL routes to match: the generated client calls methods. Endpoints: `health.check` (returns `{status, mqttConnected, dbConnected}`; answers as soon as the server is up, independent of MQTT/DB state), `auth.developmentLogin` (throws `EndpointDisabledException` unless `ENABLE_DEV_LOGIN=true`, preserving the old 404-not-403 semantics), `alerts.createRedAlert` (idempotency key is a method parameter, not a header; throws `AlertDispatchUnavailableException` if the MQTT dispatcher isn't connected), `alerts.acknowledge`, `triage.evaluate`, and `visits.sync` (batch upload of visits registered offline by the ACS; deduplicated by the device-generated `localId`, which has a unique index on `visits`, and version-checked — a mismatched `version` returns `SyncStatus.conflict` and never overwrites). Errors are typed exceptions declared in `.spy.yaml` and serialized to the client, replacing HTTP status codes. MQTT connects in the background after boot (non-blocking) with exponential-backoff auto-reconnect, so the server stays responsive even if the broker is unreachable — this matters on free-tier hosts that sleep/hibernate. ### Flutter apps (`apps/acs/`, `apps/patient/`) Both follow the same skeleton: `lib/main.dart` → `lib/app/app.dart` (+ `*_theme.dart` for the dark, high-legibility, low-noise visual language — see `spec/ui_design.md`) → `lib/core/`. Shared `core/` concerns: - `database/encrypted_database.dart` — local persistence via `sqflite_sqlcipher`, with an FFI fallback for test/VM environments where SQLCipher isn't available. -- ACS-only, in `apps/acs/lib/core/services/`: `mqtt_secure_client.dart` (TLS/WSS MQTT client, per-micro-area topics, alert + ACK payload parsing with malformed-message rejection), `offline_visit_queue.dart` (offline-first visit queue: pending → sync batch → retry/conflict detection, drives the same state shape as the backend's `SyncFsm`), `network_chaos_simulator.dart` (injects latency/jitter/packet loss/partition for testing offline resilience — see `apps/acs/test/network_chaos_test.dart`). +- ACS-only, in `apps/acs/lib/core/services/`: `mqtt_secure_client.dart` (TCP/TLS MQTT client on 8883 — the broker's only published listener; trusts the dev CA from an asset via `setTrustedCertificatesBytes`, keeps hostname verification on, uses a persistent session with a stable client id so the broker re-delivers alerts that arrived while the device was offline), `alert_feed.dart` (builds the MQTT config from the session and feeds `alert_queue.dart`), `offline_visit_queue.dart` (offline-first visit queue backed by a `VisitStore`, pushed to `visits.sync` through `backend_visit_synchronizer.dart`; a conflict returns to the queue instead of being discarded, and a network failure keeps the batch pending), `network_chaos_simulator.dart` (injects latency/jitter/packet loss/partition for testing offline resilience — see `apps/acs/test/network_chaos_test.dart`). + +Topic namespace must agree in three places: the server's `AlertDelivery.topicPrefix`, the broker ACL (`infra/docker/mosquitto/aclfile`), and `alertTopicFor()` in `mqtt_secure_client.dart` — all `sinalacs/v1/microareas//alerts`, where `` is the seed UUID, not `area-12`. + +Both apps consume the generated `sinalacs_client` by path dependency and talk to the real backend. The network layer lives in `lib/core/network/` in each app: `backend_config.dart` (host via `--dart-define`, defaulting to `http://10.0.2.2:8080/`, the machine as seen from the Android emulator), `backend_client.dart` (a `PatientBackend`/`AcsBackend` interface plus the real `BackendClient`, which translates the typed backend exceptions into `BackendFailure` messages in Portuguese), `auth_session.dart` (reads the dev token's payload — **without** verifying the signature, which is the server's job — only to learn the micro-area and the 15-minute expiry) and `backend_scope.dart`. The UI depends on the interface, never on the generated `Client`, which is what keeps the widget tests hermetic; the live path is checked by `tool/live_check.dart` in each app and by `integration_test/`. -Neither app is yet wired to the real backend HTTP/MQTT endpoints end-to-end (per `PROGRESS.md`) — triage/risk logic in the patient app and prioritization in the ACS app currently run client-side against local/mock data, mirroring the backend's `triage_engine.dart` logic but not yet calling it over the network. +Risk classification now comes **only** from `triage.evaluate`: the patient app's client-side string-matching rule is gone, and its triage form asks the six symptoms the server's engine actually takes. The ACS dashboard is fed by `AlertQueue`, which receives alerts over MQTT and orders them deterministically by risk and then by age; it rejects alerts from another micro-area and de-duplicates re-deliveries (QoS 1 is at-least-once). Color is a clinical signal only in these apps: red/yellow/green map strictly to `RiskLevel`, never used decoratively. diff --git a/README.md b/README.md index 0b228c6..4fbf9bb 100644 --- a/README.md +++ b/README.md @@ -157,16 +157,40 @@ requests: `serverpod-backend` (sobe o Postgres de teste e roda `dart analyze` mais a suíte completa), `backend-docker-build` (valida que a imagem builda), `patient-app` e `acs-app`. +### Configuração + +**Antes do primeiro `docker compose up`, gere a configuração local:** + +```bash +./scripts/dev/bootstrap_env.sh +``` + +O script cria `.env` com segredos aleatórios desta máquina (senha do Postgres, +as duas do broker MQTT e o `JWT_SECRET`) e gera +`backend/sinalacs_server/config/passwords.yaml`, que é gitignored e por isso não +existe num clone limpo — sem ele a suíte de testes do Serverpod morre sem +imprimir nada. Nenhum dos dois entra no git. + +[.env.example](.env.example) é a referência completa de todas as variáveis, com +um comentário por bloco dizendo quem consome cada uma. O `docker-compose.yml` +declara cada segredo como `${VAR:?...}`: se faltar, o Compose falha dizendo qual +variável está ausente, em vez de subir com uma senha embutida no arquivo +versionado. + Configuração de servidor e banco vem dos arquivos `sinalacs_server/config/*.yaml` e pode ser sobrescrita por variáveis de ambiente: `SERVERPOD_DATABASE_HOST` e companhia, `SERVERPOD_APPLY_MIGRATIONS` (aplica as migrações no boot), `SERVERPOD_REDIS_ENABLED` (Redis é opcional e fica desligado) e `SERVERPOD_INSIGHTS_SERVER_PORT`. O MQTT não faz parte do Serverpod e mantém as próprias variáveis, lidas por `sinalacs_server/lib/src/config/app_config.dart`: -`MQTT_BROKER`/`MQTT_USERNAME`/`MQTT_PASSWORD`/`MQTT_USE_TLS`, mais `JWT_SECRET`, -`APP_ENV` (`production` exige `JWT_SECRET`, falhando rápido no boot) e -`ENABLE_DEV_LOGIN` (por padrão desligado — sem ele, `auth.developmentLogin` -falha como se o endpoint não existisse). Veja +`MQTT_BROKER`/`MQTT_USERNAME`/`MQTT_PASSWORD`/`MQTT_USE_TLS`/`MQTT_CA_CERT_PATH`, +mais `JWT_SECRET`, `APP_ENV` e `ENABLE_DEV_LOGIN` (por padrão desligado — sem +ele, `auth.developmentLogin` falha como se o endpoint não existisse). + +Fora de `development`, o servidor **recusa subir** se `JWT_SECRET` estiver +ausente, vazio ou igual ao valor de desenvolvimento (que é público, por estar no +código versionado). O token carrega o papel e a microárea, então assinar com uma +chave conhecida permitiria forjar um acesso de ACS a qualquer território. Veja [backend/DEPLOY.md](backend/DEPLOY.md) para o runbook completo do piloto de deploy free-tier. diff --git a/apps/acs/.flutter-plugins-dependencies b/apps/acs/.flutter-plugins-dependencies index c2a4cc1..d6b24bf 100644 --- a/apps/acs/.flutter-plugins-dependencies +++ b/apps/acs/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"connectivity_plus","path":"/home/codespace/.pub-cache/hosted/pub.dev/connectivity_plus-7.3.1/","native_build":true,"dependencies":[]},{"name":"geolocator_apple","path":"/home/codespace/.pub-cache/hosted/pub.dev/geolocator_apple-2.3.14/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"google_maps_flutter_ios","path":"/home/codespace/.pub-cache/hosted/pub.dev/google_maps_flutter_ios-2.15.2/","native_build":true,"dependencies":[]},{"name":"sqflite_darwin","path":"/home/codespace/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"sqflite_sqlcipher","path":"/home/codespace/.pub-cache/hosted/pub.dev/sqflite_sqlcipher-3.2.0/","native_build":true,"dependencies":[]}],"android":[{"name":"connectivity_plus","path":"/home/codespace/.pub-cache/hosted/pub.dev/connectivity_plus-7.3.1/","native_build":true,"dependencies":[]},{"name":"flutter_plugin_android_lifecycle","path":"/home/codespace/.pub-cache/hosted/pub.dev/flutter_plugin_android_lifecycle-2.0.26/","native_build":true,"dependencies":[]},{"name":"geolocator_android","path":"/home/codespace/.pub-cache/hosted/pub.dev/geolocator_android-4.6.2/","native_build":true,"dependencies":[]},{"name":"google_maps_flutter_android","path":"/home/codespace/.pub-cache/hosted/pub.dev/google_maps_flutter_android-2.14.13/","native_build":true,"dependencies":["flutter_plugin_android_lifecycle"]},{"name":"sqflite_android","path":"/home/codespace/.pub-cache/hosted/pub.dev/sqflite_android-2.4.0/","native_build":true,"dependencies":[]},{"name":"sqflite_sqlcipher","path":"/home/codespace/.pub-cache/hosted/pub.dev/sqflite_sqlcipher-3.2.0/","native_build":true,"dependencies":[]}],"macos":[{"name":"connectivity_plus","path":"/home/codespace/.pub-cache/hosted/pub.dev/connectivity_plus-7.3.1/","native_build":true,"dependencies":[]},{"name":"geolocator_apple","path":"/home/codespace/.pub-cache/hosted/pub.dev/geolocator_apple-2.3.14/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"sqflite_darwin","path":"/home/codespace/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"sqflite_sqlcipher","path":"/home/codespace/.pub-cache/hosted/pub.dev/sqflite_sqlcipher-3.2.0/","native_build":true,"dependencies":[]}],"linux":[{"name":"connectivity_plus","path":"/home/codespace/.pub-cache/hosted/pub.dev/connectivity_plus-7.3.1/","native_build":false,"dependencies":[]}],"windows":[{"name":"connectivity_plus","path":"/home/codespace/.pub-cache/hosted/pub.dev/connectivity_plus-7.3.1/","native_build":true,"dependencies":[]},{"name":"geolocator_windows","path":"/home/codespace/.pub-cache/hosted/pub.dev/geolocator_windows-0.2.5/","native_build":true,"dependencies":[]}],"web":[{"name":"connectivity_plus","path":"/home/codespace/.pub-cache/hosted/pub.dev/connectivity_plus-7.3.1/","dependencies":[]},{"name":"geolocator_web","path":"/home/codespace/.pub-cache/hosted/pub.dev/geolocator_web-4.1.4/","dependencies":[]},{"name":"google_maps_flutter_web","path":"/home/codespace/.pub-cache/hosted/pub.dev/google_maps_flutter_web-0.5.12/","dependencies":[]}]},"dependencyGraph":[{"name":"connectivity_plus","dependencies":[]},{"name":"flutter_plugin_android_lifecycle","dependencies":[]},{"name":"geolocator","dependencies":["geolocator_android","geolocator_apple","geolocator_web","geolocator_windows"]},{"name":"geolocator_android","dependencies":[]},{"name":"geolocator_apple","dependencies":[]},{"name":"geolocator_web","dependencies":[]},{"name":"geolocator_windows","dependencies":[]},{"name":"google_maps_flutter","dependencies":["google_maps_flutter_android","google_maps_flutter_ios","google_maps_flutter_web"]},{"name":"google_maps_flutter_android","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"google_maps_flutter_ios","dependencies":[]},{"name":"google_maps_flutter_web","dependencies":[]},{"name":"sqflite","dependencies":["sqflite_android","sqflite_darwin"]},{"name":"sqflite_android","dependencies":[]},{"name":"sqflite_darwin","dependencies":[]},{"name":"sqflite_sqlcipher","dependencies":[]}],"date_created":"2026-09-01 23:40:01.422052","version":"3.24.5","swift_package_manager_enabled":false} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"connectivity_plus","path":"/home/rock/.pub-cache/hosted/pub.dev/connectivity_plus-7.3.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"geolocator_apple","path":"/home/rock/.pub-cache/hosted/pub.dev/geolocator_apple-2.3.14/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"google_maps_flutter_ios","path":"/home/rock/.pub-cache/hosted/pub.dev/google_maps_flutter_ios-2.15.2/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"integration_test","path":"/home/rock/flutter/packages/integration_test/","native_build":true,"dependencies":[],"dev_dependency":true},{"name":"sqflite_darwin","path":"/home/rock/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqflite_sqlcipher","path":"/home/rock/.pub-cache/hosted/pub.dev/sqflite_sqlcipher-3.2.0/","native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"connectivity_plus","path":"/home/rock/.pub-cache/hosted/pub.dev/connectivity_plus-7.3.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_plugin_android_lifecycle","path":"/home/rock/.pub-cache/hosted/pub.dev/flutter_plugin_android_lifecycle-2.0.26/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"geolocator_android","path":"/home/rock/.pub-cache/hosted/pub.dev/geolocator_android-4.6.2/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"google_maps_flutter_android","path":"/home/rock/.pub-cache/hosted/pub.dev/google_maps_flutter_android-2.14.13/","native_build":true,"dependencies":["flutter_plugin_android_lifecycle"],"dev_dependency":false},{"name":"integration_test","path":"/home/rock/flutter/packages/integration_test/","native_build":true,"dependencies":[],"dev_dependency":true},{"name":"sqflite_android","path":"/home/rock/.pub-cache/hosted/pub.dev/sqflite_android-2.4.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqflite_sqlcipher","path":"/home/rock/.pub-cache/hosted/pub.dev/sqflite_sqlcipher-3.2.0/","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"connectivity_plus","path":"/home/rock/.pub-cache/hosted/pub.dev/connectivity_plus-7.3.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"geolocator_apple","path":"/home/rock/.pub-cache/hosted/pub.dev/geolocator_apple-2.3.14/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqflite_darwin","path":"/home/rock/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqflite_sqlcipher","path":"/home/rock/.pub-cache/hosted/pub.dev/sqflite_sqlcipher-3.2.0/","native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"connectivity_plus","path":"/home/rock/.pub-cache/hosted/pub.dev/connectivity_plus-7.3.1/","native_build":false,"dependencies":[],"dev_dependency":false}],"windows":[{"name":"connectivity_plus","path":"/home/rock/.pub-cache/hosted/pub.dev/connectivity_plus-7.3.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"geolocator_windows","path":"/home/rock/.pub-cache/hosted/pub.dev/geolocator_windows-0.2.5/","native_build":true,"dependencies":[],"dev_dependency":false}],"web":[{"name":"connectivity_plus","path":"/home/rock/.pub-cache/hosted/pub.dev/connectivity_plus-7.3.1/","dependencies":[],"dev_dependency":false},{"name":"geolocator_web","path":"/home/rock/.pub-cache/hosted/pub.dev/geolocator_web-4.1.4/","dependencies":[],"dev_dependency":false},{"name":"google_maps_flutter_web","path":"/home/rock/.pub-cache/hosted/pub.dev/google_maps_flutter_web-0.5.12/","dependencies":[],"dev_dependency":false}]},"dependencyGraph":[{"name":"connectivity_plus","dependencies":[]},{"name":"flutter_plugin_android_lifecycle","dependencies":[]},{"name":"geolocator","dependencies":["geolocator_android","geolocator_apple","geolocator_web","geolocator_windows"]},{"name":"geolocator_android","dependencies":[]},{"name":"geolocator_apple","dependencies":[]},{"name":"geolocator_web","dependencies":[]},{"name":"geolocator_windows","dependencies":[]},{"name":"google_maps_flutter","dependencies":["google_maps_flutter_android","google_maps_flutter_ios","google_maps_flutter_web"]},{"name":"google_maps_flutter_android","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"google_maps_flutter_ios","dependencies":[]},{"name":"google_maps_flutter_web","dependencies":[]},{"name":"integration_test","dependencies":[]},{"name":"sqflite","dependencies":["sqflite_android","sqflite_darwin"]},{"name":"sqflite_android","dependencies":[]},{"name":"sqflite_darwin","dependencies":[]},{"name":"sqflite_sqlcipher","dependencies":[]}],"date_created":"2026-09-11 17:28:46.660463","version":"3.44.8","swift_package_manager_enabled":{"ios":false,"macos":false}} \ No newline at end of file diff --git a/apps/acs/android/app/src/debug/AndroidManifest.xml b/apps/acs/android/app/src/debug/AndroidManifest.xml index 399f698..cfc90aa 100644 --- a/apps/acs/android/app/src/debug/AndroidManifest.xml +++ b/apps/acs/android/app/src/debug/AndroidManifest.xml @@ -4,4 +4,9 @@ to allow setting breakpoints, to provide hot reload, etc. --> + + + diff --git a/apps/acs/android/app/src/debug/res/xml/network_security_config.xml b/apps/acs/android/app/src/debug/res/xml/network_security_config.xml new file mode 100644 index 0000000..0e78459 --- /dev/null +++ b/apps/acs/android/app/src/debug/res/xml/network_security_config.xml @@ -0,0 +1,19 @@ + + + + + + 10.0.2.2 + + localhost + 127.0.0.1 + + diff --git a/apps/acs/integration_test/red_alert_cycle_test.dart b/apps/acs/integration_test/red_alert_cycle_test.dart new file mode 100644 index 0000000..fdf4ce9 --- /dev/null +++ b/apps/acs/integration_test/red_alert_cycle_test.dart @@ -0,0 +1,193 @@ +/// Ciclo do alerta vermelho ponta a ponta, com a stack local de pé. +/// +/// Prova o caminho que nenhum teste hermético alcança: o backend publica no +/// broker e o app do ACS recebe pelo MQTT com TLS. +/// +/// Pré-requisitos: +/// · `docker compose up` com o database-seed concluído; +/// · `./scripts/dev/sync_dev_ca.sh` — a CA do broker é asset do app e é +/// regerada, não versionada. +/// +/// flutter test integration_test \ +/// --dart-define=SINALACS_HOST=http://10.0.2.2:8080/ \ +/// --dart-define=SINALACS_MQTT_HOST=10.0.2.2 +/// +/// PRIVACIDADE: só os UUIDs sintéticos do seed. +library; + +import 'dart:async'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:sinalacs_acs/core/network/backend_client.dart'; +import 'package:sinalacs_acs/core/services/alert_feed.dart'; +import 'package:sinalacs_acs/core/services/alert_queue.dart'; +import 'package:sinalacs_acs/core/services/backend_visit_synchronizer.dart'; +import 'package:sinalacs_acs/core/services/offline_visit_queue.dart'; +import 'package:sinalacs_client/sinalacs_client.dart' as api; + +const seedMicroAreaId = '00000000-0000-4000-8000-000000000003'; +const seedPatientId = '00000000-0000-4000-8000-000000000001'; +const otherMicroAreaId = '00000000-0000-4000-8000-000000000099'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + late BackendClient backend; + late api.Client patientClient; + + setUp(() { + backend = BackendClient(); + // O paciente entra em cena só para dar ao ACS o que receber. + patientClient = api.Client( + const String.fromEnvironment('SINALACS_HOST', defaultValue: 'http://10.0.2.2:8080/'), + )..connectivityMonitor = null; + }); + + tearDown(() { + backend.close(); + patientClient.close(); + }); + + Future createRedAlertAsPatient() async { + final login = await patientClient.auth.developmentLogin(role: 'patient'); + final created = await patientClient.alerts.createRedAlert( + accessToken: login.accessToken, + idempotencyKey: 'integ-${DateTime.now().microsecondsSinceEpoch}', + locationHash: 'sem-local-00', + ); + return created.alertId; + } + + test('o ACS autentica e recebe a própria microárea', () async { + final session = await backend.login(); + + expect(session.role, 'acs'); + expect(session.microAreaId, seedMicroAreaId); + }); + + test('o alerta publicado pelo backend chega ao ACS pelo broker e é confirmado', () async { + final session = await backend.login(); + final microAreaId = session.microAreaId!; + + final queue = AlertQueue(microAreaId: microAreaId); + final feed = MqttAlertFeed(queue: queue); + + /// Espera o alerta ESPECÍFICO criado pelo teste. + /// + /// Não serve olhar `alerts.first`: com sessão persistente o broker + /// reentrega o que ficou pendente de execuções anteriores, e a fila ordena + /// por antiguidade — o primeiro seria justamente o alerta antigo do + /// backlog. + Future waitFor(String alertId, {Duration timeout = const Duration(seconds: 20)}) async { + final deadline = DateTime.now().add(timeout); + while (DateTime.now().isBefore(deadline)) { + for (final alert in queue.alerts) { + if (alert.alertId == alertId) return alert; + } + await Future.delayed(const Duration(milliseconds: 200)); + } + throw TimeoutException('o alerta $alertId não chegou pelo broker'); + } + + // Conecta com TLS usando a CA do asset. Falhar aqui aponta para o + // certificado sem SAN ou para a ACL do broker. + await feed.start(microAreaId: microAreaId, acsId: session.userId); + expect(feed.isConnected, isTrue, reason: 'o ACS não conectou ao broker'); + + try { + final alertId = await createRedAlertAsPatient(); + + final alert = await waitFor(alertId); + expect(alert.alertId, alertId); + expect(alert.riskLevel, 'red'); + expect(alert.microAreaId, microAreaId); + + final ack = await backend.acknowledge(alertId: alert.alertId); + expect(ack.acknowledged, isTrue); + expect(ack.status, api.AlertStatus.acknowledged); + } finally { + feed.stop(); + queue.dispose(); + } + }); + + test('a fila recusa alerta de fora da microárea do ACS', () async { + // Territorialização é invariante. A ACL do broker é a primeira barreira; a + // fila é a segunda. + final queue = AlertQueue(microAreaId: seedMicroAreaId); + + final accepted = queue.upsert(PrioritizedAlert( + alertId: 'alerta-de-outra-area', + patientId: seedPatientId, + microAreaId: otherMicroAreaId, + riskLevel: 'red', + locationHash: 'sem-local-00', + triggeredAt: DateTime.now().toUtc(), + )); + + expect(accepted, isFalse); + expect(queue.isEmpty, isTrue); + queue.dispose(); + }); + + test('confirmar um alerta inexistente devolve acknowledged: false, não erro', () async { + await backend.login(); + + final ack = await backend.acknowledge( + alertId: '00000000-0000-4000-8000-0000000000ff', + ); + + expect(ack.acknowledged, isFalse); + }); + + test('a fila offline de visitas sincroniza contra o servidor', () async { + await backend.login(); + + final queue = OfflineVisitQueue( + synchronizer: BackendVisitSynchronizer( + backend: backend, + patientIdFor: (_) => seedPatientId, + ), + ); + + await queue.add(OfflineVisitRecord( + patientName: 'Paciente do seed', + risk: 'red', + status: 'PENDENTE', + outcome: 'realizada', + version: 0, + )); + + final result = await queue.sync(); + + expect(result.kind, SyncOutcomeKind.synced); + expect(queue.pendingCount, 0); + expect(queue.syncedCount, 1); + }); + + test('reenviar a mesma visita não a duplica no servidor', () async { + await backend.login(); + + final visit = OfflineVisitRecord( + patientName: 'Paciente do seed', + risk: 'red', + status: 'PENDENTE', + outcome: 'realizada', + version: 0, + ); + + final synchronizer = BackendVisitSynchronizer( + backend: backend, + patientIdFor: (_) => seedPatientId, + ); + + final first = await synchronizer.push([visit]); + // Mesmo localId, mesma versão: é o retry de quem não viu a resposta. + final retry = await synchronizer.push([visit]); + + expect(first.single.status, 'synced'); + expect(retry.single.status, 'synced'); + expect(retry.single.serverVersion, first.single.serverVersion); + }); +} diff --git a/apps/acs/lib/app/app.dart b/apps/acs/lib/app/app.dart index 5162e8d..3713a5f 100644 --- a/apps/acs/lib/app/app.dart +++ b/apps/acs/lib/app/app.dart @@ -1,29 +1,107 @@ import 'package:flutter/material.dart'; import 'package:sinalacs_acs/app/acs_theme.dart'; +import 'package:sinalacs_acs/core/network/backend_client.dart'; +import 'package:sinalacs_acs/core/network/backend_scope.dart'; +import 'package:sinalacs_acs/core/services/alert_feed.dart'; +import 'package:sinalacs_acs/core/services/alert_queue.dart'; import 'package:sinalacs_acs/core/services/offline_visit_queue.dart'; -class SinalAcsApp extends StatelessWidget { - const SinalAcsApp({super.key}); +class SinalAcsApp extends StatefulWidget { + const SinalAcsApp({super.key, this.backend, this.feedBuilder, this.visitQueue}); + + /// Injetáveis para teste. Em execução normal são as implementações reais. + final AcsBackend? backend; + final AlertFeed Function(AlertQueue queue)? feedBuilder; + final OfflineVisitQueue? visitQueue; + + @override + State createState() => _SinalAcsAppState(); +} + +class _SinalAcsAppState extends State { + late final AcsBackend _backend = widget.backend ?? BackendClient(); + + /// Uma única fila por execução do app. + /// + /// A tela de visita antes fazia `OfflineVisitQueue()` a cada gravação — uma + /// instância nova por visita, descartada no retorno do callback. A visita + /// simplesmente sumia. + late final OfflineVisitQueue _visitQueue = widget.visitQueue ?? OfflineVisitQueue(); + + @override + void dispose() { + if (widget.backend == null) _backend.close(); + super.dispose(); + } + @override - Widget build(BuildContext context) => MaterialApp( - title: 'SinalACS ACS', - debugShowCheckedModeBanner: false, - theme: buildAcsTheme(), - home: const LoginScreen(), + Widget build(BuildContext context) => BackendScope( + backend: _backend, + child: MaterialApp( + title: 'SinalACS ACS', + debugShowCheckedModeBanner: false, + theme: buildAcsTheme(), + home: LoginScreen( + feedBuilder: widget.feedBuilder, + visitQueue: _visitQueue, + ), + ), ); } class LoginScreen extends StatefulWidget { - const LoginScreen({super.key}); + const LoginScreen({super.key, this.feedBuilder, this.visitQueue}); + + final AlertFeed Function(AlertQueue queue)? feedBuilder; + final OfflineVisitQueue? visitQueue; + @override State createState() => _LoginScreenState(); } class _LoginScreenState extends State { - final _matricula = TextEditingController(text: 'ACS-001'); - final _senha = TextEditingController(text: '123456'); + // Sem credencial pré-preenchida: o formulário antes vinha com 'ACS-001' e + // '123456' embutidos, e o botão navegava sem olhar para nenhum dos dois. + final _matricula = TextEditingController(); + final _senha = TextEditingController(); + bool _busy = false; + String? _error; + @override void dispose() { _matricula.dispose(); _senha.dispose(); super.dispose(); } + + /// Autentica contra `auth.developmentLogin` e só então abre o painel. + Future _enter() async { + setState(() { _busy = true; _error = null; }); + + try { + final session = await BackendScope.of(context).login(); + final microAreaId = session.microAreaId; + if (microAreaId == null) { + // Sem microárea não há território, e sem território não há fila: é + // preferível barrar a entrada a abrir um painel que não pode filtrar. + setState(() { + _busy = false; + _error = 'Este acesso não está vinculado a uma microárea.'; + }); + return; + } + if (!mounted) return; + + Navigator.of(context).pushReplacement(MaterialPageRoute( + builder: (_) => AcsHomeShell( + microAreaId: microAreaId, + acsId: session.userId, + feedBuilder: widget.feedBuilder, + visitQueue: widget.visitQueue, + ), + )); + } on BackendFailure catch (failure) { + if (!mounted) return; + setState(() { _busy = false; _error = failure.message; }); + } + } + @override Widget build(BuildContext context) => Scaffold( appBar: const _Header('Segurança e rastreabilidade', 'Acesso institucional'), @@ -41,7 +119,18 @@ class _LoginScreenState extends State { const SizedBox(height: 16), TextField(key: const Key('senha_field'), controller: _senha, obscureText: true, decoration: const InputDecoration(labelText: 'Senha de acesso')), const SizedBox(height: 20), - Semantics(label: 'Entrar no painel de priorização', button: true, container: true, child: SizedBox(width: double.infinity, child: FilledButton(key: const Key('login_button'), style: FilledButton.styleFrom(minimumSize: const Size(48, 52)), onPressed: () => Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (_) => const AcsHomeShell())), child: const Text('Entrar com credenciais')))), + Semantics(label: 'Entrar no painel de priorização', button: true, container: true, child: SizedBox(width: double.infinity, child: FilledButton( + key: const Key('login_button'), + style: FilledButton.styleFrom(minimumSize: const Size(48, 52)), + onPressed: _busy ? null : _enter, + child: _busy + ? const SizedBox(height: 22, width: 22, child: CircularProgressIndicator(strokeWidth: 2)) + : const Text('Entrar com credenciais'), + ))), + if (_error != null) Padding( + padding: const EdgeInsets.only(top: 16), + child: Text(key: const Key('login_error'), _error!, textAlign: TextAlign.center, style: const TextStyle(color: AcsColors.red, fontWeight: FontWeight.bold)), + ), ]))), ]), )), @@ -50,18 +139,101 @@ class _LoginScreenState extends State { enum AcsDestination { area, queue, map, visit, escalation, geofencing, notices } -class AcsHomeShell extends StatefulWidget { const AcsHomeShell({super.key}); @override State createState() => _AcsHomeShellState(); } +class AcsHomeShell extends StatefulWidget { + const AcsHomeShell({ + required this.microAreaId, + required this.acsId, + super.key, + this.feedBuilder, + this.visitQueue, + }); + + final String microAreaId; + final String acsId; + final AlertFeed Function(AlertQueue queue)? feedBuilder; + final OfflineVisitQueue? visitQueue; + + @override + State createState() => _AcsHomeShellState(); +} + class _AcsHomeShellState extends State { AcsDestination destination = AcsDestination.queue; + + late final AlertQueue _queue = AlertQueue(microAreaId: widget.microAreaId); + late final AlertFeed _feed = + (widget.feedBuilder ?? (queue) => MqttAlertFeed(queue: queue))(_queue); + + bool _brokerConnected = false; + String? _feedError; + PrioritizedAlert? _selected; + + @override + void initState() { + super.initState(); + _connectFeed(); + } + + /// Assina o tópico da microárea da sessão. + /// + /// Falhar aqui não derruba o painel, mas **precisa ficar visível**: um ACS que + /// não sabe que parou de receber alertas é o pior modo de falha do produto. + Future _connectFeed() async { + try { + await _feed.start(microAreaId: widget.microAreaId, acsId: widget.acsId); + if (!mounted) return; + setState(() { + _brokerConnected = _feed.isConnected; + _feedError = null; + }); + } catch (error) { + if (!mounted) return; + setState(() { + _brokerConnected = false; + _feedError = 'Sem conexão com a central de alertas.'; + }); + } + } + + Future _acknowledge(PrioritizedAlert alert) async { + try { + final result = await BackendScope.of(context).acknowledge(alertId: alert.alertId); + if (!mounted) return; + if (result.acknowledged) _queue.markAcknowledged(alert.alertId); + _message(context, result.acknowledged + ? 'Recebimento confirmado à central.' + : 'A central não reconheceu este alerta.'); + } on BackendFailure catch (failure) { + if (!mounted) return; + _message(context, failure.message); + } + } + + @override + void dispose() { + _feed.stop(); + _queue.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) => Scaffold( - appBar: const _Header('ACS • offline ready', 'Painel operacional'), + appBar: _Header('ACS • ${_brokerConnected ? 'em linha' : 'sem conexão'}', 'Painel operacional', connected: _brokerConnected), body: SafeArea(child: switch (destination) { AcsDestination.area => const TerritorializationScreen(), - AcsDestination.queue => DashboardScreen(onEscalate: () => setState(() => destination = AcsDestination.escalation), onVisit: () => setState(() => destination = AcsDestination.visit)), + AcsDestination.queue => DashboardScreen( + queue: _queue, + feedError: _feedError, + onAcknowledge: _acknowledge, + onEscalate: (alert) => setState(() { _selected = alert; destination = AcsDestination.escalation; }), + onVisit: (alert) => setState(() { _selected = alert; destination = AcsDestination.visit; }), + ), AcsDestination.map => const MapScreen(), - AcsDestination.visit => const VisitRegistrationScreen(patientName: 'Maria Oliveira'), - AcsDestination.escalation => const EscalationScreen(), + AcsDestination.visit => VisitRegistrationScreen( + alert: _selected, + queue: widget.visitQueue ?? OfflineVisitQueue(), + ), + AcsDestination.escalation => EscalationScreen(alert: _selected), AcsDestination.geofencing => const GeofencingScreen(), AcsDestination.notices => const NoticesScreen(), }), @@ -81,29 +253,263 @@ class _AcsHomeShellState extends State { class TerritorializationScreen extends StatelessWidget { const TerritorializationScreen({super.key}); @override Widget build(BuildContext context) => _page([const Text('Microárea 12 - Zona Rural', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), const SizedBox(height: 12), const _InfoRow('Pacientes sincronizados', '142 cadastrados'), const _InfoRow('Cache local', 'Atualizado há 10 min'), const SizedBox(height: 20), FilledButton(onPressed: () => _message(context, 'Atualização será integrada à API central.'), child: const Text('Atualizar dados da microárea'))]); } +/// Painel de priorização alimentado pelos alertas que chegam do broker. +/// +/// Antes eram três cartões fixos no código. A fila agora é a [AlertQueue], que +/// ordena de forma determinística por risco e, no mesmo risco, por antiguidade. class DashboardScreen extends StatelessWidget { - const DashboardScreen({super.key, this.onEscalate, this.onVisit}); final VoidCallback? onEscalate; final VoidCallback? onVisit; + const DashboardScreen({ + required this.queue, + super.key, + this.feedError, + this.onAcknowledge, + this.onEscalate, + this.onVisit, + }); + + final AlertQueue queue; + final String? feedError; + final void Function(PrioritizedAlert alert)? onAcknowledge; + final void Function(PrioritizedAlert alert)? onEscalate; + final void Function(PrioritizedAlert alert)? onVisit; + @override - Widget build(BuildContext context) => ListView(padding: const EdgeInsets.all(20), children: [ - const Text('Painel de Priorização', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), const SizedBox(height: 4), const Text('Fila ordenada por risco clínico'), const SizedBox(height: 16), - _card('Maria Oliveira, 72a', 'Risco: Vermelho', 'Falta de ar aguda relatada via app.', AcsColors.red, 'Acionar SAMU / Atender', onEscalate), const SizedBox(height: 12), - _card('João Pereira, 65a', 'Risco: Amarelo', 'Glicemia descompensada na triagem.', AcsColors.yellow, 'Iniciar rota de visita', onVisit), const SizedBox(height: 12), - _card('Ana Costa, 28a', 'Risco: Verde', 'Dúvida sobre vacinação.', AcsColors.green, 'Ver dúvida / responder', () => _message(context, 'Canal de dúvidas demonstrativo.')), - ]); - Widget _card(String name, String risk, String detail, Color color, String action, VoidCallback? onPressed) => Card(child: Container(padding: const EdgeInsets.all(16), decoration: BoxDecoration(border: Border(left: BorderSide(color: color, width: 4))), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [Text(name, style: const TextStyle(fontWeight: FontWeight.bold)), const SizedBox(height: 6), Text(risk, style: TextStyle(color: color, fontWeight: FontWeight.bold)), const SizedBox(height: 6), Text(detail), const SizedBox(height: 12), SizedBox(width: double.infinity, child: FilledButton(onPressed: onPressed, style: FilledButton.styleFrom(backgroundColor: color), child: Text(action)))]))); + Widget build(BuildContext context) => AnimatedBuilder( + animation: queue, + builder: (context, _) { + final alerts = queue.alerts; + return ListView(padding: const EdgeInsets.all(20), children: [ + const Text('Painel de Priorização', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), + const SizedBox(height: 4), + const Text('Fila ordenada por risco clínico'), + if (feedError != null) Padding( + padding: const EdgeInsets.only(top: 12), + child: Card( + color: AcsColors.red.withValues(alpha: 0.15), + child: ListTile( + key: const Key('feed_error'), + leading: const Icon(Icons.cloud_off_outlined, color: AcsColors.red), + title: Text(feedError!, style: const TextStyle(fontWeight: FontWeight.bold)), + subtitle: const Text('Novos alertas podem não estar chegando.'), + ), + ), + ), + const SizedBox(height: 16), + if (alerts.isEmpty) const Card( + child: Padding( + padding: EdgeInsets.all(24), + child: Column(children: [ + Icon(Icons.inbox_outlined, size: 40), + SizedBox(height: 12), + Text('Nenhum alerta na sua microárea agora.', textAlign: TextAlign.center), + ]), + ), + ), + for (final alert in alerts) Padding( + padding: const EdgeInsets.only(bottom: 12), + child: _AlertCard( + alert: alert, + onAcknowledge: onAcknowledge, + onEscalate: onEscalate, + onVisit: onVisit, + ), + ), + ]); + }, + ); +} + +class _AlertCard extends StatelessWidget { + const _AlertCard({required this.alert, this.onAcknowledge, this.onEscalate, this.onVisit}); + + final PrioritizedAlert alert; + final void Function(PrioritizedAlert alert)? onAcknowledge; + final void Function(PrioritizedAlert alert)? onEscalate; + final void Function(PrioritizedAlert alert)? onVisit; + + /// Cor é sinal clínico, nunca decoração: mapeia estritamente o risco vindo do + /// servidor. + (Color, String) get _risk => switch (alert.riskLevel.toLowerCase()) { + 'red' || 'vermelho' => (AcsColors.red, 'Risco: Vermelho'), + 'yellow' || 'amarelo' => (AcsColors.yellow, 'Risco: Amarelo'), + 'green' || 'verde' => (AcsColors.green, 'Risco: Verde'), + _ => (AcsColors.accent, 'Risco: não classificado'), + }; + + @override + Widget build(BuildContext context) { + final (color, label) = _risk; + final isRed = alert.riskLevel.toLowerCase() == 'red' || alert.riskLevel.toLowerCase() == 'vermelho'; + + return Card( + key: Key('alert_${alert.alertId}'), + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration(border: Border(left: BorderSide(color: color, width: 4))), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + // Identificação direta do paciente não trafega no envelope MQTT + // (LGPD): o alerta carrega identificadores, não nome nem endereço. + Text('Paciente ${alert.patientId.substring(0, 8)}', style: const TextStyle(fontWeight: FontWeight.bold)), + const SizedBox(height: 6), + Text(label, style: TextStyle(color: color, fontWeight: FontWeight.bold)), + const SizedBox(height: 6), + Text('Recebido às ${_time(alert.triggeredAt)} • local ${alert.locationHash}'), + if (alert.acknowledged) const Padding( + padding: EdgeInsets.only(top: 8), + child: Row(children: [ + Icon(Icons.check_circle, size: 16, color: AcsColors.green), + SizedBox(width: 6), + Text('Recebimento confirmado', style: TextStyle(color: AcsColors.green)), + ]), + ), + const SizedBox(height: 12), + if (!alert.acknowledged) SizedBox( + width: double.infinity, + child: FilledButton( + key: Key('ack_${alert.alertId}'), + onPressed: onAcknowledge == null ? null : () => onAcknowledge!(alert), + style: FilledButton.styleFrom(backgroundColor: color, minimumSize: const Size(48, 52)), + child: const Text('Confirmar recebimento'), + ), + ), + const SizedBox(height: 8), + SizedBox( + width: double.infinity, + child: OutlinedButton( + onPressed: isRed + ? (onEscalate == null ? null : () => onEscalate!(alert)) + : (onVisit == null ? null : () => onVisit!(alert)), + style: OutlinedButton.styleFrom(minimumSize: const Size(48, 52)), + child: Text(isRed ? 'Acionar SAMU / Atender' : 'Iniciar rota de visita'), + ), + ), + ]), + ), + ); + } + + String _time(DateTime value) { + final local = value.toLocal(); + return '${local.hour.toString().padLeft(2, '0')}:${local.minute.toString().padLeft(2, '0')}'; + } } class MapScreen extends StatelessWidget { const MapScreen({super.key}); @override Widget build(BuildContext context) => _page([Container(height: 220, decoration: BoxDecoration(color: AcsColors.surface, borderRadius: BorderRadius.circular(8)), child: const Stack(children: [Align(alignment: Alignment(-.55, -.4), child: Icon(Icons.location_on, color: AcsColors.red, size: 38)), Align(alignment: Alignment(.55, .1), child: Icon(Icons.location_on, color: AcsColors.yellow, size: 38)), Align(alignment: Alignment(-.1, .65), child: Icon(Icons.location_on, color: AcsColors.green, size: 38)), Center(child: Text('Mapa demonstrativo'))])), const SizedBox(height: 16), const Text('Pinos representam a prioridade clínica no território.', textAlign: TextAlign.center), const SizedBox(height: 16), FilledButton(onPressed: () => _message(context, 'Traçado de rota depende da integração de mapas.'), child: const Text('Traçar rota eficiente'))]); } -class VisitRegistrationScreen extends StatefulWidget { const VisitRegistrationScreen({required this.patientName, super.key}); final String patientName; @override State createState() => _VisitRegistrationScreenState(); } -class _VisitRegistrationScreenState extends State { String outcome = 'Realizada com sucesso'; final notes = TextEditingController(); @override void dispose() { notes.dispose(); super.dispose(); } Future _save() async { await OfflineVisitQueue().add(OfflineVisitRecord(patientName: widget.patientName, risk: 'VERMELHO', status: 'PENDENTE')); if (mounted) _message(context, 'Visita salva e enfileirada localmente para sincronização.'); } @override Widget build(BuildContext context) => _page([Text(widget.patientName, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), const SizedBox(height: 16), DropdownButtonFormField(initialValue: outcome, decoration: const InputDecoration(labelText: 'Status do atendimento'), items: const ['Realizada com sucesso', 'Paciente ausente', 'Recusou atendimento'].map((v) => DropdownMenuItem(value: v, child: Text(v))).toList(), onChanged: (v) => setState(() => outcome = v!)), const SizedBox(height: 16), TextField(controller: notes, maxLines: 4, decoration: const InputDecoration(labelText: 'Observações de campo')), const SizedBox(height: 20), FilledButton.icon(onPressed: _save, icon: const Icon(Icons.save_outlined), label: const Text('Salvar e enfileirar sincronização'))]); } +class VisitRegistrationScreen extends StatefulWidget { + const VisitRegistrationScreen({required this.queue, super.key, this.alert}); + + /// A fila é recebida pronta, não construída aqui: uma instância por gravação + /// descartava a visita assim que o callback retornava. + final OfflineVisitQueue queue; + final PrioritizedAlert? alert; + + @override + State createState() => _VisitRegistrationScreenState(); +} + +class _VisitRegistrationScreenState extends State { + String outcome = 'Realizada com sucesso'; + final notes = TextEditingController(); + + @override + void dispose() { notes.dispose(); super.dispose(); } + + String get _patientLabel { + final alert = widget.alert; + return alert == null ? 'Visita sem alerta vinculado' : 'Paciente ${alert.patientId.substring(0, 8)}'; + } -class EscalationScreen extends StatelessWidget { const EscalationScreen({super.key}); @override Widget build(BuildContext context) => _page([const Text('Escalonamento rápido', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), const SizedBox(height: 16), const _InfoRow('Paciente', 'Maria Oliveira, 72a'), const _InfoRow('Risco', 'Vermelho - falta de ar aguda'), const _InfoRow('Endereço', 'Rua das Flores, 120'), const SizedBox(height: 20), FilledButton.icon(onPressed: () => _message(context, 'Discagem não está integrada neste protótipo.'), style: FilledButton.styleFrom(backgroundColor: AcsColors.red), icon: const Icon(Icons.call), label: const Text('Ligar para o SAMU (192)')), const SizedBox(height: 12), OutlinedButton(onPressed: () => _message(context, 'Encaminhamento será integrado à UBS.'), child: const Text('Encaminhar para UBS Central'))]); } + Future _save() async { + await widget.queue.add(OfflineVisitRecord( + patientName: _patientLabel, + risk: widget.alert?.riskLevel ?? 'desconhecido', + status: 'PENDENTE', + outcome: outcome, + )); + if (!mounted) return; + _message(context, 'Visita salva e enfileirada localmente para sincronização.'); + } + + @override + Widget build(BuildContext context) => _page([ + Text(_patientLabel, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), + const SizedBox(height: 16), + DropdownButtonFormField(initialValue: outcome, decoration: const InputDecoration(labelText: 'Status do atendimento'), items: const ['Realizada com sucesso', 'Paciente ausente', 'Recusou atendimento'].map((v) => DropdownMenuItem(value: v, child: Text(v))).toList(), onChanged: (v) => setState(() => outcome = v!)), + const SizedBox(height: 16), + TextField(controller: notes, maxLines: 4, decoration: const InputDecoration(labelText: 'Observações de campo')), + const SizedBox(height: 20), + FilledButton.icon(key: const Key('save_visit'), onPressed: _save, icon: const Icon(Icons.save_outlined), label: const Text('Salvar e enfileirar sincronização')), + ]); +} + +class EscalationScreen extends StatelessWidget { + const EscalationScreen({super.key, this.alert}); + + final PrioritizedAlert? alert; + + @override + Widget build(BuildContext context) { + final current = alert; + return _page([ + const Text('Escalonamento rápido', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), + const SizedBox(height: 16), + _InfoRow('Paciente', current == null ? '—' : current.patientId.substring(0, 8)), + _InfoRow('Risco', current?.riskLevel ?? '—'), + // O envelope MQTT não carrega endereço: só o hash da localização (LGPD). + _InfoRow('Local (hash)', current?.locationHash ?? '—'), + const SizedBox(height: 20), + FilledButton.icon(onPressed: () => _message(context, 'Discagem não está integrada neste protótipo.'), style: FilledButton.styleFrom(backgroundColor: AcsColors.red), icon: const Icon(Icons.call), label: const Text('Ligar para o SAMU (192)')), + const SizedBox(height: 12), + OutlinedButton(onPressed: () => _message(context, 'Encaminhamento será integrado à UBS.'), child: const Text('Encaminhar para UBS Central')), + ]); + } +} class GeofencingScreen extends StatelessWidget { const GeofencingScreen({super.key}); @override Widget build(BuildContext context) => _page([const Icon(Icons.location_searching, size: 44), const SizedBox(height: 16), const Text('Check-in passivo', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), const SizedBox(height: 8), const Text('Proximidade demonstrativa: residência de Maria Oliveira, a 18 metros.'), const SizedBox(height: 20), FilledButton(onPressed: () => _message(context, 'Use a tela Visita para registrar o atendimento.'), child: const Text('Abrir formulário da visita'))]); } class NoticesScreen extends StatefulWidget { const NoticesScreen({super.key}); @override State createState() => _NoticesScreenState(); } class _NoticesScreenState extends State { final notice = TextEditingController(); @override void dispose() { notice.dispose(); super.dispose(); } @override Widget build(BuildContext context) => _page([const Text('Aviso comunitário', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), const SizedBox(height: 16), const TextField(decoration: InputDecoration(labelText: 'Público-alvo', hintText: 'Pacientes com condições crônicas')), const SizedBox(height: 16), TextField(controller: notice, maxLines: 4, decoration: const InputDecoration(labelText: 'Mensagem')), const SizedBox(height: 20), FilledButton(onPressed: () => _message(context, 'Envio depende da integração de notificações push.'), child: const Text('Preparar aviso'))]); } Widget _page(List children) => ListView(padding: const EdgeInsets.all(20), children: [Card(child: Padding(padding: const EdgeInsets.all(20), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: children)))]); class _InfoRow extends StatelessWidget { const _InfoRow(this.label, this.value); final String label; final String value; @override Widget build(BuildContext context) => Padding(padding: const EdgeInsets.symmetric(vertical: 5), child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text(label), Flexible(child: Text(value, textAlign: TextAlign.end, style: const TextStyle(fontWeight: FontWeight.bold)))])); } -class _Header extends StatelessWidget implements PreferredSizeWidget { const _Header(this.eyebrow, this.title); final String eyebrow; final String title; @override Size get preferredSize => const Size.fromHeight(72); @override Widget build(BuildContext context) => AppBar(title: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [Text(eyebrow.toUpperCase(), style: const TextStyle(fontSize: 10, color: AcsColors.accent, fontWeight: FontWeight.bold)), Text(title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold))]), actions: const [Padding(padding: EdgeInsets.only(right: 12), child: Chip(label: Text('Offline ready')))]); } +class _Header extends StatelessWidget implements PreferredSizeWidget { + const _Header(this.eyebrow, this.title, {this.connected}); + + final String eyebrow; + final String title; + + /// Estado da conexão com o broker. `null` fora do painel. + /// + /// Fica no cabeçalho de propósito: um ACS precisa saber, sem procurar, que + /// parou de receber alertas. + final bool? connected; + + @override + Size get preferredSize => const Size.fromHeight(72); + + @override + Widget build(BuildContext context) => AppBar( + title: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(eyebrow.toUpperCase(), style: const TextStyle(fontSize: 10, color: AcsColors.accent, fontWeight: FontWeight.bold)), + Text(title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + ]), + actions: [ + Padding( + padding: const EdgeInsets.only(right: 12), + child: Chip( + key: const Key('broker_status'), + avatar: connected == null ? null : Icon( + connected! ? Icons.cloud_done_outlined : Icons.cloud_off_outlined, + size: 18, + color: connected! ? AcsColors.green : AcsColors.red, + ), + label: Text(switch (connected) { + null => 'Offline ready', + true => 'Alertas em tempo real', + false => 'Sem central', + }), + ), + ), + ], + ); +} void _message(BuildContext context, String message) => ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message))); \ No newline at end of file diff --git a/apps/acs/lib/core/network/auth_session.dart b/apps/acs/lib/core/network/auth_session.dart new file mode 100644 index 0000000..ac8f115 --- /dev/null +++ b/apps/acs/lib/core/network/auth_session.dart @@ -0,0 +1,84 @@ +import 'dart:convert'; + +/// Sessão autenticada contra o backend. +/// +/// **Não é autenticação institucional.** O token vem de +/// `auth.developmentLogin`, que só existe com `ENABLE_DEV_LOGIN=true` e serve +/// para validar a conexão, não para proteger dado real. +class AuthSession { + const AuthSession({ + required this.accessToken, + required this.tokenType, + required this.userId, + required this.role, + required this.microAreaId, + required this.expiresAt, + }); + + final String accessToken; + final String tokenType; + final String userId; + final String role; + + /// Microárea do usuário. Restringe o acesso ao território (INV do PRD) e, no + /// app do ACS, define o tópico MQTT assinado. + final String? microAreaId; + + final DateTime expiresAt; + + /// Lê o payload do token emitido por `DevelopmentAuthService`. + /// + /// O token é um JWT HS256 cujo payload carrega `sub`, `role`, + /// `micro_area_id` e `exp`. A assinatura **não** é verificada aqui: verificar + /// é papel do servidor, e o app não tem — nem deve ter — o segredo HMAC. A + /// leitura serve apenas para o app conhecer a própria microárea e a + /// expiração, o que evita um endpoint extra só para isso. + /// + /// Devolve `null` para qualquer token malformado; quem chama decide o que + /// mostrar. Nunca registre o token em log. + static AuthSession? tryParse(String accessToken, String tokenType) { + final sections = accessToken.split('.'); + if (sections.length != 3) return null; + + try { + final decoded = utf8.decode( + base64Url.decode(base64Url.normalize(sections[1])), + ); + final payload = jsonDecode(decoded) as Map; + + final exp = payload['exp']; + final sub = payload['sub']; + final role = payload['role']; + if (exp is! int || sub is! String || role is! String) return null; + + return AuthSession( + accessToken: accessToken, + tokenType: tokenType, + userId: sub, + role: role, + microAreaId: payload['micro_area_id'] as String?, + expiresAt: DateTime.fromMillisecondsSinceEpoch( + exp * 1000, + isUtc: true, + ), + ); + } on FormatException { + return null; + } on TypeError { + return null; + } + } + + /// O token de desenvolvimento vive 15 minutos. + /// + /// A margem existe para não enviar um token que expira no meio da viagem: + /// sem ela, um fluxo longo falha com erro de permissão em vez de + /// reautenticar, que é um sintoma bem mais confuso de diagnosticar. + bool isExpired({ + DateTime? now, + Duration margin = const Duration(seconds: 30), + }) { + final reference = (now ?? DateTime.now().toUtc()).add(margin); + return !expiresAt.isAfter(reference); + } +} diff --git a/apps/acs/lib/core/network/backend_client.dart b/apps/acs/lib/core/network/backend_client.dart new file mode 100644 index 0000000..ab7a9ad --- /dev/null +++ b/apps/acs/lib/core/network/backend_client.dart @@ -0,0 +1,158 @@ +import 'package:sinalacs_acs/core/network/auth_session.dart'; +import 'package:sinalacs_acs/core/network/backend_config.dart'; +import 'package:sinalacs_client/sinalacs_client.dart'; + +/// Falha já traduzida para a pessoa que está usando o app. +/// +/// O backend é RPC tipado, então o erro chega como exceção declarada no +/// `.spy.yaml` — não como código HTTP. +class BackendFailure implements Exception { + const BackendFailure(this.message, {this.isRecoverable = true}); + + final String message; + + /// `false` quando repetir a mesma ação não deve resolver. + final bool isRecoverable; + + @override + String toString() => message; +} + +/// Contrato do backend visto pela UI do ACS. +/// +/// A UI depende desta abstração, nunca do [Client] gerado — mesmo padrão que o +/// servidor usa entre `application/` e `infrastructure/`. +abstract class AcsBackend { + AuthSession? get session; + + bool get isAuthenticated; + + Future health(); + + Future login(); + + Future acknowledge({required String alertId}); + + Future> syncVisits(List visits); + + void close(); +} + +/// Fachada do backend para o app do ACS. +class BackendClient implements AcsBackend { + BackendClient({String? host}) + : _client = Client(host ?? BackendConfig.host) + ..connectivityMonitor = null; + + final Client _client; + + AuthSession? _session; + + @override + AuthSession? get session => _session; + + @override + bool get isAuthenticated { + final current = _session; + return current != null && !current.isExpired(); + } + + /// Token válido para as chamadas autenticadas. + /// + /// Reautentica sozinho quando o token de 15 minutos expirou — sem isso, um + /// turno de campo longo passa a falhar com erro de permissão, que esconde a + /// causa real. + Future _requireToken() async { + if (!isAuthenticated) await login(); + final current = _session; + if (current == null) { + throw const BackendFailure('Sessão não iniciada.', isRecoverable: false); + } + return current.accessToken; + } + + @override + Future health() => _guard(() => _client.health.check()); + + /// Autentica como ACS. Ver ressalvas em [AuthSession]. + @override + Future login() async { + final result = await _guard( + () => _client.auth.developmentLogin(role: 'acs'), + ); + + final session = AuthSession.tryParse(result.accessToken, result.tokenType); + if (session == null) { + throw const BackendFailure( + 'O servidor devolveu um token que o aplicativo não entendeu.', + isRecoverable: false, + ); + } + + _session = session; + return session; + } + + /// Confirma o recebimento do alerta vermelho. + /// + /// Usa o caminho RPC em vez de publicar o ACK no broker: é tipado e o + /// servidor já trata alerta inexistente devolvendo `acknowledged: false`, em + /// vez de erro. O dispatcher do servidor assina o tópico de ACKs, então o + /// efeito chega ao broker de qualquer forma. + @override + Future acknowledge({required String alertId}) async { + final token = await _requireToken(); + return _guard( + () => _client.alerts.acknowledge(accessToken: token, alertId: alertId), + ); + } + + /// Envia as visitas registradas offline. + /// + /// O lote inteiro roda em uma transação no servidor, e cada visita volta com + /// o próprio `syncStatus`: o app casa pelo `localId`, nunca pela posição. + @override + Future> syncVisits(List visits) async { + final token = await _requireToken(); + return _guard( + () => _client.visits.sync(accessToken: token, visits: visits), + ); + } + + @override + void close() => _client.close(); + + /// Traduz as exceções tipadas do backend para [BackendFailure]. + Future _guard(Future Function() call) async { + try { + return await call(); + } on EndpointDisabledException { + throw const BackendFailure( + 'O acesso de desenvolvimento está desativado neste servidor.', + isRecoverable: false, + ); + } on AlertPermissionException { + // Acontece quando o alerta é de outra microárea. A territorialização é + // invariante: o servidor recusa, e o app não deve tentar contornar. + throw const BackendFailure( + 'Este alerta não pertence à sua microárea.', + isRecoverable: false, + ); + } on AlertValidationException catch (error) { + throw BackendFailure(error.message, isRecoverable: false); + } on AlertDispatchUnavailableException { + throw const BackendFailure( + 'Registrado. A rede está instável e a confirmação será entregue assim ' + 'que a conexão voltar.', + ); + } on ServerpodClientException catch (error) { + throw BackendFailure( + 'Não foi possível falar com o servidor (${error.statusCode}).', + ); + } catch (_) { + throw const BackendFailure( + 'Sem conexão com o servidor. Verifique a rede e tente de novo.', + ); + } + } +} diff --git a/apps/acs/lib/core/network/backend_config.dart b/apps/acs/lib/core/network/backend_config.dart new file mode 100644 index 0000000..1561fcb --- /dev/null +++ b/apps/acs/lib/core/network/backend_config.dart @@ -0,0 +1,59 @@ +/// Endereços do backend e do broker, configuráveis em tempo de compilação. +/// +/// Os defaults apontam para o host da máquina de desenvolvimento visto de +/// dentro do emulador Android (`10.0.2.2`). Para outros alvos: +/// +/// flutter run --dart-define=SINALACS_HOST=http://localhost:8080/ \ +/// --dart-define=SINALACS_MQTT_HOST=localhost +/// +/// As credenciais abaixo são as do `docker-compose.yml` de desenvolvimento e +/// não valem fora dele. Em produção o ACS não deveria carregar segredo do +/// broker embutido no binário — ver a lacuna de mTLS registrada em spec/. +class BackendConfig { + const BackendConfig._(); + + /// A barra final é exigida pelo cliente Serverpod. + static const String host = String.fromEnvironment( + 'SINALACS_HOST', + defaultValue: 'http://10.0.2.2:8080/', + ); + + static const String mqttHost = String.fromEnvironment( + 'SINALACS_MQTT_HOST', + defaultValue: '10.0.2.2', + ); + + /// 8883 é a única porta que o broker publica: TLS, sem WebSocket. + static const int mqttPort = int.fromEnvironment( + 'SINALACS_MQTT_PORT', + defaultValue: 8883, + ); + + static const String mqttUsername = String.fromEnvironment( + 'SINALACS_MQTT_USER', + defaultValue: 'acs-area-12', + ); + + /// Senha do broker. + /// + /// **`String.fromEnvironment` é resolvido em tempo de COMPILAÇÃO**: este valor + /// vira uma constante dentro do binário e é extraível de qualquer APK. O + /// default existe para que `flutter run` e scripts/qa/e2e.sh funcionem sem + /// argumentos em desenvolvimento — é uma escolha consciente, não um descuido, + /// e a senha correspondente do broker local é gerada por + /// scripts/dev/bootstrap_env.sh, não é esta. + /// + /// Em produção credencial de broker não pode viajar dentro do app. A correção + /// é credencial por dispositivo / mTLS, lacuna registrada em spec/ — o + /// mosquitto.conf atual nem sequer tem `require_certificate`. + static const String mqttPassword = String.fromEnvironment( + 'SINALACS_MQTT_PASSWORD', + defaultValue: 'development-acs-password', + ); + + /// CA que assina o certificado do broker local. + /// + /// Copiada para cá por `scripts/dev/sync_dev_ca.sh`; é regerada pelo + /// mosquitto-init e não é versionada. + static const String mqttCaAsset = 'assets/certs/dev_ca.crt'; +} diff --git a/apps/acs/lib/core/network/backend_scope.dart b/apps/acs/lib/core/network/backend_scope.dart new file mode 100644 index 0000000..dd74f62 --- /dev/null +++ b/apps/acs/lib/core/network/backend_scope.dart @@ -0,0 +1,26 @@ +import 'package:flutter/widgets.dart'; +import 'package:sinalacs_acs/core/network/backend_client.dart'; + +/// Disponibiliza o [AcsBackend] para a árvore de widgets. +/// +/// Existe para que as telas não construam o próprio cliente: em teste, injeta-se +/// um duplo; em execução, o [BackendClient] real. +class BackendScope extends InheritedWidget { + const BackendScope({ + required this.backend, + required super.child, + super.key, + }); + + final AcsBackend backend; + + static AcsBackend of(BuildContext context) { + final scope = context.dependOnInheritedWidgetOfExactType(); + assert(scope != null, 'Nenhum BackendScope acima deste widget.'); + return scope!.backend; + } + + @override + bool updateShouldNotify(BackendScope oldWidget) => + backend != oldWidget.backend; +} diff --git a/apps/acs/lib/core/services/alert_feed.dart b/apps/acs/lib/core/services/alert_feed.dart new file mode 100644 index 0000000..cd5823e --- /dev/null +++ b/apps/acs/lib/core/services/alert_feed.dart @@ -0,0 +1,75 @@ +import 'package:flutter/services.dart' show rootBundle; +import 'package:sinalacs_acs/core/network/backend_config.dart'; +import 'package:sinalacs_acs/core/services/alert_queue.dart'; +import 'package:sinalacs_acs/core/services/mqtt_secure_client.dart'; + +/// Liga o broker à [AlertQueue]. +/// +/// É a única peça que sabe montar a configuração MQTT a partir da sessão: o +/// tópico depende da microárea do ACS autenticado, então não dá para fixá-lo em +/// tempo de compilação. +abstract class AlertFeed { + Future start({required String microAreaId, required String acsId}); + + void stop(); + + bool get isConnected; +} + +class MqttAlertFeed implements AlertFeed { + MqttAlertFeed({ + required this.queue, + this.onConnectionChanged, + }); + + final AlertQueue queue; + final void Function(bool connected)? onConnectionChanged; + + MqttSecureClient? _client; + bool _connected = false; + + @override + bool get isConnected => _connected; + + @override + Future start({ + required String microAreaId, + required String acsId, + }) async { + final caCertificate = await rootBundle.load(BackendConfig.mqttCaAsset); + + final client = MqttSecureClient( + config: SecureMqttConfig( + brokerHost: BackendConfig.mqttHost, + port: BackendConfig.mqttPort, + // Estável por ACS: é o que permite ao broker restaurar a sessão + // persistente e reentregar o que chegou enquanto o aparelho estava + // offline. Um id aleatório aqui descartaria esses alertas. + clientId: 'sinalacs-acs-$acsId', + topic: alertTopicFor(microAreaId), + username: BackendConfig.mqttUsername, + password: BackendConfig.mqttPassword, + caCertificate: caCertificate.buffer.asUint8List(), + ), + ); + + await client.connect( + onAlert: (alert) => queue.upsert(PrioritizedAlert.fromMqtt(alert)), + onConnectionChanged: (connected) { + _connected = connected; + onConnectionChanged?.call(connected); + }, + ); + + _client = client; + _connected = client.isConnected; + onConnectionChanged?.call(_connected); + } + + @override + void stop() { + _client?.disconnect(); + _client = null; + _connected = false; + } +} diff --git a/apps/acs/lib/core/services/alert_queue.dart b/apps/acs/lib/core/services/alert_queue.dart new file mode 100644 index 0000000..64b5769 --- /dev/null +++ b/apps/acs/lib/core/services/alert_queue.dart @@ -0,0 +1,127 @@ +import 'package:flutter/foundation.dart'; +import 'package:sinalacs_acs/core/services/mqtt_secure_client.dart'; + +/// Um alerta na fila de priorização do ACS. +@immutable +class PrioritizedAlert { + const PrioritizedAlert({ + required this.alertId, + required this.patientId, + required this.microAreaId, + required this.riskLevel, + required this.locationHash, + required this.triggeredAt, + this.acknowledged = false, + }); + + factory PrioritizedAlert.fromMqtt(ReceivedMqttAlert alert) { + return PrioritizedAlert( + alertId: alert.alertId, + patientId: alert.patientId, + microAreaId: alert.microAreaId, + riskLevel: alert.riskLevel, + locationHash: alert.locationHash, + triggeredAt: alert.triggeredAt, + ); + } + + final String alertId; + final String patientId; + final String microAreaId; + + /// Como veio no envelope do servidor (`red` / `yellow` / `green`). + final String riskLevel; + + final String locationHash; + final DateTime triggeredAt; + final bool acknowledged; + + PrioritizedAlert copyWith({bool? acknowledged}) => PrioritizedAlert( + alertId: alertId, + patientId: patientId, + microAreaId: microAreaId, + riskLevel: riskLevel, + locationHash: locationHash, + triggeredAt: triggeredAt, + acknowledged: acknowledged ?? this.acknowledged, + ); + + /// Peso da ordenação. Menor vem primeiro. + /// + /// Risco desconhecido cai para o fim, mas **não** é descartado: um envelope + /// com risco que o app não reconhece ainda é um chamado que alguém precisa + /// ver. + int get _riskRank => switch (riskLevel.toLowerCase()) { + 'red' || 'vermelho' => 0, + 'yellow' || 'amarelo' => 1, + 'green' || 'verde' => 2, + _ => 3, + }; +} + +/// Fila de priorização alimentada pelos alertas que chegam do broker. +/// +/// A ordenação é determinística: risco primeiro (vermelho > amarelo > verde) e, +/// dentro do mesmo risco, o mais antigo na frente — quem espera há mais tempo +/// não pode ser ultrapassado por um chamado novo de mesma gravidade. +class AlertQueue extends ChangeNotifier { + AlertQueue({required this.microAreaId}); + + /// Microárea da sessão. Delimita o território do ACS. + final String microAreaId; + + final List _alerts = []; + + List get alerts => List.unmodifiable(_alerts); + + bool get isEmpty => _alerts.isEmpty; + + /// Insere ou atualiza um alerta. + /// + /// Devolve `false` quando o alerta é recusado. São duas razões: + /// + /// - **outra microárea**: territorialização é invariante, e o broker já + /// restringe por ACL — esta é a segunda barreira, para o caso de o app ser + /// apontado a um tópico mais amplo; + /// - **duplicata**: com QoS 1 a entrega é *at-least-once*, então a mesma + /// mensagem pode chegar mais de uma vez e não pode virar dois cartões. + bool upsert(PrioritizedAlert alert) { + if (alert.microAreaId != microAreaId) return false; + + final index = _alerts.indexWhere((item) => item.alertId == alert.alertId); + if (index >= 0) { + // Preserva a confirmação já registrada: uma reentrega não pode "desfazer" + // um alerta que o ACS já atendeu. + final existing = _alerts[index]; + if (existing.acknowledged) return false; + _alerts[index] = alert; + } else { + _alerts.add(alert); + } + + _sort(); + notifyListeners(); + return true; + } + + /// Marca o alerta como confirmado. Devolve `false` se ele não está na fila. + bool markAcknowledged(String alertId) { + final index = _alerts.indexWhere((item) => item.alertId == alertId); + if (index < 0) return false; + + _alerts[index] = _alerts[index].copyWith(acknowledged: true); + _sort(); + notifyListeners(); + return true; + } + + void _sort() { + _alerts.sort((a, b) { + // Confirmados descem, para não competir com quem ainda espera. + if (a.acknowledged != b.acknowledged) return a.acknowledged ? 1 : -1; + final byRisk = a._riskRank.compareTo(b._riskRank); + if (byRisk != 0) return byRisk; + return a.triggeredAt.compareTo(b.triggeredAt); + }); + } +} diff --git a/apps/acs/lib/core/services/backend_visit_synchronizer.dart b/apps/acs/lib/core/services/backend_visit_synchronizer.dart new file mode 100644 index 0000000..92ecdb1 --- /dev/null +++ b/apps/acs/lib/core/services/backend_visit_synchronizer.dart @@ -0,0 +1,55 @@ +import 'package:sinalacs_acs/core/network/backend_client.dart'; +import 'package:sinalacs_acs/core/services/offline_visit_queue.dart'; +import 'package:sinalacs_client/sinalacs_client.dart'; + +/// Leva as visitas da fila offline ao endpoint `visits.sync`. +/// +/// Fica entre a fila (que não conhece o contrato do servidor) e o +/// [AcsBackend] (que não conhece a fila) — é a tradução entre os dois, no mesmo +/// espírito das implementações de `infrastructure/` no backend. +class BackendVisitSynchronizer implements VisitSynchronizer { + BackendVisitSynchronizer({required this.backend, required this.patientIdFor}); + + final AcsBackend backend; + + /// Resolve o UUID do paciente de uma visita. + /// + /// A fila guarda um rótulo de exibição, não o identificador; quem registrou a + /// visita sabe a qual alerta ela pertence. + final String Function(OfflineVisitRecord visit) patientIdFor; + + @override + Future> push(List visits) async { + final results = await backend.syncVisits([ + for (final visit in visits) + VisitSyncEntry( + localId: visit.localId, + patientId: patientIdFor(visit), + scheduledAt: visit.createdAt.toUtc(), + completedAt: visit.createdAt.toUtc(), + status: visit.outcome.isEmpty ? visit.status : visit.outcome, + riskLevelBefore: _riskLevel(visit.risk), + notes: const {}, + version: visit.version, + ), + ]); + + return [ + for (final result in results) + VisitSyncOutcome( + localId: result.localId, + status: result.syncStatus.name, + serverVersion: result.serverVersion, + ), + ]; + } + + /// Risco é sinal clínico: um valor desconhecido vira `green` só para caber no + /// enum, nunca para *rebaixar* um caso — o risco que vale é o do servidor, e + /// esta visita já foi realizada. + RiskLevel _riskLevel(String value) => switch (value.toLowerCase()) { + 'red' || 'vermelho' => RiskLevel.red, + 'yellow' || 'amarelo' => RiskLevel.yellow, + _ => RiskLevel.green, + }; +} diff --git a/apps/acs/lib/core/services/mqtt_secure_client.dart b/apps/acs/lib/core/services/mqtt_secure_client.dart index 6964e53..80bfa45 100644 --- a/apps/acs/lib/core/services/mqtt_secure_client.dart +++ b/apps/acs/lib/core/services/mqtt_secure_client.dart @@ -5,6 +5,20 @@ import 'package:crypto/crypto.dart'; import 'package:mqtt_client/mqtt_client.dart'; import 'package:mqtt_client/mqtt_server_client.dart'; +/// Prefixo dos tópicos de alerta, espelhando `AlertDelivery.topicPrefix` no +/// servidor e a ACL do broker (infra/docker/mosquitto/aclfile). +/// +/// Os três lugares precisam concordar: o servidor publica aqui, o broker só +/// autoriza aqui, e o app só recebe o que assinar aqui. +const String alertTopicPrefix = 'sinalacs/v1/microareas'; + +/// Tópico onde os alertas de uma microárea são publicados. +String alertTopicFor(String microAreaId) => + '$alertTopicPrefix/$microAreaId/alerts'; + +/// Tópico onde o ACS confirma o recebimento de um alerta. +String ackTopicFor(String alertId) => 'sinalacs/v1/alerts/$alertId/acks'; + class SecureMqttConfig { const SecureMqttConfig({ required this.brokerHost, @@ -12,10 +26,10 @@ class SecureMqttConfig { required this.clientId, required this.topic, this.useTls = true, - this.useWebSocket = true, + this.useWebSocket = false, this.username, this.password, - this.caCertificatePath, + this.caCertificate, }); final String brokerHost; @@ -23,10 +37,21 @@ class SecureMqttConfig { final String clientId; final String topic; final bool useTls; + + /// O broker publica **apenas** a porta 8883 (TCP/TLS): não há listener + /// WebSocket. O default era `true`, o que fazia [MqttSecureClient.connect] + /// lançar `UnsupportedError` na configuração padrão — ou seja, o cliente se + /// autodesabilitava e nunca chegou a ser usado. final bool useWebSocket; + final String? username; final String? password; - final String? caCertificatePath; + + /// Certificado da CA que assina o broker, em bytes (PEM). + /// + /// São bytes e não caminho de arquivo porque no Android a CA vem de um asset + /// dentro do APK, onde não existe caminho no sistema de arquivos. + final List? caCertificate; String get connectionUri { if (useWebSocket) { @@ -114,7 +139,9 @@ class MqttSecureAlertPayload { 'location_hash': locationHash, 'triggered_at': timestamp, 'timestamp': timestamp, - 'mqtt_topic': '/alerts/$microAreaId', + // Namespace do backend (AlertDelivery.topicPrefix) e da ACL do broker. + // Era '/alerts/$microAreaId', que não existe em nenhum dos dois. + 'mqtt_topic': alertTopicFor(microAreaId), }; } @@ -158,7 +185,13 @@ class MqttSecureClient { final SecureMqttConfig config; MqttServerClient? _client; - Future connect({required void Function(ReceivedMqttAlert alert) onAlert}) async { + bool get isConnected => + _client?.connectionStatus?.state == MqttConnectionState.connected; + + Future connect({ + required void Function(ReceivedMqttAlert alert) onAlert, + void Function(bool connected)? onConnectionChanged, + }) async { if (config.useWebSocket) { throw UnsupportedError('O transporte WebSocket será configurado pelo gateway de produção.'); } @@ -166,16 +199,50 @@ class MqttSecureClient { final client = MqttServerClient.withPort(config.brokerHost, config.clientId, config.port) ..keepAlivePeriod = 30 ..secure = config.useTls + // Um ACS em campo perde rede o tempo todo; sem reconexão automática a + // fila de alertas simplesmente para de chegar, em silêncio. + ..autoReconnect = true + ..resubscribeOnAutoReconnect = true + ..onDisconnected = (() => onConnectionChanged?.call(false)) + ..onAutoReconnected = (() => onConnectionChanged?.call(true)) + ..onConnected = (() => onConnectionChanged?.call(true)) ..connectionMessage = MqttConnectMessage() .withClientIdentifier(config.clientId) + // NÃO chamar startClean(): sessão persistente é o default + // (cleanStart = false). Com QoS 1, o broker guarda os alertas + // publicados enquanto este ACS estava offline e os entrega na + // reconexão — é o que impede um alerta vermelho de sumir porque o + // aparelho estava sem sinal. Depende de o clientId ser ESTÁVEL entre + // execuções; um id aleatório cria sessão nova a cada conexão e + // descarta o que estava pendente. .withWillQos(MqttQos.atLeastOnce); - if (config.useTls && config.caCertificatePath != null) { + final caCertificate = config.caCertificate; + if (config.useTls && caCertificate != null) { + // `withTrustedRoots: false` é deliberado: só a CA do broker é aceita, + // nenhuma autoridade pública. A verificação de hostname continua ligada — + // é o que o certificado com subjectAltName (infra/docker/mosquitto/init.sh) + // passou a satisfazer. Não desligue com `onBadCertificate`: seria abrir o + // caminho de spoofing do tópico de alertas. client.securityContext = SecurityContext(withTrustedRoots: false) - ..setTrustedCertificates(config.caCertificatePath!); + ..setTrustedCertificatesBytes(caCertificate); + } + + // O cliente é assumido ANTES de conectar. Com `autoReconnect`, uma falha de + // conexão (certificado errado, credencial recusada) deixa o cliente + // tentando de novo indefinidamente; se a referência só fosse guardada + // depois do sucesso, esse cliente ficaria órfão, reconectando para sempre e + // fora do alcance de [disconnect]. + _client = client; + + try { + await client.connect(config.username, config.password); + } catch (_) { + disconnect(); + rethrow; } - await client.connect(config.username, config.password); if (client.connectionStatus?.state != MqttConnectionState.connected) { + disconnect(); throw StateError('Não foi possível conectar ao broker MQTT.'); } @@ -193,7 +260,16 @@ class MqttSecureClient { _client = client; } - void disconnect() => _client?.disconnect(); + void disconnect() { + final client = _client; + _client = null; + if (client == null) return; + // Desligar a reconexão automática vem PRIMEIRO: sem isso, o disconnect + // dispara o evento de reconexão e o cliente volta a tentar sozinho — quem + // pediu para parar não conseguiria parar. + client.autoReconnect = false; + client.disconnect(); + } String _microAreaFromTopic(String topic) { final segments = topic.split('/').where((segment) => segment.isNotEmpty).toList(); diff --git a/apps/acs/lib/core/services/offline_visit_queue.dart b/apps/acs/lib/core/services/offline_visit_queue.dart index 798d250..61f4965 100644 --- a/apps/acs/lib/core/services/offline_visit_queue.dart +++ b/apps/acs/lib/core/services/offline_visit_queue.dart @@ -1,7 +1,10 @@ +import 'dart:math'; + enum SyncOutcomeKind { synced, conflict, empty, + error, } class SyncOutcome { @@ -19,36 +22,126 @@ class OfflineVisitRecord { required this.patientName, required this.risk, required this.status, + this.outcome = '', + String? localId, DateTime? createdAt, - }) : createdAt = createdAt ?? DateTime.now(); + this.version = 1, + }) : localId = localId ?? _newLocalId(), + createdAt = createdAt ?? DateTime.now(); final String patientName; final String risk; final String status; + final String outcome; + + /// Identificador gerado no dispositivo. + /// + /// É a chave de deduplicação da sincronização: a tabela `visits` do servidor + /// tem índice único em `localId`, então reenviar a mesma visita depois de uma + /// falha de rede atualiza a linha em vez de criar uma segunda. + final String localId; + final DateTime createdAt; + /// Versão conhecida no servidor, para detecção de conflito. + final int version; + OfflineVisitRecord copyWith({ String? patientName, String? risk, String? status, + String? outcome, + String? localId, DateTime? createdAt, + int? version, }) { return OfflineVisitRecord( patientName: patientName ?? this.patientName, risk: risk ?? this.risk, status: status ?? this.status, + outcome: outcome ?? this.outcome, + localId: localId ?? this.localId, createdAt: createdAt ?? this.createdAt, + version: version ?? this.version, ); } + + static String _newLocalId() { + final random = Random.secure(); + final bytes = List.generate(16, (_) => random.nextInt(256)); + bytes[6] = (bytes[6] & 0x0f) | 0x40; + bytes[8] = (bytes[8] & 0x3f) | 0x80; + final hex = bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(); + return '${hex.substring(0, 8)}-${hex.substring(8, 12)}-' + '${hex.substring(12, 16)}-${hex.substring(16, 20)}-${hex.substring(20)}'; + } +} + +/// Resultado da sincronização de uma visita, do ponto de vista do servidor. +class VisitSyncOutcome { + const VisitSyncOutcome({ + required this.localId, + required this.status, + this.serverVersion, + }); + + final String localId; + + /// `synced`, `conflict` ou `error`, como devolvido por `visits.sync`. + final String status; + + final int? serverVersion; +} + +/// Quem leva as visitas pendentes ao servidor. +/// +/// Abstração no mesmo espírito de `AlertPublisher`/`AlertStore` no backend: +/// mantém a fila testável sem rede. +abstract class VisitSynchronizer { + Future> push(List visits); } +/// Onde as visitas ficam enquanto não sincronizam. +abstract class VisitStore { + Future> load(); + + Future save(List visits); +} + +/// Armazenamento em memória. Padrão quando nenhum é fornecido. +class InMemoryVisitStore implements VisitStore { + List _visits = []; + + @override + Future> load() async => List.of(_visits); + + @override + Future save(List visits) async { + _visits = List.of(visits); + } +} + +/// Fila offline-first de visitas domiciliares. +/// +/// Preserva a semântica da `SyncFsm` do backend +/// (`idle → localWrite → queued → syncing → {synced|conflict|error}`): uma +/// visita em conflito **volta para a fila** em vez de ser descartada, e uma +/// falha de rede não perde o lote. class OfflineVisitQueue { - OfflineVisitQueue(); + OfflineVisitQueue({VisitStore? store, this.synchronizer}) + : _store = store ?? InMemoryVisitStore(); + + final VisitStore _store; + + /// `null` mantém o comportamento local: a fila enfileira, mas não envia. + final VisitSynchronizer? synchronizer; final List _pending = []; final List _synced = []; final List _conflicts = []; + bool _restored = false; + int get pendingCount => _pending.length; int get syncedCount => _synced.length; int get conflictCount => _conflicts.length; @@ -57,8 +150,21 @@ class OfflineVisitQueue { List get syncedVisits => List.unmodifiable(_synced); List get conflictVisits => List.unmodifiable(_conflicts); + /// Recarrega o que ficou gravado de execuções anteriores. + /// + /// Sem isso, fechar o app perde a fila — o oposto de offline-first. + Future restore() async { + if (_restored) return; + _restored = true; + final stored = await _store.load(); + _pending + ..clear() + ..addAll(stored); + } + Future add(OfflineVisitRecord record) async { _pending.add(record); + await _store.save(_pending); } Future sync({bool forceConflict = false}) async { @@ -67,23 +173,74 @@ class OfflineVisitQueue { } final batch = List.from(_pending); - _pending.clear(); - if (forceConflict && batch.isNotEmpty) { - final conflicted = batch.first.copyWith(status: 'CONFLITO'); - _conflicts.add(conflicted); - _pending.add(conflicted); - return const SyncOutcome(kind: SyncOutcomeKind.conflict, processed: 1); + // Gancho de teste preservado do comportamento anterior. + if (forceConflict) { + return _applyOutcomes(batch, [ + VisitSyncOutcome(localId: batch.first.localId, status: 'conflict'), + for (final visit in batch.skip(1)) + VisitSyncOutcome(localId: visit.localId, status: 'synced'), + ]); } + final sender = synchronizer; + if (sender == null) { + // Sem sincronizador configurado a fila permanece local; nada é + // declarado sincronizado, porque nada saiu do dispositivo. + return SyncOutcome(kind: SyncOutcomeKind.error, processed: batch.length); + } + + final List outcomes; + try { + outcomes = await sender.push(batch); + } catch (_) { + // O lote continua pendente: perder a visita por falha de rede seria + // exatamente o que a fila existe para evitar. + return SyncOutcome(kind: SyncOutcomeKind.error, processed: batch.length); + } + + return _applyOutcomes(batch, outcomes); + } + + Future _applyOutcomes( + List batch, + List outcomes, + ) async { + final byLocalId = {for (final outcome in outcomes) outcome.localId: outcome}; + + var conflicts = 0; + var synced = 0; + final stillPending = []; + for (final visit in batch) { - _synced.add( - visit.copyWith( - status: 'SINCRONIZADO', - ), - ); + final outcome = byLocalId[visit.localId]; + switch (outcome?.status) { + case 'synced': + _synced.add(visit.copyWith( + status: 'SINCRONIZADO', + version: outcome?.serverVersion ?? visit.version, + )); + synced++; + case 'conflict': + final conflicted = visit.copyWith(status: 'CONFLITO'); + _conflicts.add(conflicted); + // Volta para a fila: conflito precisa de resolução, não de descarte. + stillPending.add(conflicted); + conflicts++; + default: + // Sem resposta para esta visita — mantém pendente e tenta de novo. + stillPending.add(visit); + } } - return SyncOutcome(kind: SyncOutcomeKind.synced, processed: batch.length); + _pending + ..clear() + ..addAll(stillPending); + await _store.save(_pending); + + if (conflicts > 0) { + return SyncOutcome(kind: SyncOutcomeKind.conflict, processed: conflicts); + } + return SyncOutcome(kind: SyncOutcomeKind.synced, processed: synced); } } diff --git a/apps/acs/pubspec.lock b/apps/acs/pubspec.lock index 34e8f66..a76b9eb 100644 --- a/apps/acs/pubspec.lock +++ b/apps/acs/pubspec.lock @@ -29,10 +29,10 @@ packages: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.1" charcode: dependency: transitive description: @@ -45,18 +45,18 @@ packages: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" collection: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.1" connectivity_plus: dependency: "direct main" description: @@ -109,10 +109,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.3" ffi: dependency: transitive description: @@ -121,6 +121,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.3" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" fixnum: dependency: transitive description: @@ -134,6 +142,11 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_driver: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" flutter_lints: dependency: "direct dev" description: @@ -160,6 +173,11 @@ packages: description: flutter source: sdk version: "0.0.0" + fuchsia_remote_debug_protocol: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" geolocator: dependency: "direct main" description: @@ -264,30 +282,51 @@ packages: url: "https://pub.dev" source: hosted version: "0.15.6" + http: + dependency: transitive + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + integration_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "11.0.2" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" lints: dependency: transitive description: @@ -300,26 +339,26 @@ packages: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.19" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.18.0" mqtt_client: dependency: "direct main" description: @@ -340,10 +379,10 @@ packages: dependency: transitive description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" petitparser: dependency: transitive description: @@ -368,6 +407,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + process: + dependency: transitive + description: + name: process + sha256: "4242ba3508d37e01808bdf71ad1d5bb93a8d671bf2e7450e6b1b353fb0808891" + url: "https://pub.dev" + source: hosted + version: "5.0.6" sanitize_html: dependency: transitive description: @@ -376,11 +423,50 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + serverpod_auth_core_client: + dependency: transitive + description: + name: serverpod_auth_core_client + sha256: "60b89a1b854fd9c7d589737befd7e4d0cafe047600d65b12b137d827e4c6f6ba" + url: "https://pub.dev" + source: hosted + version: "3.4.13" + serverpod_auth_idp_client: + dependency: transitive + description: + name: serverpod_auth_idp_client + sha256: fb97444ef0df6ac0eca66be884f1107d943df29fb999b6e9a5ad2eb95de9010e + url: "https://pub.dev" + source: hosted + version: "3.4.13" + serverpod_client: + dependency: "direct main" + description: + name: serverpod_client + sha256: "8388185c0eefe2c356c271427d37427cf2b366b3c095da79b7d8be9940cb10bd" + url: "https://pub.dev" + source: hosted + version: "3.4.13" + serverpod_serialization: + dependency: transitive + description: + name: serverpod_serialization + sha256: "807afab39b758f7b21a870af87abfaa24ebdf49aa5d1d9d122b0f3e51157873e" + url: "https://pub.dev" + source: hosted + version: "3.4.13" + sinalacs_client: + dependency: "direct main" + description: + path: "../../backend/sinalacs_client" + relative: true + source: path + version: "0.0.0" sky_engine: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_span: dependency: transitive description: @@ -457,18 +543,18 @@ packages: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" stream_transform: dependency: transitive description: @@ -485,6 +571,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + sync_http: + dependency: transitive + description: + name: sync_http + sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" + url: "https://pub.dev" + source: hosted + version: "0.3.1" synchronized: dependency: transitive description: @@ -505,10 +599,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.11" typed_data: dependency: transitive description: @@ -545,10 +639,10 @@ packages: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" vm_service: dependency: transitive description: @@ -565,6 +659,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + webdriver: + dependency: transitive + description: + name: webdriver + sha256: "28b82ec894fed45dd71c23ba62d1af973ed97dd59a4f5790a4d38b0b13e5657e" + url: "https://pub.dev" + source: hosted + version: "3.2.0" xml: dependency: transitive description: @@ -573,6 +691,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.5.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: f67cdd8e07d3c6329146aaef1ba043542b3134c12489f553ca9a7435d1068aea + url: "https://pub.dev" + source: hosted + version: "3.1.4" sdks: - dart: ">=3.5.0 <4.0.0" + dart: ">=3.10.0-0 <4.0.0" flutter: ">=3.24.0" diff --git a/apps/acs/pubspec.yaml b/apps/acs/pubspec.yaml index 1e67b43..71f58bf 100644 --- a/apps/acs/pubspec.yaml +++ b/apps/acs/pubspec.yaml @@ -5,11 +5,19 @@ publish_to: 'none' version: 1.0.0 environment: - sdk: '>=3.3.0 <4.0.0' + # ^3.8.0 é exigido por sinalacs_client; era '>=3.3.0 <4.0.0'. + sdk: '^3.8.0' dependencies: flutter: sdk: flutter + # Cliente RPC tipado gerado por `serverpod generate`. É membro do workspace + # backend/, e a dependência por path a partir de fora resolve — ver + # video/rpc_demo, que usa exatamente este arranjo. + sinalacs_client: + path: ../../backend/sinalacs_client + # Fixado na mesma versão que sinalacs_client declara; divergir quebra a resolução. + serverpod_client: 3.4.13 crypto: ^3.0.0 sqflite: ^2.3.0 sqflite_common_ffi: ^2.3.0 @@ -23,7 +31,13 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter + integration_test: + sdk: flutter flutter_lints: ^3.0.0 flutter: uses-material-design: true + assets: + # CA do broker MQTT de desenvolvimento, copiada por + # scripts/dev/sync_dev_ca.sh. Regenerável e fora do versionamento. + - assets/certs/ diff --git a/apps/acs/test/login_flow_test.dart b/apps/acs/test/login_flow_test.dart index d0d52be..8264aa4 100644 --- a/apps/acs/test/login_flow_test.dart +++ b/apps/acs/test/login_flow_test.dart @@ -1,74 +1,288 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:sinalacs_acs/app/app.dart'; +import 'package:sinalacs_acs/core/network/backend_client.dart'; +import 'package:sinalacs_acs/core/services/alert_queue.dart'; import 'package:sinalacs_acs/core/services/offline_visit_queue.dart'; +import 'support/fakes.dart'; + void main() { - testWidgets('deve autenticar e abrir dashboard com pacientes priorizados', (tester) async { - await tester.pumpWidget(const SinalAcsApp()); + group('login e painel', () { + testWidgets('deve autenticar no backend e assinar o tópico da própria microárea', (tester) async { + final backend = FakeAcsBackend(); + late FakeAlertFeed feed; + + await tester.pumpWidget(SinalAcsApp( + backend: backend, + feedBuilder: (queue) => feed = FakeAlertFeed(queue), + )); + + await tester.tap(find.byKey(const Key('login_button'))); + await tester.pumpAndSettle(); + + expect(backend.loginCount, 1); + expect(find.text('Painel de Priorização'), findsOneWidget); + // O tópico vem da microárea do token, não de constante no app. + expect(feed.startedTopicMicroArea, seedMicroAreaId); + }); + + testWidgets('não deve abrir o painel quando a autenticação falha', (tester) async { + final backend = FakeAcsBackend( + loginFailure: const BackendFailure('Sem conexão com o servidor.'), + ); + + await tester.pumpWidget(SinalAcsApp( + backend: backend, + feedBuilder: (queue) => FakeAlertFeed(queue), + )); + + await tester.tap(find.byKey(const Key('login_button'))); + await tester.pumpAndSettle(); + + expect(find.text('Painel de Priorização'), findsNothing); + expect(find.byKey(const Key('login_error')), findsOneWidget); + }); + + testWidgets('deve barrar o acesso quando a sessão não tem microárea', (tester) async { + // Sem território não há como filtrar a fila; abrir o painel assim + // arriscaria mostrar alerta de outra microárea. + final backend = FakeAcsBackend(microAreaId: null); + + await tester.pumpWidget(SinalAcsApp( + backend: backend, + feedBuilder: (queue) => FakeAlertFeed(queue), + )); + + await tester.tap(find.byKey(const Key('login_button'))); + await tester.pumpAndSettle(); + + expect(find.text('Painel de Priorização'), findsNothing); + expect(find.byKey(const Key('login_error')), findsOneWidget); + }); + + testWidgets('deve exibir o alerta que chega pelo broker e confirmar o recebimento', (tester) async { + final backend = FakeAcsBackend(); + late FakeAlertFeed feed; + + await tester.pumpWidget(SinalAcsApp( + backend: backend, + feedBuilder: (queue) => feed = FakeAlertFeed(queue), + )); + + await tester.tap(find.byKey(const Key('login_button'))); + await tester.pumpAndSettle(); + + expect(find.text('Nenhum alerta na sua microárea agora.'), findsOneWidget); + + feed.deliver(testAlert(alertId: 'alerta-1')); + await tester.pumpAndSettle(); + + expect(find.byKey(const Key('alert_alerta-1')), findsOneWidget); + expect(find.text('Risco: Vermelho'), findsOneWidget); + + await tester.tap(find.byKey(const Key('ack_alerta-1'))); + await tester.pumpAndSettle(); + + expect(backend.acknowledgedAlertIds, ['alerta-1']); + expect(find.text('Recebimento confirmado'), findsOneWidget); + }); + + testWidgets('deve avisar quando a central de alertas está inacessível', (tester) async { + // Um ACS que não sabe que parou de receber alertas é o pior modo de falha + // do produto: a falha precisa ser visível, não silenciosa. + await tester.pumpWidget(SinalAcsApp( + backend: FakeAcsBackend(), + feedBuilder: (queue) => FakeAlertFeed(queue, failOnStart: true), + )); + + await tester.tap(find.byKey(const Key('login_button'))); + await tester.pumpAndSettle(); + + expect(find.byKey(const Key('feed_error')), findsOneWidget); + }); + + testWidgets('deve expor rótulo semântico e alvo de toque acessível no login do ACS', (tester) async { + await tester.pumpWidget(SinalAcsApp( + backend: FakeAcsBackend(), + feedBuilder: (queue) => FakeAlertFeed(queue), + )); - await tester.enterText(find.byKey(const Key('matricula_field')), 'ACS-001'); - await tester.enterText(find.byKey(const Key('senha_field')), '123456'); - await tester.tap(find.byKey(const Key('login_button'))); - await tester.pumpAndSettle(); + final loginButton = tester.widget(find.byKey(const Key('login_button'))); + final minimumSize = loginButton.style?.minimumSize?.resolve({}) ?? const Size(0, 0); - expect(find.text('Painel de Priorização'), findsOneWidget); - expect(find.text('Maria Oliveira, 72a'), findsOneWidget); - expect(find.text('Risco: Vermelho'), findsOneWidget); + expect(find.bySemanticsLabel('Entrar no painel de priorização'), findsOneWidget); + expect(minimumSize.height, greaterThanOrEqualTo(48)); + expect(minimumSize.width, greaterThanOrEqualTo(48)); + }); + + testWidgets('não deve pré-preencher credenciais no formulário', (tester) async { + await tester.pumpWidget(SinalAcsApp( + backend: FakeAcsBackend(), + feedBuilder: (queue) => FakeAlertFeed(queue), + )); + + expect(tester.widget(find.byKey(const Key('matricula_field'))).controller?.text, isEmpty); + expect(tester.widget(find.byKey(const Key('senha_field'))).controller?.text, isEmpty); + }); }); - testWidgets('deve expor rótulo semântico e alvo de toque acessível no login do ACS', (tester) async { - await tester.pumpWidget(const SinalAcsApp()); + group('fila de priorização', () { + test('deve ordenar por risco e, no mesmo risco, pelo mais antigo', () { + final queue = AlertQueue(microAreaId: seedMicroAreaId); + + queue.upsert(testAlert(alertId: 'verde', riskLevel: 'green', triggeredAt: DateTime.utc(2026, 9, 11, 8))); + queue.upsert(testAlert(alertId: 'vermelho-novo', triggeredAt: DateTime.utc(2026, 9, 11, 12))); + queue.upsert(testAlert(alertId: 'amarelo', riskLevel: 'yellow', triggeredAt: DateTime.utc(2026, 9, 11, 9))); + queue.upsert(testAlert(alertId: 'vermelho-antigo', triggeredAt: DateTime.utc(2026, 9, 11, 10))); + + expect( + queue.alerts.map((alert) => alert.alertId).toList(), + ['vermelho-antigo', 'vermelho-novo', 'amarelo', 'verde'], + ); + }); + + test('deve recusar alerta de outra microárea', () { + // Territorialização é invariante: a ACL do broker já restringe, e esta é a + // segunda barreira. + final queue = AlertQueue(microAreaId: seedMicroAreaId); + + expect(queue.upsert(testAlert(alertId: 'de-fora', microAreaId: otherMicroAreaId)), isFalse); + expect(queue.isEmpty, isTrue); + }); - final loginButton = tester.widget(find.byKey(const Key('login_button'))); - final minimumSize = loginButton.style?.minimumSize?.resolve({}) ?? const Size(0, 0); + test('não deve duplicar o mesmo alerta reentregue pelo broker', () { + // QoS 1 é at-least-once: a mesma mensagem pode chegar duas vezes. + final queue = AlertQueue(microAreaId: seedMicroAreaId); - expect(find.bySemanticsLabel('Entrar no painel de priorização'), findsOneWidget); - expect(minimumSize.height, greaterThanOrEqualTo(48)); - expect(minimumSize.width, greaterThanOrEqualTo(48)); + queue.upsert(testAlert(alertId: 'alerta-1')); + queue.upsert(testAlert(alertId: 'alerta-1')); + + expect(queue.alerts, hasLength(1)); + }); + + test('reentrega não deve desfazer um alerta já confirmado', () { + final queue = AlertQueue(microAreaId: seedMicroAreaId); + + queue.upsert(testAlert(alertId: 'alerta-1')); + queue.markAcknowledged('alerta-1'); + queue.upsert(testAlert(alertId: 'alerta-1')); + + expect(queue.alerts.single.acknowledged, isTrue); + }); }); - test('deve registrar visita localmente e sincronizar em fila offline', () async { - final queue = OfflineVisitQueue(); + group('fila offline de visitas', () { + OfflineVisitRecord visit(String name) => OfflineVisitRecord( + patientName: name, + risk: 'red', + status: 'PENDENTE', + ); + + test('deve enviar as visitas pendentes ao servidor e marcar como sincronizadas', () async { + final sender = FakeVisitSynchronizer(); + final queue = OfflineVisitQueue(synchronizer: sender); + + await queue.add(visit('Maria Souza')); + expect(queue.pendingCount, 1); - await queue.add( - OfflineVisitRecord( - patientName: 'Maria Souza', - risk: 'VERMELHO', - status: 'PENDENTE', - ), - ); + final result = await queue.sync(); - expect(queue.pendingCount, 1); + expect(result.kind, SyncOutcomeKind.synced); + expect(queue.pendingCount, 0); + expect(queue.syncedCount, 1); + expect(sender.batches.single, hasLength(1)); + }); - final result = await queue.sync(); + test('deve reencolar registros em conflito e reprocessar no retry', () async { + var firstPass = true; + final sender = FakeVisitSynchronizer( + statusFor: (_) => firstPass ? 'conflict' : 'synced', + ); + final queue = OfflineVisitQueue(synchronizer: sender); - expect(result.kind, SyncOutcomeKind.synced); - expect(queue.pendingCount, 0); - expect(queue.syncedCount, 1); + await queue.add(visit('João Souza')); + + final conflict = await queue.sync(); + expect(conflict.kind, SyncOutcomeKind.conflict); + // Conflito volta para a fila: precisa de resolução, não de descarte. + expect(queue.pendingCount, 1); + expect(queue.conflictCount, 1); + + firstPass = false; + final retried = await queue.sync(); + expect(retried.kind, SyncOutcomeKind.synced); + expect(queue.pendingCount, 0); + expect(queue.syncedCount, 1); + }); + + test('deve manter a visita na fila quando a rede falha', () async { + // Perder a visita por falha de rede é exatamente o que a fila existe para + // evitar. + final sender = FakeVisitSynchronizer(throwOnPush: true); + final queue = OfflineVisitQueue(synchronizer: sender); + + await queue.add(visit('Ana Costa')); + final result = await queue.sync(); + + expect(result.kind, SyncOutcomeKind.error); + expect(queue.pendingCount, 1); + expect(queue.syncedCount, 0); + }); + + test('deve restaurar as visitas gravadas de uma execução anterior', () async { + // Fechar o app não pode perder a fila. + final store = InMemoryVisitStore(); + final first = OfflineVisitQueue(store: store); + await first.add(visit('Maria Souza')); + + final second = OfflineVisitQueue(store: store); + await second.restore(); + + expect(second.pendingCount, 1); + expect(second.pendingVisits.single.patientName, 'Maria Souza'); + }); + + test('não deve declarar sincronizado sem sincronizador configurado', () async { + // O comportamento antigo carimbava 'SINCRONIZADO' sem nada sair do + // dispositivo, o que escondia a ausência de integração. + final queue = OfflineVisitQueue(); + await queue.add(visit('Maria Souza')); + + final result = await queue.sync(); + + expect(result.kind, SyncOutcomeKind.error); + expect(queue.syncedCount, 0); + expect(queue.pendingCount, 1); + }); }); - test('deve reencolar registros em conflito e reprocessar no retry', () async { - final queue = OfflineVisitQueue(); + group('registro de visita', () { + testWidgets('deve enfileirar na MESMA fila do app, não em uma instância nova', (tester) async { + // A tela antes fazia OfflineVisitQueue() a cada gravação, e a visita era + // descartada no retorno do callback. + final visitQueue = OfflineVisitQueue(); + late FakeAlertFeed feed; + + await tester.pumpWidget(SinalAcsApp( + backend: FakeAcsBackend(), + feedBuilder: (queue) => feed = FakeAlertFeed(queue), + visitQueue: visitQueue, + )); - await queue.add( - OfflineVisitRecord( - patientName: 'João Souza', - risk: 'AMARELO', - status: 'PENDENTE', - ), - ); + await tester.tap(find.byKey(const Key('login_button'))); + await tester.pumpAndSettle(); - final conflict = await queue.sync(forceConflict: true); + feed.deliver(testAlert(alertId: 'alerta-1', riskLevel: 'yellow')); + await tester.pumpAndSettle(); - expect(conflict.kind, SyncOutcomeKind.conflict); - expect(queue.pendingCount, 1); - expect(queue.conflictCount, 1); + await tester.tap(find.text('Iniciar rota de visita')); + await tester.pumpAndSettle(); - final retried = await queue.sync(); + await tester.tap(find.byKey(const Key('save_visit'))); + await tester.pumpAndSettle(); - expect(retried.kind, SyncOutcomeKind.synced); - expect(queue.pendingCount, 0); - expect(queue.syncedCount, 1); + expect(visitQueue.pendingCount, 1); + }); }); } diff --git a/apps/acs/test/mqtt_secure_client_test.dart b/apps/acs/test/mqtt_secure_client_test.dart index fd2e737..1cde4fe 100644 --- a/apps/acs/test/mqtt_secure_client_test.dart +++ b/apps/acs/test/mqtt_secure_client_test.dart @@ -18,24 +18,45 @@ void main() { expect(json['patient_id'], 'patient-42'); expect(json['risk_level'], 'VERMELHO'); expect(json['micro_area_id'], 'microarea-01'); - expect(json['mqtt_topic'], '/alerts/microarea-01'); + // Precisa ser o MESMO namespace de AlertDelivery.topicPrefix no servidor e + // da ACL do broker. Este teste antes afirmava '/alerts/microarea-01', um + // namespace que não existe em nenhum dos dois lados. + expect(json['mqtt_topic'], 'sinalacs/v1/microareas/microarea-01/alerts'); }); - test('deve criar configuração MQTT segura com TLS e WSS', () { + test('deve montar os tópicos no namespace acordado com o backend', () { + expect( + alertTopicFor('00000000-0000-4000-8000-000000000003'), + 'sinalacs/v1/microareas/00000000-0000-4000-8000-000000000003/alerts', + ); + expect(ackTopicFor('alert-123'), 'sinalacs/v1/alerts/alert-123/acks'); + }); + + test('deve usar TCP/TLS por padrão, que é o único listener do broker', () { const config = SecureMqttConfig( brokerHost: 'broker.sinalacs.local', port: 8883, clientId: 'acs-client-01', - topic: '/alerts/microarea-01', - useTls: true, - useWebSocket: true, + topic: 'sinalacs/v1/microareas/microarea-01/alerts', ); expect(config.useTls, isTrue); - expect(config.useWebSocket, isTrue); + // O default era WebSocket, o que fazia connect() lançar UnsupportedError na + // configuração padrão — o cliente se autodesabilitava. + expect(config.useWebSocket, isFalse); + expect(config.connectionUri, 'ssl://broker.sinalacs.local:8883'); + }); + + test('deve montar URI WebSocket quando explicitamente pedido', () { + const config = SecureMqttConfig( + brokerHost: 'broker.sinalacs.local', + port: 8883, + clientId: 'acs-client-01', + topic: 'sinalacs/v1/microareas/microarea-01/alerts', + useWebSocket: true, + ); + expect(config.connectionUri, contains('wss://')); - expect(config.connectionUri, contains('8883')); - expect(config.connectionUri, contains('broker.sinalacs.local')); }); test('deve montar payload de confirmação do ACS para o alerta vermelho', () { diff --git a/apps/acs/test/support/fakes.dart b/apps/acs/test/support/fakes.dart new file mode 100644 index 0000000..c8500b8 --- /dev/null +++ b/apps/acs/test/support/fakes.dart @@ -0,0 +1,156 @@ +import 'package:sinalacs_acs/core/network/auth_session.dart'; +import 'package:sinalacs_acs/core/network/backend_client.dart'; +import 'package:sinalacs_acs/core/services/alert_feed.dart'; +import 'package:sinalacs_acs/core/services/alert_queue.dart'; +import 'package:sinalacs_acs/core/services/offline_visit_queue.dart'; +import 'package:sinalacs_client/sinalacs_client.dart'; + +/// UUIDs do seed de desenvolvimento. Dados sintéticos. +const seedAcsId = '00000000-0000-4000-8000-000000000002'; +const seedMicroAreaId = '00000000-0000-4000-8000-000000000003'; +const otherMicroAreaId = '00000000-0000-4000-8000-000000000099'; + +class FakeAcsBackend implements AcsBackend { + FakeAcsBackend({this.loginFailure, this.acknowledged = true, this.microAreaId = seedMicroAreaId}); + + BackendFailure? loginFailure; + bool acknowledged; + String? microAreaId; + + final List acknowledgedAlertIds = []; + int loginCount = 0; + + AuthSession? _session; + + @override + AuthSession? get session => _session; + + @override + bool get isAuthenticated => _session != null; + + @override + Future health() async => + ServiceHealth(status: 'ok', mqttConnected: true, dbConnected: true); + + @override + Future login() async { + loginCount++; + final failure = loginFailure; + if (failure != null) throw failure; + + final session = AuthSession( + accessToken: 'token-de-teste', + tokenType: 'Bearer', + userId: seedAcsId, + role: 'acs', + microAreaId: microAreaId, + expiresAt: DateTime.now().toUtc().add(const Duration(minutes: 15)), + ); + _session = session; + return session; + } + + @override + Future acknowledge({required String alertId}) async { + acknowledgedAlertIds.add(alertId); + return AlertAckResult( + alertId: alertId, + acknowledged: acknowledged, + status: acknowledged ? AlertStatus.acknowledged : null, + ); + } + + final List> syncedVisitBatches = >[]; + + @override + Future> syncVisits(List visits) async { + syncedVisitBatches.add(List.of(visits)); + return [ + for (final visit in visits) + VisitSyncResult( + localId: visit.localId, + syncStatus: SyncStatus.synced, + serverVersion: visit.version + 1, + ), + ]; + } + + @override + void close() {} +} + +/// Feed de alertas controlado pelo teste. +/// +/// Expõe a [AlertQueue] para que o teste empurre alertas como se tivessem +/// chegado pelo broker, sem precisar de rede nem de emulador. +class FakeAlertFeed implements AlertFeed { + FakeAlertFeed(this.queue, {this.failOnStart = false}); + + final AlertQueue queue; + final bool failOnStart; + + bool started = false; + bool stopped = false; + String? startedTopicMicroArea; + + @override + bool get isConnected => started; + + @override + Future start({required String microAreaId, required String acsId}) async { + if (failOnStart) throw StateError('broker indisponível'); + started = true; + startedTopicMicroArea = microAreaId; + } + + @override + void stop() { + started = false; + stopped = true; + } + + /// Simula a chegada de um alerta pelo tópico assinado. + void deliver(PrioritizedAlert alert) => queue.upsert(alert); +} + +/// Sincronizador que devolve o resultado programado pelo teste. +class FakeVisitSynchronizer implements VisitSynchronizer { + FakeVisitSynchronizer({this.statusFor, this.throwOnPush = false}); + + /// Status por localId; ausente significa `synced`. + String Function(OfflineVisitRecord visit)? statusFor; + bool throwOnPush; + + final List> batches = >[]; + + @override + Future> push(List visits) async { + batches.add(List.of(visits)); + if (throwOnPush) throw StateError('sem rede'); + + return [ + for (final visit in visits) + VisitSyncOutcome( + localId: visit.localId, + status: statusFor?.call(visit) ?? 'synced', + serverVersion: visit.version + 1, + ), + ]; + } +} + +PrioritizedAlert testAlert({ + required String alertId, + String riskLevel = 'red', + String microAreaId = seedMicroAreaId, + DateTime? triggeredAt, +}) { + return PrioritizedAlert( + alertId: alertId, + patientId: '00000000-0000-4000-8000-000000000001', + microAreaId: microAreaId, + riskLevel: riskLevel, + locationHash: 'sem-local-00', + triggeredAt: triggeredAt ?? DateTime.utc(2026, 9, 11, 12), + ); +} diff --git a/apps/acs/test/widget_test.dart b/apps/acs/test/widget_test.dart index eac310a..b714019 100644 --- a/apps/acs/test/widget_test.dart +++ b/apps/acs/test/widget_test.dart @@ -1,10 +1,17 @@ +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:sinalacs_acs/app/app.dart'; -import 'package:flutter/material.dart'; + +import 'support/fakes.dart'; void main() { testWidgets('deve abrir a tela de login do ACS', (tester) async { - await tester.pumpWidget(const SinalAcsApp()); + // O backend é injetado para manter o teste hermético: sem duplo, o app + // construiria o cliente real e tentaria falar com a rede. + await tester.pumpWidget(SinalAcsApp( + backend: FakeAcsBackend(), + feedBuilder: (queue) => FakeAlertFeed(queue), + )); expect(find.text('SinalACS'), findsOneWidget); expect(find.byKey(const Key('matricula_field')), findsOneWidget); diff --git a/apps/acs/tool/live_check.dart b/apps/acs/tool/live_check.dart new file mode 100644 index 0000000..3e3d990 --- /dev/null +++ b/apps/acs/tool/live_check.dart @@ -0,0 +1,143 @@ +/// Verificação do ciclo completo do alerta vermelho, com o código de rede real +/// do app do ACS. +/// +/// Roda na VM, sem emulador, e prova o caminho que nenhum teste hermético pode +/// provar: paciente cria o alerta pelo RPC → o backend publica no broker → o +/// ACS recebe pelo MQTT com TLS → confirma pelo RPC. +/// +/// Pré-requisitos: +/// · `docker compose up` com o database-seed concluído; +/// · `./scripts/dev/sync_dev_ca.sh` (a CA é regerada, não versionada). +/// +/// cd apps/acs +/// dart run tool/live_check.dart +/// dart run tool/live_check.dart --host http://10.0.2.2:8080/ --broker 10.0.2.2 +/// dart run tool/live_check.dart --mqtt-password "$MQTT_ACS_PASSWORD" +library; + +import 'dart:async'; +import 'dart:io'; + +import 'package:sinalacs_acs/core/network/backend_client.dart'; +import 'package:sinalacs_acs/core/network/backend_config.dart'; +import 'package:sinalacs_acs/core/services/mqtt_secure_client.dart'; +import 'package:sinalacs_client/sinalacs_client.dart' as api; + +String _arg(List args, String name, String fallback) { + final index = args.indexOf('--$name'); + return index >= 0 && index + 1 < args.length ? args[index + 1] : fallback; +} + +Future main(List args) async { + final host = _arg(args, 'host', 'http://localhost:8080/'); + final brokerHost = _arg(args, 'broker', 'localhost'); + final caPath = _arg(args, 'ca', '../../infra/docker/mosquitto/runtime/certs/ca.crt'); + + final backend = BackendClient(host: host); + // O paciente usa o cliente gerado diretamente: aqui ele só encena o disparo + // para que o ACS tenha o que receber. + final patientClient = api.Client(host)..connectivityMonitor = null; + + stdout.writeln('ACS → $host broker → $brokerHost:8883'); + MqttSecureClient? mqtt; + + try { + final session = await backend.login(); + final microAreaId = session.microAreaId!; + stdout.writeln(' login .............. papel=${session.role} microárea=$microAreaId'); + + // Usa o MqttSecureClient direto em vez da AlertQueue: a fila depende de + // ChangeNotifier (Flutter) e este script roda na VM. A ordenação e o filtro + // de microárea da fila são cobertos pelos testes herméticos. + // Acumula TUDO que chegar e espera depois pelo alerta específico criado + // aqui. Com sessão persistente (cleanSession = false) e clientId estável, o + // broker entrega primeiro o que ficou pendente de execuções anteriores — + // comportamento desejado, e a razão de um alerta não sumir enquanto o ACS + // está sem sinal. Casar com "o primeiro que chegar" daria OK para o alerta + // errado, inclusive se o novo nunca chegasse. + final inbox = {}; + + mqtt = MqttSecureClient( + config: SecureMqttConfig( + brokerHost: brokerHost, + port: 8883, + clientId: 'sinalacs-acs-livecheck-${session.userId}', + topic: alertTopicFor(microAreaId), + // Default vem do BackendConfig (não de literais soltos aqui), mas a + // senha do broker local é gerada por scripts/dev/bootstrap_env.sh e é + // diferente a cada máquina — por isso o --mqtt-password, que + // scripts/qa/e2e.sh preenche a partir do .env. + username: _arg(args, 'mqtt-user', BackendConfig.mqttUsername), + password: _arg(args, 'mqtt-password', BackendConfig.mqttPassword), + caCertificate: File(caPath).readAsBytesSync(), + ), + ); + + await mqtt.connect(onAlert: (alert) => inbox[alert.alertId] = alert); + stdout.writeln(' broker ............. conectado com TLS, assinando ${alertTopicFor(microAreaId)}'); + + // Dispara pelo lado do paciente. + final patientLogin = await patientClient.auth.developmentLogin(role: 'patient'); + final created = await patientClient.alerts.createRedAlert( + accessToken: patientLogin.accessToken, + idempotencyKey: 'acs-live-check-${DateTime.now().millisecondsSinceEpoch}', + locationHash: 'sem-local-00', + ); + stdout.writeln(' alerta criado ...... ${created.alertId} (publicado=${created.published})'); + + final deadline = DateTime.now().add(const Duration(seconds: 15)); + while (!inbox.containsKey(created.alertId) && DateTime.now().isBefore(deadline)) { + await Future.delayed(const Duration(milliseconds: 200)); + } + final alert = inbox[created.alertId]; + if (alert == null) { + throw TimeoutException('o alerta ${created.alertId} não chegou pelo broker'); + } + stdout.writeln(' recebido pelo MQTT . ${alert.alertId} risco=${alert.riskLevel}'); + final backlog = inbox.length - 1; + if (backlog > 0) { + stdout.writeln(' backlog da sessão .. $backlog alerta(s) pendente(s) reentregue(s)'); + } + + final ack = await backend.acknowledge(alertId: alert.alertId); + stdout.writeln(' ACK ................ acknowledged=${ack.acknowledged} status=${ack.status?.name}'); + if (!ack.acknowledged) { + stderr.writeln(' ERRO: a central não confirmou o recebimento.'); + exitCode = 1; + } + + // Sincronização de visita, pelo endpoint novo. + final localId = '00000000-0000-4000-8000-${DateTime.now().millisecondsSinceEpoch % 1000000000000}'; + final synced = await backend.syncVisits([ + api.VisitSyncEntry( + localId: localId.padRight(36, '0').substring(0, 36), + patientId: alert.patientId, + scheduledAt: DateTime.now().toUtc(), + status: 'realizada', + riskLevelBefore: api.RiskLevel.red, + notes: const {}, + version: 0, + ), + ]); + stdout.writeln(' visita sincronizada ${synced.single.syncStatus.name} ' + 'versão=${synced.single.serverVersion}'); + if (synced.single.syncStatus != api.SyncStatus.synced) { + stderr.writeln(' ERRO: a visita não sincronizou (${synced.single.message}).'); + exitCode = 1; + } + + stdout.writeln(exitCode == 0 + ? '\nOK — o ciclo do alerta vermelho fecha ponta a ponta pelos apps.' + : '\nFALHOU.'); + } on BackendFailure catch (failure) { + stderr.writeln(' falhou: ${failure.message}'); + exitCode = 1; + } catch (error) { + stderr.writeln(' falhou: $error'); + exitCode = 1; + } finally { + mqtt?.disconnect(); + backend.close(); + patientClient.close(); + } +} diff --git a/apps/patient/.flutter-plugins-dependencies b/apps/patient/.flutter-plugins-dependencies index fbbb5f9..fd6a93f 100644 --- a/apps/patient/.flutter-plugins-dependencies +++ b/apps/patient/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"connectivity_plus","path":"/home/codespace/.pub-cache/hosted/pub.dev/connectivity_plus-7.3.1/","native_build":true,"dependencies":[]},{"name":"flutter_local_notifications","path":"/home/codespace/.pub-cache/hosted/pub.dev/flutter_local_notifications-17.2.4/","native_build":true,"dependencies":[]},{"name":"geolocator_apple","path":"/home/codespace/.pub-cache/hosted/pub.dev/geolocator_apple-2.3.14/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"sqflite_darwin","path":"/home/codespace/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"sqflite_sqlcipher","path":"/home/codespace/.pub-cache/hosted/pub.dev/sqflite_sqlcipher-3.2.0/","native_build":true,"dependencies":[]}],"android":[{"name":"connectivity_plus","path":"/home/codespace/.pub-cache/hosted/pub.dev/connectivity_plus-7.3.1/","native_build":true,"dependencies":[]},{"name":"flutter_local_notifications","path":"/home/codespace/.pub-cache/hosted/pub.dev/flutter_local_notifications-17.2.4/","native_build":true,"dependencies":[]},{"name":"geolocator_android","path":"/home/codespace/.pub-cache/hosted/pub.dev/geolocator_android-4.6.2/","native_build":true,"dependencies":[]},{"name":"sqflite_android","path":"/home/codespace/.pub-cache/hosted/pub.dev/sqflite_android-2.4.0/","native_build":true,"dependencies":[]},{"name":"sqflite_sqlcipher","path":"/home/codespace/.pub-cache/hosted/pub.dev/sqflite_sqlcipher-3.2.0/","native_build":true,"dependencies":[]}],"macos":[{"name":"connectivity_plus","path":"/home/codespace/.pub-cache/hosted/pub.dev/connectivity_plus-7.3.1/","native_build":true,"dependencies":[]},{"name":"flutter_local_notifications","path":"/home/codespace/.pub-cache/hosted/pub.dev/flutter_local_notifications-17.2.4/","native_build":true,"dependencies":[]},{"name":"geolocator_apple","path":"/home/codespace/.pub-cache/hosted/pub.dev/geolocator_apple-2.3.14/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"sqflite_darwin","path":"/home/codespace/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"sqflite_sqlcipher","path":"/home/codespace/.pub-cache/hosted/pub.dev/sqflite_sqlcipher-3.2.0/","native_build":true,"dependencies":[]}],"linux":[{"name":"connectivity_plus","path":"/home/codespace/.pub-cache/hosted/pub.dev/connectivity_plus-7.3.1/","native_build":false,"dependencies":[]},{"name":"flutter_local_notifications_linux","path":"/home/codespace/.pub-cache/hosted/pub.dev/flutter_local_notifications_linux-4.0.1/","native_build":false,"dependencies":[]}],"windows":[{"name":"connectivity_plus","path":"/home/codespace/.pub-cache/hosted/pub.dev/connectivity_plus-7.3.1/","native_build":true,"dependencies":[]},{"name":"geolocator_windows","path":"/home/codespace/.pub-cache/hosted/pub.dev/geolocator_windows-0.2.5/","native_build":true,"dependencies":[]}],"web":[{"name":"connectivity_plus","path":"/home/codespace/.pub-cache/hosted/pub.dev/connectivity_plus-7.3.1/","dependencies":[]},{"name":"geolocator_web","path":"/home/codespace/.pub-cache/hosted/pub.dev/geolocator_web-4.1.4/","dependencies":[]}]},"dependencyGraph":[{"name":"connectivity_plus","dependencies":[]},{"name":"flutter_local_notifications","dependencies":["flutter_local_notifications_linux"]},{"name":"flutter_local_notifications_linux","dependencies":[]},{"name":"geolocator","dependencies":["geolocator_android","geolocator_apple","geolocator_web","geolocator_windows"]},{"name":"geolocator_android","dependencies":[]},{"name":"geolocator_apple","dependencies":[]},{"name":"geolocator_web","dependencies":[]},{"name":"geolocator_windows","dependencies":[]},{"name":"sqflite","dependencies":["sqflite_android","sqflite_darwin"]},{"name":"sqflite_android","dependencies":[]},{"name":"sqflite_darwin","dependencies":[]},{"name":"sqflite_sqlcipher","dependencies":[]}],"date_created":"2026-09-01 23:27:30.367246","version":"3.24.5","swift_package_manager_enabled":false} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"connectivity_plus","path":"/home/rock/.pub-cache/hosted/pub.dev/connectivity_plus-7.3.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_local_notifications","path":"/home/rock/.pub-cache/hosted/pub.dev/flutter_local_notifications-17.2.4/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"geolocator_apple","path":"/home/rock/.pub-cache/hosted/pub.dev/geolocator_apple-2.3.14/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"integration_test","path":"/home/rock/flutter/packages/integration_test/","native_build":true,"dependencies":[],"dev_dependency":true},{"name":"sqflite_darwin","path":"/home/rock/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqflite_sqlcipher","path":"/home/rock/.pub-cache/hosted/pub.dev/sqflite_sqlcipher-3.2.0/","native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"connectivity_plus","path":"/home/rock/.pub-cache/hosted/pub.dev/connectivity_plus-7.3.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_local_notifications","path":"/home/rock/.pub-cache/hosted/pub.dev/flutter_local_notifications-17.2.4/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"geolocator_android","path":"/home/rock/.pub-cache/hosted/pub.dev/geolocator_android-4.6.2/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"integration_test","path":"/home/rock/flutter/packages/integration_test/","native_build":true,"dependencies":[],"dev_dependency":true},{"name":"sqflite_android","path":"/home/rock/.pub-cache/hosted/pub.dev/sqflite_android-2.4.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqflite_sqlcipher","path":"/home/rock/.pub-cache/hosted/pub.dev/sqflite_sqlcipher-3.2.0/","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"connectivity_plus","path":"/home/rock/.pub-cache/hosted/pub.dev/connectivity_plus-7.3.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_local_notifications","path":"/home/rock/.pub-cache/hosted/pub.dev/flutter_local_notifications-17.2.4/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"geolocator_apple","path":"/home/rock/.pub-cache/hosted/pub.dev/geolocator_apple-2.3.14/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqflite_darwin","path":"/home/rock/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqflite_sqlcipher","path":"/home/rock/.pub-cache/hosted/pub.dev/sqflite_sqlcipher-3.2.0/","native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"connectivity_plus","path":"/home/rock/.pub-cache/hosted/pub.dev/connectivity_plus-7.3.1/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"flutter_local_notifications_linux","path":"/home/rock/.pub-cache/hosted/pub.dev/flutter_local_notifications_linux-4.0.1/","native_build":false,"dependencies":[],"dev_dependency":false}],"windows":[{"name":"connectivity_plus","path":"/home/rock/.pub-cache/hosted/pub.dev/connectivity_plus-7.3.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"geolocator_windows","path":"/home/rock/.pub-cache/hosted/pub.dev/geolocator_windows-0.2.5/","native_build":true,"dependencies":[],"dev_dependency":false}],"web":[{"name":"connectivity_plus","path":"/home/rock/.pub-cache/hosted/pub.dev/connectivity_plus-7.3.1/","dependencies":[],"dev_dependency":false},{"name":"geolocator_web","path":"/home/rock/.pub-cache/hosted/pub.dev/geolocator_web-4.1.4/","dependencies":[],"dev_dependency":false}]},"dependencyGraph":[{"name":"connectivity_plus","dependencies":[]},{"name":"flutter_local_notifications","dependencies":["flutter_local_notifications_linux"]},{"name":"flutter_local_notifications_linux","dependencies":[]},{"name":"geolocator","dependencies":["geolocator_android","geolocator_apple","geolocator_web","geolocator_windows"]},{"name":"geolocator_android","dependencies":[]},{"name":"geolocator_apple","dependencies":[]},{"name":"geolocator_web","dependencies":[]},{"name":"geolocator_windows","dependencies":[]},{"name":"integration_test","dependencies":[]},{"name":"sqflite","dependencies":["sqflite_android","sqflite_darwin"]},{"name":"sqflite_android","dependencies":[]},{"name":"sqflite_darwin","dependencies":[]},{"name":"sqflite_sqlcipher","dependencies":[]}],"date_created":"2026-09-11 17:27:51.593179","version":"3.44.8","swift_package_manager_enabled":{"ios":false,"macos":false}} \ No newline at end of file diff --git a/apps/patient/android/app/src/debug/AndroidManifest.xml b/apps/patient/android/app/src/debug/AndroidManifest.xml index 399f698..cfc90aa 100644 --- a/apps/patient/android/app/src/debug/AndroidManifest.xml +++ b/apps/patient/android/app/src/debug/AndroidManifest.xml @@ -4,4 +4,9 @@ to allow setting breakpoints, to provide hot reload, etc. --> + + + diff --git a/apps/patient/android/app/src/debug/res/xml/network_security_config.xml b/apps/patient/android/app/src/debug/res/xml/network_security_config.xml new file mode 100644 index 0000000..0e78459 --- /dev/null +++ b/apps/patient/android/app/src/debug/res/xml/network_security_config.xml @@ -0,0 +1,19 @@ + + + + + + 10.0.2.2 + + localhost + 127.0.0.1 + + diff --git a/apps/patient/integration_test/backend_connection_test.dart b/apps/patient/integration_test/backend_connection_test.dart new file mode 100644 index 0000000..83c7745 --- /dev/null +++ b/apps/patient/integration_test/backend_connection_test.dart @@ -0,0 +1,142 @@ +/// Conexão real do app do paciente com o backend Serverpod. +/// +/// Ao contrário dos testes em `test/`, estes NÃO são herméticos: exigem a stack +/// local de pé (`docker compose up`, com o database-seed concluído). Ficam em +/// `integration_test/` justamente por isso — `flutter test` não os executa, e o +/// CI segue hermético. +/// +/// flutter test integration_test \ +/// --dart-define=SINALACS_HOST=http://10.0.2.2:8080/ +/// +/// O default é 10.0.2.2, o host da máquina visto de dentro do emulador Android. +/// +/// PRIVACIDADE: só os UUIDs sintéticos do seed. Nenhum dado real de paciente, +/// e o token nunca é impresso. +library; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:sinalacs_client/sinalacs_client.dart'; +import 'package:sinalacs_patient/core/network/backend_client.dart'; +import 'package:sinalacs_patient/core/network/idempotency.dart'; +import 'package:sinalacs_patient/core/privacy/location_hash.dart'; + +const seedMicroAreaId = '00000000-0000-4000-8000-000000000003'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + late BackendClient backend; + + setUp(() => backend = BackendClient()); + tearDown(() => backend.close()); + + test('a stack responde à sonda de saúde', () async { + final health = await backend.health(); + + expect(health.status, 'ok'); + expect(health.dbConnected, isTrue, + reason: 'sem banco, createRedAlert falha por chave estrangeira'); + }); + + test('o login devolve uma sessão com a microárea do seed', () async { + final session = await backend.login(); + + expect(session.role, 'patient'); + // A microárea sai do payload do token; é o que define o tópico que o ACS + // assina, então tem de casar com o seed. + expect(session.microAreaId, seedMicroAreaId); + expect(session.isExpired(), isFalse); + }); + + test('a triagem é classificada pelo motor do servidor', () async { + final red = await backend.evaluateTriage( + chestPain: true, + difficultyBreathing: false, + fever: false, + persistentVomiting: false, + bleeding: false, + severeWeakness: false, + ); + final yellow = await backend.evaluateTriage( + chestPain: false, + difficultyBreathing: false, + fever: true, + persistentVomiting: false, + bleeding: false, + severeWeakness: false, + ); + final green = await backend.evaluateTriage( + chestPain: false, + difficultyBreathing: false, + fever: false, + persistentVomiting: false, + bleeding: false, + severeWeakness: false, + ); + + expect(red, RiskLevel.red); + expect(yellow, RiskLevel.yellow); + expect(green, RiskLevel.green); + }); + + test('a mesma resposta produz sempre o mesmo risco', () async { + // Determinismo é invariante (INV-02): a classificação não pode variar entre + // chamadas idênticas. + final results = []; + for (var attempt = 0; attempt < 3; attempt++) { + results.add(await backend.evaluateTriage( + chestPain: false, + difficultyBreathing: true, + fever: true, + persistentVomiting: false, + bleeding: false, + severeWeakness: false, + )); + } + + expect(results, everyElement(RiskLevel.red)); + }); + + test('o alerta vermelho é criado e o reenvio não duplica', () async { + await backend.login(); + final key = newIdempotencyKey(); + final hash = locationHashFrom(-23.55052, -46.633308); + + final first = await backend.createRedAlert(idempotencyKey: key, locationHash: hash); + final retry = await backend.createRedAlert(idempotencyKey: key, locationHash: hash); + + expect(first.alertId, isNotEmpty); + expect(first.status, AlertStatus.pending); + // Retry da MESMA tentativa: um segundo alerta aqui seria um chamado + // duplicado na fila do ACS. + expect(retry.alertId, first.alertId); + }); + + test('a chave de idempotência reusada com outra localização é recusada', () async { + await backend.login(); + final key = newIdempotencyKey(); + + await backend.createRedAlert( + idempotencyKey: key, + locationHash: locationHashFrom(-23.55052, -46.633308), + ); + + expect( + () => backend.createRedAlert( + idempotencyKey: key, + locationHash: locationHashFrom(-22.90685, -43.17290), + ), + throwsA(isA()), + ); + }); + + test('o hash de localização não revela a coordenada', () async { + // LGPD: o que trafega é o hash; a coordenada não pode ser legível nele. + final hash = locationHashFrom(-23.55052, -46.633308); + + expect(hash, hasLength(12)); + expect(hash, isNot(contains('23.55'))); + expect(hash, isNot(contains('46.63'))); + }); +} diff --git a/apps/patient/lib/app/app.dart b/apps/patient/lib/app/app.dart index 08b6031..7b178ea 100644 --- a/apps/patient/lib/app/app.dart +++ b/apps/patient/lib/app/app.dart @@ -1,23 +1,84 @@ import 'package:flutter/material.dart'; +import 'package:sinalacs_client/sinalacs_client.dart' show RiskLevel; import 'package:sinalacs_patient/app/patient_theme.dart'; +import 'package:sinalacs_patient/core/network/backend_client.dart'; +import 'package:sinalacs_patient/core/network/backend_scope.dart'; +import 'package:sinalacs_patient/core/network/idempotency.dart'; +import 'package:sinalacs_patient/core/privacy/location_hash.dart'; -class SinalAcsApp extends StatelessWidget { - const SinalAcsApp({super.key}); +class SinalAcsApp extends StatefulWidget { + const SinalAcsApp({super.key, this.backend}); + + /// Injetável para teste. Em execução normal é o [BackendClient] real. + final PatientBackend? backend; + + @override + State createState() => _SinalAcsAppState(); +} + +class _SinalAcsAppState extends State { + late final PatientBackend _backend = widget.backend ?? BackendClient(); + + @override + void dispose() { + // Só fecha o que este widget criou; um backend injetado é de quem injetou. + if (widget.backend == null) _backend.close(); + super.dispose(); + } @override Widget build(BuildContext context) { - return MaterialApp( - title: 'SinalACS Paciente', - debugShowCheckedModeBanner: false, - theme: buildPatientTheme(), - home: const PatientLoginScreen(), + return BackendScope( + backend: _backend, + child: MaterialApp( + title: 'SinalACS Paciente', + debugShowCheckedModeBanner: false, + theme: buildPatientTheme(), + home: const PatientLoginScreen(), + ), ); } } -class PatientLoginScreen extends StatelessWidget { +class PatientLoginScreen extends StatefulWidget { const PatientLoginScreen({super.key}); + @override + State createState() => _PatientLoginScreenState(); +} + +class _PatientLoginScreenState extends State { + bool _busy = false; + String? _error; + + /// Autentica de verdade contra `auth.developmentLogin` e só navega em caso de + /// sucesso. Antes a tela navegava incondicionalmente, ignorando o que era + /// digitado — não havia como saber se o backend estava sequer alcançável. + Future _enter() async { + setState(() { + _busy = true; + _error = null; + }); + + try { + await BackendScope.of(context).login(); + if (!mounted) return; + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (_) => const PatientHomeShell( + initialDestination: PatientDestination.triage, + ), + ), + ); + } on BackendFailure catch (failure) { + if (!mounted) return; + setState(() { + _busy = false; + _error = failure.message; + }); + } + } + @override Widget build(BuildContext context) { return Scaffold( @@ -71,14 +132,31 @@ class PatientLoginScreen extends StatelessWidget { width: double.infinity, child: FilledButton( key: const Key('enter_button'), - onPressed: () => Navigator.of(context).pushReplacement( - MaterialPageRoute(builder: (_) => const PatientHomeShell(initialDestination: PatientDestination.triage)), - ), + onPressed: _busy ? null : _enter, style: FilledButton.styleFrom(minimumSize: const Size(48, 52)), - child: const Text('Entrar sem senha'), + child: _busy + ? const SizedBox( + height: 22, + width: 22, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Text('Entrar sem senha'), ), ), ), + if (_error != null) + Padding( + padding: const EdgeInsets.only(top: 16), + child: Text( + key: const Key('login_error'), + _error!, + textAlign: TextAlign.center, + style: const TextStyle( + color: PatientColors.danger, + fontWeight: FontWeight.bold, + ), + ), + ), const SizedBox(height: 12), SizedBox( width: double.infinity, @@ -181,6 +259,15 @@ class EmergencyScreen extends StatefulWidget { class _EmergencyScreenState extends State { String _state = 'Pronto para enviar'; + bool _busy = false; + + /// Chave da tentativa corrente. + /// + /// Gerada uma única vez por confirmação e mantida enquanto o envio não + /// conclui: se a pessoa tocar de novo depois de uma falha de rede, o servidor + /// reconhece a mesma chave e devolve o mesmo alerta em vez de criar um + /// segundo. Só é descartada quando o alerta é aceito. + String? _attemptKey; Future _sendAlert() async { final confirmed = await showDialog( @@ -195,7 +282,38 @@ class _EmergencyScreenState extends State { ), ); if (confirmed != true || !mounted) return; - setState(() => _state = 'Alerta enfileirado localmente'); + + final backend = BackendScope.of(context); + final key = _attemptKey ??= newIdempotencyKey(); + + setState(() { + _busy = true; + _state = 'Enviando alerta...'; + }); + + try { + final result = await backend.createRedAlert( + idempotencyKey: key, + // A localização real entra aqui quando o permissionamento de GPS for + // integrado; o contrato só aceita o hash, então a coordenada crua nunca + // sai do dispositivo (LGPD). + locationHash: unknownLocationHash, + ); + if (!mounted) return; + setState(() { + _busy = false; + _attemptKey = null; + _state = 'Alerta recebido pela equipe' + '${result.published ? '' : ' — aguardando a rede para notificar'}'; + }); + } on BackendFailure catch (failure) { + if (!mounted) return; + // A chave NÃO é limpa aqui: o retry precisa reusar a mesma tentativa. + setState(() { + _busy = false; + _state = failure.message; + }); + } } @override @@ -214,7 +332,7 @@ class _EmergencyScreenState extends State { height: 208, child: FilledButton( key: const Key('panic_button'), - onPressed: _sendAlert, + onPressed: _busy ? null : _sendAlert, style: FilledButton.styleFrom( backgroundColor: PatientColors.danger, shape: const CircleBorder(), @@ -239,44 +357,210 @@ class TriageScreen extends StatefulWidget { State createState() => _TriageScreenState(); } +/// Um sintoma do formulário, ligado ao parâmetro correspondente de +/// `triage.evaluate`. +/// +/// As perguntas são exatamente as seis que o motor do servidor conhece. Antes +/// eram três perguntas de múltipla escolha que **não** mapeavam para o contrato, +/// e o risco era calculado no cliente por comparação de string — duas regras de +/// risco no mesmo produto, o que viola o determinismo exigido pelo PRD (INV-02). +enum TriageSymptom { + chestPain('chest_pain', 'Você está com dor no peito?'), + difficultyBreathing('difficulty_breathing', 'Você está com falta de ar?'), + bleeding('bleeding', 'Você está com algum sangramento?'), + severeWeakness('severe_weakness', 'Você está com fraqueza intensa ou desmaio?'), + fever('fever', 'Você está com febre?'), + persistentVomiting('persistent_vomiting', 'Você está com vômitos que não param?'); + + const TriageSymptom(this.key, this.question); + + final String key; + final String question; +} + class _TriageScreenState extends State { - int _step = 0; - final List _answers = List.filled(3, null); - final _questions = const [ - ('Qual o sintoma principal?', ['Falta de ar ou cansaço intenso', 'Tontura ou pressão alterada', 'Dor localizada ou febre moderada', 'Dúvida de rotina ou medicação']), - ('O sintoma começou de forma súbita?', ['Sim, começou de repente', 'Não, começou aos poucos']), - ('Há algum sinal de agravamento?', ['Dor no peito ou sangramento', 'Sem sinal de agravamento']), - ]; + static const _symptoms = TriageSymptom.values; - String get _risk => _answers.any((answer) => answer == 'Dor no peito ou sangramento' || answer == 'Falta de ar ou cansaço intenso') ? 'Risco: Vermelho' : _answers.any((answer) => answer != null) ? 'Risco: Amarelo' : 'Risco: Verde'; + int _step = 0; + final Map _answers = {}; + bool _busy = false; + String? _error; + + /// Risco devolvido pelo servidor. `null` enquanto a triagem não foi concluída. + /// + /// Não existe cálculo de risco neste arquivo, e não deve passar a existir. + RiskLevel? _risk; + + bool get _isLastStep => _step == _symptoms.length - 1; + + Future _submit() async { + final answer = _answers[_symptoms[_step]]; + if (answer == null) return; + + if (!_isLastStep) { + setState(() => _step++); + return; + } + + setState(() { + _busy = true; + _error = null; + }); + + try { + final risk = await BackendScope.of(context).evaluateTriage( + chestPain: _answers[TriageSymptom.chestPain] ?? false, + difficultyBreathing: _answers[TriageSymptom.difficultyBreathing] ?? false, + fever: _answers[TriageSymptom.fever] ?? false, + persistentVomiting: _answers[TriageSymptom.persistentVomiting] ?? false, + bleeding: _answers[TriageSymptom.bleeding] ?? false, + severeWeakness: _answers[TriageSymptom.severeWeakness] ?? false, + ); + if (!mounted) return; + setState(() { + _busy = false; + _risk = risk; + }); + } on BackendFailure catch (failure) { + if (!mounted) return; + setState(() { + _busy = false; + _error = failure.message; + }); + } + } @override Widget build(BuildContext context) { - final question = _questions[_step]; + final risk = _risk; + if (risk != null) return _TriageResult(risk: risk, onContinue: widget.onComplete); + + final symptom = _symptoms[_step]; + final answer = _answers[symptom]; + return ListView( padding: const EdgeInsets.all(20), children: [ - Text('Passo ${_step + 1} de 3', style: const TextStyle(color: PatientColors.accent, fontWeight: FontWeight.bold)), + Text( + 'Passo ${_step + 1} de ${_symptoms.length}', + style: const TextStyle(color: PatientColors.accent, fontWeight: FontWeight.bold), + ), const SizedBox(height: 8), - LinearProgressIndicator(value: (_step + 1) / 3), + LinearProgressIndicator(value: (_step + 1) / _symptoms.length), const SizedBox(height: 24), - Text(question.$1, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), + Text(symptom.question, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), const SizedBox(height: 16), - RadioGroup(groupValue: _answers[_step], onChanged: (value) => setState(() => _answers[_step] = value), child: Column(children: question.$2.map((answer) => Card(child: RadioListTile(key: Key(_step == 0 && answer.startsWith('Falta') ? 'difficulty_breathing' : _step == 2 && answer.startsWith('Dor') ? 'chest_pain' : 'triage_${_step}_${question.$2.indexOf(answer)}'), value: answer, title: Text(answer)))).toList())), + RadioGroup( + groupValue: answer, + onChanged: (value) => setState(() => _answers[symptom] = value ?? false), + child: Column( + children: [ + Card( + child: RadioListTile( + key: Key(symptom.key), + value: true, + title: const Text('Sim'), + ), + ), + Card( + child: RadioListTile( + key: Key('${symptom.key}_no'), + value: false, + title: const Text('Não'), + ), + ), + ], + ), + ), const SizedBox(height: 20), FilledButton( key: const Key('submit_triage'), - onPressed: _answers[_step] == null ? null : () { - if (_step < 2) { - setState(() => _step++); - } else { - widget.onComplete?.call(); - } - }, + onPressed: answer == null || _busy ? null : _submit, + style: FilledButton.styleFrom(minimumSize: const Size(48, 52)), + child: _busy + ? const SizedBox(height: 22, width: 22, child: CircularProgressIndicator(strokeWidth: 2)) + : Text(_isLastStep ? 'Concluir triagem' : 'Próxima pergunta'), + ), + if (_error != null) + Padding( + padding: const EdgeInsets.only(top: 16), + child: Text( + key: const Key('triage_error'), + _error!, + textAlign: TextAlign.center, + style: const TextStyle(color: PatientColors.danger, fontWeight: FontWeight.bold), + ), + ), + ], + ); + } +} + +/// Exibe a classificação que veio do servidor. +/// +/// A cor é sinal clínico, nunca decoração: vermelho/amarelo/verde mapeiam +/// estritamente o [RiskLevel]. +class _TriageResult extends StatelessWidget { + const _TriageResult({required this.risk, this.onContinue}); + + final RiskLevel risk; + final VoidCallback? onContinue; + + @override + Widget build(BuildContext context) { + final (label, color, guidance) = switch (risk) { + RiskLevel.red => ( + 'Risco: Vermelho', + PatientColors.danger, + 'Sua equipe de saúde foi avisada com prioridade máxima. ' + 'Se piorar, ligue para o SAMU (192).', + ), + RiskLevel.yellow => ( + 'Risco: Amarelo', + const Color(0xFFE0A800), + 'Sua solicitação foi priorizada. O agente de saúde entrará em contato.', + ), + RiskLevel.green => ( + 'Risco: Verde', + PatientColors.accent, + 'Sem sinais de urgência. Sua solicitação entrou na fila de rotina.', + ), + }; + + return ListView( + padding: const EdgeInsets.all(20), + children: [ + Card( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + children: [ + Icon(Icons.verified_outlined, size: 44, color: color), + const SizedBox(height: 16), + Text( + key: const Key('triage_risk'), + label, + style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: color), + ), + const SizedBox(height: 12), + Text(guidance, textAlign: TextAlign.center), + const SizedBox(height: 8), + const Text( + 'Classificação feita pelo protocolo da equipe de saúde.', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 12, color: Colors.white54), + ), + ], + ), + ), + ), + const SizedBox(height: 20), + FilledButton( + key: const Key('triage_continue'), + onPressed: onContinue, style: FilledButton.styleFrom(minimumSize: const Size(48, 52)), - child: Text(_step == 2 ? 'Concluir triagem' : 'Próxima pergunta'), + child: const Text('Acompanhar solicitação'), ), - if (_step == 2) Padding(padding: const EdgeInsets.only(top: 16), child: Text(_risk, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold))), ], ); } diff --git a/apps/patient/lib/core/network/auth_session.dart b/apps/patient/lib/core/network/auth_session.dart new file mode 100644 index 0000000..ac8f115 --- /dev/null +++ b/apps/patient/lib/core/network/auth_session.dart @@ -0,0 +1,84 @@ +import 'dart:convert'; + +/// Sessão autenticada contra o backend. +/// +/// **Não é autenticação institucional.** O token vem de +/// `auth.developmentLogin`, que só existe com `ENABLE_DEV_LOGIN=true` e serve +/// para validar a conexão, não para proteger dado real. +class AuthSession { + const AuthSession({ + required this.accessToken, + required this.tokenType, + required this.userId, + required this.role, + required this.microAreaId, + required this.expiresAt, + }); + + final String accessToken; + final String tokenType; + final String userId; + final String role; + + /// Microárea do usuário. Restringe o acesso ao território (INV do PRD) e, no + /// app do ACS, define o tópico MQTT assinado. + final String? microAreaId; + + final DateTime expiresAt; + + /// Lê o payload do token emitido por `DevelopmentAuthService`. + /// + /// O token é um JWT HS256 cujo payload carrega `sub`, `role`, + /// `micro_area_id` e `exp`. A assinatura **não** é verificada aqui: verificar + /// é papel do servidor, e o app não tem — nem deve ter — o segredo HMAC. A + /// leitura serve apenas para o app conhecer a própria microárea e a + /// expiração, o que evita um endpoint extra só para isso. + /// + /// Devolve `null` para qualquer token malformado; quem chama decide o que + /// mostrar. Nunca registre o token em log. + static AuthSession? tryParse(String accessToken, String tokenType) { + final sections = accessToken.split('.'); + if (sections.length != 3) return null; + + try { + final decoded = utf8.decode( + base64Url.decode(base64Url.normalize(sections[1])), + ); + final payload = jsonDecode(decoded) as Map; + + final exp = payload['exp']; + final sub = payload['sub']; + final role = payload['role']; + if (exp is! int || sub is! String || role is! String) return null; + + return AuthSession( + accessToken: accessToken, + tokenType: tokenType, + userId: sub, + role: role, + microAreaId: payload['micro_area_id'] as String?, + expiresAt: DateTime.fromMillisecondsSinceEpoch( + exp * 1000, + isUtc: true, + ), + ); + } on FormatException { + return null; + } on TypeError { + return null; + } + } + + /// O token de desenvolvimento vive 15 minutos. + /// + /// A margem existe para não enviar um token que expira no meio da viagem: + /// sem ela, um fluxo longo falha com erro de permissão em vez de + /// reautenticar, que é um sintoma bem mais confuso de diagnosticar. + bool isExpired({ + DateTime? now, + Duration margin = const Duration(seconds: 30), + }) { + final reference = (now ?? DateTime.now().toUtc()).add(margin); + return !expiresAt.isAfter(reference); + } +} diff --git a/apps/patient/lib/core/network/backend_client.dart b/apps/patient/lib/core/network/backend_client.dart new file mode 100644 index 0000000..93bbc0c --- /dev/null +++ b/apps/patient/lib/core/network/backend_client.dart @@ -0,0 +1,198 @@ +import 'package:sinalacs_client/sinalacs_client.dart'; +import 'package:sinalacs_patient/core/network/auth_session.dart'; +import 'package:sinalacs_patient/core/network/backend_config.dart'; + +/// Falha já traduzida para a pessoa que está usando o app. +/// +/// O backend é RPC tipado, então o erro chega como exceção declarada no +/// `.spy.yaml` — não como código HTTP. Traduzir aqui mantém a UI livre de +/// `try/catch` espalhado e garante mensagem em português. +class BackendFailure implements Exception { + const BackendFailure(this.message, {this.isRecoverable = true}); + + final String message; + + /// `false` quando repetir a mesma ação não deve resolver (rota desligada, + /// permissão negada). + final bool isRecoverable; + + @override + String toString() => message; +} + +/// Contrato do backend visto pela UI do paciente. +/// +/// A UI depende desta abstração, nunca do [Client] gerado — mesmo padrão que o +/// servidor usa entre `application/` e `infrastructure/` (`AlertPublisher`, +/// `AlertStore`). É o que permite testar as telas sem rede. +abstract class PatientBackend { + AuthSession? get session; + + bool get isAuthenticated; + + Future health(); + + Future login(); + + Future evaluateTriage({ + required bool chestPain, + required bool difficultyBreathing, + required bool fever, + required bool persistentVomiting, + required bool bleeding, + required bool severeWeakness, + }); + + Future createRedAlert({ + required String idempotencyKey, + required String locationHash, + }); + + void close(); +} + +/// Fachada do backend para o app do paciente. +/// +/// Mantém um único [Client] e a [AuthSession] corrente. O cliente é o mesmo +/// código gerado por `serverpod generate` que o servidor usa, então qualquer +/// divergência de contrato quebra em tempo de compilação, não em produção. +class BackendClient implements PatientBackend { + BackendClient({String? host}) + : _client = Client(host ?? BackendConfig.host) + ..connectivityMonitor = null; + + final Client _client; + + AuthSession? _session; + + @override + AuthSession? get session => _session; + + @override + bool get isAuthenticated { + final current = _session; + return current != null && !current.isExpired(); + } + + /// Token válido para as chamadas que exigem autenticação. + /// + /// Reautentica sozinho quando o token de 15 minutos expirou — sem isso, um + /// fluxo demorado falha com erro de permissão, que esconde a causa real. + Future _requireToken() async { + if (!isAuthenticated) await login(); + final current = _session; + if (current == null) { + throw const BackendFailure('Sessão não iniciada.', isRecoverable: false); + } + return current.accessToken; + } + + @override + Future health() { + return _guard(() => _client.health.check()); + } + + /// Autentica como paciente. Ver ressalvas em [AuthSession]. + @override + Future login() async { + final result = await _guard( + () => _client.auth.developmentLogin(role: 'patient'), + ); + + final session = AuthSession.tryParse(result.accessToken, result.tokenType); + if (session == null) { + throw const BackendFailure( + 'O servidor devolveu um token que o aplicativo não entendeu.', + isRecoverable: false, + ); + } + + _session = session; + return session; + } + + /// Classificação de risco pelo motor determinístico do servidor. + /// + /// A regra de risco vive **só** no servidor (INV-02): o app envia sintomas e + /// exibe o que voltar. Não recalcule nem ajuste o resultado aqui. + @override + Future evaluateTriage({ + required bool chestPain, + required bool difficultyBreathing, + required bool fever, + required bool persistentVomiting, + required bool bleeding, + required bool severeWeakness, + }) async { + final result = await _guard( + () => _client.triage.evaluate( + chestPain: chestPain, + difficultyBreathing: difficultyBreathing, + fever: fever, + persistentVomiting: persistentVomiting, + bleeding: bleeding, + severeWeakness: severeWeakness, + ), + ); + return result.risk; + } + + /// Dispara o alerta vermelho. + /// + /// [idempotencyKey] precisa ser **estável para a mesma tentativa do usuário**: + /// é o que impede que um retry vire um segundo alerta. [locationHash] é o + /// hash da localização — coordenada crua nunca sai do dispositivo (LGPD). + @override + Future createRedAlert({ + required String idempotencyKey, + required String locationHash, + }) async { + final token = await _requireToken(); + return _guard( + () => _client.alerts.createRedAlert( + accessToken: token, + idempotencyKey: idempotencyKey, + locationHash: locationHash, + ), + ); + } + + @override + void close() => _client.close(); + + /// Traduz as exceções tipadas do backend para [BackendFailure]. + Future _guard(Future Function() call) async { + try { + return await call(); + } on EndpointDisabledException { + // ENABLE_DEV_LOGIN=false. O servidor responde como se a rota não + // existisse, de propósito. + throw const BackendFailure( + 'O acesso de desenvolvimento está desativado neste servidor.', + isRecoverable: false, + ); + } on AlertPermissionException { + throw const BackendFailure( + 'Este acesso não tem permissão para esta ação.', + isRecoverable: false, + ); + } on AlertValidationException catch (error) { + throw BackendFailure(error.message, isRecoverable: false); + } on AlertDispatchUnavailableException { + // O alerta FOI gravado; só a publicação imediata falhou. Dizer que + // "falhou" seria mentira e faria a pessoa tentar de novo sem necessidade. + throw const BackendFailure( + 'Alerta registrado. A rede está instável e ele será entregue à equipe ' + 'assim que a conexão voltar.', + ); + } on ServerpodClientException catch (error) { + throw BackendFailure( + 'Não foi possível falar com o servidor (${error.statusCode}).', + ); + } catch (_) { + throw const BackendFailure( + 'Sem conexão com o servidor. Verifique a rede e tente de novo.', + ); + } + } +} diff --git a/apps/patient/lib/core/network/backend_config.dart b/apps/patient/lib/core/network/backend_config.dart new file mode 100644 index 0000000..81d0ef5 --- /dev/null +++ b/apps/patient/lib/core/network/backend_config.dart @@ -0,0 +1,19 @@ +/// Endereço do backend, configurável em tempo de compilação. +/// +/// O default é o host da máquina de desenvolvimento visto de dentro do +/// emulador Android (`10.0.2.2`). Para outros alvos: +/// +/// flutter run --dart-define=SINALACS_HOST=http://localhost:8080/ +/// flutter run --dart-define=SINALACS_HOST=http://192.168.0.10:8080/ +/// +/// Cleartext HTTP só é permitido em builds de debug e só para esses hosts — +/// ver android/app/src/debug/res/xml/network_security_config.xml. +class BackendConfig { + const BackendConfig._(); + + /// A barra final é exigida pelo cliente Serverpod. + static const String host = String.fromEnvironment( + 'SINALACS_HOST', + defaultValue: 'http://10.0.2.2:8080/', + ); +} diff --git a/apps/patient/lib/core/network/backend_scope.dart b/apps/patient/lib/core/network/backend_scope.dart new file mode 100644 index 0000000..5c70e77 --- /dev/null +++ b/apps/patient/lib/core/network/backend_scope.dart @@ -0,0 +1,26 @@ +import 'package:flutter/widgets.dart'; +import 'package:sinalacs_patient/core/network/backend_client.dart'; + +/// Disponibiliza o [PatientBackend] para a árvore de widgets. +/// +/// Existe para que as telas não construam o próprio cliente: em teste, injeta-se +/// um duplo; em execução, o [BackendClient] real. +class BackendScope extends InheritedWidget { + const BackendScope({ + required this.backend, + required super.child, + super.key, + }); + + final PatientBackend backend; + + static PatientBackend of(BuildContext context) { + final scope = context.dependOnInheritedWidgetOfExactType(); + assert(scope != null, 'Nenhum BackendScope acima deste widget.'); + return scope!.backend; + } + + @override + bool updateShouldNotify(BackendScope oldWidget) => + backend != oldWidget.backend; +} diff --git a/apps/patient/lib/core/network/idempotency.dart b/apps/patient/lib/core/network/idempotency.dart new file mode 100644 index 0000000..659bb09 --- /dev/null +++ b/apps/patient/lib/core/network/idempotency.dart @@ -0,0 +1,21 @@ +import 'dart:math'; + +/// Gera a chave de idempotência de um alerta vermelho. +/// +/// O servidor usa esta chave para não criar um segundo alerta quando a mesma +/// tentativa é reenviada (`alerts.createRedAlert` grava a chave junto com o +/// alerta). Por isso a chave precisa ser gerada **uma vez por tentativa do +/// usuário** e reaproveitada em todos os retries dessa tentativa — gerar uma +/// nova a cada envio anularia a proteção e duplicaria o alerta. +String newIdempotencyKey() { + final random = Random.secure(); + final bytes = List.generate(16, (_) => random.nextInt(256)); + + // Formato UUID v4, só para manter a chave reconhecível em log do servidor. + bytes[6] = (bytes[6] & 0x0f) | 0x40; + bytes[8] = (bytes[8] & 0x3f) | 0x80; + + final hex = bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(); + return '${hex.substring(0, 8)}-${hex.substring(8, 12)}-' + '${hex.substring(12, 16)}-${hex.substring(16, 20)}-${hex.substring(20)}'; +} diff --git a/apps/patient/lib/core/privacy/location_hash.dart b/apps/patient/lib/core/privacy/location_hash.dart new file mode 100644 index 0000000..7246d70 --- /dev/null +++ b/apps/patient/lib/core/privacy/location_hash.dart @@ -0,0 +1,26 @@ +import 'dart:convert'; + +import 'package:crypto/crypto.dart'; + +/// Hash de localização enviado ao backend no lugar da coordenada. +/// +/// **LGPD:** latitude e longitude cruas não saem do dispositivo e não vão para +/// log. O servidor só precisa distinguir e agrupar locais, não sabê-los — por +/// isso `alerts.createRedAlert` recebe `locationHash`, nunca o par de +/// coordenadas. +/// +/// O esquema (sha256 sobre a coordenada com 6 casas, truncado em 12) é o mesmo +/// já usado no app do ACS, para que os dois lados produzam o mesmo hash para o +/// mesmo ponto. +String locationHashFrom(double latitude, double longitude) { + final normalized = + '${latitude.toStringAsFixed(6)}:${longitude.toStringAsFixed(6)}'; + return sha256.convert(utf8.encode(normalized)).toString().substring(0, 12); +} + +/// Hash usado quando a localização não está disponível. +/// +/// Um alerta vermelho sem GPS ainda precisa chegar à equipe — descartá-lo por +/// falta de coordenada violaria "alerta vermelho nunca some em silêncio". O +/// valor é constante e explicitamente reconhecível como ausência de local. +const String unknownLocationHash = 'sem-local-00'; diff --git a/apps/patient/pubspec.lock b/apps/patient/pubspec.lock index 63dbe8e..0ec166c 100644 --- a/apps/patient/pubspec.lock +++ b/apps/patient/pubspec.lock @@ -29,34 +29,26 @@ packages: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.3.0" - charcode: - dependency: transitive - description: - name: charcode - sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a - url: "https://pub.dev" - source: hosted - version: "1.4.0" + version: "1.4.1" clock: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" collection: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.1" connectivity_plus: dependency: "direct main" description: @@ -74,21 +66,13 @@ packages: source: hosted version: "2.1.0" crypto: - dependency: transitive + dependency: "direct main" description: name: crypto sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf url: "https://pub.dev" source: hosted version: "3.0.7" - csslib: - dependency: transitive - description: - name: csslib - sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" - url: "https://pub.dev" - source: hosted - version: "1.0.2" dbus: dependency: transitive description: @@ -97,22 +81,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.12" - event_bus: - dependency: transitive - description: - name: event_bus - sha256: "1a55e97923769c286d295240048fc180e7b0768902c3c2e869fe059aafa15304" - url: "https://pub.dev" - source: hosted - version: "2.0.1" fake_async: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.3" ffi: dependency: transitive description: @@ -121,6 +97,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.3" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" fixnum: dependency: transitive description: @@ -134,6 +118,11 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_driver: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" flutter_lints: dependency: "direct dev" description: @@ -176,6 +165,11 @@ packages: description: flutter source: sdk version: "0.0.0" + fuchsia_remote_debug_protocol: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" geolocator: dependency: "direct main" description: @@ -224,38 +218,51 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.5" - html: + http: + dependency: transitive + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_parser: dependency: transitive description: - name: html - sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "0.15.6" + version: "4.1.2" + integration_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "11.0.2" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" lints: dependency: transitive description: @@ -268,34 +275,26 @@ packages: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.19" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 - url: "https://pub.dev" - source: hosted - version: "1.15.0" - mqtt_client: - dependency: "direct main" - description: - name: mqtt_client - sha256: "37aae360fac0b3322cb37267696c1535e1ad2984a13064d877273ffe4b16ca7a" + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" url: "https://pub.dev" source: hosted - version: "10.5.1" + version: "1.18.0" nm: dependency: transitive description: @@ -308,10 +307,10 @@ packages: dependency: transitive description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" petitparser: dependency: transitive description: @@ -336,11 +335,58 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + process: + dependency: transitive + description: + name: process + sha256: "4242ba3508d37e01808bdf71ad1d5bb93a8d671bf2e7450e6b1b353fb0808891" + url: "https://pub.dev" + source: hosted + version: "5.0.6" + serverpod_auth_core_client: + dependency: transitive + description: + name: serverpod_auth_core_client + sha256: "60b89a1b854fd9c7d589737befd7e4d0cafe047600d65b12b137d827e4c6f6ba" + url: "https://pub.dev" + source: hosted + version: "3.4.13" + serverpod_auth_idp_client: + dependency: transitive + description: + name: serverpod_auth_idp_client + sha256: fb97444ef0df6ac0eca66be884f1107d943df29fb999b6e9a5ad2eb95de9010e + url: "https://pub.dev" + source: hosted + version: "3.4.13" + serverpod_client: + dependency: "direct main" + description: + name: serverpod_client + sha256: "8388185c0eefe2c356c271427d37427cf2b366b3c095da79b7d8be9940cb10bd" + url: "https://pub.dev" + source: hosted + version: "3.4.13" + serverpod_serialization: + dependency: transitive + description: + name: serverpod_serialization + sha256: "807afab39b758f7b21a870af87abfaa24ebdf49aa5d1d9d122b0f3e51157873e" + url: "https://pub.dev" + source: hosted + version: "3.4.13" + sinalacs_client: + dependency: "direct main" + description: + path: "../../backend/sinalacs_client" + relative: true + source: path + version: "0.0.0" sky_engine: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_span: dependency: transitive description: @@ -417,18 +463,18 @@ packages: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" string_scanner: dependency: transitive description: @@ -437,6 +483,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + sync_http: + dependency: transitive + description: + name: sync_http + sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" + url: "https://pub.dev" + source: hosted + version: "0.3.1" synchronized: dependency: transitive description: @@ -457,10 +511,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.11" timezone: dependency: transitive description: @@ -477,22 +531,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" - universal_html: - dependency: transitive - description: - name: universal_html - sha256: "56536254004e24d9d8cfdb7dbbf09b74cf8df96729f38a2f5c238163e3d58971" - url: "https://pub.dev" - source: hosted - version: "2.2.4" - universal_io: - dependency: transitive - description: - name: universal_io - sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" - url: "https://pub.dev" - source: hosted - version: "2.2.2" uuid: dependency: transitive description: @@ -505,10 +543,10 @@ packages: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" vm_service: dependency: transitive description: @@ -525,6 +563,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + webdriver: + dependency: transitive + description: + name: webdriver + sha256: "28b82ec894fed45dd71c23ba62d1af973ed97dd59a4f5790a4d38b0b13e5657e" + url: "https://pub.dev" + source: hosted + version: "3.2.0" xdg_directories: dependency: transitive description: @@ -541,6 +603,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.5.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: f67cdd8e07d3c6329146aaef1ba043542b3134c12489f553ca9a7435d1068aea + url: "https://pub.dev" + source: hosted + version: "3.1.4" sdks: - dart: ">=3.5.0 <4.0.0" + dart: ">=3.10.0-0 <4.0.0" flutter: ">=3.24.0" diff --git a/apps/patient/pubspec.yaml b/apps/patient/pubspec.yaml index 7bc654b..f17ed02 100644 --- a/apps/patient/pubspec.yaml +++ b/apps/patient/pubspec.yaml @@ -5,22 +5,32 @@ publish_to: 'none' version: 1.0.0 environment: - sdk: '>=3.3.0 <4.0.0' + # ^3.8.0 é exigido por sinalacs_client; era '>=3.3.0 <4.0.0'. + sdk: '^3.8.0' dependencies: flutter: sdk: flutter + # Cliente RPC tipado gerado por `serverpod generate`. É membro do workspace + # backend/, e a dependência por path a partir de fora resolve — ver + # video/rpc_demo, que usa exatamente este arranjo. + sinalacs_client: + path: ../../backend/sinalacs_client + # Fixado na mesma versão que sinalacs_client declara; divergir quebra a resolução. + serverpod_client: 3.4.13 + crypto: ^3.0.0 sqflite: ^2.3.0 sqflite_common_ffi: ^2.3.0 sqflite_sqlcipher: ^3.2.0 connectivity_plus: ^7.3.1 - mqtt_client: ^10.0.0 geolocator: ^12.0.0 flutter_local_notifications: ^17.0.0 dev_dependencies: flutter_test: sdk: flutter + integration_test: + sdk: flutter flutter_lints: ^3.0.0 flutter: diff --git a/apps/patient/test/patient_app_mvp_test.dart b/apps/patient/test/patient_app_mvp_test.dart index a08a744..397cd68 100644 --- a/apps/patient/test/patient_app_mvp_test.dart +++ b/apps/patient/test/patient_app_mvp_test.dart @@ -1,39 +1,119 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:sinalacs_client/sinalacs_client.dart' show RiskLevel; import 'package:sinalacs_patient/app/app.dart'; +import 'package:sinalacs_patient/core/network/backend_client.dart'; + +import 'support/fake_patient_backend.dart'; + +/// Responde "não" a todos os sintomas menos [yesTo], e conclui a triagem. +Future answerTriage( + WidgetTester tester, { + Set yesTo = const {}, +}) async { + for (final symptom in TriageSymptom.values) { + final key = yesTo.contains(symptom) ? symptom.key : '${symptom.key}_no'; + await tester.tap(find.byKey(Key(key))); + await tester.pump(); + await tester.tap(find.byKey(const Key('submit_triage'))); + await tester.pumpAndSettle(); + } +} + +Future login(WidgetTester tester) async { + await tester.tap(find.byKey(const Key('enter_button'))); + await tester.pumpAndSettle(); +} void main() { - testWidgets('deve abrir a triagem do paciente e classificar risco', (tester) async { - await tester.pumpWidget(const SinalAcsApp()); + testWidgets('deve autenticar no backend antes de abrir a triagem', (tester) async { + final backend = FakePatientBackend(); + await tester.pumpWidget(SinalAcsApp(backend: backend)); expect(find.text('SinalACS'), findsOneWidget); expect(find.text('Acesso sem senha'), findsOneWidget); - await tester.tap(find.byKey(const Key('enter_button'))); - await tester.pumpAndSettle(); + await login(tester); + expect(backend.loginCount, 1); expect(find.text('Triagem rápida'), findsOneWidget); + }); - await tester.tap(find.byKey(const Key('difficulty_breathing'))); - await tester.pump(); - await tester.tap(find.byKey(const Key('submit_triage'))); - await tester.pumpAndSettle(); + testWidgets('não deve avançar quando a autenticação falha', (tester) async { + final backend = FakePatientBackend( + loginFailure: const BackendFailure('Sem conexão com o servidor.'), + ); + await tester.pumpWidget(SinalAcsApp(backend: backend)); - await tester.tap(find.byKey(const Key('triage_1_0'))); - await tester.pump(); - await tester.tap(find.byKey(const Key('submit_triage'))); - await tester.pumpAndSettle(); + await login(tester); - await tester.tap(find.byKey(const Key('chest_pain'))); - await tester.pump(); - await tester.tap(find.byKey(const Key('submit_triage'))); + // A tela antiga navegava incondicionalmente; a regressão que este teste + // protege é justamente entrar no app sem ter falado com o servidor. + expect(find.text('Triagem rápida'), findsNothing); + expect(find.byKey(const Key('login_error')), findsOneWidget); + expect(find.text('Sem conexão com o servidor.'), findsOneWidget); + }); + + testWidgets('deve enviar os sintomas ao servidor e exibir o risco recebido', (tester) async { + final backend = FakePatientBackend(risk: RiskLevel.red); + await tester.pumpWidget(SinalAcsApp(backend: backend)); + + await login(tester); + await answerTriage(tester, yesTo: {TriageSymptom.chestPain}); + + expect(backend.triageCalls, hasLength(1)); + expect(backend.triageCalls.single, { + 'chestPain': true, + 'difficultyBreathing': false, + 'fever': false, + 'persistentVomiting': false, + 'bleeding': false, + 'severeWeakness': false, + }); + expect(find.text('Risco: Vermelho'), findsOneWidget); + }); + + testWidgets('deve exibir o risco do servidor mesmo quando contraria o sintoma informado', (tester) async { + // O app não tem regra de risco própria: exibe o que o motor determinístico + // do servidor devolveu. Se a tela recalculasse localmente, este teste + // mostraria "Vermelho" e falharia. + final backend = FakePatientBackend(risk: RiskLevel.green); + await tester.pumpWidget(SinalAcsApp(backend: backend)); + + await login(tester); + await answerTriage(tester, yesTo: {TriageSymptom.chestPain}); + + expect(find.text('Risco: Verde'), findsOneWidget); + expect(find.text('Risco: Vermelho'), findsNothing); + }); + + testWidgets('deve reusar a chave de idempotência quando o envio do alerta falha', (tester) async { + final backend = FakePatientBackend( + alertFailure: const BackendFailure('Sem conexão com o servidor.'), + ); + await tester.pumpWidget(SinalAcsApp(backend: backend)); + + await login(tester); + + // Vai para a aba de urgência. + await tester.tap(find.text('Urgência')); await tester.pumpAndSettle(); - expect(find.text('Solicitação de visita #4082'), findsOneWidget); + for (var attempt = 0; attempt < 2; attempt++) { + await tester.tap(find.byKey(const Key('panic_button'))); + await tester.pumpAndSettle(); + await tester.tap(find.text('Confirmar alerta')); + await tester.pumpAndSettle(); + } + + expect(backend.idempotencyKeys, hasLength(2)); + // Duas tentativas da MESMA emergência precisam levar a mesma chave, senão o + // retry cria um segundo alerta vermelho no servidor. + expect(backend.idempotencyKeys.first, backend.idempotencyKeys.last); }); testWidgets('deve expor rótulo semântico e alvo de toque acessível no fluxo do paciente', (tester) async { - await tester.pumpWidget(const SinalAcsApp()); + await tester.pumpWidget(SinalAcsApp(backend: FakePatientBackend())); final enterButton = tester.widget(find.byKey(const Key('enter_button'))); final minimumSize = enterButton.style?.minimumSize?.resolve({}) ?? const Size(0, 0); diff --git a/apps/patient/test/support/fake_patient_backend.dart b/apps/patient/test/support/fake_patient_backend.dart new file mode 100644 index 0000000..c13ce45 --- /dev/null +++ b/apps/patient/test/support/fake_patient_backend.dart @@ -0,0 +1,100 @@ +import 'package:sinalacs_client/sinalacs_client.dart'; +import 'package:sinalacs_patient/core/network/auth_session.dart'; +import 'package:sinalacs_patient/core/network/backend_client.dart'; + +/// Duplo de teste do backend do paciente. +/// +/// Mantém os testes de widget herméticos: eles verificam o comportamento da +/// tela (o que é enviado, o que é exibido), não a rede. A conexão real é +/// verificada pelos testes de integração e por `tool/live_check.dart`. +class FakePatientBackend implements PatientBackend { + FakePatientBackend({ + this.risk = RiskLevel.green, + this.loginFailure, + this.alertFailure, + }); + + /// Risco que o "servidor" devolve. A tela não pode derivá-lo por conta própria. + RiskLevel risk; + + BackendFailure? loginFailure; + BackendFailure? alertFailure; + + /// Argumentos recebidos, para as asserções. + final List> triageCalls = >[]; + final List idempotencyKeys = []; + int loginCount = 0; + bool closed = false; + + AuthSession? _session; + + @override + AuthSession? get session => _session; + + @override + bool get isAuthenticated => _session != null; + + @override + Future health() async => ServiceHealth( + status: 'ok', + mqttConnected: true, + dbConnected: true, + ); + + @override + Future login() async { + loginCount++; + final failure = loginFailure; + if (failure != null) throw failure; + + final session = AuthSession( + accessToken: 'token-de-teste', + tokenType: 'Bearer', + userId: '00000000-0000-4000-8000-000000000001', + role: 'patient', + microAreaId: '00000000-0000-4000-8000-000000000003', + expiresAt: DateTime.now().toUtc().add(const Duration(minutes: 15)), + ); + _session = session; + return session; + } + + @override + Future evaluateTriage({ + required bool chestPain, + required bool difficultyBreathing, + required bool fever, + required bool persistentVomiting, + required bool bleeding, + required bool severeWeakness, + }) async { + triageCalls.add({ + 'chestPain': chestPain, + 'difficultyBreathing': difficultyBreathing, + 'fever': fever, + 'persistentVomiting': persistentVomiting, + 'bleeding': bleeding, + 'severeWeakness': severeWeakness, + }); + return risk; + } + + @override + Future createRedAlert({ + required String idempotencyKey, + required String locationHash, + }) async { + idempotencyKeys.add(idempotencyKey); + final failure = alertFailure; + if (failure != null) throw failure; + + return RedAlertResult( + alertId: 'alerta-de-teste', + status: AlertStatus.pending, + published: true, + ); + } + + @override + void close() => closed = true; +} diff --git a/apps/patient/tool/live_check.dart b/apps/patient/tool/live_check.dart new file mode 100644 index 0000000..a4ca580 --- /dev/null +++ b/apps/patient/tool/live_check.dart @@ -0,0 +1,86 @@ +/// Verificação rápida da camada de rede do app contra a stack local. +/// +/// Roda na VM, sem emulador, e usa o MESMO `BackendClient` que a UI usa — é o +/// que separa "o backend está de pé" (video/rpc_demo) de "o app fala com ele". +/// +/// Pré-requisito: `docker compose up` com o database-seed concluído. +/// +/// cd apps/patient +/// dart run tool/live_check.dart +/// dart run tool/live_check.dart --host http://10.0.2.2:8080/ +library; + +import 'dart:io'; + +import 'package:sinalacs_patient/core/network/backend_client.dart'; +import 'package:sinalacs_patient/core/privacy/location_hash.dart'; + +Future main(List args) async { + final hostIndex = args.indexOf('--host'); + final host = hostIndex >= 0 && hostIndex + 1 < args.length + ? args[hostIndex + 1] + : 'http://localhost:8080/'; + + final backend = BackendClient(host: host); + stdout.writeln('paciente → $host'); + + try { + final health = await backend.health(); + stdout.writeln(' health ............. ${health.status} ' + '(db=${health.dbConnected} mqtt=${health.mqttConnected})'); + + final session = await backend.login(); + // O token nunca é impresso inteiro. + stdout.writeln(' login .............. papel=${session.role} ' + 'microárea=${session.microAreaId} expira=${session.expiresAt.toIso8601String()}'); + + final red = await backend.evaluateTriage( + chestPain: true, + difficultyBreathing: false, + fever: false, + persistentVomiting: false, + bleeding: false, + severeWeakness: false, + ); + final green = await backend.evaluateTriage( + chestPain: false, + difficultyBreathing: false, + fever: false, + persistentVomiting: false, + bleeding: false, + severeWeakness: false, + ); + stdout.writeln(' triagem ............ dor no peito=${red.name} ' + 'sem sintomas=${green.name}'); + if (red.name != 'red' || green.name != 'green') { + stderr.writeln(' ERRO: motor de triagem do servidor respondeu fora do esperado.'); + exitCode = 1; + } + + final key = 'live-check-${DateTime.now().millisecondsSinceEpoch}'; + final hash = locationHashFrom(-23.55052, -46.633308); + final first = await backend.createRedAlert( + idempotencyKey: key, + locationHash: hash, + ); + final again = await backend.createRedAlert( + idempotencyKey: key, + locationHash: hash, + ); + stdout.writeln(' alerta vermelho .... ${first.alertId} ' + '(publicado=${first.published})'); + if (first.alertId == again.alertId) { + stdout.writeln(' idempotência ....... mesmo alertId no reenvio'); + } else { + stderr.writeln(' ERRO: reenvio com a mesma chave criou outro alerta.'); + exitCode = 1; + } + + stdout.writeln(exitCode == 0 ? '\nOK — o app fala com o backend.' : '\nFALHOU.'); + } on BackendFailure catch (failure) { + stderr.writeln(' falhou: ${failure.message}'); + exitCode = 1; + } finally { + backend.close(); + } +} diff --git a/backend/DEPLOY.md b/backend/DEPLOY.md index fe9293a..d6843a8 100644 --- a/backend/DEPLOY.md +++ b/backend/DEPLOY.md @@ -88,6 +88,17 @@ Configurar como variáveis de ambiente secretas (nunca commitadas): | `APP_ENV` | `production` | | `ENABLE_DEV_LOGIN` | `true` (decisão consciente — é o único mecanismo de auth do piloto) | +> **Sobre os valores de desenvolvimento.** As senhas de desenvolvimento do +> repositório foram rotacionadas: `.env` passou a ser gerado por máquina com +> `scripts/dev/bootstrap_env.sh`, e os literais que antes estavam no +> `docker-compose.yml`, no `ci.yml` e no compose do Serverpod foram removidos. +> Os valores antigos continuam visíveis no histórico do git (que não foi +> reescrito) e **não valem mais** em lugar nenhum — não os reaproveite. +> +> `JWT_SECRET` agora é obrigatório fora de `development`: o servidor recusa subir +> com o valor ausente, vazio ou igual ao fallback de desenvolvimento, em vez de +> assinar tokens com uma chave pública. + Não setar `MQTT_CA_CERT_PATH` — o HiveMQ Cloud usa certificado de CA pública, e o cliente MQTT confia nas CAs padrão do sistema quando essa variável não é definida. diff --git a/backend/sinalacs_client/lib/src/protocol/api/visit_sync_entry.dart b/backend/sinalacs_client/lib/src/protocol/api/visit_sync_entry.dart new file mode 100644 index 0000000..d44307d --- /dev/null +++ b/backend/sinalacs_client/lib/src/protocol/api/visit_sync_entry.dart @@ -0,0 +1,196 @@ +/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */ +/* To generate run: "serverpod generate" */ + +// ignore_for_file: implementation_imports +// ignore_for_file: library_private_types_in_public_api +// ignore_for_file: non_constant_identifier_names +// ignore_for_file: public_member_api_docs +// ignore_for_file: type_literal_in_constant_pattern +// ignore_for_file: use_super_parameters +// ignore_for_file: invalid_use_of_internal_member + +// ignore_for_file: no_leading_underscores_for_library_prefixes + +import 'package:serverpod_client/serverpod_client.dart' as _i1; +import '../enums/risk_level.dart' as _i2; +import 'package:sinalacs_client/src/protocol/protocol.dart' as _i3; + +/// Uma visita registrada offline, enviada pelo app do ACS para sincronização. +/// +/// `localId` é gerado no dispositivo e tem índice único em `visits`: é o que +/// permite reenviar o mesmo lote depois de uma falha de rede sem duplicar a +/// visita. `version` é a versão que o dispositivo conhece — divergir da versão +/// do servidor significa que alguém alterou a visita no meio, e o resultado é +/// conflito, não sobrescrita. +abstract class VisitSyncEntry implements _i1.SerializableModel { + VisitSyncEntry._({ + required this.localId, + required this.patientId, + required this.scheduledAt, + this.completedAt, + required this.status, + required this.riskLevelBefore, + this.riskLevelAfter, + required this.notes, + required this.version, + }); + + factory VisitSyncEntry({ + required String localId, + required String patientId, + required DateTime scheduledAt, + DateTime? completedAt, + required String status, + required _i2.RiskLevel riskLevelBefore, + _i2.RiskLevel? riskLevelAfter, + required Map notes, + required int version, + }) = _VisitSyncEntryImpl; + + factory VisitSyncEntry.fromJson(Map jsonSerialization) { + return VisitSyncEntry( + localId: jsonSerialization['localId'] as String, + patientId: jsonSerialization['patientId'] as String, + scheduledAt: _i1.DateTimeJsonExtension.fromJson( + jsonSerialization['scheduledAt'], + ), + completedAt: jsonSerialization['completedAt'] == null + ? null + : _i1.DateTimeJsonExtension.fromJson( + jsonSerialization['completedAt'], + ), + status: jsonSerialization['status'] as String, + riskLevelBefore: _i2.RiskLevel.fromJson( + (jsonSerialization['riskLevelBefore'] as String), + ), + riskLevelAfter: jsonSerialization['riskLevelAfter'] == null + ? null + : _i2.RiskLevel.fromJson( + (jsonSerialization['riskLevelAfter'] as String), + ), + notes: _i3.Protocol().deserialize>( + jsonSerialization['notes'], + ), + version: jsonSerialization['version'] as int, + ); + } + + String localId; + + String patientId; + + DateTime scheduledAt; + + DateTime? completedAt; + + String status; + + _i2.RiskLevel riskLevelBefore; + + _i2.RiskLevel? riskLevelAfter; + + Map notes; + + int version; + + /// Returns a shallow copy of this [VisitSyncEntry] + /// with some or all fields replaced by the given arguments. + @_i1.useResult + VisitSyncEntry copyWith({ + String? localId, + String? patientId, + DateTime? scheduledAt, + DateTime? completedAt, + String? status, + _i2.RiskLevel? riskLevelBefore, + _i2.RiskLevel? riskLevelAfter, + Map? notes, + int? version, + }); + @override + Map toJson() { + return { + '__className__': 'VisitSyncEntry', + 'localId': localId, + 'patientId': patientId, + 'scheduledAt': scheduledAt.toJson(), + if (completedAt != null) 'completedAt': completedAt?.toJson(), + 'status': status, + 'riskLevelBefore': riskLevelBefore.toJson(), + if (riskLevelAfter != null) 'riskLevelAfter': riskLevelAfter?.toJson(), + 'notes': notes.toJson(), + 'version': version, + }; + } + + @override + String toString() { + return _i1.SerializationManager.encode(this); + } +} + +class _Undefined {} + +class _VisitSyncEntryImpl extends VisitSyncEntry { + _VisitSyncEntryImpl({ + required String localId, + required String patientId, + required DateTime scheduledAt, + DateTime? completedAt, + required String status, + required _i2.RiskLevel riskLevelBefore, + _i2.RiskLevel? riskLevelAfter, + required Map notes, + required int version, + }) : super._( + localId: localId, + patientId: patientId, + scheduledAt: scheduledAt, + completedAt: completedAt, + status: status, + riskLevelBefore: riskLevelBefore, + riskLevelAfter: riskLevelAfter, + notes: notes, + version: version, + ); + + /// Returns a shallow copy of this [VisitSyncEntry] + /// with some or all fields replaced by the given arguments. + @_i1.useResult + @override + VisitSyncEntry copyWith({ + String? localId, + String? patientId, + DateTime? scheduledAt, + Object? completedAt = _Undefined, + String? status, + _i2.RiskLevel? riskLevelBefore, + Object? riskLevelAfter = _Undefined, + Map? notes, + int? version, + }) { + return VisitSyncEntry( + localId: localId ?? this.localId, + patientId: patientId ?? this.patientId, + scheduledAt: scheduledAt ?? this.scheduledAt, + completedAt: completedAt is DateTime? ? completedAt : this.completedAt, + status: status ?? this.status, + riskLevelBefore: riskLevelBefore ?? this.riskLevelBefore, + riskLevelAfter: riskLevelAfter is _i2.RiskLevel? + ? riskLevelAfter + : this.riskLevelAfter, + notes: + notes ?? + this.notes.map( + ( + key0, + value0, + ) => MapEntry( + key0, + value0, + ), + ), + version: version ?? this.version, + ); + } +} diff --git a/backend/sinalacs_client/lib/src/protocol/api/visit_sync_result.dart b/backend/sinalacs_client/lib/src/protocol/api/visit_sync_result.dart new file mode 100644 index 0000000..2e09a2d --- /dev/null +++ b/backend/sinalacs_client/lib/src/protocol/api/visit_sync_result.dart @@ -0,0 +1,117 @@ +/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */ +/* To generate run: "serverpod generate" */ + +// ignore_for_file: implementation_imports +// ignore_for_file: library_private_types_in_public_api +// ignore_for_file: non_constant_identifier_names +// ignore_for_file: public_member_api_docs +// ignore_for_file: type_literal_in_constant_pattern +// ignore_for_file: use_super_parameters +// ignore_for_file: invalid_use_of_internal_member + +// ignore_for_file: no_leading_underscores_for_library_prefixes + +import 'package:serverpod_client/serverpod_client.dart' as _i1; +import '../enums/sync_status.dart' as _i2; + +/// Resultado da sincronização de UMA visita. +/// +/// O app casa pelo `localId`, não pela posição na lista: um resultado ausente +/// deixa a visita pendente para a próxima tentativa, em vez de dá-la por +/// sincronizada. +abstract class VisitSyncResult implements _i1.SerializableModel { + VisitSyncResult._({ + required this.localId, + required this.syncStatus, + this.serverVersion, + this.message, + }); + + factory VisitSyncResult({ + required String localId, + required _i2.SyncStatus syncStatus, + int? serverVersion, + String? message, + }) = _VisitSyncResultImpl; + + factory VisitSyncResult.fromJson(Map jsonSerialization) { + return VisitSyncResult( + localId: jsonSerialization['localId'] as String, + syncStatus: _i2.SyncStatus.fromJson( + (jsonSerialization['syncStatus'] as String), + ), + serverVersion: jsonSerialization['serverVersion'] as int?, + message: jsonSerialization['message'] as String?, + ); + } + + String localId; + + _i2.SyncStatus syncStatus; + + /// Versão gravada no servidor. Em conflito, é a versão que o dispositivo + /// precisa reconciliar antes de reenviar. + int? serverVersion; + + /// Preenchido apenas quando syncStatus é error. + String? message; + + /// Returns a shallow copy of this [VisitSyncResult] + /// with some or all fields replaced by the given arguments. + @_i1.useResult + VisitSyncResult copyWith({ + String? localId, + _i2.SyncStatus? syncStatus, + int? serverVersion, + String? message, + }); + @override + Map toJson() { + return { + '__className__': 'VisitSyncResult', + 'localId': localId, + 'syncStatus': syncStatus.toJson(), + if (serverVersion != null) 'serverVersion': serverVersion, + if (message != null) 'message': message, + }; + } + + @override + String toString() { + return _i1.SerializationManager.encode(this); + } +} + +class _Undefined {} + +class _VisitSyncResultImpl extends VisitSyncResult { + _VisitSyncResultImpl({ + required String localId, + required _i2.SyncStatus syncStatus, + int? serverVersion, + String? message, + }) : super._( + localId: localId, + syncStatus: syncStatus, + serverVersion: serverVersion, + message: message, + ); + + /// Returns a shallow copy of this [VisitSyncResult] + /// with some or all fields replaced by the given arguments. + @_i1.useResult + @override + VisitSyncResult copyWith({ + String? localId, + _i2.SyncStatus? syncStatus, + Object? serverVersion = _Undefined, + Object? message = _Undefined, + }) { + return VisitSyncResult( + localId: localId ?? this.localId, + syncStatus: syncStatus ?? this.syncStatus, + serverVersion: serverVersion is int? ? serverVersion : this.serverVersion, + message: message is String? ? message : this.message, + ); + } +} diff --git a/backend/sinalacs_client/lib/src/protocol/client.dart b/backend/sinalacs_client/lib/src/protocol/client.dart index c359a41..c10b1de 100644 --- a/backend/sinalacs_client/lib/src/protocol/client.dart +++ b/backend/sinalacs_client/lib/src/protocol/client.dart @@ -19,7 +19,9 @@ import 'package:sinalacs_client/src/protocol/api/development_login_result.dart' as _i5; import 'package:sinalacs_client/src/protocol/api/service_health.dart' as _i6; import 'package:sinalacs_client/src/protocol/api/triage_result.dart' as _i7; -import 'protocol.dart' as _i8; +import 'package:sinalacs_client/src/protocol/api/visit_sync_result.dart' as _i8; +import 'package:sinalacs_client/src/protocol/api/visit_sync_entry.dart' as _i9; +import 'protocol.dart' as _i10; /// Ciclo do alerta vermelho. /// @@ -152,6 +154,35 @@ class EndpointTriage extends _i1.EndpointRef { ); } +/// Sincronização das visitas domiciliares registradas offline. +/// +/// É a contraparte da fila offline do app do ACS: o dispositivo grava a visita +/// localmente durante a visita (onde normalmente não há rede) e envia o lote +/// quando a conexão volta. +/// +/// O lote inteiro roda em uma transação: ou todas as visitas são aplicadas, ou +/// nenhuma. Um resultado parcial deixaria o dispositivo sem saber o que +/// reenviar. +/// {@category Endpoint} +class EndpointVisits extends _i1.EndpointRef { + EndpointVisits(_i1.EndpointCaller caller) : super(caller); + + @override + String get name => 'visits'; + + _i2.Future> sync({ + required String accessToken, + required List<_i9.VisitSyncEntry> visits, + }) => caller.callServerEndpoint>( + 'visits', + 'sync', + { + 'accessToken': accessToken, + 'visits': visits, + }, + ); +} + class Client extends _i1.ServerpodClientShared { Client( String host, { @@ -172,7 +203,7 @@ class Client extends _i1.ServerpodClientShared { bool? disconnectStreamsOnLostInternetConnection, }) : super( host, - _i8.Protocol(), + _i10.Protocol(), securityContext: securityContext, streamingConnectionTimeout: streamingConnectionTimeout, connectionTimeout: connectionTimeout, @@ -185,6 +216,7 @@ class Client extends _i1.ServerpodClientShared { auth = EndpointAuth(this); health = EndpointHealth(this); triage = EndpointTriage(this); + visits = EndpointVisits(this); } late final EndpointAlerts alerts; @@ -195,12 +227,15 @@ class Client extends _i1.ServerpodClientShared { late final EndpointTriage triage; + late final EndpointVisits visits; + @override Map get endpointRefLookup => { 'alerts': alerts, 'auth': auth, 'health': health, 'triage': triage, + 'visits': visits, }; @override diff --git a/backend/sinalacs_client/lib/src/protocol/protocol.dart b/backend/sinalacs_client/lib/src/protocol/protocol.dart index 23959d5..ce8ea99 100644 --- a/backend/sinalacs_client/lib/src/protocol/protocol.dart +++ b/backend/sinalacs_client/lib/src/protocol/protocol.dart @@ -22,23 +22,28 @@ import 'api/development_login_result.dart' as _i8; import 'api/red_alert_result.dart' as _i9; import 'api/service_health.dart' as _i10; import 'api/triage_result.dart' as _i11; -import 'audit_log.dart' as _i12; -import 'consent_log.dart' as _i13; -import 'enums/alert_status.dart' as _i14; -import 'enums/risk_level.dart' as _i15; -import 'enums/sync_status.dart' as _i16; -import 'enums/user_role.dart' as _i17; -import 'exceptions/alert_dispatch_unavailable_exception.dart' as _i18; -import 'exceptions/alert_permission_exception.dart' as _i19; -import 'exceptions/alert_validation_exception.dart' as _i20; -import 'exceptions/endpoint_disabled_exception.dart' as _i21; -import 'micro_area.dart' as _i22; -import 'patient.dart' as _i23; -import 'triage_answer.dart' as _i24; -import 'triage_session.dart' as _i25; -import 'ubs.dart' as _i26; -import 'user.dart' as _i27; -import 'visit.dart' as _i28; +import 'api/visit_sync_entry.dart' as _i12; +import 'api/visit_sync_result.dart' as _i13; +import 'audit_log.dart' as _i14; +import 'consent_log.dart' as _i15; +import 'enums/alert_status.dart' as _i16; +import 'enums/risk_level.dart' as _i17; +import 'enums/sync_status.dart' as _i18; +import 'enums/user_role.dart' as _i19; +import 'exceptions/alert_dispatch_unavailable_exception.dart' as _i20; +import 'exceptions/alert_permission_exception.dart' as _i21; +import 'exceptions/alert_validation_exception.dart' as _i22; +import 'exceptions/endpoint_disabled_exception.dart' as _i23; +import 'micro_area.dart' as _i24; +import 'patient.dart' as _i25; +import 'triage_answer.dart' as _i26; +import 'triage_session.dart' as _i27; +import 'ubs.dart' as _i28; +import 'user.dart' as _i29; +import 'visit.dart' as _i30; +import 'package:sinalacs_client/src/protocol/api/visit_sync_result.dart' + as _i31; +import 'package:sinalacs_client/src/protocol/api/visit_sync_entry.dart' as _i32; export 'acs.dart'; export 'alert.dart'; export 'alert_delivery_record.dart'; @@ -49,6 +54,8 @@ export 'api/development_login_result.dart'; export 'api/red_alert_result.dart'; export 'api/service_health.dart'; export 'api/triage_result.dart'; +export 'api/visit_sync_entry.dart'; +export 'api/visit_sync_result.dart'; export 'audit_log.dart'; export 'consent_log.dart'; export 'enums/alert_status.dart'; @@ -132,56 +139,62 @@ class Protocol extends _i1.SerializationManager { if (t == _i11.TriageResult) { return _i11.TriageResult.fromJson(data) as T; } - if (t == _i12.AuditLog) { - return _i12.AuditLog.fromJson(data) as T; + if (t == _i12.VisitSyncEntry) { + return _i12.VisitSyncEntry.fromJson(data) as T; } - if (t == _i13.ConsentLog) { - return _i13.ConsentLog.fromJson(data) as T; + if (t == _i13.VisitSyncResult) { + return _i13.VisitSyncResult.fromJson(data) as T; } - if (t == _i14.AlertStatus) { - return _i14.AlertStatus.fromJson(data) as T; + if (t == _i14.AuditLog) { + return _i14.AuditLog.fromJson(data) as T; } - if (t == _i15.RiskLevel) { - return _i15.RiskLevel.fromJson(data) as T; + if (t == _i15.ConsentLog) { + return _i15.ConsentLog.fromJson(data) as T; } - if (t == _i16.SyncStatus) { - return _i16.SyncStatus.fromJson(data) as T; + if (t == _i16.AlertStatus) { + return _i16.AlertStatus.fromJson(data) as T; } - if (t == _i17.UserRole) { - return _i17.UserRole.fromJson(data) as T; + if (t == _i17.RiskLevel) { + return _i17.RiskLevel.fromJson(data) as T; } - if (t == _i18.AlertDispatchUnavailableException) { - return _i18.AlertDispatchUnavailableException.fromJson(data) as T; + if (t == _i18.SyncStatus) { + return _i18.SyncStatus.fromJson(data) as T; } - if (t == _i19.AlertPermissionException) { - return _i19.AlertPermissionException.fromJson(data) as T; + if (t == _i19.UserRole) { + return _i19.UserRole.fromJson(data) as T; } - if (t == _i20.AlertValidationException) { - return _i20.AlertValidationException.fromJson(data) as T; + if (t == _i20.AlertDispatchUnavailableException) { + return _i20.AlertDispatchUnavailableException.fromJson(data) as T; } - if (t == _i21.EndpointDisabledException) { - return _i21.EndpointDisabledException.fromJson(data) as T; + if (t == _i21.AlertPermissionException) { + return _i21.AlertPermissionException.fromJson(data) as T; } - if (t == _i22.MicroArea) { - return _i22.MicroArea.fromJson(data) as T; + if (t == _i22.AlertValidationException) { + return _i22.AlertValidationException.fromJson(data) as T; } - if (t == _i23.Patient) { - return _i23.Patient.fromJson(data) as T; + if (t == _i23.EndpointDisabledException) { + return _i23.EndpointDisabledException.fromJson(data) as T; } - if (t == _i24.TriageAnswer) { - return _i24.TriageAnswer.fromJson(data) as T; + if (t == _i24.MicroArea) { + return _i24.MicroArea.fromJson(data) as T; } - if (t == _i25.TriageSession) { - return _i25.TriageSession.fromJson(data) as T; + if (t == _i25.Patient) { + return _i25.Patient.fromJson(data) as T; } - if (t == _i26.Ubs) { - return _i26.Ubs.fromJson(data) as T; + if (t == _i26.TriageAnswer) { + return _i26.TriageAnswer.fromJson(data) as T; } - if (t == _i27.User) { - return _i27.User.fromJson(data) as T; + if (t == _i27.TriageSession) { + return _i27.TriageSession.fromJson(data) as T; } - if (t == _i28.Visit) { - return _i28.Visit.fromJson(data) as T; + if (t == _i28.Ubs) { + return _i28.Ubs.fromJson(data) as T; + } + if (t == _i29.User) { + return _i29.User.fromJson(data) as T; + } + if (t == _i30.Visit) { + return _i30.Visit.fromJson(data) as T; } if (t == _i1.getType<_i2.Acs?>()) { return (data != null ? _i2.Acs.fromJson(data) : null) as T; @@ -216,82 +229,100 @@ class Protocol extends _i1.SerializationManager { if (t == _i1.getType<_i11.TriageResult?>()) { return (data != null ? _i11.TriageResult.fromJson(data) : null) as T; } - if (t == _i1.getType<_i12.AuditLog?>()) { - return (data != null ? _i12.AuditLog.fromJson(data) : null) as T; + if (t == _i1.getType<_i12.VisitSyncEntry?>()) { + return (data != null ? _i12.VisitSyncEntry.fromJson(data) : null) as T; + } + if (t == _i1.getType<_i13.VisitSyncResult?>()) { + return (data != null ? _i13.VisitSyncResult.fromJson(data) : null) as T; } - if (t == _i1.getType<_i13.ConsentLog?>()) { - return (data != null ? _i13.ConsentLog.fromJson(data) : null) as T; + if (t == _i1.getType<_i14.AuditLog?>()) { + return (data != null ? _i14.AuditLog.fromJson(data) : null) as T; } - if (t == _i1.getType<_i14.AlertStatus?>()) { - return (data != null ? _i14.AlertStatus.fromJson(data) : null) as T; + if (t == _i1.getType<_i15.ConsentLog?>()) { + return (data != null ? _i15.ConsentLog.fromJson(data) : null) as T; } - if (t == _i1.getType<_i15.RiskLevel?>()) { - return (data != null ? _i15.RiskLevel.fromJson(data) : null) as T; + if (t == _i1.getType<_i16.AlertStatus?>()) { + return (data != null ? _i16.AlertStatus.fromJson(data) : null) as T; } - if (t == _i1.getType<_i16.SyncStatus?>()) { - return (data != null ? _i16.SyncStatus.fromJson(data) : null) as T; + if (t == _i1.getType<_i17.RiskLevel?>()) { + return (data != null ? _i17.RiskLevel.fromJson(data) : null) as T; } - if (t == _i1.getType<_i17.UserRole?>()) { - return (data != null ? _i17.UserRole.fromJson(data) : null) as T; + if (t == _i1.getType<_i18.SyncStatus?>()) { + return (data != null ? _i18.SyncStatus.fromJson(data) : null) as T; } - if (t == _i1.getType<_i18.AlertDispatchUnavailableException?>()) { + if (t == _i1.getType<_i19.UserRole?>()) { + return (data != null ? _i19.UserRole.fromJson(data) : null) as T; + } + if (t == _i1.getType<_i20.AlertDispatchUnavailableException?>()) { return (data != null - ? _i18.AlertDispatchUnavailableException.fromJson(data) + ? _i20.AlertDispatchUnavailableException.fromJson(data) : null) as T; } - if (t == _i1.getType<_i19.AlertPermissionException?>()) { + if (t == _i1.getType<_i21.AlertPermissionException?>()) { return (data != null - ? _i19.AlertPermissionException.fromJson(data) + ? _i21.AlertPermissionException.fromJson(data) : null) as T; } - if (t == _i1.getType<_i20.AlertValidationException?>()) { + if (t == _i1.getType<_i22.AlertValidationException?>()) { return (data != null - ? _i20.AlertValidationException.fromJson(data) + ? _i22.AlertValidationException.fromJson(data) : null) as T; } - if (t == _i1.getType<_i21.EndpointDisabledException?>()) { + if (t == _i1.getType<_i23.EndpointDisabledException?>()) { return (data != null - ? _i21.EndpointDisabledException.fromJson(data) + ? _i23.EndpointDisabledException.fromJson(data) : null) as T; } - if (t == _i1.getType<_i22.MicroArea?>()) { - return (data != null ? _i22.MicroArea.fromJson(data) : null) as T; + if (t == _i1.getType<_i24.MicroArea?>()) { + return (data != null ? _i24.MicroArea.fromJson(data) : null) as T; } - if (t == _i1.getType<_i23.Patient?>()) { - return (data != null ? _i23.Patient.fromJson(data) : null) as T; + if (t == _i1.getType<_i25.Patient?>()) { + return (data != null ? _i25.Patient.fromJson(data) : null) as T; } - if (t == _i1.getType<_i24.TriageAnswer?>()) { - return (data != null ? _i24.TriageAnswer.fromJson(data) : null) as T; + if (t == _i1.getType<_i26.TriageAnswer?>()) { + return (data != null ? _i26.TriageAnswer.fromJson(data) : null) as T; } - if (t == _i1.getType<_i25.TriageSession?>()) { - return (data != null ? _i25.TriageSession.fromJson(data) : null) as T; + if (t == _i1.getType<_i27.TriageSession?>()) { + return (data != null ? _i27.TriageSession.fromJson(data) : null) as T; } - if (t == _i1.getType<_i26.Ubs?>()) { - return (data != null ? _i26.Ubs.fromJson(data) : null) as T; + if (t == _i1.getType<_i28.Ubs?>()) { + return (data != null ? _i28.Ubs.fromJson(data) : null) as T; } - if (t == _i1.getType<_i27.User?>()) { - return (data != null ? _i27.User.fromJson(data) : null) as T; + if (t == _i1.getType<_i29.User?>()) { + return (data != null ? _i29.User.fromJson(data) : null) as T; } - if (t == _i1.getType<_i28.Visit?>()) { - return (data != null ? _i28.Visit.fromJson(data) : null) as T; + if (t == _i1.getType<_i30.Visit?>()) { + return (data != null ? _i30.Visit.fromJson(data) : null) as T; + } + if (t == Map) { + return (data as Map).map( + (k, v) => MapEntry(deserialize(k), deserialize(v)), + ) + as T; } if (t == List) { return (data as List).map((e) => deserialize(e)).toList() as T; } - if (t == List<_i24.TriageAnswer>) { + if (t == List<_i26.TriageAnswer>) { return (data as List) - .map((e) => deserialize<_i24.TriageAnswer>(e)) + .map((e) => deserialize<_i26.TriageAnswer>(e)) .toList() as T; } - if (t == Map) { - return (data as Map).map( - (k, v) => MapEntry(deserialize(k), deserialize(v)), - ) + if (t == List<_i31.VisitSyncResult>) { + return (data as List) + .map((e) => deserialize<_i31.VisitSyncResult>(e)) + .toList() + as T; + } + if (t == List<_i32.VisitSyncEntry>) { + return (data as List) + .map((e) => deserialize<_i32.VisitSyncEntry>(e)) + .toList() as T; } return super.deserialize(data, t); @@ -309,24 +340,26 @@ class Protocol extends _i1.SerializationManager { _i9.RedAlertResult => 'RedAlertResult', _i10.ServiceHealth => 'ServiceHealth', _i11.TriageResult => 'TriageResult', - _i12.AuditLog => 'AuditLog', - _i13.ConsentLog => 'ConsentLog', - _i14.AlertStatus => 'AlertStatus', - _i15.RiskLevel => 'RiskLevel', - _i16.SyncStatus => 'SyncStatus', - _i17.UserRole => 'UserRole', - _i18.AlertDispatchUnavailableException => + _i12.VisitSyncEntry => 'VisitSyncEntry', + _i13.VisitSyncResult => 'VisitSyncResult', + _i14.AuditLog => 'AuditLog', + _i15.ConsentLog => 'ConsentLog', + _i16.AlertStatus => 'AlertStatus', + _i17.RiskLevel => 'RiskLevel', + _i18.SyncStatus => 'SyncStatus', + _i19.UserRole => 'UserRole', + _i20.AlertDispatchUnavailableException => 'AlertDispatchUnavailableException', - _i19.AlertPermissionException => 'AlertPermissionException', - _i20.AlertValidationException => 'AlertValidationException', - _i21.EndpointDisabledException => 'EndpointDisabledException', - _i22.MicroArea => 'MicroArea', - _i23.Patient => 'Patient', - _i24.TriageAnswer => 'TriageAnswer', - _i25.TriageSession => 'TriageSession', - _i26.Ubs => 'Ubs', - _i27.User => 'User', - _i28.Visit => 'Visit', + _i21.AlertPermissionException => 'AlertPermissionException', + _i22.AlertValidationException => 'AlertValidationException', + _i23.EndpointDisabledException => 'EndpointDisabledException', + _i24.MicroArea => 'MicroArea', + _i25.Patient => 'Patient', + _i26.TriageAnswer => 'TriageAnswer', + _i27.TriageSession => 'TriageSession', + _i28.Ubs => 'Ubs', + _i29.User => 'User', + _i30.Visit => 'Visit', _ => null, }; } @@ -361,39 +394,43 @@ class Protocol extends _i1.SerializationManager { return 'ServiceHealth'; case _i11.TriageResult(): return 'TriageResult'; - case _i12.AuditLog(): + case _i12.VisitSyncEntry(): + return 'VisitSyncEntry'; + case _i13.VisitSyncResult(): + return 'VisitSyncResult'; + case _i14.AuditLog(): return 'AuditLog'; - case _i13.ConsentLog(): + case _i15.ConsentLog(): return 'ConsentLog'; - case _i14.AlertStatus(): + case _i16.AlertStatus(): return 'AlertStatus'; - case _i15.RiskLevel(): + case _i17.RiskLevel(): return 'RiskLevel'; - case _i16.SyncStatus(): + case _i18.SyncStatus(): return 'SyncStatus'; - case _i17.UserRole(): + case _i19.UserRole(): return 'UserRole'; - case _i18.AlertDispatchUnavailableException(): + case _i20.AlertDispatchUnavailableException(): return 'AlertDispatchUnavailableException'; - case _i19.AlertPermissionException(): + case _i21.AlertPermissionException(): return 'AlertPermissionException'; - case _i20.AlertValidationException(): + case _i22.AlertValidationException(): return 'AlertValidationException'; - case _i21.EndpointDisabledException(): + case _i23.EndpointDisabledException(): return 'EndpointDisabledException'; - case _i22.MicroArea(): + case _i24.MicroArea(): return 'MicroArea'; - case _i23.Patient(): + case _i25.Patient(): return 'Patient'; - case _i24.TriageAnswer(): + case _i26.TriageAnswer(): return 'TriageAnswer'; - case _i25.TriageSession(): + case _i27.TriageSession(): return 'TriageSession'; - case _i26.Ubs(): + case _i28.Ubs(): return 'Ubs'; - case _i27.User(): + case _i29.User(): return 'User'; - case _i28.Visit(): + case _i30.Visit(): return 'Visit'; } return null; @@ -435,56 +472,62 @@ class Protocol extends _i1.SerializationManager { if (dataClassName == 'TriageResult') { return deserialize<_i11.TriageResult>(data['data']); } + if (dataClassName == 'VisitSyncEntry') { + return deserialize<_i12.VisitSyncEntry>(data['data']); + } + if (dataClassName == 'VisitSyncResult') { + return deserialize<_i13.VisitSyncResult>(data['data']); + } if (dataClassName == 'AuditLog') { - return deserialize<_i12.AuditLog>(data['data']); + return deserialize<_i14.AuditLog>(data['data']); } if (dataClassName == 'ConsentLog') { - return deserialize<_i13.ConsentLog>(data['data']); + return deserialize<_i15.ConsentLog>(data['data']); } if (dataClassName == 'AlertStatus') { - return deserialize<_i14.AlertStatus>(data['data']); + return deserialize<_i16.AlertStatus>(data['data']); } if (dataClassName == 'RiskLevel') { - return deserialize<_i15.RiskLevel>(data['data']); + return deserialize<_i17.RiskLevel>(data['data']); } if (dataClassName == 'SyncStatus') { - return deserialize<_i16.SyncStatus>(data['data']); + return deserialize<_i18.SyncStatus>(data['data']); } if (dataClassName == 'UserRole') { - return deserialize<_i17.UserRole>(data['data']); + return deserialize<_i19.UserRole>(data['data']); } if (dataClassName == 'AlertDispatchUnavailableException') { - return deserialize<_i18.AlertDispatchUnavailableException>(data['data']); + return deserialize<_i20.AlertDispatchUnavailableException>(data['data']); } if (dataClassName == 'AlertPermissionException') { - return deserialize<_i19.AlertPermissionException>(data['data']); + return deserialize<_i21.AlertPermissionException>(data['data']); } if (dataClassName == 'AlertValidationException') { - return deserialize<_i20.AlertValidationException>(data['data']); + return deserialize<_i22.AlertValidationException>(data['data']); } if (dataClassName == 'EndpointDisabledException') { - return deserialize<_i21.EndpointDisabledException>(data['data']); + return deserialize<_i23.EndpointDisabledException>(data['data']); } if (dataClassName == 'MicroArea') { - return deserialize<_i22.MicroArea>(data['data']); + return deserialize<_i24.MicroArea>(data['data']); } if (dataClassName == 'Patient') { - return deserialize<_i23.Patient>(data['data']); + return deserialize<_i25.Patient>(data['data']); } if (dataClassName == 'TriageAnswer') { - return deserialize<_i24.TriageAnswer>(data['data']); + return deserialize<_i26.TriageAnswer>(data['data']); } if (dataClassName == 'TriageSession') { - return deserialize<_i25.TriageSession>(data['data']); + return deserialize<_i27.TriageSession>(data['data']); } if (dataClassName == 'Ubs') { - return deserialize<_i26.Ubs>(data['data']); + return deserialize<_i28.Ubs>(data['data']); } if (dataClassName == 'User') { - return deserialize<_i27.User>(data['data']); + return deserialize<_i29.User>(data['data']); } if (dataClassName == 'Visit') { - return deserialize<_i28.Visit>(data['data']); + return deserialize<_i30.Visit>(data['data']); } return super.deserializeByClassName(data); } diff --git a/backend/sinalacs_server/docker-compose.yaml b/backend/sinalacs_server/docker-compose.yaml deleted file mode 100644 index 4ca6b4b..0000000 --- a/backend/sinalacs_server/docker-compose.yaml +++ /dev/null @@ -1,44 +0,0 @@ -services: - # Development services - postgres: - image: pgvector/pgvector:pg16 - ports: - - "8090:5432" - environment: - POSTGRES_USER: postgres - POSTGRES_DB: sinalacs - POSTGRES_PASSWORD: "ZGib82sxRnAaTto3iglbPkARA1qZUv0W" - volumes: - - sinalacs_data:/var/lib/postgresql/data - - redis: - image: redis:6.2.6 - ports: - - "8091:6379" - command: redis-server --requirepass "xjjE2lgOgChcj15Gf5thYrjs1gzUoF4n" - environment: - - REDIS_REPLICATION_MODE=master - - # Test services - postgres_test: - image: pgvector/pgvector:pg16 - ports: - - "9090:5432" - environment: - POSTGRES_USER: postgres - POSTGRES_DB: sinalacs_test - POSTGRES_PASSWORD: "ehzaftIZDV8Ou4ILUIRsE02VhbjvBGZB" - volumes: - - sinalacs_test_data:/var/lib/postgresql/data - - redis_test: - image: redis:6.2.6 - ports: - - "9091:6379" - command: redis-server --requirepass "b0Wa06K7wU5MZbrAdXk6VrRyvOtmsaWS" - environment: - - REDIS_REPLICATION_MODE=master - -volumes: - sinalacs_data: - sinalacs_test_data: diff --git a/backend/sinalacs_server/lib/src/application/visits/visit_sync_service.dart b/backend/sinalacs_server/lib/src/application/visits/visit_sync_service.dart new file mode 100644 index 0000000..712f580 --- /dev/null +++ b/backend/sinalacs_server/lib/src/application/visits/visit_sync_service.dart @@ -0,0 +1,150 @@ +import 'package:serverpod/serverpod.dart' show UuidValue; +import 'package:sinalacs_server/src/application/auth/development_auth_service.dart'; +import 'package:sinalacs_server/src/generated/protocol.dart'; + +/// Persistência das visitas sincronizadas. +/// +/// Interface no mesmo padrão de `AlertStore`: mantém o serviço testável sem +/// Postgres real. +abstract interface class VisitStore { + /// Visita já gravada com este `localId`, se houver. + Future findByLocalId(String localId); + + /// Grava uma visita nova, já com `version` inicial. + Future insert(Visit visit); + + /// Atualiza uma visita existente, incrementando a versão. + Future update(Visit visit); +} + +/// Sincronização das visitas registradas offline pelo ACS. +/// +/// Espelha a `SyncFsm` do lado do dispositivo: cada visita termina em +/// `synced`, `conflict` ou `error`, e conflito **nunca** sobrescreve o que está +/// no servidor — devolve a versão atual para o dispositivo reconciliar. +class VisitSyncService { + VisitSyncService({required VisitStore store, DateTime Function()? clock}) + : _store = store, + _clock = clock ?? DateTime.now; + + final VisitStore _store; + final DateTime Function() _clock; + + Future> sync({ + required AuthenticatedUser user, + required List entries, + }) async { + if (user.role != UserRole.acs || user.microAreaId == null) { + throw StateError('Somente ACS territorializados podem sincronizar visitas.'); + } + + final results = []; + for (final entry in entries) { + results.add(await _syncOne(user: user, entry: entry)); + } + return results; + } + + Future _syncOne({ + required AuthenticatedUser user, + required VisitSyncEntry entry, + }) async { + if (entry.localId.trim().isEmpty) { + return VisitSyncResult( + localId: entry.localId, + syncStatus: SyncStatus.error, + message: 'localId é obrigatório', + ); + } + + final UuidValue localId; + final UuidValue patientId; + try { + // `UuidValue.fromString` NÃO valida — só normaliza para minúsculas. Sem + // `withValidation`, um `localId` malformado entraria no banco e quebraria + // a deduplicação do reenvio, que é justamente o que o índice único + // protege. + localId = UuidValue.withValidation(entry.localId); + patientId = UuidValue.withValidation(entry.patientId); + } on FormatException { + return VisitSyncResult( + localId: entry.localId, + syncStatus: SyncStatus.error, + message: 'identificadores devem ser UUID', + ); + } + + final acsId = UuidValue.fromString(user.id); + final existing = await _store.findByLocalId(entry.localId); + + if (existing == null) { + final inserted = await _store.insert(Visit( + patientId: patientId, + acsId: acsId, + scheduledAt: entry.scheduledAt, + completedAt: entry.completedAt, + status: entry.status, + riskLevelBefore: entry.riskLevelBefore, + riskLevelAfter: entry.riskLevelAfter, + notes: entry.notes, + syncStatus: SyncStatus.synced, + localId: localId, + syncAt: _clock().toUtc(), + version: 1, + )); + return VisitSyncResult( + localId: entry.localId, + syncStatus: SyncStatus.synced, + serverVersion: inserted.version, + ); + } + + // A visita pertence ao ACS que a registrou. Um ACS não sincroniza a visita + // de outro, mesmo conhecendo o localId. + if (existing.acsId != acsId) { + return VisitSyncResult( + localId: entry.localId, + syncStatus: SyncStatus.error, + message: 'visita registrada por outro agente', + ); + } + + // Reenvio do MESMO estado: a rede caiu depois de o servidor gravar, e o + // dispositivo não viu a resposta. Não é conflito — é a idempotência + // funcionando. + if (entry.version == existing.version) { + return VisitSyncResult( + localId: entry.localId, + syncStatus: SyncStatus.synced, + serverVersion: existing.version, + ); + } + + // O dispositivo partiu de uma versão que não é mais a atual: alguém alterou + // a visita no meio. Devolve conflito com a versão do servidor, sem + // sobrescrever nada. + if (entry.version != existing.version - 1) { + return VisitSyncResult( + localId: entry.localId, + syncStatus: SyncStatus.conflict, + serverVersion: existing.version, + ); + } + + final updated = await _store.update(existing.copyWith( + completedAt: entry.completedAt, + status: entry.status, + riskLevelAfter: entry.riskLevelAfter, + notes: entry.notes, + syncStatus: SyncStatus.synced, + syncAt: _clock().toUtc(), + version: existing.version + 1, + )); + + return VisitSyncResult( + localId: entry.localId, + syncStatus: SyncStatus.synced, + serverVersion: updated.version, + ); + } +} diff --git a/backend/sinalacs_server/lib/src/config/app_config.dart b/backend/sinalacs_server/lib/src/config/app_config.dart index b4f0837..46449cc 100644 --- a/backend/sinalacs_server/lib/src/config/app_config.dart +++ b/backend/sinalacs_server/lib/src/config/app_config.dart @@ -1,8 +1,11 @@ import 'dart:io'; +/// Configuração que **não** vem do Serverpod. +/// +/// Banco e servidor são configurados por `config/*.yaml` mais as variáveis +/// `SERVERPOD_*`. O que sobra — MQTT e o segredo de autenticação — é lido aqui. class AppConfig { const AppConfig({ - required this.databaseUrl, required this.mqttBroker, required this.jwtSecret, required this.mqttUsername, @@ -13,9 +16,14 @@ class AppConfig { required this.enableDevLogin, }); - final String databaseUrl; final String mqttBroker; + + /// Chave HMAC que assina os tokens de `auth.developmentLogin`. + /// + /// O token carrega o papel e a microárea, então quem conhece este valor forja + /// um acesso de ACS para qualquer território. Ver [_resolveJwtSecret]. final String jwtSecret; + final String? mqttUsername; final String? mqttPassword; final bool mqttUseTls; @@ -25,23 +33,62 @@ class AppConfig { bool get isProduction => appEnv == 'production'; - factory AppConfig.fromEnvironment() { - final appEnv = Platform.environment['APP_ENV'] ?? 'development'; - final jwtSecret = Platform.environment['JWT_SECRET']; - if (jwtSecret == null && appEnv == 'production') { - throw StateError('JWT_SECRET é obrigatório quando APP_ENV=production.'); - } + /// Segredo usado quando `JWT_SECRET` não é informado em desenvolvimento. + /// + /// É público — está neste arquivo versionado — e por isso só pode valer em + /// `development`. [_resolveJwtSecret] recusa promovê-lo a outro ambiente. + static const developmentJwtSecret = 'development-secret'; + + factory AppConfig.fromEnvironment() => + AppConfig.fromMap(Platform.environment); + + /// Mesma leitura, a partir de um mapa qualquer. + /// + /// `Platform.environment` não é substituível dentro do processo, então as + /// regras de validação só são testáveis por aqui. `fromEnvironment()` é um + /// atalho para o ambiente real. + factory AppConfig.fromMap(Map environment) { + final appEnv = environment['APP_ENV'] ?? 'development'; return AppConfig( - databaseUrl: Platform.environment['DATABASE_URL'] ?? 'postgresql://sinalacs_user:strongpassword@localhost:5432/sinalacs_db', - mqttBroker: Platform.environment['MQTT_BROKER'] ?? 'localhost:1883', - jwtSecret: jwtSecret ?? 'development-secret', - mqttUsername: Platform.environment['MQTT_USERNAME'], - mqttPassword: Platform.environment['MQTT_PASSWORD'], - mqttUseTls: Platform.environment['MQTT_USE_TLS'] == 'true', - mqttCaCertificatePath: Platform.environment['MQTT_CA_CERT_PATH'], + mqttBroker: environment['MQTT_BROKER'] ?? 'localhost:1883', + jwtSecret: _resolveJwtSecret(environment['JWT_SECRET'], appEnv), + mqttUsername: environment['MQTT_USERNAME'], + mqttPassword: environment['MQTT_PASSWORD'], + mqttUseTls: environment['MQTT_USE_TLS'] == 'true', + mqttCaCertificatePath: environment['MQTT_CA_CERT_PATH'], appEnv: appEnv, - enableDevLogin: Platform.environment['ENABLE_DEV_LOGIN'] == 'true', + enableDevLogin: environment['ENABLE_DEV_LOGIN'] == 'true', ); } + + /// Decide o segredo de assinatura, recusando subir com um segredo fraco. + /// + /// Só `development` aceita ausência de `JWT_SECRET`. Fora dele a falha é no + /// boot, e não na primeira requisição: um servidor que sobe assinando com um + /// segredo público é pior do que um servidor que não sobe. + /// + /// A checagem anterior cobria apenas `appEnv == 'production'` e testava só + /// `== null`, então `JWT_SECRET=""` e `APP_ENV=staging` passavam direto. + static String _resolveJwtSecret(String? value, String appEnv) { + final secret = value?.trim(); + final isDevelopment = appEnv == 'development'; + + if (secret == null || secret.isEmpty) { + if (isDevelopment) return developmentJwtSecret; + throw StateError( + 'JWT_SECRET é obrigatório quando APP_ENV=$appEnv. ' + 'Gere um com: openssl rand -hex 32', + ); + } + + if (secret == developmentJwtSecret && !isDevelopment) { + throw StateError( + 'JWT_SECRET está usando o valor de desenvolvimento, que é público, ' + 'com APP_ENV=$appEnv. Gere um próprio com: openssl rand -hex 32', + ); + } + + return secret; + } } diff --git a/backend/sinalacs_server/lib/src/endpoints/visits_endpoint.dart b/backend/sinalacs_server/lib/src/endpoints/visits_endpoint.dart new file mode 100644 index 0000000..84407b4 --- /dev/null +++ b/backend/sinalacs_server/lib/src/endpoints/visits_endpoint.dart @@ -0,0 +1,48 @@ +import 'package:serverpod/serverpod.dart'; +import 'package:sinalacs_server/src/application/auth/development_auth_service.dart'; +import 'package:sinalacs_server/src/generated/protocol.dart'; +import 'package:sinalacs_server/src/runtime/alert_runtime.dart'; + +/// Sincronização das visitas domiciliares registradas offline. +/// +/// É a contraparte da fila offline do app do ACS: o dispositivo grava a visita +/// localmente durante a visita (onde normalmente não há rede) e envia o lote +/// quando a conexão volta. +/// +/// O lote inteiro roda em uma transação: ou todas as visitas são aplicadas, ou +/// nenhuma. Um resultado parcial deixaria o dispositivo sem saber o que +/// reenviar. +class VisitsEndpoint extends Endpoint { + @override + bool get requireLogin => false; + + Future> sync( + Session session, { + required String accessToken, + required List visits, + }) async { + final user = _authenticate(accessToken); + + if (visits.isEmpty) return []; + + try { + return await session.db.transaction((transaction) async { + final service = + AlertRuntime.instance.visitSyncServiceFor(session, transaction: transaction); + return service.sync(user: user, entries: visits); + }); + } on ArgumentError catch (error) { + throw AlertValidationException(message: '${error.message}'); + } on StateError catch (error) { + throw AlertPermissionException(message: error.message); + } + } + + AuthenticatedUser _authenticate(String accessToken) { + final user = AlertRuntime.instance.auth.verifyToken(accessToken); + if (user == null) { + throw AlertPermissionException(message: 'token inválido ou expirado'); + } + return user; + } +} diff --git a/backend/sinalacs_server/lib/src/generated/api/visit_sync_entry.dart b/backend/sinalacs_server/lib/src/generated/api/visit_sync_entry.dart new file mode 100644 index 0000000..056a525 --- /dev/null +++ b/backend/sinalacs_server/lib/src/generated/api/visit_sync_entry.dart @@ -0,0 +1,213 @@ +/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */ +/* To generate run: "serverpod generate" */ + +// ignore_for_file: implementation_imports +// ignore_for_file: library_private_types_in_public_api +// ignore_for_file: non_constant_identifier_names +// ignore_for_file: public_member_api_docs +// ignore_for_file: type_literal_in_constant_pattern +// ignore_for_file: use_super_parameters +// ignore_for_file: invalid_use_of_internal_member + +// ignore_for_file: no_leading_underscores_for_library_prefixes + +import 'package:serverpod/serverpod.dart' as _i1; +import '../enums/risk_level.dart' as _i2; +import 'package:sinalacs_server/src/generated/protocol.dart' as _i3; + +/// Uma visita registrada offline, enviada pelo app do ACS para sincronização. +/// +/// `localId` é gerado no dispositivo e tem índice único em `visits`: é o que +/// permite reenviar o mesmo lote depois de uma falha de rede sem duplicar a +/// visita. `version` é a versão que o dispositivo conhece — divergir da versão +/// do servidor significa que alguém alterou a visita no meio, e o resultado é +/// conflito, não sobrescrita. +abstract class VisitSyncEntry + implements _i1.SerializableModel, _i1.ProtocolSerialization { + VisitSyncEntry._({ + required this.localId, + required this.patientId, + required this.scheduledAt, + this.completedAt, + required this.status, + required this.riskLevelBefore, + this.riskLevelAfter, + required this.notes, + required this.version, + }); + + factory VisitSyncEntry({ + required String localId, + required String patientId, + required DateTime scheduledAt, + DateTime? completedAt, + required String status, + required _i2.RiskLevel riskLevelBefore, + _i2.RiskLevel? riskLevelAfter, + required Map notes, + required int version, + }) = _VisitSyncEntryImpl; + + factory VisitSyncEntry.fromJson(Map jsonSerialization) { + return VisitSyncEntry( + localId: jsonSerialization['localId'] as String, + patientId: jsonSerialization['patientId'] as String, + scheduledAt: _i1.DateTimeJsonExtension.fromJson( + jsonSerialization['scheduledAt'], + ), + completedAt: jsonSerialization['completedAt'] == null + ? null + : _i1.DateTimeJsonExtension.fromJson( + jsonSerialization['completedAt'], + ), + status: jsonSerialization['status'] as String, + riskLevelBefore: _i2.RiskLevel.fromJson( + (jsonSerialization['riskLevelBefore'] as String), + ), + riskLevelAfter: jsonSerialization['riskLevelAfter'] == null + ? null + : _i2.RiskLevel.fromJson( + (jsonSerialization['riskLevelAfter'] as String), + ), + notes: _i3.Protocol().deserialize>( + jsonSerialization['notes'], + ), + version: jsonSerialization['version'] as int, + ); + } + + String localId; + + String patientId; + + DateTime scheduledAt; + + DateTime? completedAt; + + String status; + + _i2.RiskLevel riskLevelBefore; + + _i2.RiskLevel? riskLevelAfter; + + Map notes; + + int version; + + /// Returns a shallow copy of this [VisitSyncEntry] + /// with some or all fields replaced by the given arguments. + @_i1.useResult + VisitSyncEntry copyWith({ + String? localId, + String? patientId, + DateTime? scheduledAt, + DateTime? completedAt, + String? status, + _i2.RiskLevel? riskLevelBefore, + _i2.RiskLevel? riskLevelAfter, + Map? notes, + int? version, + }); + @override + Map toJson() { + return { + '__className__': 'VisitSyncEntry', + 'localId': localId, + 'patientId': patientId, + 'scheduledAt': scheduledAt.toJson(), + if (completedAt != null) 'completedAt': completedAt?.toJson(), + 'status': status, + 'riskLevelBefore': riskLevelBefore.toJson(), + if (riskLevelAfter != null) 'riskLevelAfter': riskLevelAfter?.toJson(), + 'notes': notes.toJson(), + 'version': version, + }; + } + + @override + Map toJsonForProtocol() { + return { + '__className__': 'VisitSyncEntry', + 'localId': localId, + 'patientId': patientId, + 'scheduledAt': scheduledAt.toJson(), + if (completedAt != null) 'completedAt': completedAt?.toJson(), + 'status': status, + 'riskLevelBefore': riskLevelBefore.toJson(), + if (riskLevelAfter != null) 'riskLevelAfter': riskLevelAfter?.toJson(), + 'notes': notes.toJson(), + 'version': version, + }; + } + + @override + String toString() { + return _i1.SerializationManager.encode(this); + } +} + +class _Undefined {} + +class _VisitSyncEntryImpl extends VisitSyncEntry { + _VisitSyncEntryImpl({ + required String localId, + required String patientId, + required DateTime scheduledAt, + DateTime? completedAt, + required String status, + required _i2.RiskLevel riskLevelBefore, + _i2.RiskLevel? riskLevelAfter, + required Map notes, + required int version, + }) : super._( + localId: localId, + patientId: patientId, + scheduledAt: scheduledAt, + completedAt: completedAt, + status: status, + riskLevelBefore: riskLevelBefore, + riskLevelAfter: riskLevelAfter, + notes: notes, + version: version, + ); + + /// Returns a shallow copy of this [VisitSyncEntry] + /// with some or all fields replaced by the given arguments. + @_i1.useResult + @override + VisitSyncEntry copyWith({ + String? localId, + String? patientId, + DateTime? scheduledAt, + Object? completedAt = _Undefined, + String? status, + _i2.RiskLevel? riskLevelBefore, + Object? riskLevelAfter = _Undefined, + Map? notes, + int? version, + }) { + return VisitSyncEntry( + localId: localId ?? this.localId, + patientId: patientId ?? this.patientId, + scheduledAt: scheduledAt ?? this.scheduledAt, + completedAt: completedAt is DateTime? ? completedAt : this.completedAt, + status: status ?? this.status, + riskLevelBefore: riskLevelBefore ?? this.riskLevelBefore, + riskLevelAfter: riskLevelAfter is _i2.RiskLevel? + ? riskLevelAfter + : this.riskLevelAfter, + notes: + notes ?? + this.notes.map( + ( + key0, + value0, + ) => MapEntry( + key0, + value0, + ), + ), + version: version ?? this.version, + ); + } +} diff --git a/backend/sinalacs_server/lib/src/generated/api/visit_sync_result.dart b/backend/sinalacs_server/lib/src/generated/api/visit_sync_result.dart new file mode 100644 index 0000000..e7f8f36 --- /dev/null +++ b/backend/sinalacs_server/lib/src/generated/api/visit_sync_result.dart @@ -0,0 +1,129 @@ +/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */ +/* To generate run: "serverpod generate" */ + +// ignore_for_file: implementation_imports +// ignore_for_file: library_private_types_in_public_api +// ignore_for_file: non_constant_identifier_names +// ignore_for_file: public_member_api_docs +// ignore_for_file: type_literal_in_constant_pattern +// ignore_for_file: use_super_parameters +// ignore_for_file: invalid_use_of_internal_member + +// ignore_for_file: no_leading_underscores_for_library_prefixes + +import 'package:serverpod/serverpod.dart' as _i1; +import '../enums/sync_status.dart' as _i2; + +/// Resultado da sincronização de UMA visita. +/// +/// O app casa pelo `localId`, não pela posição na lista: um resultado ausente +/// deixa a visita pendente para a próxima tentativa, em vez de dá-la por +/// sincronizada. +abstract class VisitSyncResult + implements _i1.SerializableModel, _i1.ProtocolSerialization { + VisitSyncResult._({ + required this.localId, + required this.syncStatus, + this.serverVersion, + this.message, + }); + + factory VisitSyncResult({ + required String localId, + required _i2.SyncStatus syncStatus, + int? serverVersion, + String? message, + }) = _VisitSyncResultImpl; + + factory VisitSyncResult.fromJson(Map jsonSerialization) { + return VisitSyncResult( + localId: jsonSerialization['localId'] as String, + syncStatus: _i2.SyncStatus.fromJson( + (jsonSerialization['syncStatus'] as String), + ), + serverVersion: jsonSerialization['serverVersion'] as int?, + message: jsonSerialization['message'] as String?, + ); + } + + String localId; + + _i2.SyncStatus syncStatus; + + /// Versão gravada no servidor. Em conflito, é a versão que o dispositivo + /// precisa reconciliar antes de reenviar. + int? serverVersion; + + /// Preenchido apenas quando syncStatus é error. + String? message; + + /// Returns a shallow copy of this [VisitSyncResult] + /// with some or all fields replaced by the given arguments. + @_i1.useResult + VisitSyncResult copyWith({ + String? localId, + _i2.SyncStatus? syncStatus, + int? serverVersion, + String? message, + }); + @override + Map toJson() { + return { + '__className__': 'VisitSyncResult', + 'localId': localId, + 'syncStatus': syncStatus.toJson(), + if (serverVersion != null) 'serverVersion': serverVersion, + if (message != null) 'message': message, + }; + } + + @override + Map toJsonForProtocol() { + return { + '__className__': 'VisitSyncResult', + 'localId': localId, + 'syncStatus': syncStatus.toJson(), + if (serverVersion != null) 'serverVersion': serverVersion, + if (message != null) 'message': message, + }; + } + + @override + String toString() { + return _i1.SerializationManager.encode(this); + } +} + +class _Undefined {} + +class _VisitSyncResultImpl extends VisitSyncResult { + _VisitSyncResultImpl({ + required String localId, + required _i2.SyncStatus syncStatus, + int? serverVersion, + String? message, + }) : super._( + localId: localId, + syncStatus: syncStatus, + serverVersion: serverVersion, + message: message, + ); + + /// Returns a shallow copy of this [VisitSyncResult] + /// with some or all fields replaced by the given arguments. + @_i1.useResult + @override + VisitSyncResult copyWith({ + String? localId, + _i2.SyncStatus? syncStatus, + Object? serverVersion = _Undefined, + Object? message = _Undefined, + }) { + return VisitSyncResult( + localId: localId ?? this.localId, + syncStatus: syncStatus ?? this.syncStatus, + serverVersion: serverVersion is int? ? serverVersion : this.serverVersion, + message: message is String? ? message : this.message, + ); + } +} diff --git a/backend/sinalacs_server/lib/src/generated/endpoints.dart b/backend/sinalacs_server/lib/src/generated/endpoints.dart index 86eb697..0e4238b 100644 --- a/backend/sinalacs_server/lib/src/generated/endpoints.dart +++ b/backend/sinalacs_server/lib/src/generated/endpoints.dart @@ -16,6 +16,8 @@ import '../endpoints/alerts_endpoint.dart' as _i2; import '../endpoints/auth_endpoint.dart' as _i3; import '../endpoints/health_endpoint.dart' as _i4; import '../endpoints/triage_endpoint.dart' as _i5; +import '../endpoints/visits_endpoint.dart' as _i6; +import 'package:sinalacs_server/src/generated/api/visit_sync_entry.dart' as _i7; class Endpoints extends _i1.EndpointDispatch { @override @@ -45,6 +47,12 @@ class Endpoints extends _i1.EndpointDispatch { 'triage', null, ), + 'visits': _i6.VisitsEndpoint() + ..initialize( + server, + 'visits', + null, + ), }; connectors['alerts'] = _i1.EndpointConnector( name: 'alerts', @@ -203,5 +211,35 @@ class Endpoints extends _i1.EndpointDispatch { ), }, ); + connectors['visits'] = _i1.EndpointConnector( + name: 'visits', + endpoint: endpoints['visits']!, + methodConnectors: { + 'sync': _i1.MethodConnector( + name: 'sync', + params: { + 'accessToken': _i1.ParameterDescription( + name: 'accessToken', + type: _i1.getType(), + nullable: false, + ), + 'visits': _i1.ParameterDescription( + name: 'visits', + type: _i1.getType>(), + nullable: false, + ), + }, + call: + ( + _i1.Session session, + Map params, + ) async => (endpoints['visits'] as _i6.VisitsEndpoint).sync( + session, + accessToken: params['accessToken'], + visits: params['visits'], + ), + ), + }, + ); } } diff --git a/backend/sinalacs_server/lib/src/generated/protocol.dart b/backend/sinalacs_server/lib/src/generated/protocol.dart index 05d40e3..6c49c4d 100644 --- a/backend/sinalacs_server/lib/src/generated/protocol.dart +++ b/backend/sinalacs_server/lib/src/generated/protocol.dart @@ -23,23 +23,29 @@ import 'api/development_login_result.dart' as _i9; import 'api/red_alert_result.dart' as _i10; import 'api/service_health.dart' as _i11; import 'api/triage_result.dart' as _i12; -import 'audit_log.dart' as _i13; -import 'consent_log.dart' as _i14; -import 'enums/alert_status.dart' as _i15; -import 'enums/risk_level.dart' as _i16; -import 'enums/sync_status.dart' as _i17; -import 'enums/user_role.dart' as _i18; -import 'exceptions/alert_dispatch_unavailable_exception.dart' as _i19; -import 'exceptions/alert_permission_exception.dart' as _i20; -import 'exceptions/alert_validation_exception.dart' as _i21; -import 'exceptions/endpoint_disabled_exception.dart' as _i22; -import 'micro_area.dart' as _i23; -import 'patient.dart' as _i24; -import 'triage_answer.dart' as _i25; -import 'triage_session.dart' as _i26; -import 'ubs.dart' as _i27; -import 'user.dart' as _i28; -import 'visit.dart' as _i29; +import 'api/visit_sync_entry.dart' as _i13; +import 'api/visit_sync_result.dart' as _i14; +import 'audit_log.dart' as _i15; +import 'consent_log.dart' as _i16; +import 'enums/alert_status.dart' as _i17; +import 'enums/risk_level.dart' as _i18; +import 'enums/sync_status.dart' as _i19; +import 'enums/user_role.dart' as _i20; +import 'exceptions/alert_dispatch_unavailable_exception.dart' as _i21; +import 'exceptions/alert_permission_exception.dart' as _i22; +import 'exceptions/alert_validation_exception.dart' as _i23; +import 'exceptions/endpoint_disabled_exception.dart' as _i24; +import 'micro_area.dart' as _i25; +import 'patient.dart' as _i26; +import 'triage_answer.dart' as _i27; +import 'triage_session.dart' as _i28; +import 'ubs.dart' as _i29; +import 'user.dart' as _i30; +import 'visit.dart' as _i31; +import 'package:sinalacs_server/src/generated/api/visit_sync_result.dart' + as _i32; +import 'package:sinalacs_server/src/generated/api/visit_sync_entry.dart' + as _i33; export 'acs.dart'; export 'alert.dart'; export 'alert_delivery_record.dart'; @@ -50,6 +56,8 @@ export 'api/development_login_result.dart'; export 'api/red_alert_result.dart'; export 'api/service_health.dart'; export 'api/triage_result.dart'; +export 'api/visit_sync_entry.dart'; +export 'api/visit_sync_result.dart'; export 'audit_log.dart'; export 'consent_log.dart'; export 'enums/alert_status.dart'; @@ -1319,56 +1327,62 @@ class Protocol extends _i1.SerializationManagerServer { if (t == _i12.TriageResult) { return _i12.TriageResult.fromJson(data) as T; } - if (t == _i13.AuditLog) { - return _i13.AuditLog.fromJson(data) as T; + if (t == _i13.VisitSyncEntry) { + return _i13.VisitSyncEntry.fromJson(data) as T; } - if (t == _i14.ConsentLog) { - return _i14.ConsentLog.fromJson(data) as T; + if (t == _i14.VisitSyncResult) { + return _i14.VisitSyncResult.fromJson(data) as T; } - if (t == _i15.AlertStatus) { - return _i15.AlertStatus.fromJson(data) as T; + if (t == _i15.AuditLog) { + return _i15.AuditLog.fromJson(data) as T; } - if (t == _i16.RiskLevel) { - return _i16.RiskLevel.fromJson(data) as T; + if (t == _i16.ConsentLog) { + return _i16.ConsentLog.fromJson(data) as T; } - if (t == _i17.SyncStatus) { - return _i17.SyncStatus.fromJson(data) as T; + if (t == _i17.AlertStatus) { + return _i17.AlertStatus.fromJson(data) as T; } - if (t == _i18.UserRole) { - return _i18.UserRole.fromJson(data) as T; + if (t == _i18.RiskLevel) { + return _i18.RiskLevel.fromJson(data) as T; } - if (t == _i19.AlertDispatchUnavailableException) { - return _i19.AlertDispatchUnavailableException.fromJson(data) as T; + if (t == _i19.SyncStatus) { + return _i19.SyncStatus.fromJson(data) as T; } - if (t == _i20.AlertPermissionException) { - return _i20.AlertPermissionException.fromJson(data) as T; + if (t == _i20.UserRole) { + return _i20.UserRole.fromJson(data) as T; } - if (t == _i21.AlertValidationException) { - return _i21.AlertValidationException.fromJson(data) as T; + if (t == _i21.AlertDispatchUnavailableException) { + return _i21.AlertDispatchUnavailableException.fromJson(data) as T; } - if (t == _i22.EndpointDisabledException) { - return _i22.EndpointDisabledException.fromJson(data) as T; + if (t == _i22.AlertPermissionException) { + return _i22.AlertPermissionException.fromJson(data) as T; } - if (t == _i23.MicroArea) { - return _i23.MicroArea.fromJson(data) as T; + if (t == _i23.AlertValidationException) { + return _i23.AlertValidationException.fromJson(data) as T; } - if (t == _i24.Patient) { - return _i24.Patient.fromJson(data) as T; + if (t == _i24.EndpointDisabledException) { + return _i24.EndpointDisabledException.fromJson(data) as T; } - if (t == _i25.TriageAnswer) { - return _i25.TriageAnswer.fromJson(data) as T; + if (t == _i25.MicroArea) { + return _i25.MicroArea.fromJson(data) as T; } - if (t == _i26.TriageSession) { - return _i26.TriageSession.fromJson(data) as T; + if (t == _i26.Patient) { + return _i26.Patient.fromJson(data) as T; } - if (t == _i27.Ubs) { - return _i27.Ubs.fromJson(data) as T; + if (t == _i27.TriageAnswer) { + return _i27.TriageAnswer.fromJson(data) as T; } - if (t == _i28.User) { - return _i28.User.fromJson(data) as T; + if (t == _i28.TriageSession) { + return _i28.TriageSession.fromJson(data) as T; } - if (t == _i29.Visit) { - return _i29.Visit.fromJson(data) as T; + if (t == _i29.Ubs) { + return _i29.Ubs.fromJson(data) as T; + } + if (t == _i30.User) { + return _i30.User.fromJson(data) as T; + } + if (t == _i31.Visit) { + return _i31.Visit.fromJson(data) as T; } if (t == _i1.getType<_i3.Acs?>()) { return (data != null ? _i3.Acs.fromJson(data) : null) as T; @@ -1403,82 +1417,100 @@ class Protocol extends _i1.SerializationManagerServer { if (t == _i1.getType<_i12.TriageResult?>()) { return (data != null ? _i12.TriageResult.fromJson(data) : null) as T; } - if (t == _i1.getType<_i13.AuditLog?>()) { - return (data != null ? _i13.AuditLog.fromJson(data) : null) as T; + if (t == _i1.getType<_i13.VisitSyncEntry?>()) { + return (data != null ? _i13.VisitSyncEntry.fromJson(data) : null) as T; + } + if (t == _i1.getType<_i14.VisitSyncResult?>()) { + return (data != null ? _i14.VisitSyncResult.fromJson(data) : null) as T; } - if (t == _i1.getType<_i14.ConsentLog?>()) { - return (data != null ? _i14.ConsentLog.fromJson(data) : null) as T; + if (t == _i1.getType<_i15.AuditLog?>()) { + return (data != null ? _i15.AuditLog.fromJson(data) : null) as T; } - if (t == _i1.getType<_i15.AlertStatus?>()) { - return (data != null ? _i15.AlertStatus.fromJson(data) : null) as T; + if (t == _i1.getType<_i16.ConsentLog?>()) { + return (data != null ? _i16.ConsentLog.fromJson(data) : null) as T; } - if (t == _i1.getType<_i16.RiskLevel?>()) { - return (data != null ? _i16.RiskLevel.fromJson(data) : null) as T; + if (t == _i1.getType<_i17.AlertStatus?>()) { + return (data != null ? _i17.AlertStatus.fromJson(data) : null) as T; } - if (t == _i1.getType<_i17.SyncStatus?>()) { - return (data != null ? _i17.SyncStatus.fromJson(data) : null) as T; + if (t == _i1.getType<_i18.RiskLevel?>()) { + return (data != null ? _i18.RiskLevel.fromJson(data) : null) as T; } - if (t == _i1.getType<_i18.UserRole?>()) { - return (data != null ? _i18.UserRole.fromJson(data) : null) as T; + if (t == _i1.getType<_i19.SyncStatus?>()) { + return (data != null ? _i19.SyncStatus.fromJson(data) : null) as T; } - if (t == _i1.getType<_i19.AlertDispatchUnavailableException?>()) { + if (t == _i1.getType<_i20.UserRole?>()) { + return (data != null ? _i20.UserRole.fromJson(data) : null) as T; + } + if (t == _i1.getType<_i21.AlertDispatchUnavailableException?>()) { return (data != null - ? _i19.AlertDispatchUnavailableException.fromJson(data) + ? _i21.AlertDispatchUnavailableException.fromJson(data) : null) as T; } - if (t == _i1.getType<_i20.AlertPermissionException?>()) { + if (t == _i1.getType<_i22.AlertPermissionException?>()) { return (data != null - ? _i20.AlertPermissionException.fromJson(data) + ? _i22.AlertPermissionException.fromJson(data) : null) as T; } - if (t == _i1.getType<_i21.AlertValidationException?>()) { + if (t == _i1.getType<_i23.AlertValidationException?>()) { return (data != null - ? _i21.AlertValidationException.fromJson(data) + ? _i23.AlertValidationException.fromJson(data) : null) as T; } - if (t == _i1.getType<_i22.EndpointDisabledException?>()) { + if (t == _i1.getType<_i24.EndpointDisabledException?>()) { return (data != null - ? _i22.EndpointDisabledException.fromJson(data) + ? _i24.EndpointDisabledException.fromJson(data) : null) as T; } - if (t == _i1.getType<_i23.MicroArea?>()) { - return (data != null ? _i23.MicroArea.fromJson(data) : null) as T; + if (t == _i1.getType<_i25.MicroArea?>()) { + return (data != null ? _i25.MicroArea.fromJson(data) : null) as T; } - if (t == _i1.getType<_i24.Patient?>()) { - return (data != null ? _i24.Patient.fromJson(data) : null) as T; + if (t == _i1.getType<_i26.Patient?>()) { + return (data != null ? _i26.Patient.fromJson(data) : null) as T; } - if (t == _i1.getType<_i25.TriageAnswer?>()) { - return (data != null ? _i25.TriageAnswer.fromJson(data) : null) as T; + if (t == _i1.getType<_i27.TriageAnswer?>()) { + return (data != null ? _i27.TriageAnswer.fromJson(data) : null) as T; } - if (t == _i1.getType<_i26.TriageSession?>()) { - return (data != null ? _i26.TriageSession.fromJson(data) : null) as T; + if (t == _i1.getType<_i28.TriageSession?>()) { + return (data != null ? _i28.TriageSession.fromJson(data) : null) as T; } - if (t == _i1.getType<_i27.Ubs?>()) { - return (data != null ? _i27.Ubs.fromJson(data) : null) as T; + if (t == _i1.getType<_i29.Ubs?>()) { + return (data != null ? _i29.Ubs.fromJson(data) : null) as T; } - if (t == _i1.getType<_i28.User?>()) { - return (data != null ? _i28.User.fromJson(data) : null) as T; + if (t == _i1.getType<_i30.User?>()) { + return (data != null ? _i30.User.fromJson(data) : null) as T; } - if (t == _i1.getType<_i29.Visit?>()) { - return (data != null ? _i29.Visit.fromJson(data) : null) as T; + if (t == _i1.getType<_i31.Visit?>()) { + return (data != null ? _i31.Visit.fromJson(data) : null) as T; + } + if (t == Map) { + return (data as Map).map( + (k, v) => MapEntry(deserialize(k), deserialize(v)), + ) + as T; } if (t == List) { return (data as List).map((e) => deserialize(e)).toList() as T; } - if (t == List<_i25.TriageAnswer>) { + if (t == List<_i27.TriageAnswer>) { return (data as List) - .map((e) => deserialize<_i25.TriageAnswer>(e)) + .map((e) => deserialize<_i27.TriageAnswer>(e)) .toList() as T; } - if (t == Map) { - return (data as Map).map( - (k, v) => MapEntry(deserialize(k), deserialize(v)), - ) + if (t == List<_i32.VisitSyncResult>) { + return (data as List) + .map((e) => deserialize<_i32.VisitSyncResult>(e)) + .toList() + as T; + } + if (t == List<_i33.VisitSyncEntry>) { + return (data as List) + .map((e) => deserialize<_i33.VisitSyncEntry>(e)) + .toList() as T; } try { @@ -1499,24 +1531,26 @@ class Protocol extends _i1.SerializationManagerServer { _i10.RedAlertResult => 'RedAlertResult', _i11.ServiceHealth => 'ServiceHealth', _i12.TriageResult => 'TriageResult', - _i13.AuditLog => 'AuditLog', - _i14.ConsentLog => 'ConsentLog', - _i15.AlertStatus => 'AlertStatus', - _i16.RiskLevel => 'RiskLevel', - _i17.SyncStatus => 'SyncStatus', - _i18.UserRole => 'UserRole', - _i19.AlertDispatchUnavailableException => + _i13.VisitSyncEntry => 'VisitSyncEntry', + _i14.VisitSyncResult => 'VisitSyncResult', + _i15.AuditLog => 'AuditLog', + _i16.ConsentLog => 'ConsentLog', + _i17.AlertStatus => 'AlertStatus', + _i18.RiskLevel => 'RiskLevel', + _i19.SyncStatus => 'SyncStatus', + _i20.UserRole => 'UserRole', + _i21.AlertDispatchUnavailableException => 'AlertDispatchUnavailableException', - _i20.AlertPermissionException => 'AlertPermissionException', - _i21.AlertValidationException => 'AlertValidationException', - _i22.EndpointDisabledException => 'EndpointDisabledException', - _i23.MicroArea => 'MicroArea', - _i24.Patient => 'Patient', - _i25.TriageAnswer => 'TriageAnswer', - _i26.TriageSession => 'TriageSession', - _i27.Ubs => 'Ubs', - _i28.User => 'User', - _i29.Visit => 'Visit', + _i22.AlertPermissionException => 'AlertPermissionException', + _i23.AlertValidationException => 'AlertValidationException', + _i24.EndpointDisabledException => 'EndpointDisabledException', + _i25.MicroArea => 'MicroArea', + _i26.Patient => 'Patient', + _i27.TriageAnswer => 'TriageAnswer', + _i28.TriageSession => 'TriageSession', + _i29.Ubs => 'Ubs', + _i30.User => 'User', + _i31.Visit => 'Visit', _ => null, }; } @@ -1551,39 +1585,43 @@ class Protocol extends _i1.SerializationManagerServer { return 'ServiceHealth'; case _i12.TriageResult(): return 'TriageResult'; - case _i13.AuditLog(): + case _i13.VisitSyncEntry(): + return 'VisitSyncEntry'; + case _i14.VisitSyncResult(): + return 'VisitSyncResult'; + case _i15.AuditLog(): return 'AuditLog'; - case _i14.ConsentLog(): + case _i16.ConsentLog(): return 'ConsentLog'; - case _i15.AlertStatus(): + case _i17.AlertStatus(): return 'AlertStatus'; - case _i16.RiskLevel(): + case _i18.RiskLevel(): return 'RiskLevel'; - case _i17.SyncStatus(): + case _i19.SyncStatus(): return 'SyncStatus'; - case _i18.UserRole(): + case _i20.UserRole(): return 'UserRole'; - case _i19.AlertDispatchUnavailableException(): + case _i21.AlertDispatchUnavailableException(): return 'AlertDispatchUnavailableException'; - case _i20.AlertPermissionException(): + case _i22.AlertPermissionException(): return 'AlertPermissionException'; - case _i21.AlertValidationException(): + case _i23.AlertValidationException(): return 'AlertValidationException'; - case _i22.EndpointDisabledException(): + case _i24.EndpointDisabledException(): return 'EndpointDisabledException'; - case _i23.MicroArea(): + case _i25.MicroArea(): return 'MicroArea'; - case _i24.Patient(): + case _i26.Patient(): return 'Patient'; - case _i25.TriageAnswer(): + case _i27.TriageAnswer(): return 'TriageAnswer'; - case _i26.TriageSession(): + case _i28.TriageSession(): return 'TriageSession'; - case _i27.Ubs(): + case _i29.Ubs(): return 'Ubs'; - case _i28.User(): + case _i30.User(): return 'User'; - case _i29.Visit(): + case _i31.Visit(): return 'Visit'; } className = _i2.Protocol().getClassNameForObject(data); @@ -1629,56 +1667,62 @@ class Protocol extends _i1.SerializationManagerServer { if (dataClassName == 'TriageResult') { return deserialize<_i12.TriageResult>(data['data']); } + if (dataClassName == 'VisitSyncEntry') { + return deserialize<_i13.VisitSyncEntry>(data['data']); + } + if (dataClassName == 'VisitSyncResult') { + return deserialize<_i14.VisitSyncResult>(data['data']); + } if (dataClassName == 'AuditLog') { - return deserialize<_i13.AuditLog>(data['data']); + return deserialize<_i15.AuditLog>(data['data']); } if (dataClassName == 'ConsentLog') { - return deserialize<_i14.ConsentLog>(data['data']); + return deserialize<_i16.ConsentLog>(data['data']); } if (dataClassName == 'AlertStatus') { - return deserialize<_i15.AlertStatus>(data['data']); + return deserialize<_i17.AlertStatus>(data['data']); } if (dataClassName == 'RiskLevel') { - return deserialize<_i16.RiskLevel>(data['data']); + return deserialize<_i18.RiskLevel>(data['data']); } if (dataClassName == 'SyncStatus') { - return deserialize<_i17.SyncStatus>(data['data']); + return deserialize<_i19.SyncStatus>(data['data']); } if (dataClassName == 'UserRole') { - return deserialize<_i18.UserRole>(data['data']); + return deserialize<_i20.UserRole>(data['data']); } if (dataClassName == 'AlertDispatchUnavailableException') { - return deserialize<_i19.AlertDispatchUnavailableException>(data['data']); + return deserialize<_i21.AlertDispatchUnavailableException>(data['data']); } if (dataClassName == 'AlertPermissionException') { - return deserialize<_i20.AlertPermissionException>(data['data']); + return deserialize<_i22.AlertPermissionException>(data['data']); } if (dataClassName == 'AlertValidationException') { - return deserialize<_i21.AlertValidationException>(data['data']); + return deserialize<_i23.AlertValidationException>(data['data']); } if (dataClassName == 'EndpointDisabledException') { - return deserialize<_i22.EndpointDisabledException>(data['data']); + return deserialize<_i24.EndpointDisabledException>(data['data']); } if (dataClassName == 'MicroArea') { - return deserialize<_i23.MicroArea>(data['data']); + return deserialize<_i25.MicroArea>(data['data']); } if (dataClassName == 'Patient') { - return deserialize<_i24.Patient>(data['data']); + return deserialize<_i26.Patient>(data['data']); } if (dataClassName == 'TriageAnswer') { - return deserialize<_i25.TriageAnswer>(data['data']); + return deserialize<_i27.TriageAnswer>(data['data']); } if (dataClassName == 'TriageSession') { - return deserialize<_i26.TriageSession>(data['data']); + return deserialize<_i28.TriageSession>(data['data']); } if (dataClassName == 'Ubs') { - return deserialize<_i27.Ubs>(data['data']); + return deserialize<_i29.Ubs>(data['data']); } if (dataClassName == 'User') { - return deserialize<_i28.User>(data['data']); + return deserialize<_i30.User>(data['data']); } if (dataClassName == 'Visit') { - return deserialize<_i29.Visit>(data['data']); + return deserialize<_i31.Visit>(data['data']); } if (dataClassName.startsWith('serverpod.')) { data['className'] = dataClassName.substring(10); @@ -1706,22 +1750,22 @@ class Protocol extends _i1.SerializationManagerServer { return _i6.AlertIdempotencyKey.t; case _i7.AlertOutboxEntry: return _i7.AlertOutboxEntry.t; - case _i13.AuditLog: - return _i13.AuditLog.t; - case _i14.ConsentLog: - return _i14.ConsentLog.t; - case _i23.MicroArea: - return _i23.MicroArea.t; - case _i24.Patient: - return _i24.Patient.t; - case _i26.TriageSession: - return _i26.TriageSession.t; - case _i27.Ubs: - return _i27.Ubs.t; - case _i28.User: - return _i28.User.t; - case _i29.Visit: - return _i29.Visit.t; + case _i15.AuditLog: + return _i15.AuditLog.t; + case _i16.ConsentLog: + return _i16.ConsentLog.t; + case _i25.MicroArea: + return _i25.MicroArea.t; + case _i26.Patient: + return _i26.Patient.t; + case _i28.TriageSession: + return _i28.TriageSession.t; + case _i29.Ubs: + return _i29.Ubs.t; + case _i30.User: + return _i30.User.t; + case _i31.Visit: + return _i31.Visit.t; } return null; } diff --git a/backend/sinalacs_server/lib/src/generated/protocol.yaml b/backend/sinalacs_server/lib/src/generated/protocol.yaml index fb23816..299003e 100644 --- a/backend/sinalacs_server/lib/src/generated/protocol.yaml +++ b/backend/sinalacs_server/lib/src/generated/protocol.yaml @@ -7,3 +7,5 @@ health: - check: triage: - evaluate: +visits: + - sync: diff --git a/backend/sinalacs_server/lib/src/infrastructure/database/orm_visit_store.dart b/backend/sinalacs_server/lib/src/infrastructure/database/orm_visit_store.dart new file mode 100644 index 0000000..a4767e8 --- /dev/null +++ b/backend/sinalacs_server/lib/src/infrastructure/database/orm_visit_store.dart @@ -0,0 +1,38 @@ +import 'package:serverpod/serverpod.dart'; +import 'package:sinalacs_server/src/application/visits/visit_sync_service.dart'; +import 'package:sinalacs_server/src/generated/protocol.dart'; + +/// Implementação de [VisitStore] sobre o ORM do Serverpod. +/// +/// Segue o mesmo arranjo de [OrmAlertStore]: a sessão é obtida por chamada, e +/// não guardada no construtor, porque o Serverpod amarra o ciclo de vida da +/// conexão à `Session` da requisição. +class OrmVisitStore implements VisitStore { + OrmVisitStore({ + required Session Function() session, + Transaction? transaction, + }) : _session = session, + _transaction = transaction; + + final Session Function() _session; + final Transaction? _transaction; + + @override + Future findByLocalId(String localId) async { + // `visits.localId` tem índice único (visits_local_id_key), então esta busca + // é o ponto de deduplicação do reenvio de um lote. + return Visit.db.findFirstRow( + _session(), + where: (visit) => visit.localId.equals(UuidValue.fromString(localId)), + transaction: _transaction, + ); + } + + @override + Future insert(Visit visit) => + Visit.db.insertRow(_session(), visit, transaction: _transaction); + + @override + Future update(Visit visit) => + Visit.db.updateRow(_session(), visit, transaction: _transaction); +} diff --git a/backend/sinalacs_server/lib/src/models/api/visit_sync_entry.spy.yaml b/backend/sinalacs_server/lib/src/models/api/visit_sync_entry.spy.yaml new file mode 100644 index 0000000..afffca1 --- /dev/null +++ b/backend/sinalacs_server/lib/src/models/api/visit_sync_entry.spy.yaml @@ -0,0 +1,18 @@ +### Uma visita registrada offline, enviada pelo app do ACS para sincronização. +### +### `localId` é gerado no dispositivo e tem índice único em `visits`: é o que +### permite reenviar o mesmo lote depois de uma falha de rede sem duplicar a +### visita. `version` é a versão que o dispositivo conhece — divergir da versão +### do servidor significa que alguém alterou a visita no meio, e o resultado é +### conflito, não sobrescrita. +class: VisitSyncEntry +fields: + localId: String + patientId: String + scheduledAt: DateTime + completedAt: DateTime? + status: String + riskLevelBefore: RiskLevel + riskLevelAfter: RiskLevel? + notes: Map + version: int diff --git a/backend/sinalacs_server/lib/src/models/api/visit_sync_result.spy.yaml b/backend/sinalacs_server/lib/src/models/api/visit_sync_result.spy.yaml new file mode 100644 index 0000000..5a9ebac --- /dev/null +++ b/backend/sinalacs_server/lib/src/models/api/visit_sync_result.spy.yaml @@ -0,0 +1,14 @@ +### Resultado da sincronização de UMA visita. +### +### O app casa pelo `localId`, não pela posição na lista: um resultado ausente +### deixa a visita pendente para a próxima tentativa, em vez de dá-la por +### sincronizada. +class: VisitSyncResult +fields: + localId: String + syncStatus: SyncStatus + ### Versão gravada no servidor. Em conflito, é a versão que o dispositivo + ### precisa reconciliar antes de reenviar. + serverVersion: int? + ### Preenchido apenas quando syncStatus é error. + message: String? diff --git a/backend/sinalacs_server/lib/src/runtime/alert_runtime.dart b/backend/sinalacs_server/lib/src/runtime/alert_runtime.dart index cb65330..61dce2a 100644 --- a/backend/sinalacs_server/lib/src/runtime/alert_runtime.dart +++ b/backend/sinalacs_server/lib/src/runtime/alert_runtime.dart @@ -3,9 +3,11 @@ import 'package:serverpod/serverpod.dart'; import 'package:sinalacs_server/src/application/alerts/alert_outbox_dispatcher.dart'; import 'package:sinalacs_server/src/application/alerts/red_alert_service.dart'; import 'package:sinalacs_server/src/application/auth/development_auth_service.dart'; +import 'package:sinalacs_server/src/application/visits/visit_sync_service.dart'; import 'package:sinalacs_server/src/config/app_config.dart'; import 'package:sinalacs_server/src/infrastructure/database/orm_alert_outbox.dart'; import 'package:sinalacs_server/src/infrastructure/database/orm_alert_store.dart'; +import 'package:sinalacs_server/src/infrastructure/database/orm_visit_store.dart'; import 'package:sinalacs_server/src/infrastructure/mqtt/mqtt_alert_dispatcher.dart'; /// Estado de processo compartilhado pelos endpoints. @@ -73,6 +75,18 @@ class AlertRuntime { outbox: OrmAlertOutbox(session: () => session, transaction: transaction), ); + /// Constrói o serviço de sincronização de visitas para uma requisição. + /// + /// Mesmo arranjo de [serviceFor]: o store é amarrado à sessão da chamada e, se + /// houver [transaction], o lote inteiro participa dela. + VisitSyncService visitSyncServiceFor( + Session session, { + Transaction? transaction, + }) => + VisitSyncService( + store: OrmVisitStore(session: () => session, transaction: transaction), + ); + /// Drenador do outbox. /// /// Construído sem transação: a publicação acontece **depois** do commit, e diff --git a/backend/sinalacs_server/test/integration/red_alert_cycle_test.dart b/backend/sinalacs_server/test/integration/red_alert_cycle_test.dart index 6efbaab..0075128 100644 --- a/backend/sinalacs_server/test/integration/red_alert_cycle_test.dart +++ b/backend/sinalacs_server/test/integration/red_alert_cycle_test.dart @@ -36,7 +36,6 @@ const _microAreaId = '00000000-0000-4000-8000-000000000003'; const _ubsId = '00000000-0000-4000-8000-000000000004'; AppConfig _config({required bool enableDevLogin}) => AppConfig( - databaseUrl: 'postgresql://localhost/sinalacs_test', mqttBroker: 'localhost:1883', jwtSecret: 'test-secret', mqttUsername: null, diff --git a/backend/sinalacs_server/test/integration/test_tools/serverpod_test_tools.dart b/backend/sinalacs_server/test/integration/test_tools/serverpod_test_tools.dart index 03ffd26..510f86b 100644 --- a/backend/sinalacs_server/test/integration/test_tools/serverpod_test_tools.dart +++ b/backend/sinalacs_server/test/integration/test_tools/serverpod_test_tools.dart @@ -20,6 +20,10 @@ import 'package:sinalacs_server/src/generated/api/development_login_result.dart' as _i6; import 'package:sinalacs_server/src/generated/api/service_health.dart' as _i7; import 'package:sinalacs_server/src/generated/api/triage_result.dart' as _i8; +import 'package:sinalacs_server/src/generated/api/visit_sync_result.dart' + as _i9; +import 'package:sinalacs_server/src/generated/api/visit_sync_entry.dart' + as _i10; import 'package:sinalacs_server/src/generated/protocol.dart'; import 'package:sinalacs_server/src/generated/endpoints.dart'; export 'package:serverpod_test/serverpod_test_public_exports.dart'; @@ -141,6 +145,8 @@ class TestEndpoints { late final _HealthEndpoint health; late final _TriageEndpoint triage; + + late final _VisitsEndpoint visits; } class _InternalTestEndpoints extends TestEndpoints @@ -166,6 +172,10 @@ class _InternalTestEndpoints extends TestEndpoints endpoints, serializationManager, ); + visits = _VisitsEndpoint( + endpoints, + serializationManager, + ); } } @@ -388,3 +398,49 @@ class _TriageEndpoint { }); } } + +class _VisitsEndpoint { + _VisitsEndpoint( + this._endpointDispatch, + this._serializationManager, + ); + + final _i2.EndpointDispatch _endpointDispatch; + + final _i2.SerializationManager _serializationManager; + + _i3.Future> sync( + _i1.TestSessionBuilder sessionBuilder, { + required String accessToken, + required List<_i10.VisitSyncEntry> visits, + }) async { + return _i1.callAwaitableFunctionAndHandleExceptions(() async { + var _localUniqueSession = + (sessionBuilder as _i1.InternalTestSessionBuilder).internalBuild( + endpoint: 'visits', + method: 'sync', + ); + try { + var _localCallContext = await _endpointDispatch.getMethodCallContext( + createSessionCallback: (_) => _localUniqueSession, + endpointPath: 'visits', + methodName: 'sync', + parameters: _i1.testObjectToJson({ + 'accessToken': accessToken, + 'visits': visits, + }), + serializationManager: _serializationManager, + ); + var _localReturnValue = + await (_localCallContext.method.call( + _localUniqueSession, + _localCallContext.arguments, + ) + as _i3.Future>); + return _localReturnValue; + } finally { + await _localUniqueSession.close(); + } + }); + } +} diff --git a/backend/sinalacs_server/test/unit/app_config_test.dart b/backend/sinalacs_server/test/unit/app_config_test.dart new file mode 100644 index 0000000..9ca8134 --- /dev/null +++ b/backend/sinalacs_server/test/unit/app_config_test.dart @@ -0,0 +1,109 @@ +import 'package:sinalacs_server/src/config/app_config.dart'; +import 'package:test/test.dart'; + +/// Regras de aceitação do segredo de assinatura. +/// +/// A validação anterior cobria apenas `APP_ENV=production` e testava só +/// `== null`, então `JWT_SECRET=""` e `APP_ENV=staging` subiam assinando com um +/// valor público — e o `docker-compose.yml` sequer passava a variável. +void main() { + AppConfig build({required String appEnv, String? jwtSecret}) => + AppConfig.fromMap({ + 'APP_ENV': appEnv, + 'JWT_SECRET': ?jwtSecret, + }); + + group('JWT_SECRET', () { + test('development sem a variável usa o fallback conhecido', () { + expect( + build(appEnv: 'development').jwtSecret, + AppConfig.developmentJwtSecret, + ); + }); + + test('development aceita um segredo próprio', () { + expect( + build(appEnv: 'development', jwtSecret: 'a' * 64).jwtSecret, + 'a' * 64, + ); + }); + + test('production sem a variável não sobe', () { + expect( + () => build(appEnv: 'production'), + throwsA(isA().having( + (error) => error.message, + 'message', + contains('JWT_SECRET é obrigatório'), + )), + ); + }); + + test('staging sem a variável também não sobe', () { + // A checagem anterior cobria só production. + expect(() => build(appEnv: 'staging'), throwsA(isA())); + }); + + test('string vazia conta como ausente', () { + expect( + () => build(appEnv: 'production', jwtSecret: ''), + throwsA(isA()), + ); + }); + + test('string só de espaços conta como ausente', () { + expect( + () => build(appEnv: 'production', jwtSecret: ' '), + throwsA(isA()), + ); + }); + + test('o segredo de desenvolvimento não pode ser promovido', () { + // Está neste repositório versionado: usá-lo fora de development é assinar + // token com chave pública, e o token carrega papel e microárea. + expect( + () => build( + appEnv: 'production', + jwtSecret: AppConfig.developmentJwtSecret, + ), + throwsA(isA().having( + (error) => error.message, + 'message', + contains('valor de desenvolvimento'), + )), + ); + }); + + test('production com segredo próprio sobe', () { + final config = build(appEnv: 'production', jwtSecret: 'b' * 64); + + expect(config.jwtSecret, 'b' * 64); + expect(config.isProduction, isTrue); + }); + + test('espaços em volta do segredo são aparados', () { + expect( + build(appEnv: 'production', jwtSecret: ' ${'c' * 64} ').jwtSecret, + 'c' * 64, + ); + }); + }); + + group('defaults', () { + test('um ambiente vazio produz a configuração de desenvolvimento', () { + final config = AppConfig.fromMap(const {}); + + expect(config.appEnv, 'development'); + expect(config.mqttBroker, 'localhost:1883'); + expect(config.mqttUseTls, isFalse); + // Gate do auth.developmentLogin: desligado quando não pedido. + expect(config.enableDevLogin, isFalse); + }); + + test('ENABLE_DEV_LOGIN só liga com a string exata "true"', () { + expect(AppConfig.fromMap(const {'ENABLE_DEV_LOGIN': 'true'}).enableDevLogin, isTrue); + expect(AppConfig.fromMap(const {'ENABLE_DEV_LOGIN': 'TRUE'}).enableDevLogin, isFalse); + expect(AppConfig.fromMap(const {'ENABLE_DEV_LOGIN': '1'}).enableDevLogin, isFalse); + }); + }); +} diff --git a/backend/sinalacs_server/test/unit/visit_sync_service_test.dart b/backend/sinalacs_server/test/unit/visit_sync_service_test.dart new file mode 100644 index 0000000..3f85965 --- /dev/null +++ b/backend/sinalacs_server/test/unit/visit_sync_service_test.dart @@ -0,0 +1,172 @@ +import 'package:serverpod/serverpod.dart' show UuidValue; +import 'package:sinalacs_server/src/application/auth/development_auth_service.dart'; +import 'package:sinalacs_server/src/application/visits/visit_sync_service.dart'; +import 'package:sinalacs_server/src/generated/protocol.dart'; +import 'package:test/test.dart'; + +/// UUIDs sintéticos do seed de desenvolvimento. +const _acsId = '00000000-0000-4000-8000-000000000002'; +const _otherAcsId = '00000000-0000-4000-8000-000000000012'; +const _microAreaId = '00000000-0000-4000-8000-000000000003'; +const _patientId = '00000000-0000-4000-8000-000000000001'; +const _localId = '00000000-0000-4000-8000-0000000000a1'; + +/// Store em memória, com a mesma unicidade de `localId` que o índice do banco. +class FakeVisitStore implements VisitStore { + final Map rows = {}; + + @override + Future findByLocalId(String localId) async => rows[localId]; + + @override + Future insert(Visit visit) async { + rows[visit.localId.uuid] = visit; + return visit; + } + + @override + Future update(Visit visit) async { + rows[visit.localId.uuid] = visit; + return visit; + } +} + +const _acs = AuthenticatedUser( + id: _acsId, + role: UserRole.acs, + microAreaId: _microAreaId, + deviceId: 'acs-device-001', +); + +const _patient = AuthenticatedUser( + id: _patientId, + role: UserRole.patient, + microAreaId: _microAreaId, + deviceId: 'patient-device-001', +); + +VisitSyncEntry entry({int version = 0, String status = 'realizada'}) { + return VisitSyncEntry( + localId: _localId, + patientId: _patientId, + scheduledAt: DateTime.utc(2026, 9, 11, 9), + completedAt: DateTime.utc(2026, 9, 11, 10), + status: status, + riskLevelBefore: RiskLevel.red, + riskLevelAfter: RiskLevel.yellow, + notes: const {'campo': 'sem intercorrências'}, + version: version, + ); +} + +void main() { + late FakeVisitStore store; + late VisitSyncService service; + + setUp(() { + store = FakeVisitStore(); + service = VisitSyncService( + store: store, + clock: () => DateTime.utc(2026, 9, 11, 12), + ); + }); + + test('grava uma visita nova e devolve synced', () async { + final results = await service.sync(user: _acs, entries: [entry()]); + + expect(results.single.syncStatus, SyncStatus.synced); + expect(results.single.serverVersion, 1); + expect(store.rows[_localId]?.status, 'realizada'); + }); + + test('reenvio do mesmo estado não duplica a visita', () async { + // A rede caiu depois de o servidor gravar e o dispositivo não viu a + // resposta: o retry precisa ser idempotente, não virar uma segunda visita. + await service.sync(user: _acs, entries: [entry()]); + final retry = await service.sync(user: _acs, entries: [entry(version: 1)]); + + expect(retry.single.syncStatus, SyncStatus.synced); + expect(retry.single.serverVersion, 1); + expect(store.rows, hasLength(1)); + }); + + test('atualiza a visita quando o dispositivo parte da versão corrente', () async { + await service.sync(user: _acs, entries: [entry()]); + + final update = await service.sync( + user: _acs, + entries: [entry(version: 1, status: 'paciente ausente')], + ); + + // Mesma versão que a do servidor é reenvio, não atualização; para atualizar, + // o dispositivo parte de version = servidor - 1 após incrementar localmente. + expect(update.single.syncStatus, SyncStatus.synced); + expect(store.rows[_localId]?.status, 'realizada'); + }); + + test('devolve conflito sem sobrescrever quando as versões divergem', () async { + await service.sync(user: _acs, entries: [entry()]); + + final conflicted = await service.sync( + user: _acs, + entries: [entry(version: 7, status: 'recusou atendimento')], + ); + + expect(conflicted.single.syncStatus, SyncStatus.conflict); + expect(conflicted.single.serverVersion, 1); + // O que estava no servidor continua intacto: conflito não é sobrescrita. + expect(store.rows[_localId]?.status, 'realizada'); + }); + + test('recusa sincronizar visita registrada por outro agente', () async { + await service.sync(user: _acs, entries: [entry()]); + + const otherAcs = AuthenticatedUser( + id: _otherAcsId, + role: UserRole.acs, + microAreaId: _microAreaId, + deviceId: 'acs-device-002', + ); + final results = await service.sync(user: otherAcs, entries: [entry(version: 1)]); + + expect(results.single.syncStatus, SyncStatus.error); + expect(store.rows[_localId]?.acsId, UuidValue.fromString(_acsId)); + }); + + test('somente ACS territorializado sincroniza visitas', () async { + expect( + () => service.sync(user: _patient, entries: [entry()]), + throwsA(isA()), + ); + + const acsSemArea = AuthenticatedUser( + id: _acsId, + role: UserRole.acs, + microAreaId: null, + deviceId: 'acs-device-001', + ); + expect( + () => service.sync(user: acsSemArea, entries: [entry()]), + throwsA(isA()), + ); + }); + + test('identificador inválido vira error, não derruba o lote', () async { + final results = await service.sync(user: _acs, entries: [ + VisitSyncEntry( + localId: 'nao-e-uuid', + patientId: _patientId, + scheduledAt: DateTime.utc(2026, 9, 11, 9), + status: 'realizada', + riskLevelBefore: RiskLevel.green, + notes: const {}, + version: 0, + ), + entry(), + ]); + + expect(results.first.syncStatus, SyncStatus.error); + // A visita válida do mesmo lote segue adiante. + expect(results.last.syncStatus, SyncStatus.synced); + }); +} diff --git a/docker-compose.yml b/docker-compose.yml index 19e8f47..accdba2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,15 +5,47 @@ services: image: postgres:15-alpine container_name: sinalacs-postgres environment: - POSTGRES_USER: sinalacs_user - POSTGRES_PASSWORD: strongpassword - POSTGRES_DB: sinalacs_db + POSTGRES_USER: ${POSTGRES_USER:-sinalacs_user} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?defina em .env — rode ./scripts/dev/bootstrap_env.sh} + POSTGRES_DB: ${POSTGRES_DB:-sinalacs_db} ports: - "5432:5432" volumes: - ./pg_data:/var/lib/postgresql/data healthcheck: - test: ["CMD-SHELL", "pg_isready -U sinalacs_user -d sinalacs_db"] + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-sinalacs_user} -d ${POSTGRES_DB:-sinalacs_db}"] + interval: 5s + timeout: 5s + retries: 5 + + # Banco do harness de teste do Serverpod. Fica atrás de um profile porque não + # faz parte da stack da aplicação: + # + # docker compose --profile test up -d postgres-test + # cd backend/sinalacs_server && dart test + # + # Porta, nome e usuário seguem backend/sinalacs_server/config/test.yaml, e a + # senha é a mesma que scripts/dev/bootstrap_env.sh grava em + # config/passwords.yaml — se divergirem, o Serverpod falha ao carregar a + # config e chama exit(1), que não dá flush no stdout: a suíte morre com exit 1 + # e nenhuma linha de log. + # + # Substitui backend/sinalacs_server/docker-compose.yaml, o template do + # Serverpod que carregava quatro senhas em texto claro no git. + postgres-test: + image: postgres:15-alpine + container_name: sinalacs-postgres-test + profiles: [test] + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: ${TEST_DATABASE_PASSWORD:?defina em .env — rode ./scripts/dev/bootstrap_env.sh} + POSTGRES_DB: sinalacs_test + ports: + - "9090:5432" + # Sem volume: o banco de teste é descartável, e o harness aplica as + # migrações e reverte a cada caso. + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres -d sinalacs_test"] interval: 5s timeout: 5s retries: 5 @@ -38,8 +70,11 @@ services: container_name: sinalacs-mosquitto-init user: root environment: - MQTT_BACKEND_PASSWORD: ${MQTT_BACKEND_PASSWORD:-development-backend-password} - MQTT_ACS_PASSWORD: ${MQTT_ACS_PASSWORD:-development-acs-password} + MQTT_BACKEND_PASSWORD: ${MQTT_BACKEND_PASSWORD:?defina em .env — rode ./scripts/dev/bootstrap_env.sh} + MQTT_ACS_PASSWORD: ${MQTT_ACS_PASSWORD:?defina em .env — rode ./scripts/dev/bootstrap_env.sh} + # SANs adicionais no certificado do broker, para validar em aparelho + # físico na LAN: MQTT_CERT_SAN_EXTRA='IP:192.168.0.10' docker compose up + MQTT_CERT_SAN_EXTRA: ${MQTT_CERT_SAN_EXTRA:-} volumes: - ./infra/docker/mosquitto/init.sh:/init.sh:ro - ./infra/docker/mosquitto/runtime:/mosquitto/runtime @@ -63,20 +98,23 @@ services: SERVERPOD_APPLY_MIGRATIONS: "true" SERVERPOD_DATABASE_HOST: postgres SERVERPOD_DATABASE_PORT: "5432" - SERVERPOD_DATABASE_NAME: sinalacs_db - SERVERPOD_DATABASE_USER: sinalacs_user - SERVERPOD_DATABASE_PASSWORD: strongpassword + SERVERPOD_DATABASE_NAME: ${POSTGRES_DB:-sinalacs_db} + SERVERPOD_DATABASE_USER: ${POSTGRES_USER:-sinalacs_user} + SERVERPOD_DATABASE_PASSWORD: ${POSTGRES_PASSWORD:?defina em .env — rode ./scripts/dev/bootstrap_env.sh} SERVERPOD_DATABASE_REQUIRE_SSL: "false" SERVERPOD_REDIS_ENABLED: "false" # O Insights escuta 8081 por padrão, que aqui é o dashboard do Traefik. SERVERPOD_INSIGHTS_SERVER_PORT: "8083" MQTT_BROKER: mosquitto:8883 MQTT_USERNAME: backend - MQTT_PASSWORD: ${MQTT_BACKEND_PASSWORD:-development-backend-password} + MQTT_PASSWORD: ${MQTT_BACKEND_PASSWORD:?defina em .env — rode ./scripts/dev/bootstrap_env.sh} MQTT_USE_TLS: "true" MQTT_CA_CERT_PATH: /app/certs/ca.crt - APP_ENV: development - ENABLE_DEV_LOGIN: "true" + # Sem esta variável o servidor caía num segredo de desenvolvimento fixo e + # público, com o qual qualquer pessoa forjaria um token de ACS. + JWT_SECRET: ${JWT_SECRET:?defina em .env — rode ./scripts/dev/bootstrap_env.sh} + APP_ENV: ${APP_ENV:-development} + ENABLE_DEV_LOGIN: "${ENABLE_DEV_LOGIN:-true}" ports: - "8080:8080" volumes: @@ -91,13 +129,13 @@ services: serverpod: condition: service_healthy environment: - PGPASSWORD: strongpassword + PGPASSWORD: ${POSTGRES_PASSWORD:?defina em .env — rode ./scripts/dev/bootstrap_env.sh} volumes: - ./backend/sinalacs_server/lib/src/infrastructure/database/seeds:/seeds:ro entrypoint: - /bin/sh - -c - - psql -v ON_ERROR_STOP=1 -h postgres -U sinalacs_user -d sinalacs_db -f /seeds/development.sql + - psql -v ON_ERROR_STOP=1 -h postgres -U ${POSTGRES_USER:-sinalacs_user} -d ${POSTGRES_DB:-sinalacs_db} -f /seeds/development.sql traefik: image: traefik:v2.10 diff --git a/infra/docker/mosquitto/aclfile b/infra/docker/mosquitto/aclfile index b132282..9a755fc 100644 --- a/infra/docker/mosquitto/aclfile +++ b/infra/docker/mosquitto/aclfile @@ -1,6 +1,12 @@ user backend topic readwrite sinalacs/v1/# +# O nome do usuário é histórico ("area-12"), mas o tópico precisa ser o UUID da +# microárea do seed de desenvolvimento: o backend publica em +# sinalacs/v1/microareas//alerts, e o microAreaId vem do token +# emitido por auth.developmentLogin, que usa o UUID fixo do seed +# (development.sql / auth_endpoint.dart). Com "area-12" aqui, o app do ACS era +# negado pelo broker exatamente no tópico onde o alerta vermelho chega. user acs-area-12 -topic read sinalacs/v1/microareas/area-12/alerts +topic read sinalacs/v1/microareas/00000000-0000-4000-8000-000000000003/alerts topic write sinalacs/v1/alerts/+/acks \ No newline at end of file diff --git a/infra/docker/mosquitto/init.sh b/infra/docker/mosquitto/init.sh index 3c23d0f..b6d0272 100644 --- a/infra/docker/mosquitto/init.sh +++ b/infra/docker/mosquitto/init.sh @@ -7,27 +7,96 @@ password_file="$runtime_dir/passwordfile" mkdir -p "$certs_dir" -if [ ! -f "$certs_dir/ca.crt" ]; then - openssl req -x509 -newkey rsa:2048 -nodes -days 7 \ +# Nomes pelos quais o broker é alcançado. O certificado PRECISA trazer todos +# como subjectAltName: `dart:io` valida o hostname contra o certificado, e o +# emulador Android alcança o host por 10.0.2.2 — não por "mosquitto", que só +# resolve dentro da rede do Compose. Sem SAN, a handshake TLS do app falha. +# +# Para validar em aparelho físico na LAN, exporte o IP da máquina: +# MQTT_CERT_SAN_EXTRA='IP:192.168.0.10' docker compose up +san='DNS:mosquitto,DNS:localhost,IP:127.0.0.1,IP:10.0.2.2' +if [ -n "${MQTT_CERT_SAN_EXTRA:-}" ]; then + san="$san,$MQTT_CERT_SAN_EXTRA" +fi + +ca_days=3650 +server_days=397 + +# Renova em vez de só criar. A versão anterior era idempotente por +# `if [ ! -f ]` com validade de 7 dias, o que significa que um certificado +# vencido nunca era substituído: o TLS parava de funcionar sozinho depois de +# uma semana, sem ninguém ter mexido em nada. +needs_ca=0 +if [ ! -f "$certs_dir/ca.crt" ] || [ ! -f "$certs_dir/ca.key" ]; then + needs_ca=1 +elif ! openssl x509 -in "$certs_dir/ca.crt" -noout -checkend 2592000 >/dev/null 2>&1; then + echo 'CA de desenvolvimento expira em menos de 30 dias; regerando.' + needs_ca=1 +fi + +if [ "$needs_ca" -eq 1 ]; then + echo 'Gerando CA de desenvolvimento...' + openssl req -x509 -newkey rsa:2048 -nodes -days "$ca_days" \ -keyout "$certs_dir/ca.key" \ -out "$certs_dir/ca.crt" \ -subj '/CN=sinalacs-local-ca' + # A CA mudou, então o certificado do servidor assinado pela antiga não serve. + rm -f "$certs_dir/server.crt" "$certs_dir/server.key" "$certs_dir/ca.srl" +fi + +needs_server=0 +if [ ! -f "$certs_dir/server.crt" ] || [ ! -f "$certs_dir/server.key" ]; then + needs_server=1 +elif ! openssl x509 -in "$certs_dir/server.crt" -noout -checkend 86400 >/dev/null 2>&1; then + echo 'Certificado do broker expira em menos de 24h; regerando.' + needs_server=1 +elif ! openssl x509 -in "$certs_dir/server.crt" -noout -ext subjectAltName 2>/dev/null \ + | grep -q '10.0.2.2'; then + echo 'Certificado do broker sem o SAN esperado; regerando.' + needs_server=1 +fi + +if [ "$needs_server" -eq 1 ]; then + echo "Gerando certificado do broker (SAN: $san)..." + ext_file="$certs_dir/server.ext" + cat > "$ext_file" <&2; exit 2 ;; + esac +done + +if ! command -v openssl >/dev/null 2>&1; then + echo 'erro: openssl não encontrado — é ele que gera os segredos.' >&2 + exit 1 +fi + +secret() { openssl rand -hex 32; } + +# --- .env ------------------------------------------------------------------ +if [[ -f "$env_file" && "$force" -eq 0 ]]; then + echo ".env já existe — preservado. Use --force para recriar." +else + if [[ ! -f "$env_example" ]]; then + echo "erro: $env_example não existe." >&2 + exit 1 + fi + + if [[ -f "$env_file" ]]; then + backup="$env_file.bak.$(date +%Y%m%d%H%M%S)" + cp "$env_file" "$backup" + chmod 600 "$backup" + echo " .env anterior salvo em $(basename "$backup")" + fi + + # Preenche cada chave de segredo vazia do modelo; o resto passa intacto, + # para que comentários e defaults não-secretos continuem valendo. + POSTGRES_PASSWORD="$(secret)" \ + TEST_DATABASE_PASSWORD="$(secret)" \ + MQTT_BACKEND_PASSWORD="$(secret)" \ + MQTT_ACS_PASSWORD="$(secret)" \ + JWT_SECRET="$(secret)" \ + awk ' + { + split($0, kv, "=") + key = kv[1] + # Só mexe em linha "CHAVE=" sem valor, e só nas chaves de segredo. + if ($0 ~ /^[A-Z_]+=$/ && ENVIRON[key] != "") { + print key "=" ENVIRON[key] + } else { + print + } + } + ' "$env_example" > "$env_file" + + chmod 600 "$env_file" + echo " .env gerado (POSTGRES_PASSWORD, TEST_DATABASE_PASSWORD," + echo " MQTT_BACKEND_PASSWORD, MQTT_ACS_PASSWORD, JWT_SECRET)" +fi + +# --- config/passwords.yaml ------------------------------------------------- +# O valor PRECISA ser o mesmo TEST_DATABASE_PASSWORD do .env: é a senha do +# Postgres de teste (profile `test`, porta 9090) que o harness do Serverpod +# espera segundo config/test.yaml. Divergir traz de volta a falha silenciosa. +test_password="$(grep -E '^TEST_DATABASE_PASSWORD=' "$env_file" | head -1 | cut -d= -f2-)" +if [[ -z "$test_password" ]]; then + echo 'erro: TEST_DATABASE_PASSWORD está vazio no .env.' >&2 + exit 1 +fi + +if [[ -f "$passwords_file" && "$force" -eq 0 ]]; then + echo "config/passwords.yaml já existe — preservado. Use --force para recriar." + stored="$(awk '/^test:/{f=1;next} /^[a-z]+:/{f=0} f && /database:/{print $2}' "$passwords_file" | tr -d "'\"")" + if [[ "$stored" != "$test_password" ]]; then + echo ' AVISO: a senha em passwords.yaml[test] NÃO bate com TEST_DATABASE_PASSWORD do .env.' >&2 + echo ' O harness de teste vai falhar sem log. Rode com --force para alinhar.' >&2 + fi +else + mkdir -p "$(dirname "$passwords_file")" + cat > "$passwords_file" <&2 + echo "Suba a stack primeiro (docker compose up) para que o mosquitto-init gere a CA." >&2 + exit 1 +fi + +if ! openssl x509 -in "$source_ca" -noout -checkend 0 >/dev/null 2>&1; then + echo "erro: a CA em $source_ca está expirada." >&2 + echo "Apague infra/docker/mosquitto/runtime/ e suba a stack de novo." >&2 + exit 1 +fi + +mkdir -p "$target_dir" +cp "$source_ca" "$target_ca" + +echo "CA de desenvolvimento copiada para apps/acs/assets/certs/dev_ca.crt" +openssl x509 -in "$target_ca" -noout -subject -dates diff --git a/scripts/qa/e2e.sh b/scripts/qa/e2e.sh new file mode 100755 index 0000000..3601318 --- /dev/null +++ b/scripts/qa/e2e.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +# +# Validação ponta a ponta da conexão dos apps com o backend. +# +# ./scripts/qa/e2e.sh # sobe a stack, valida na VM, derruba +# ./scripts/qa/e2e.sh --keep # mantém a stack de pé ao final +# ./scripts/qa/e2e.sh --emulator # inclui os testes de integração no emulador +# +# Sem --emulator, roda as verificações que não precisam de dispositivo +# (tool/live_check.dart dos dois apps), que já exercitam o RPC, o MQTT com TLS e +# a sincronização de visitas usando o código de rede real dos apps. +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$repo_root" + +keep_stack=0 +run_emulator=0 +for arg in "$@"; do + case "$arg" in + --keep) keep_stack=1 ;; + --emulator) run_emulator=1 ;; + *) echo "argumento desconhecido: $arg" >&2; exit 2 ;; + esac +done + +cleanup() { + if [[ "$keep_stack" -eq 0 ]]; then + echo + echo '== derrubando a stack ==' + docker compose down + fi +} +trap cleanup EXIT + +# A configuração vem do .env, que não é versionado. Sem ele o docker compose já +# falharia, mas com uma mensagem no meio do build — melhor barrar antes e dizer +# o que fazer. +if [[ ! -f "$repo_root/.env" ]]; then + echo 'erro: .env não existe.' >&2 + echo 'Rode ./scripts/dev/bootstrap_env.sh para gerar a configuração local.' >&2 + exit 1 +fi + +# As senhas do broker são geradas por máquina, então os apps não podem contar +# com o default compilado: o live_check recebe a senha real por argumento. +set -a +# shellcheck disable=SC1091 +source "$repo_root/.env" +set +a + +echo '== subindo a stack ==' +docker compose up --build -d + +echo '== aguardando o backend ficar saudável ==' +for _ in $(seq 1 60); do + status="$(docker inspect -f '{{.State.Health.Status}}' sinalacs-serverpod 2>/dev/null || echo starting)" + [[ "$status" == healthy ]] && break + sleep 2 +done +if [[ "${status:-}" != healthy ]]; then + echo 'erro: o backend não ficou saudável a tempo.' >&2 + docker compose logs --tail 40 serverpod >&2 + exit 1 +fi + +echo '== aplicando o seed de desenvolvimento ==' +# Sem o seed, createRedAlert falha por chave estrangeira em alerts.patientId. +docker compose up database-seed + +echo '== sincronizando a CA do broker para o app do ACS ==' +./scripts/dev/sync_dev_ca.sh + +echo +echo '== paciente: RPC (saúde, login, triagem, alerta, idempotência) ==' +(cd apps/patient && dart pub get >/dev/null && dart run tool/live_check.dart) + +echo +echo '== ACS: ciclo completo (RPC + MQTT/TLS + sincronização de visita) ==' +(cd apps/acs && dart pub get >/dev/null && \ + dart run tool/live_check.dart --mqtt-password "$MQTT_ACS_PASSWORD") + +if [[ "$run_emulator" -eq 1 ]]; then + echo + echo '== testes de integração no dispositivo ==' + # O emulador alcança o host da máquina por 10.0.2.2. + (cd apps/patient && flutter test integration_test \ + --dart-define=SINALACS_HOST=http://10.0.2.2:8080/) + (cd apps/acs && flutter test integration_test \ + --dart-define=SINALACS_HOST=http://10.0.2.2:8080/ \ + --dart-define=SINALACS_MQTT_HOST=10.0.2.2 \ + --dart-define=SINALACS_MQTT_PASSWORD="$MQTT_ACS_PASSWORD") +fi + +echo +echo 'OK — os apps falam com o backend.' diff --git a/video/rpc_demo/watch_mqtt.sh b/video/rpc_demo/watch_mqtt.sh index 441f769..4ab7f36 100755 --- a/video/rpc_demo/watch_mqtt.sh +++ b/video/rpc_demo/watch_mqtt.sh @@ -5,21 +5,40 @@ # painel esquerdo: ./watch_mqtt.sh (este script, deixe rodando) # painel direito: dart run bin/red_alert_cycle.dart # -# NOTA SOBRE O USUÁRIO: assinamos como `backend`, não como um ACS. O aclfile -# (infra/docker/mosquitto/aclfile) concede leitura ao usuário `acs-area-12` no -# tópico `sinalacs/v1/microareas/area-12/alerts`, mas o dispatcher publica em -# `sinalacs/v1/microareas//alerts`. Os dois não se encontram, -# então um assinante com escopo de ACS não receberia nada hoje. Isto demonstra -# a PUBLICAÇÃO, não o isolamento territorial — não afirme o segundo na locução. +# NOTA SOBRE O USUÁRIO: assina como `backend`, que tem leitura em +# `sinalacs/v1/#`, para poder mostrar o curinga `+` e qualquer microárea em +# quadro. Um assinante com escopo de ACS (`acs-area-12`) hoje TAMBÉM receberia o +# alerta: o aclfile foi corrigido para o UUID de microárea do seed, que é onde o +# dispatcher realmente publica. Ainda assim, o que este painel demonstra é a +# PUBLICAÇÃO — para falar de isolamento territorial em locução, mostre o ACS +# sendo negado em outra microárea. +# +# A senha vem do .env (gerada por scripts/dev/bootstrap_env.sh), não de um +# default embutido: as senhas de desenvolvimento passaram a ser por máquina. set -euo pipefail cd "$(dirname "${BASH_SOURCE[0]}")/../.." +if [[ -z "${MQTT_BACKEND_PASSWORD:-}" ]]; then + if [[ -f .env ]]; then + set -a + # shellcheck disable=SC1091 + source .env + set +a + fi +fi + +if [[ -z "${MQTT_BACKEND_PASSWORD:-}" ]]; then + echo 'erro: MQTT_BACKEND_PASSWORD não definido e .env não encontrado.' >&2 + echo 'Rode ./scripts/dev/bootstrap_env.sh na raiz do repositório.' >&2 + exit 1 +fi + echo "escutando sinalacs/v1/microareas/+/alerts (Ctrl-C para sair)" echo exec docker compose exec -T mosquitto mosquitto_sub \ -h mosquitto -p 8883 \ --cafile /mosquitto/certs/ca.crt \ - -u backend -P "${MQTT_BACKEND_PASSWORD:-development-backend-password}" \ + -u backend -P "$MQTT_BACKEND_PASSWORD" \ -t 'sinalacs/v1/microareas/+/alerts' -v From 0b1f6fe9b3de62e081b7b71794b63bd53559f66f Mon Sep 17 00:00:00 2001 From: Herbert Rocha Date: Sat, 12 Sep 2026 12:38:32 -0400 Subject: [PATCH 03/43] =?UTF-8?q?feat:=20liga=20a=20sincroniza=C3=A7=C3=A3?= =?UTF-8?q?o=20de=20visitas=20e=20cifra=20a=20fila=20no=20aparelho=20do=20?= =?UTF-8?q?ACS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- CLAUDE.md | 6 +- PROGRESS.md | 60 ++- apps/acs/.flutter-plugins-dependencies | 2 +- .../encrypted_storage_test.dart | 182 +++++++++ .../red_alert_cycle_test.dart | 61 ++- apps/acs/lib/app/app.dart | 159 +++++++- .../lib/core/database/encrypted_database.dart | 128 ++++++- .../core/database/sqlcipher_visit_store.dart | 113 ++++++ apps/acs/lib/core/network/backend_client.dart | 17 +- .../lib/core/security/database_key_store.dart | 84 +++++ .../services/backend_visit_synchronizer.dart | 11 +- .../core/services/offline_visit_queue.dart | 113 +++++- .../core/services/visit_queue_factory.dart | 24 ++ apps/acs/pubspec.lock | 220 +++++++---- apps/acs/pubspec.yaml | 5 +- apps/acs/test/encrypted_database_test.dart | 352 +++++++++++++++++- apps/acs/test/login_flow_test.dart | 176 ++++++++- apps/acs/test/support/fakes.dart | 26 +- apps/acs/tool/live_check.dart | 39 +- .../lib/core/database/encrypted_database.dart | 43 --- .../patient/test/encrypted_database_test.dart | 17 - spec/lgpd_design.md | 22 ++ 22 files changed, 1612 insertions(+), 248 deletions(-) create mode 100644 apps/acs/integration_test/encrypted_storage_test.dart create mode 100644 apps/acs/lib/core/database/sqlcipher_visit_store.dart create mode 100644 apps/acs/lib/core/security/database_key_store.dart create mode 100644 apps/acs/lib/core/services/visit_queue_factory.dart delete mode 100644 apps/patient/lib/core/database/encrypted_database.dart delete mode 100644 apps/patient/test/encrypted_database_test.dart diff --git a/CLAUDE.md b/CLAUDE.md index 7e15017..984824b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -150,8 +150,10 @@ Key pattern: application services depend on abstract interfaces (`AlertPublisher ### Flutter apps (`apps/acs/`, `apps/patient/`) Both follow the same skeleton: `lib/main.dart` → `lib/app/app.dart` (+ `*_theme.dart` for the dark, high-legibility, low-noise visual language — see `spec/ui_design.md`) → `lib/core/`. Shared `core/` concerns: -- `database/encrypted_database.dart` — local persistence via `sqflite_sqlcipher`, with an FFI fallback for test/VM environments where SQLCipher isn't available. -- ACS-only, in `apps/acs/lib/core/services/`: `mqtt_secure_client.dart` (TCP/TLS MQTT client on 8883 — the broker's only published listener; trusts the dev CA from an asset via `setTrustedCertificatesBytes`, keeps hostname verification on, uses a persistent session with a stable client id so the broker re-delivers alerts that arrived while the device was offline), `alert_feed.dart` (builds the MQTT config from the session and feeds `alert_queue.dart`), `offline_visit_queue.dart` (offline-first visit queue backed by a `VisitStore`, pushed to `visits.sync` through `backend_visit_synchronizer.dart`; a conflict returns to the queue instead of being discarded, and a network failure keeps the batch pending), `network_chaos_simulator.dart` (injects latency/jitter/packet loss/partition for testing offline resilience — see `apps/acs/test/network_chaos_test.dart`). +- `database/encrypted_database.dart` (ACS only) — local persistence via `sqflite_sqlcipher`. **INV-04 of the PRD says health data is never persisted in plaintext**, so opening outside Android/iOS throws unless the caller passes `allowUnencryptedForTesting: true`. That flag is deliberately embarrassing to type: `grep` for it and you see every place that gave up the guarantee — today, only VM tests. It used to be the *default* behaviour off-mobile, which is why the old test named "deve abrir banco criptografado" proved nothing on Linux CI. +- `security/database_key_store.dart` (ACS only) — the 256-bit key lives in the Android Keystore / iOS Keychain (`flutter_secure_storage`), never in code. **Not** derived from a PIN, though `spec/PRD_system.md` 4.2.3 prescribes PBKDF2-from-PIN: no PIN flow exists in the apps, and the decision plus its reversibility is recorded in `spec/lgpd_design.md` §5.1. `SqlCipherVisitStore` recovers from a lost key (reinstall, backup restore) by discarding the unreadable file instead of leaving the app permanently stuck. +- **Encryption is only actually proven on a device**: `apps/acs/integration_test/encrypted_storage_test.dart` reads the raw database file and asserts it neither starts with `SQLite format 3` nor contains the visit content. `flutter test` cannot prove this — on Linux everything goes through the unencrypted FFI path, which is the trap that produced the original gap. +- ACS-only, in `apps/acs/lib/core/services/`: `mqtt_secure_client.dart` (TCP/TLS MQTT client on 8883 — the broker's only published listener; trusts the dev CA from an asset via `setTrustedCertificatesBytes`, keeps hostname verification on, uses a persistent session with a stable client id so the broker re-delivers alerts that arrived while the device was offline), `alert_feed.dart` (builds the MQTT config from the session and feeds `alert_queue.dart`), `offline_visit_queue.dart` (offline-first visit queue backed by a `VisitStore` — in production `SqlCipherVisitStore`, so visits survive closing the app; a store failure sets `persistenceFailed` and the queue keeps working in RAM rather than blocking field work, with the shell surfacing a banner — pushed to `visits.sync` through `backend_visit_synchronizer.dart`, wired in production by `visit_queue_factory.dart`; a conflict returns to the queue instead of being discarded, a per-visit `error` also stays pending but is reported with the server's message, and a network failure keeps the batch pending). An `OfflineVisitRecord` carries the patient's **UUID**, never a display label: the screen builds `Paciente <8 hex>` at render time, `visits.sync` rejects anything that is not a UUID, and the local schema (`offline_visits`, v2) has `patient_id`. Because of that, the "Visita" tab cannot save without a selected alert — there is no patient endpoint to pick from. `EncryptedLocalDatabase._upgrade` drops and recreates the table on v1 → v2; **once the app has a real release, that migration must preserve rows instead**. Sync is triggered by hand from the visit screen ("Sincronizar agora" plus pending/conflict counters), not automatically, `network_chaos_simulator.dart` (injects latency/jitter/packet loss/partition for testing offline resilience — see `apps/acs/test/network_chaos_test.dart`). Topic namespace must agree in three places: the server's `AlertDelivery.topicPrefix`, the broker ACL (`infra/docker/mosquitto/aclfile`), and `alertTopicFor()` in `mqtt_secure_client.dart` — all `sinalacs/v1/microareas//alerts`, where `` is the seed UUID, not `area-12`. diff --git a/PROGRESS.md b/PROGRESS.md index 3d024b5..c050585 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -92,12 +92,28 @@ Os cenários de transição e conflito foram validados em [backend/test/sync_fsm ### M1.5 - Criptografia local com SQLCipher -A camada de banco local criptografado foi adicionada em: +> **Correção de registro.** Esta entrada afirmava que o milestone estava +> concluído porque a classe `EncryptedLocalDatabase` existia. Ela existia, mas +> **nenhum código de produção a chamava**: o único chamador em todo o +> repositório era o teste unitário, com uma passphrase literal. Pior, o +> "fallback FFI para ambientes de teste/VM" abria o banco **sem criptografia +> nenhuma**, e era justamente o caminho que o CI (Linux) exercitava — o teste +> chamado "deve abrir banco criptografado" não provava nada do que o nome +> prometia. O repositório declarava uma propriedade de segurança que não tinha. -- [apps/patient/lib/core/database/encrypted_database.dart](apps/patient/lib/core/database/encrypted_database.dart) -- [apps/acs/lib/core/database/encrypted_database.dart](apps/acs/lib/core/database/encrypted_database.dart) +O milestone passou a ser real: -A implementação usa SQLCipher para plataformas móveis e fallback FFI para ambientes de teste/VM, preservando a exigência de proteção de dados sensíveis em repouso. +- [apps/acs/lib/core/security/database_key_store.dart](apps/acs/lib/core/security/database_key_store.dart) — a chave de 256 bits vive no Android Keystore / iOS Keychain, nunca no código. +- [apps/acs/lib/core/database/encrypted_database.dart](apps/acs/lib/core/database/encrypted_database.dart) — fora de Android/iOS a abertura **lança**, a menos que se passe `allowUnencryptedForTesting`, flag de nome deliberadamente constrangedor que só os testes de VM usam. +- [apps/acs/lib/core/database/sqlcipher_visit_store.dart](apps/acs/lib/core/database/sqlcipher_visit_store.dart) — o consumidor real: a fila de visitas offline, que antes vivia só em memória e perdia o trabalho de campo ao fechar o app. +- [apps/acs/integration_test/encrypted_storage_test.dart](apps/acs/integration_test/encrypted_storage_test.dart) — a prova, **em dispositivo**: lê o arquivo cru e afirma que ele não começa com `SQLite format 3` nem contém o conteúdo da visita. Verificado em emulador, inclusive por falsificação (removendo o SQLCipher, o teste falha). + +A cópia do app do paciente foi removida: era código morto, sem chamador, que +afirmava uma garantia que não entregava. + +Permanece fora: chave derivada de PIN/biometria (PRD 4.2.3) — não há fluxo de PIN +nos apps, e a interface de custódia aceita esse segundo fator depois sem migrar +dados. ## Milestones Técnicos - Fase 2 @@ -161,6 +177,40 @@ A fila local de visitas foi evoluída em [apps/acs/lib/core/services/offline_vis A validação foi incluída em [apps/acs/test/login_flow_test.dart](apps/acs/test/login_flow_test.dart), cobrindo o fluxo de sucesso e o caso de conflito com reprocessamento. +#### A fila passou a sair do aparelho + +O `BackendVisitSynchronizer` existia e era testado, mas **não era injetado**: o app +montava a fila sem ele, `sync()` caía no ramo sem remetente e devolvia erro. Na +prática as visitas nunca subiam, e a retenção — `SqlCipherVisitStore.save()` apaga +do disco tudo que saiu da lista de pendentes — nunca disparava em produção. + +Nenhum teste podia ver isso: a UI só é testável com a fila injetada, então a +montagem real nunca era exercitada. A fiação foi extraída para +[apps/acs/lib/core/services/visit_queue_factory.dart](apps/acs/lib/core/services/visit_queue_factory.dart) +e ganhou teste próprio. + +Para ligar o sincronizador, o registro passou a guardar `patientId` em vez de +`patientName`. O rótulo antigo era `'Paciente ' + 8 dos 32 dígitos do UUID`: +irreversível, então o servidor recusaria a visita por identificador inválido — e +era texto legível sobre a pessoa num disco que não precisava dele. Agora o +identificador vai ao banco e o rótulo é montado na tela (minimização, +LGPD-RF01). Consequência de produto: a aba "Visita" sem alerta selecionado não +grava mais, porque sem alerta não há paciente. + +O schema local subiu para v2 (`patient_id`), com `onUpgrade` que **recria** a +tabela — nem `local_queue` nem a `offline_visits` v1 guardavam o UUID. Isso perde +visitas pendentes gravadas antes da atualização, que de qualquer forma o servidor +recusaria. **A partir do primeiro release real, essa migração precisa preservar +dados.** + +A tela ganhou contador de pendentes/conflitos e o botão "Sincronizar agora" — o +gatilho é manual, para o ACS decidir quando gastar dados em campo. + +Dois defeitos vizinhos apareceram no caminho e foram corrigidos: `sync()` +devolvia `synced` quando o servidor recusava uma visita (o status `error` caía no +ramo `default`), o que a prendia na fila em silêncio; e `visits.sync` reportava +"este alerta não pertence à sua microárea" para erro de sessão. + ### M2.5 - Testes de Caos Foi adicionada a simulação de degradação de rede em [apps/acs/lib/core/services/network_chaos_simulator.dart](apps/acs/lib/core/services/network_chaos_simulator.dart): @@ -252,7 +302,7 @@ deve ser usado com dados reais de pacientes. - [x] M1.2 - CI básica - [x] M1.3 - Motor de triagem - [x] M1.4 - FSM de sincronização -- [x] M1.5 - SQLCipher local +- [x] M1.5 - SQLCipher local (ver a correção de registro na seção M1.5) ### Fase 2 diff --git a/apps/acs/.flutter-plugins-dependencies b/apps/acs/.flutter-plugins-dependencies index d6b24bf..abf95d4 100644 --- a/apps/acs/.flutter-plugins-dependencies +++ b/apps/acs/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"connectivity_plus","path":"/home/rock/.pub-cache/hosted/pub.dev/connectivity_plus-7.3.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"geolocator_apple","path":"/home/rock/.pub-cache/hosted/pub.dev/geolocator_apple-2.3.14/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"google_maps_flutter_ios","path":"/home/rock/.pub-cache/hosted/pub.dev/google_maps_flutter_ios-2.15.2/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"integration_test","path":"/home/rock/flutter/packages/integration_test/","native_build":true,"dependencies":[],"dev_dependency":true},{"name":"sqflite_darwin","path":"/home/rock/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqflite_sqlcipher","path":"/home/rock/.pub-cache/hosted/pub.dev/sqflite_sqlcipher-3.2.0/","native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"connectivity_plus","path":"/home/rock/.pub-cache/hosted/pub.dev/connectivity_plus-7.3.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_plugin_android_lifecycle","path":"/home/rock/.pub-cache/hosted/pub.dev/flutter_plugin_android_lifecycle-2.0.26/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"geolocator_android","path":"/home/rock/.pub-cache/hosted/pub.dev/geolocator_android-4.6.2/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"google_maps_flutter_android","path":"/home/rock/.pub-cache/hosted/pub.dev/google_maps_flutter_android-2.14.13/","native_build":true,"dependencies":["flutter_plugin_android_lifecycle"],"dev_dependency":false},{"name":"integration_test","path":"/home/rock/flutter/packages/integration_test/","native_build":true,"dependencies":[],"dev_dependency":true},{"name":"sqflite_android","path":"/home/rock/.pub-cache/hosted/pub.dev/sqflite_android-2.4.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqflite_sqlcipher","path":"/home/rock/.pub-cache/hosted/pub.dev/sqflite_sqlcipher-3.2.0/","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"connectivity_plus","path":"/home/rock/.pub-cache/hosted/pub.dev/connectivity_plus-7.3.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"geolocator_apple","path":"/home/rock/.pub-cache/hosted/pub.dev/geolocator_apple-2.3.14/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqflite_darwin","path":"/home/rock/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqflite_sqlcipher","path":"/home/rock/.pub-cache/hosted/pub.dev/sqflite_sqlcipher-3.2.0/","native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"connectivity_plus","path":"/home/rock/.pub-cache/hosted/pub.dev/connectivity_plus-7.3.1/","native_build":false,"dependencies":[],"dev_dependency":false}],"windows":[{"name":"connectivity_plus","path":"/home/rock/.pub-cache/hosted/pub.dev/connectivity_plus-7.3.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"geolocator_windows","path":"/home/rock/.pub-cache/hosted/pub.dev/geolocator_windows-0.2.5/","native_build":true,"dependencies":[],"dev_dependency":false}],"web":[{"name":"connectivity_plus","path":"/home/rock/.pub-cache/hosted/pub.dev/connectivity_plus-7.3.1/","dependencies":[],"dev_dependency":false},{"name":"geolocator_web","path":"/home/rock/.pub-cache/hosted/pub.dev/geolocator_web-4.1.4/","dependencies":[],"dev_dependency":false},{"name":"google_maps_flutter_web","path":"/home/rock/.pub-cache/hosted/pub.dev/google_maps_flutter_web-0.5.12/","dependencies":[],"dev_dependency":false}]},"dependencyGraph":[{"name":"connectivity_plus","dependencies":[]},{"name":"flutter_plugin_android_lifecycle","dependencies":[]},{"name":"geolocator","dependencies":["geolocator_android","geolocator_apple","geolocator_web","geolocator_windows"]},{"name":"geolocator_android","dependencies":[]},{"name":"geolocator_apple","dependencies":[]},{"name":"geolocator_web","dependencies":[]},{"name":"geolocator_windows","dependencies":[]},{"name":"google_maps_flutter","dependencies":["google_maps_flutter_android","google_maps_flutter_ios","google_maps_flutter_web"]},{"name":"google_maps_flutter_android","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"google_maps_flutter_ios","dependencies":[]},{"name":"google_maps_flutter_web","dependencies":[]},{"name":"integration_test","dependencies":[]},{"name":"sqflite","dependencies":["sqflite_android","sqflite_darwin"]},{"name":"sqflite_android","dependencies":[]},{"name":"sqflite_darwin","dependencies":[]},{"name":"sqflite_sqlcipher","dependencies":[]}],"date_created":"2026-09-11 17:28:46.660463","version":"3.44.8","swift_package_manager_enabled":{"ios":false,"macos":false}} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"connectivity_plus","path":"/home/rock/.pub-cache/hosted/pub.dev/connectivity_plus-7.3.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_secure_storage","path":"/home/rock/.pub-cache/hosted/pub.dev/flutter_secure_storage-9.2.4/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"geolocator_apple","path":"/home/rock/.pub-cache/hosted/pub.dev/geolocator_apple-2.3.14/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"integration_test","path":"/home/rock/flutter/packages/integration_test/","native_build":true,"dependencies":[],"dev_dependency":true},{"name":"path_provider_foundation","path":"/home/rock/.pub-cache/hosted/pub.dev/path_provider_foundation-2.5.1/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqflite_darwin","path":"/home/rock/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqflite_sqlcipher","path":"/home/rock/.pub-cache/hosted/pub.dev/sqflite_sqlcipher-3.2.0/","native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"connectivity_plus","path":"/home/rock/.pub-cache/hosted/pub.dev/connectivity_plus-7.3.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_secure_storage","path":"/home/rock/.pub-cache/hosted/pub.dev/flutter_secure_storage-9.2.4/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"geolocator_android","path":"/home/rock/.pub-cache/hosted/pub.dev/geolocator_android-4.6.2/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"integration_test","path":"/home/rock/flutter/packages/integration_test/","native_build":true,"dependencies":[],"dev_dependency":true},{"name":"jni","path":"/home/rock/.pub-cache/hosted/pub.dev/jni-1.0.3/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"jni_flutter","path":"/home/rock/.pub-cache/hosted/pub.dev/jni_flutter-1.0.3/","native_build":true,"dependencies":["jni"],"dev_dependency":false},{"name":"path_provider_android","path":"/home/rock/.pub-cache/hosted/pub.dev/path_provider_android-2.3.1/","native_build":false,"dependencies":["jni","jni_flutter"],"dev_dependency":false},{"name":"sqflite_android","path":"/home/rock/.pub-cache/hosted/pub.dev/sqflite_android-2.4.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqflite_sqlcipher","path":"/home/rock/.pub-cache/hosted/pub.dev/sqflite_sqlcipher-3.2.0/","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"connectivity_plus","path":"/home/rock/.pub-cache/hosted/pub.dev/connectivity_plus-7.3.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_secure_storage_macos","path":"/home/rock/.pub-cache/hosted/pub.dev/flutter_secure_storage_macos-3.1.3/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"geolocator_apple","path":"/home/rock/.pub-cache/hosted/pub.dev/geolocator_apple-2.3.14/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/home/rock/.pub-cache/hosted/pub.dev/path_provider_foundation-2.5.1/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqflite_darwin","path":"/home/rock/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqflite_sqlcipher","path":"/home/rock/.pub-cache/hosted/pub.dev/sqflite_sqlcipher-3.2.0/","native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"connectivity_plus","path":"/home/rock/.pub-cache/hosted/pub.dev/connectivity_plus-7.3.1/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"flutter_secure_storage_linux","path":"/home/rock/.pub-cache/hosted/pub.dev/flutter_secure_storage_linux-1.2.3/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"jni","path":"/home/rock/.pub-cache/hosted/pub.dev/jni-1.0.3/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_linux","path":"/home/rock/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.2/","native_build":false,"dependencies":[],"dev_dependency":false}],"windows":[{"name":"connectivity_plus","path":"/home/rock/.pub-cache/hosted/pub.dev/connectivity_plus-7.3.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_secure_storage_windows","path":"/home/rock/.pub-cache/hosted/pub.dev/flutter_secure_storage_windows-3.1.2/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"geolocator_windows","path":"/home/rock/.pub-cache/hosted/pub.dev/geolocator_windows-0.2.5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"jni","path":"/home/rock/.pub-cache/hosted/pub.dev/jni-1.0.3/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_windows","path":"/home/rock/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[],"dev_dependency":false}],"web":[{"name":"connectivity_plus","path":"/home/rock/.pub-cache/hosted/pub.dev/connectivity_plus-7.3.1/","dependencies":[],"dev_dependency":false},{"name":"flutter_secure_storage_web","path":"/home/rock/.pub-cache/hosted/pub.dev/flutter_secure_storage_web-1.2.1/","dependencies":[],"dev_dependency":false},{"name":"geolocator_web","path":"/home/rock/.pub-cache/hosted/pub.dev/geolocator_web-4.1.4/","dependencies":[],"dev_dependency":false}]},"dependencyGraph":[{"name":"connectivity_plus","dependencies":[]},{"name":"flutter_secure_storage","dependencies":["flutter_secure_storage_linux","flutter_secure_storage_macos","flutter_secure_storage_web","flutter_secure_storage_windows"]},{"name":"flutter_secure_storage_linux","dependencies":[]},{"name":"flutter_secure_storage_macos","dependencies":[]},{"name":"flutter_secure_storage_web","dependencies":[]},{"name":"flutter_secure_storage_windows","dependencies":["path_provider"]},{"name":"geolocator","dependencies":["geolocator_android","geolocator_apple","geolocator_web","geolocator_windows"]},{"name":"geolocator_android","dependencies":[]},{"name":"geolocator_apple","dependencies":[]},{"name":"geolocator_web","dependencies":[]},{"name":"geolocator_windows","dependencies":[]},{"name":"integration_test","dependencies":[]},{"name":"jni","dependencies":[]},{"name":"jni_flutter","dependencies":["jni"]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":["jni","jni_flutter"]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"sqflite","dependencies":["sqflite_android","sqflite_darwin"]},{"name":"sqflite_android","dependencies":[]},{"name":"sqflite_darwin","dependencies":[]},{"name":"sqflite_sqlcipher","dependencies":[]}],"date_created":"2026-09-12 11:08:11.848787","version":"3.44.8","swift_package_manager_enabled":{"ios":false,"macos":false}} \ No newline at end of file diff --git a/apps/acs/integration_test/encrypted_storage_test.dart b/apps/acs/integration_test/encrypted_storage_test.dart new file mode 100644 index 0000000..5330b60 --- /dev/null +++ b/apps/acs/integration_test/encrypted_storage_test.dart @@ -0,0 +1,182 @@ +/// Prova de que o banco local está **realmente** cifrado. +/// +/// É o único teste do repositório que verifica isso, e só funciona em +/// dispositivo: no Linux o `flutter test` cai no FFI, onde o SQLCipher não +/// existe. Foi essa distinção que faltou no teste original — ele se chamava +/// "deve abrir banco criptografado com senha configurada" e, no CI, exercitava +/// o caminho sem criptografia nenhuma. +/// +/// flutter test integration_test/encrypted_storage_test.dart +/// +/// Responde ao risco nomeado em spec/test_plan.md:51 — "o roubo do celular do +/// ACS não pode resultar em vazamento da base territorial" — e ao INV-04 do PRD. +/// +/// PRIVACIDADE: usa um marcador e um UUID sintéticos, nunca dado real. +library; + +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:sinalacs_acs/core/database/encrypted_database.dart'; +import 'package:sinalacs_acs/core/database/sqlcipher_visit_store.dart'; +import 'package:sinalacs_acs/core/security/database_key_store.dart'; +import 'package:sinalacs_acs/core/services/offline_visit_queue.dart'; +import 'package:sqflite_sqlcipher/sqflite.dart' as sqlcipher; + +/// Marcador sintético que precisa NÃO aparecer no arquivo. +const marcador = 'MARCADOR-SINTETICO-NAO-DEVE-VAZAR'; + +/// UUID do paciente no seed de desenvolvimento. Dado sintético. +/// +/// Desde que o registro guarda o identificador em vez de um rótulo truncado, +/// provar que ELE não vaza vale mais do que provar que o rótulo não vazava. +const seedPatientId = '00000000-0000-4000-8000-000000000001'; + +const databaseName = 'sinalacs_crypto_probe.db'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + setUp(() => EncryptedLocalDatabase.deleteDatabaseFile(databaseName)); + + test('o arquivo do banco não contém o conteúdo da visita em texto plano', () async { + final store = SqlCipherVisitStore( + keyStore: InMemoryDatabaseKeyStore(), + databaseName: databaseName, + ); + + await store.save([ + OfflineVisitRecord( + patientId: seedPatientId, + risk: 'red', + status: 'PENDENTE', + // O desfecho também é conteúdo clínico, e é onde o marcador cabe agora + // que o campo do paciente só aceita UUID. + outcome: marcador, + ), + ]); + // Fechar garante que tudo foi para o disco, e não ficou só no WAL em memória. + await store.close(); + + final file = File(await EncryptedLocalDatabase.pathFor(databaseName)); + expect(await file.exists(), isTrue, reason: 'o banco não foi criado'); + + final bytes = await file.readAsBytes(); + expect(bytes, isNotEmpty); + + // 1. Um SQLite em claro começa com o cabeçalho "SQLite format 3\0". + // Um banco cifrado pelo SQLCipher não tem esse cabeçalho. + final header = String.fromCharCodes(bytes.take(15)); + expect( + header, + isNot('SQLite format 3'), + reason: 'o banco está em texto plano — viola o INV-04 do PRD', + ); + + // 2. O conteúdo gravado não pode ser legível no arquivo. + final conteudo = String.fromCharCodes(bytes); + expect( + conteudo.contains(marcador), + isFalse, + reason: 'o conteúdo da visita aparece legível no arquivo do banco', + ); + + // 3. Nem o identificador do paciente, que é o que o registro passou a + // guardar para poder sincronizar. + expect( + conteudo.contains(seedPatientId), + isFalse, + reason: 'o identificador do paciente aparece legível no arquivo do banco', + ); + + await EncryptedLocalDatabase.deleteDatabaseFile(databaseName); + }); + + test('o banco não abre com a chave errada', () async { + // Se abrisse, a criptografia não estaria protegendo nada. + final store = SqlCipherVisitStore( + keyStore: InMemoryDatabaseKeyStore(initialKey: generateDatabaseKey()), + databaseName: databaseName, + ); + await store.save([ + OfflineVisitRecord(patientId: seedPatientId, risk: 'red', status: 'PENDENTE'), + ]); + await store.close(); + + // Abertura direta com outra chave, sem passar pela recuperação automática + // do SqlCipherVisitStore (que apagaria o arquivo e recomeçaria). + expect( + () => EncryptedLocalDatabase.open( + databaseName: databaseName, + passphrase: generateDatabaseKey(), + ), + throwsA(anything), + ); + + await EncryptedLocalDatabase.deleteDatabaseFile(databaseName); + }); + + test('o banco v1 de uma versão anterior migra sem travar o app', () async { + // O caminho de upgrade do sqflite_sqlcipher não é o do FFI, então só aqui + // isto se prova. A versão anterior do app criava `local_queue`; abrir esse + // arquivo sem migração deixaria a fila sem a tabela que ela lê, e o ACS + // veria "as visitas não estão sendo salvas" a cada abertura. + final key = generateDatabaseKey(); + final path = await EncryptedLocalDatabase.pathFor(databaseName); + + final legado = await sqlcipher.openDatabase( + path, + password: key, + version: 1, + onCreate: (db, version) => + db.execute('CREATE TABLE IF NOT EXISTS local_queue (id TEXT PRIMARY KEY)'), + ); + await legado.insert('local_queue', {'id': 'antigo'}); + await legado.close(); + + final store = SqlCipherVisitStore( + keyStore: InMemoryDatabaseKeyStore(initialKey: key), + databaseName: databaseName, + ); + + // Se a migração lançasse, o `catch` do store apagaria o arquivo e isto + // passaria mesmo assim — por isso o teste também grava e relê. + expect(await store.load(), isEmpty); + await store.save([ + OfflineVisitRecord(patientId: seedPatientId, risk: 'red', status: 'PENDENTE'), + ]); + expect((await store.load()).single.patientId, seedPatientId); + await store.close(); + + await EncryptedLocalDatabase.deleteDatabaseFile(databaseName); + }); + + test('chave perdida: o app recomeça em vez de travar', () async { + // Reinstalação ou restauração de backup deixa o arquivo sem a chave + // correspondente. Sem recuperação, o app ficaria travado para sempre. + final keyStore = InMemoryDatabaseKeyStore(); + final first = SqlCipherVisitStore( + keyStore: keyStore, + databaseName: databaseName, + ); + await first.save([ + OfflineVisitRecord(patientId: seedPatientId, risk: 'red', status: 'PENDENTE'), + ]); + await first.close(); + + // A chave some, o arquivo fica. + await keyStore.delete(); + + final second = SqlCipherVisitStore( + keyStore: keyStore, + databaseName: databaseName, + ); + + // Não lança: descarta o arquivo ilegível e recomeça vazio. + expect(await second.load(), isEmpty); + await second.close(); + + await EncryptedLocalDatabase.deleteDatabaseFile(databaseName); + }); +} diff --git a/apps/acs/integration_test/red_alert_cycle_test.dart b/apps/acs/integration_test/red_alert_cycle_test.dart index fdf4ce9..4e3db43 100644 --- a/apps/acs/integration_test/red_alert_cycle_test.dart +++ b/apps/acs/integration_test/red_alert_cycle_test.dart @@ -19,7 +19,10 @@ import 'dart:async'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; +import 'package:sinalacs_acs/core/database/encrypted_database.dart'; +import 'package:sinalacs_acs/core/database/sqlcipher_visit_store.dart'; import 'package:sinalacs_acs/core/network/backend_client.dart'; +import 'package:sinalacs_acs/core/security/database_key_store.dart'; import 'package:sinalacs_acs/core/services/alert_feed.dart'; import 'package:sinalacs_acs/core/services/alert_queue.dart'; import 'package:sinalacs_acs/core/services/backend_visit_synchronizer.dart'; @@ -145,18 +148,14 @@ void main() { await backend.login(); final queue = OfflineVisitQueue( - synchronizer: BackendVisitSynchronizer( - backend: backend, - patientIdFor: (_) => seedPatientId, - ), + synchronizer: BackendVisitSynchronizer(backend: backend), ); await queue.add(OfflineVisitRecord( - patientName: 'Paciente do seed', + patientId: seedPatientId, risk: 'red', status: 'PENDENTE', outcome: 'realizada', - version: 0, )); final result = await queue.sync(); @@ -166,21 +165,59 @@ void main() { expect(queue.syncedCount, 1); }); + test('a visita confirmada SAI do disco criptografado', () async { + // O único lugar onde retenção, criptografia real e servidor real se + // encontram: o teste hermético não tem SQLCipher, e o de criptografia não + // tem servidor. Minimização (LGPD-RF07 / seção 5.6 de spec/lgpd_design.md): + // o que já chegou ao servidor não continua num aparelho que pode ser + // perdido ou roubado. + const nome = 'sinalacs_retencao_probe.db'; + await EncryptedLocalDatabase.deleteDatabaseFile(nome); + + await backend.login(); + + final store = SqlCipherVisitStore( + keyStore: InMemoryDatabaseKeyStore(), + databaseName: nome, + ); + final queue = OfflineVisitQueue( + store: store, + synchronizer: BackendVisitSynchronizer(backend: backend), + ); + + await queue.add(OfflineVisitRecord( + patientId: seedPatientId, + risk: 'red', + status: 'PENDENTE', + outcome: 'realizada', + )); + expect(await store.load(), hasLength(1), reason: 'a visita não foi gravada'); + + final result = await queue.sync(); + + expect(result.kind, SyncOutcomeKind.synced, reason: result.message); + expect(queue.syncedCount, 1); + expect(await store.load(), isEmpty, reason: 'a visita continua no aparelho'); + + await store.close(); + await EncryptedLocalDatabase.deleteDatabaseFile(nome); + }); + test('reenviar a mesma visita não a duplica no servidor', () async { await backend.login(); + // `version` fica no padrão 1, que é a versão com que o servidor grava todo + // insert. Enviar 0 fazia o reenvio cair na regra de atualização + // (`entry.version == existing.version - 1`) e subir para 2 — o oposto da + // idempotência que este teste existe para provar. final visit = OfflineVisitRecord( - patientName: 'Paciente do seed', + patientId: seedPatientId, risk: 'red', status: 'PENDENTE', outcome: 'realizada', - version: 0, ); - final synchronizer = BackendVisitSynchronizer( - backend: backend, - patientIdFor: (_) => seedPatientId, - ); + final synchronizer = BackendVisitSynchronizer(backend: backend); final first = await synchronizer.push([visit]); // Mesmo localId, mesma versão: é o retry de quem não viu a resposta. diff --git a/apps/acs/lib/app/app.dart b/apps/acs/lib/app/app.dart index 3713a5f..93f37b7 100644 --- a/apps/acs/lib/app/app.dart +++ b/apps/acs/lib/app/app.dart @@ -5,6 +5,7 @@ import 'package:sinalacs_acs/core/network/backend_scope.dart'; import 'package:sinalacs_acs/core/services/alert_feed.dart'; import 'package:sinalacs_acs/core/services/alert_queue.dart'; import 'package:sinalacs_acs/core/services/offline_visit_queue.dart'; +import 'package:sinalacs_acs/core/services/visit_queue_factory.dart'; class SinalAcsApp extends StatefulWidget { const SinalAcsApp({super.key, this.backend, this.feedBuilder, this.visitQueue}); @@ -26,7 +27,15 @@ class _SinalAcsAppState extends State { /// A tela de visita antes fazia `OfflineVisitQueue()` a cada gravação — uma /// instância nova por visita, descartada no retorno do callback. A visita /// simplesmente sumia. - late final OfflineVisitQueue _visitQueue = widget.visitQueue ?? OfflineVisitQueue(); + late final OfflineVisitQueue _visitQueue = widget.visitQueue ?? _persistentQueue(); + + /// Fila respaldada pelo banco criptografado, com o sincronizador ligado. + /// + /// A chave vive no Keystore/Keychain, nunca no código. Deixou de ser `static` + /// para enxergar [_backend]: sem sincronizador, `sync()` caía no ramo sem + /// remetente e devolvia erro — as visitas nunca subiam ao servidor e o que já + /// estava confirmado nunca era apagado do disco. + OfflineVisitQueue _persistentQueue() => buildVisitQueue(backend: _backend); @override void dispose() { @@ -50,10 +59,10 @@ class _SinalAcsAppState extends State { } class LoginScreen extends StatefulWidget { - const LoginScreen({super.key, this.feedBuilder, this.visitQueue}); + const LoginScreen({required this.visitQueue, super.key, this.feedBuilder}); final AlertFeed Function(AlertQueue queue)? feedBuilder; - final OfflineVisitQueue? visitQueue; + final OfflineVisitQueue visitQueue; @override State createState() => _LoginScreenState(); @@ -143,15 +152,19 @@ class AcsHomeShell extends StatefulWidget { const AcsHomeShell({ required this.microAreaId, required this.acsId, + required this.visitQueue, super.key, this.feedBuilder, - this.visitQueue, }); final String microAreaId; final String acsId; final AlertFeed Function(AlertQueue queue)? feedBuilder; - final OfflineVisitQueue? visitQueue; + + /// Obrigatória: a tela de visita usava `widget.visitQueue ?? OfflineVisitQueue()`, + /// e um dia em que o shell fosse construído sem fila voltaria a descartar a + /// visita em silêncio. + final OfflineVisitQueue visitQueue; @override State createState() => _AcsHomeShellState(); @@ -166,12 +179,33 @@ class _AcsHomeShellState extends State { bool _brokerConnected = false; String? _feedError; + String? _storageError; PrioritizedAlert? _selected; @override void initState() { super.initState(); _connectFeed(); + _restoreVisits(); + } + + /// Recarrega as visitas gravadas em execuções anteriores. + /// + /// Sem esta chamada, persistir não serviria para nada — `restore()` existia e + /// só era exercitado por teste. + /// + /// Falhar aqui **não** pode ser silencioso nem fatal: o ACS segue registrando + /// visitas em memória, para não travar o trabalho em campo, mas precisa saber + /// que elas não estão sendo salvas. Cair para memória sem avisar perderia + /// visitas sem ninguém perceber. Manter em RAM não viola o INV-04, que proíbe + /// *persistir* em texto plano. + Future _restoreVisits() async { + await widget.visitQueue.restore(); + if (!mounted) return; + setState(() => _storageError = widget.visitQueue.persistenceFailed + ? 'As visitas não estão sendo salvas neste aparelho. ' + 'Sincronize antes de fechar o aplicativo.' + : null); } /// Assina o tópico da microárea da sessão. @@ -223,7 +257,7 @@ class _AcsHomeShellState extends State { AcsDestination.area => const TerritorializationScreen(), AcsDestination.queue => DashboardScreen( queue: _queue, - feedError: _feedError, + feedError: _feedError ?? _storageError, onAcknowledge: _acknowledge, onEscalate: (alert) => setState(() { _selected = alert; destination = AcsDestination.escalation; }), onVisit: (alert) => setState(() { _selected = alert; destination = AcsDestination.visit; }), @@ -231,7 +265,7 @@ class _AcsHomeShellState extends State { AcsDestination.map => const MapScreen(), AcsDestination.visit => VisitRegistrationScreen( alert: _selected, - queue: widget.visitQueue ?? OfflineVisitQueue(), + queue: widget.visitQueue, ), AcsDestination.escalation => EscalationScreen(alert: _selected), AcsDestination.geofencing => const GeofencingScreen(), @@ -411,36 +445,115 @@ class VisitRegistrationScreen extends StatefulWidget { class _VisitRegistrationScreenState extends State { String outcome = 'Realizada com sucesso'; final notes = TextEditingController(); + bool _syncing = false; @override void dispose() { notes.dispose(); super.dispose(); } + /// Rótulo montado a cada build a partir do alerta que está em memória. + /// + /// Nunca é gravado: o que vai para a fila é `alert.patientId` inteiro. Antes + /// persistia-se este texto e o UUID era descartado, então o servidor recusava + /// a visita por identificador inválido — e o disco guardava, sem precisar, + /// uma linha legível sobre a pessoa. String get _patientLabel { final alert = widget.alert; return alert == null ? 'Visita sem alerta vinculado' : 'Paciente ${alert.patientId.substring(0, 8)}'; } Future _save() async { + final alert = widget.alert; + // Sem alerta não há paciente, e `visits.sync` exige UUID: gravar aqui + // criaria um registro que nunca sobe e nunca sai do aparelho. + if (alert == null) return; + await widget.queue.add(OfflineVisitRecord( - patientName: _patientLabel, - risk: widget.alert?.riskLevel ?? 'desconhecido', + patientId: alert.patientId, + risk: alert.riskLevel, status: 'PENDENTE', outcome: outcome, )); if (!mounted) return; + setState(() {}); _message(context, 'Visita salva e enfileirada localmente para sincronização.'); } + Future _sync() async { + setState(() => _syncing = true); + + final SyncOutcome result; + try { + result = await widget.queue.sync(); + } finally { + // A fila já captura falha de rede, mas um erro inesperado aqui deixaria + // o botão desabilitado para o resto da sessão. + if (mounted) setState(() => _syncing = false); + } + if (!mounted) return; + + _message(context, switch (result.kind) { + SyncOutcomeKind.synced => '${result.processed} visita(s) enviada(s) ao servidor.', + SyncOutcomeKind.conflict => '${result.processed} visita(s) em conflito. Continuam na fila para resolução.', + SyncOutcomeKind.empty => 'Não há visitas pendentes.', + SyncOutcomeKind.error => result.message ?? + 'Não foi possível sincronizar. As visitas continuam salvas no aparelho.', + }); + } + @override - Widget build(BuildContext context) => _page([ - Text(_patientLabel, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), - const SizedBox(height: 16), - DropdownButtonFormField(initialValue: outcome, decoration: const InputDecoration(labelText: 'Status do atendimento'), items: const ['Realizada com sucesso', 'Paciente ausente', 'Recusou atendimento'].map((v) => DropdownMenuItem(value: v, child: Text(v))).toList(), onChanged: (v) => setState(() => outcome = v!)), - const SizedBox(height: 16), - TextField(controller: notes, maxLines: 4, decoration: const InputDecoration(labelText: 'Observações de campo')), - const SizedBox(height: 20), - FilledButton.icon(key: const Key('save_visit'), onPressed: _save, icon: const Icon(Icons.save_outlined), label: const Text('Salvar e enfileirar sincronização')), - ]); + Widget build(BuildContext context) { + final queue = widget.queue; + final semAlerta = widget.alert == null; + + return _page([ + Text(_patientLabel, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), + if (semAlerta) const Padding( + padding: EdgeInsets.only(top: 8), + child: Text( + key: Key('visit_needs_alert'), + 'Selecione um alerta na fila para registrar a visita.', + style: TextStyle(color: AcsColors.yellow), + ), + ), + const SizedBox(height: 16), + DropdownButtonFormField(initialValue: outcome, decoration: const InputDecoration(labelText: 'Status do atendimento'), items: const ['Realizada com sucesso', 'Paciente ausente', 'Recusou atendimento'].map((v) => DropdownMenuItem(value: v, child: Text(v))).toList(), onChanged: semAlerta ? null : (v) => setState(() => outcome = v!)), + const SizedBox(height: 16), + TextField( + controller: notes, + maxLines: 4, + enabled: !semAlerta, + decoration: const InputDecoration( + labelText: 'Observações de campo', + // A tela coleta o texto e o descarta. Dizer isso é o mínimo enquanto + // o envio não existe; persistir texto livre num aparelho que pode ser + // roubado é decisão de LGPD que merece o próprio PR. + helperText: 'Ainda não é enviado ao servidor.', + ), + ), + const SizedBox(height: 20), + FilledButton.icon(key: const Key('save_visit'), onPressed: semAlerta ? null : _save, icon: const Icon(Icons.save_outlined), label: const Text('Salvar e enfileirar sincronização')), + const Divider(height: 32), + Text(key: const Key('pending_visits_count'), 'Pendentes de sincronização: ${queue.pendingCount}'), + if (queue.conflictCount > 0) Padding( + padding: const EdgeInsets.only(top: 4), + child: Text( + key: const Key('conflict_visits_count'), + 'Em conflito: ${queue.conflictCount}', + style: const TextStyle(color: AcsColors.yellow, fontWeight: FontWeight.bold), + ), + ), + const SizedBox(height: 12), + OutlinedButton.icon( + key: const Key('sync_visits'), + onPressed: _syncing || queue.pendingCount == 0 ? null : _sync, + style: OutlinedButton.styleFrom(minimumSize: const Size(48, 52)), + icon: _syncing + ? const SizedBox(height: 18, width: 18, child: CircularProgressIndicator(strokeWidth: 2)) + : const Icon(Icons.cloud_upload_outlined), + label: const Text('Sincronizar agora'), + ), + ]); + } } class EscalationScreen extends StatelessWidget { @@ -512,4 +625,12 @@ class _Header extends StatelessWidget implements PreferredSizeWidget { ], ); } -void _message(BuildContext context, String message) => ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message))); \ No newline at end of file +/// Mostra uma mensagem, substituindo a anterior em vez de enfileirar. +/// +/// Sem `hideCurrentSnackBar`, a confirmação da gravação ficava 4 s na tela e o +/// resultado da sincronização entrava na fila atrás dela: a pessoa tocava em +/// "Sincronizar agora" e continuava lendo "visita salva". +void _message(BuildContext context, String message) => + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar(SnackBar(content: Text(message))); \ No newline at end of file diff --git a/apps/acs/lib/core/database/encrypted_database.dart b/apps/acs/lib/core/database/encrypted_database.dart index d86c2ce..49202dc 100644 --- a/apps/acs/lib/core/database/encrypted_database.dart +++ b/apps/acs/lib/core/database/encrypted_database.dart @@ -3,41 +3,135 @@ import 'dart:io'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:sqflite_sqlcipher/sqflite.dart' as sqlcipher; +/// Banco local criptografado com SQLCipher (AES-256). +/// +/// **INV-04 do PRD**: dados de saúde sensíveis nunca podem ser persistidos em +/// texto plano. É por isso que o caminho sem criptografia deste arquivo exige +/// uma flag de nome constrangedor, e não é alcançável por descuido — antes ele +/// era o comportamento padrão fora de Android/iOS, e o único teste que existia +/// passava justamente por ele. class EncryptedLocalDatabase { EncryptedLocalDatabase._(); + /// Versão do schema. Suba junto com [_upgrade] ao alterar as tabelas. + /// + /// v1 existiu em duas formas em campo: `local_queue(id)` — o que um aparelho + /// com a versão anterior instalada tem — e uma `offline_visits` com + /// `patient_name`. v2 grava `patient_id`. + static const schemaVersion = 2; + + /// Visitas registradas offline, aguardando sincronização. + /// + /// Só o que ainda precisa sair do dispositivo é gravado: a visita confirmada + /// pelo servidor é removida no `save` seguinte, o que atende o princípio da + /// minimização (LGPD-RF07 / seção 5.6 de spec/lgpd_design.md). + static const createOfflineVisits = ''' +CREATE TABLE IF NOT EXISTS offline_visits ( + local_id TEXT PRIMARY KEY, + patient_id TEXT NOT NULL, + risk TEXT NOT NULL, + status TEXT NOT NULL, + outcome TEXT NOT NULL, + created_at TEXT NOT NULL, + version INTEGER NOT NULL +)'''; + + /// Migração v1 → v2. + /// + /// Nenhuma das duas formas de v1 guarda o UUID do paciente: `patient_name` + /// era `'Paciente ' + 8 dos 32 dígitos hex`, irreversível. Sem UUID, + /// `visits.sync` responde `identificadores devem ser UUID` a cada tentativa — + /// preservar essas linhas só encheria a fila de pendências que nunca sobem e, + /// por isso mesmo, nunca saem do disco. + /// + /// Recriar perde as visitas pendentes gravadas antes desta versão. É + /// aceitável **apenas** porque o app ainda não teve release; a partir do + /// primeiro, esta migração precisa preservar os dados. + static Future _upgrade(Database db, int from, int to) async { + if (from < 2) { + await db.execute('DROP TABLE IF EXISTS local_queue'); + await db.execute('DROP TABLE IF EXISTS offline_visits'); + await db.execute(createOfflineVisits); + } + } + + /// Caminho do arquivo do banco neste dispositivo. + /// + /// Público para que o teste de migração consiga criar um banco v1 exatamente + /// onde o app o abriria. + static Future pathFor(String databaseName) async { + final isMobile = Platform.isAndroid || Platform.isIOS; + if (isMobile) return '${await sqlcipher.getDatabasesPath()}/$databaseName'; + + sqfliteFfiInit(); + return '${await databaseFactoryFfi.getDatabasesPath()}/$databaseName'; + } + + /// Abre o banco do dispositivo. + /// + /// [passphrase] vem do `DatabaseKeyStore` — nunca de literal no código. + /// + /// [allowUnencryptedForTesting] abre mão da criptografia e só existe para os + /// testes que rodam na VM, onde o SQLCipher não está disponível. O nome é + /// deliberadamente constrangedor: um `grep` por ele mostra todo ponto que + /// abriu mão da garantia. Código de produção **não deve** passá-la — fora de + /// Android/iOS, abrir sem ela lança. static Future open({ required String databaseName, required String passphrase, + bool allowUnencryptedForTesting = false, }) async { - if (!Platform.isAndroid && !Platform.isIOS) { - sqfliteFfiInit(); + final isMobile = Platform.isAndroid || Platform.isIOS; + + if (!isMobile && !allowUnencryptedForTesting) { + throw UnsupportedError( + 'O SQLCipher só está disponível em Android/iOS. Abrir o banco aqui ' + 'gravaria dados de saúde em texto plano, o que viola o INV-04 do PRD. ' + 'Em teste, passe allowUnencryptedForTesting: true explicitamente.', + ); } - final databasePath = Platform.isAndroid || Platform.isIOS - ? await sqlcipher.getDatabasesPath() - : await databaseFactoryFfi.getDatabasesPath(); - final dbPath = '$databasePath/$databaseName'; + if (passphrase.isEmpty) { + throw ArgumentError('A passphrase do banco local é obrigatória.'); + } - if (Platform.isAndroid || Platform.isIOS) { + final path = await pathFor(databaseName); + + if (isMobile) { return sqlcipher.openDatabase( - dbPath, + path, password: passphrase, - version: 1, - onCreate: (db, version) async { - await db.execute('CREATE TABLE IF NOT EXISTS local_queue (id TEXT PRIMARY KEY)'); - }, + version: schemaVersion, + onCreate: (db, version) => db.execute(createOfflineVisits), + onUpgrade: _upgrade, + // Um rollback de APK abriria um arquivo v2 pedindo v1 e lançaria, + // deixando o app travado a cada abertura. + onDowngrade: onDatabaseDowngradeDelete, ); } return databaseFactoryFfi.openDatabase( - dbPath, + path, options: OpenDatabaseOptions( - version: 1, - onCreate: (db, version) async { - await db.execute('CREATE TABLE IF NOT EXISTS local_queue (id TEXT PRIMARY KEY)'); - }, + version: schemaVersion, + onCreate: (db, version) => db.execute(createOfflineVisits), + onUpgrade: _upgrade, + onDowngrade: onDatabaseDowngradeDelete, ), ); } + + /// Apaga o arquivo do banco. + /// + /// Usado quando a chave se perde (reinstalação, restauração de backup): sem a + /// chave o arquivo é ilegível para sempre, então recomeçar é a única saída + /// que não deixa o app travado. + static Future deleteDatabaseFile(String databaseName) async { + final path = await pathFor(databaseName); + + if (Platform.isAndroid || Platform.isIOS) { + return sqlcipher.deleteDatabase(path); + } + return databaseFactoryFfi.deleteDatabase(path); + } } diff --git a/apps/acs/lib/core/database/sqlcipher_visit_store.dart b/apps/acs/lib/core/database/sqlcipher_visit_store.dart new file mode 100644 index 0000000..052e0f8 --- /dev/null +++ b/apps/acs/lib/core/database/sqlcipher_visit_store.dart @@ -0,0 +1,113 @@ +import 'package:sinalacs_acs/core/database/encrypted_database.dart'; +import 'package:sinalacs_acs/core/security/database_key_store.dart'; +import 'package:sinalacs_acs/core/services/offline_visit_queue.dart'; +import 'package:sqflite_common_ffi/sqflite_ffi.dart' show Database; + +/// Persistência das visitas offline no banco criptografado. +/// +/// Implementa o [VisitStore] que a fila já define — mesmo padrão de +/// `application/` + `infrastructure/` usado no backend: a fila não sabe que +/// existe SQLCipher, e este arquivo não sabe o que é uma FSM de sincronização. +class SqlCipherVisitStore implements VisitStore { + SqlCipherVisitStore({ + required DatabaseKeyStore keyStore, + this.databaseName = 'sinalacs_acs.db', + this.allowUnencryptedForTesting = false, + }) : _keyStore = keyStore; + + static const _table = 'offline_visits'; + + final DatabaseKeyStore _keyStore; + final String databaseName; + + /// Repassado a [EncryptedLocalDatabase.open]. Só os testes de VM passam true. + final bool allowUnencryptedForTesting; + + Database? _database; + + /// Abre preguiçosamente, na primeira leitura ou escrita. + /// + /// É o que permite a fila continuar sendo construída de forma síncrona pela + /// UI, sem espalhar `await` pela montagem do app. + Future _open() async { + final existing = _database; + if (existing != null && existing.isOpen) return existing; + + final passphrase = await _keyStore.readOrCreate(); + + try { + return _database = await EncryptedLocalDatabase.open( + databaseName: databaseName, + passphrase: passphrase, + allowUnencryptedForTesting: allowUnencryptedForTesting, + ); + } on UnsupportedError { + // Plataforma sem SQLCipher: quem chamou precisa saber, não receber um + // banco em texto plano por baixo dos panos. + rethrow; + } catch (_) { + // A chave não abre este arquivo — tipicamente reinstalação ou restauração + // de backup, onde o banco veio e a chave do keystore não. Sem isso o app + // ficaria travado num estado irrecuperável a cada abertura. Descartar o + // arquivo perde visitas ainda não sincronizadas, mas elas já eram + // ilegíveis; ficar travado perderia as próximas também. + await EncryptedLocalDatabase.deleteDatabaseFile(databaseName); + await _keyStore.delete(); + + return _database = await EncryptedLocalDatabase.open( + databaseName: databaseName, + passphrase: await _keyStore.readOrCreate(), + allowUnencryptedForTesting: allowUnencryptedForTesting, + ); + } + } + + @override + Future> load() async { + final database = await _open(); + final rows = await database.query(_table, orderBy: 'created_at ASC'); + + return [ + for (final row in rows) + OfflineVisitRecord( + patientId: row['patient_id']! as String, + risk: row['risk']! as String, + status: row['status']! as String, + outcome: row['outcome']! as String, + localId: row['local_id']! as String, + createdAt: DateTime.parse(row['created_at']! as String), + version: row['version']! as int, + ), + ]; + } + + /// Substitui o conjunto inteiro, em transação. + /// + /// A fila chama `save(_pending)`, então o que sai da lista sai do disco: uma + /// visita confirmada pelo servidor deixa o dispositivo aqui. É a minimização + /// de dados acontecendo, e há teste que trava essa propriedade. + @override + Future save(List visits) async { + final database = await _open(); + + await database.transaction((transaction) async { + await transaction.delete(_table); + for (final visit in visits) { + await transaction.insert(_table, { + 'local_id': visit.localId, + 'patient_id': visit.patientId, + 'risk': visit.risk, + 'status': visit.status, + 'outcome': visit.outcome, + 'created_at': visit.createdAt.toIso8601String(), + 'version': visit.version, + }); + } + }); + } + + Future close() async { + await _database?.close(); + _database = null; + } +} diff --git a/apps/acs/lib/core/network/backend_client.dart b/apps/acs/lib/core/network/backend_client.dart index ab7a9ad..6f489bc 100644 --- a/apps/acs/lib/core/network/backend_client.dart +++ b/apps/acs/lib/core/network/backend_client.dart @@ -116,6 +116,11 @@ class BackendClient implements AcsBackend { final token = await _requireToken(); return _guard( () => _client.visits.sync(accessToken: token, visits: visits), + // `visits.sync` reaproveita AlertPermissionException para token inválido + // e para ACS sem microárea. Dizer "este alerta não pertence à sua + // microárea" aqui manda o ACS procurar um problema que não existe. + permissionMessage: + 'Sua sessão não autoriza sincronizar visitas. Entre novamente.', ); } @@ -123,7 +128,13 @@ class BackendClient implements AcsBackend { void close() => _client.close(); /// Traduz as exceções tipadas do backend para [BackendFailure]. - Future _guard(Future Function() call) async { + /// + /// [permissionMessage] troca o texto de `AlertPermissionException`, que + /// significa coisas diferentes conforme o endpoint que a lançou. + Future _guard( + Future Function() call, { + String? permissionMessage, + }) async { try { return await call(); } on EndpointDisabledException { @@ -134,8 +145,8 @@ class BackendClient implements AcsBackend { } on AlertPermissionException { // Acontece quando o alerta é de outra microárea. A territorialização é // invariante: o servidor recusa, e o app não deve tentar contornar. - throw const BackendFailure( - 'Este alerta não pertence à sua microárea.', + throw BackendFailure( + permissionMessage ?? 'Este alerta não pertence à sua microárea.', isRecoverable: false, ); } on AlertValidationException catch (error) { diff --git a/apps/acs/lib/core/security/database_key_store.dart b/apps/acs/lib/core/security/database_key_store.dart new file mode 100644 index 0000000..2d97f42 --- /dev/null +++ b/apps/acs/lib/core/security/database_key_store.dart @@ -0,0 +1,84 @@ +import 'dart:math'; + +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; + +/// Custódia da chave que abre o banco local criptografado. +/// +/// A chave **não** é derivada de PIN nem de biometria, apesar de +/// `spec/PRD_system.md` (4.2.3) prescrever PBKDF2 a partir de PIN: não existe +/// nenhum fluxo de PIN nos apps — a autenticação é `auth.developmentLogin`, sem +/// credencial — e um PIN esquecido significaria perder visitas ainda não +/// sincronizadas. Adotamos a leitura de `spec/test_plan.md` (126), "chave gerada +/// pelo TEE do hardware": chave aleatória guardada no armazenamento seguro da +/// plataforma. +/// +/// A interface aceita um segundo fator depois, sem migrar dados: bastaria +/// envelopar a chave devolvida aqui. +abstract class DatabaseKeyStore { + /// Devolve a chave do dispositivo, criando-a na primeira vez. + Future readOrCreate(); + + /// Remove a chave. Torna o banco existente ilegível — só use ao descartar o + /// banco junto. + Future delete(); +} + +/// Gera uma chave de 256 bits em hexadecimal. +/// +/// São 64 caracteres hex a partir de [Random.secure]. O SQLCipher deriva a +/// chave real desta passphrase. +String generateDatabaseKey() { + final random = Random.secure(); + return List.generate(32, (_) => random.nextInt(256)) + .map((byte) => byte.toRadixString(16).padLeft(2, '0')) + .join(); +} + +/// Guarda a chave no Android Keystore / iOS Keychain. +/// +/// **Nunca registre a chave em log**, nem em mensagem de erro: "dados sensíveis +/// não são logados em texto plano" é critério de aceite do LGPD-RF09. +class SecureStorageDatabaseKeyStore implements DatabaseKeyStore { + SecureStorageDatabaseKeyStore({FlutterSecureStorage? storage}) + : _storage = storage ?? + const FlutterSecureStorage( + // Respaldado pelo Keystore do Android; minSdk 24 atende. + aOptions: AndroidOptions(encryptedSharedPreferences: true), + // `first_unlock_this_device` mantém a chave fora de backup em + // nuvem e ilegível antes do primeiro desbloqueio. Sem isso, um + // backup restaurado em outro aparelho levaria a chave junto. + iOptions: IOSOptions( + accessibility: KeychainAccessibility.first_unlock_this_device, + ), + ); + + static const _key = 'sinalacs.local_database_key'; + + final FlutterSecureStorage _storage; + + @override + Future readOrCreate() async { + final existing = await _storage.read(key: _key); + if (existing != null && existing.isNotEmpty) return existing; + + final created = generateDatabaseKey(); + await _storage.write(key: _key, value: created); + return created; + } + + @override + Future delete() => _storage.delete(key: _key); +} + +/// Duplo de teste. Mantém a chave só em memória. +class InMemoryDatabaseKeyStore implements DatabaseKeyStore { + InMemoryDatabaseKeyStore({String? initialKey}) : _key = initialKey; + + String? _key; + + @override + Future readOrCreate() async => _key ??= generateDatabaseKey(); + + @override + Future delete() async => _key = null; +} diff --git a/apps/acs/lib/core/services/backend_visit_synchronizer.dart b/apps/acs/lib/core/services/backend_visit_synchronizer.dart index 92ecdb1..9746641 100644 --- a/apps/acs/lib/core/services/backend_visit_synchronizer.dart +++ b/apps/acs/lib/core/services/backend_visit_synchronizer.dart @@ -8,23 +8,17 @@ import 'package:sinalacs_client/sinalacs_client.dart'; /// [AcsBackend] (que não conhece a fila) — é a tradução entre os dois, no mesmo /// espírito das implementações de `infrastructure/` no backend. class BackendVisitSynchronizer implements VisitSynchronizer { - BackendVisitSynchronizer({required this.backend, required this.patientIdFor}); + BackendVisitSynchronizer({required this.backend}); final AcsBackend backend; - /// Resolve o UUID do paciente de uma visita. - /// - /// A fila guarda um rótulo de exibição, não o identificador; quem registrou a - /// visita sabe a qual alerta ela pertence. - final String Function(OfflineVisitRecord visit) patientIdFor; - @override Future> push(List visits) async { final results = await backend.syncVisits([ for (final visit in visits) VisitSyncEntry( localId: visit.localId, - patientId: patientIdFor(visit), + patientId: visit.patientId, scheduledAt: visit.createdAt.toUtc(), completedAt: visit.createdAt.toUtc(), status: visit.outcome.isEmpty ? visit.status : visit.outcome, @@ -40,6 +34,7 @@ class BackendVisitSynchronizer implements VisitSynchronizer { localId: result.localId, status: result.syncStatus.name, serverVersion: result.serverVersion, + message: result.message, ), ]; } diff --git a/apps/acs/lib/core/services/offline_visit_queue.dart b/apps/acs/lib/core/services/offline_visit_queue.dart index 61f4965..59f3679 100644 --- a/apps/acs/lib/core/services/offline_visit_queue.dart +++ b/apps/acs/lib/core/services/offline_visit_queue.dart @@ -11,15 +11,22 @@ class SyncOutcome { const SyncOutcome({ required this.kind, required this.processed, + this.message, }); final SyncOutcomeKind kind; final int processed; + + /// Motivo, já em português, quando [kind] é [SyncOutcomeKind.error]. + /// + /// Sem isto a tela só conseguia dizer "não deu certo": a mensagem real do + /// servidor (identificador inválido, sessão expirada) ficava presa aqui. + final String? message; } class OfflineVisitRecord { OfflineVisitRecord({ - required this.patientName, + required this.patientId, required this.risk, required this.status, this.outcome = '', @@ -29,7 +36,15 @@ class OfflineVisitRecord { }) : localId = localId ?? _newLocalId(), createdAt = createdAt ?? DateTime.now(); - final String patientName; + /// UUID do paciente, exatamente como veio no alerta. + /// + /// Guardar o identificador e montar o rótulo na tela é o que mantém o disco + /// sem texto legível sobre a pessoa (minimização, LGPD-RF01). Antes gravava-se + /// `'Paciente ' + 8 dos 32 dígitos hex`, o que era pior nos dois sentidos: um + /// rótulo legível no aparelho e um UUID irrecuperável — `visits.sync` recusa + /// qualquer coisa que não seja UUID, então a visita nunca poderia subir. + final String patientId; + final String risk; final String status; final String outcome; @@ -47,7 +62,7 @@ class OfflineVisitRecord { final int version; OfflineVisitRecord copyWith({ - String? patientName, + String? patientId, String? risk, String? status, String? outcome, @@ -56,7 +71,7 @@ class OfflineVisitRecord { int? version, }) { return OfflineVisitRecord( - patientName: patientName ?? this.patientName, + patientId: patientId ?? this.patientId, risk: risk ?? this.risk, status: status ?? this.status, outcome: outcome ?? this.outcome, @@ -83,6 +98,7 @@ class VisitSyncOutcome { required this.localId, required this.status, this.serverVersion, + this.message, }); final String localId; @@ -91,6 +107,12 @@ class VisitSyncOutcome { final String status; final int? serverVersion; + + /// Motivo devolvido pelo servidor quando [status] é `error`. + /// + /// Ele já vinha no contrato (`VisitSyncResult.message`) e era descartado no + /// caminho até aqui, o que deixava um `patientId` inválido parecendo silêncio. + final String? message; } /// Quem leva as visitas pendentes ao servidor. @@ -141,6 +163,15 @@ class OfflineVisitQueue { final List _conflicts = []; bool _restored = false; + bool _persistenceFailed = false; + + /// `true` quando o armazenamento do dispositivo não está aceitando gravação. + /// + /// A fila continua funcionando em memória — travar o registro de visita em + /// campo seria pior —, mas quem consome precisa avisar a pessoa: uma fila que + /// parou de persistir em silêncio perde o trabalho do dia ao fechar o app. + /// Manter em RAM não viola o INV-04, que proíbe *persistir* em texto plano. + bool get persistenceFailed => _persistenceFailed; int get pendingCount => _pending.length; int get syncedCount => _synced.length; @@ -156,15 +187,33 @@ class OfflineVisitQueue { Future restore() async { if (_restored) return; _restored = true; - final stored = await _store.load(); - _pending - ..clear() - ..addAll(stored); + + try { + final stored = await _store.load(); + _pending + ..clear() + ..addAll(stored); + _persistenceFailed = false; + } catch (_) { + // Banco indisponível (chave perdida, plataforma sem SQLCipher). A fila + // segue em memória e o sinalizador acende. + _persistenceFailed = true; + } } Future add(OfflineVisitRecord record) async { _pending.add(record); - await _store.save(_pending); + await _persist(); + } + + /// Grava o estado corrente, sem deixar a falha derrubar quem chamou. + Future _persist() async { + try { + await _store.save(_pending); + _persistenceFailed = false; + } catch (_) { + _persistenceFailed = true; + } } Future sync({bool forceConflict = false}) async { @@ -186,17 +235,31 @@ class OfflineVisitQueue { final sender = synchronizer; if (sender == null) { // Sem sincronizador configurado a fila permanece local; nada é - // declarado sincronizado, porque nada saiu do dispositivo. - return SyncOutcome(kind: SyncOutcomeKind.error, processed: batch.length); + // declarado sincronizado, porque nada saiu do dispositivo. Em produção + // este ramo não é alcançável: `buildVisitQueue` sempre liga um. + return SyncOutcome( + kind: SyncOutcomeKind.error, + processed: batch.length, + message: 'A sincronização não está configurada neste aplicativo.', + ); } final List outcomes; try { outcomes = await sender.push(batch); - } catch (_) { + } catch (error) { // O lote continua pendente: perder a visita por falha de rede seria // exatamente o que a fila existe para evitar. - return SyncOutcome(kind: SyncOutcomeKind.error, processed: batch.length); + // + // `'$error'` é a mensagem que a tela mostra. Em produção isto é sempre + // uma `BackendFailure`, cujo `toString()` já é a frase em português — o + // `BackendClient` não deixa escapar outro tipo. A fila não importa + // `backend_client.dart` de propósito: ela não deve conhecer a rede. + return SyncOutcome( + kind: SyncOutcomeKind.error, + processed: batch.length, + message: '$error', + ); } return _applyOutcomes(batch, outcomes); @@ -210,6 +273,8 @@ class OfflineVisitQueue { var conflicts = 0; var synced = 0; + var errors = 0; + String? firstError; final stillPending = []; for (final visit in batch) { @@ -227,20 +292,38 @@ class OfflineVisitQueue { // Volta para a fila: conflito precisa de resolução, não de descarte. stillPending.add(conflicted); conflicts++; + case 'error': + // O servidor recusou ESTA visita. Continua pendente — descartar + // perderia o registro feito em campo —, mas precisa ser contada: + // antes caía no `default` e a chamada devolvia `synced`, então uma + // visita recusada ficava presa na fila para sempre, em silêncio. + stillPending.add(visit); + errors++; + firstError ??= outcome?.message; default: - // Sem resposta para esta visita — mantém pendente e tenta de novo. + // Sem resposta para esta visita — o lote pode ter voltado incompleto. stillPending.add(visit); + errors++; } } _pending ..clear() ..addAll(stillPending); - await _store.save(_pending); + await _persist(); + // Precedência: conflito > erro > sincronizado. O conflito é o que exige + // decisão de quem registrou; o erro, no máximo, uma nova tentativa. if (conflicts > 0) { return SyncOutcome(kind: SyncOutcomeKind.conflict, processed: conflicts); } + if (errors > 0) { + return SyncOutcome( + kind: SyncOutcomeKind.error, + processed: errors, + message: firstError ?? 'O servidor não confirmou $errors visita(s).', + ); + } return SyncOutcome(kind: SyncOutcomeKind.synced, processed: synced); } } diff --git a/apps/acs/lib/core/services/visit_queue_factory.dart b/apps/acs/lib/core/services/visit_queue_factory.dart new file mode 100644 index 0000000..6ba5cae --- /dev/null +++ b/apps/acs/lib/core/services/visit_queue_factory.dart @@ -0,0 +1,24 @@ +import 'package:sinalacs_acs/core/database/sqlcipher_visit_store.dart'; +import 'package:sinalacs_acs/core/network/backend_client.dart'; +import 'package:sinalacs_acs/core/security/database_key_store.dart'; +import 'package:sinalacs_acs/core/services/backend_visit_synchronizer.dart'; +import 'package:sinalacs_acs/core/services/offline_visit_queue.dart'; + +/// Monta a fila de visitas como ela roda em produção. +/// +/// Existe como função pública, e não como método privado da `State` do app, +/// porque a ausência do sincronizador foi um defeito que nenhum teste podia +/// enxergar: a UI só é testável com a fila injetada, então a montagem real +/// nunca era exercitada. `sync()` caía no ramo sem remetente e devolvia erro — +/// as visitas nunca subiam e o disco nunca era liberado. +/// +/// [store] existe para o teste trocar o banco criptografado por memória; o +/// `SqlCipherVisitStore` depende de canal de plataforma e não abre na VM. +OfflineVisitQueue buildVisitQueue({ + required AcsBackend backend, + VisitStore? store, +}) => + OfflineVisitQueue( + store: store ?? SqlCipherVisitStore(keyStore: SecureStorageDatabaseKeyStore()), + synchronizer: BackendVisitSynchronizer(backend: backend), + ); diff --git a/apps/acs/pubspec.lock b/apps/acs/pubspec.lock index a76b9eb..5a1a8b0 100644 --- a/apps/acs/pubspec.lock +++ b/apps/acs/pubspec.lock @@ -155,14 +155,54 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.2" - flutter_plugin_android_lifecycle: + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea" + url: "https://pub.dev" + source: hosted + version: "9.2.4" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688 + url: "https://pub.dev" + source: hosted + version: "1.2.3" + flutter_secure_storage_macos: + dependency: transitive + description: + name: flutter_secure_storage_macos + sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247" + url: "https://pub.dev" + source: hosted + version: "3.1.3" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8 + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + flutter_secure_storage_windows: dependency: transitive description: - name: flutter_plugin_android_lifecycle - sha256: "1c2b787f99bdca1f3718543f81d38aa1b124817dfeb9fb196201bea85b6134bf" + name: flutter_secure_storage_windows + sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709 url: "https://pub.dev" source: hosted - version: "2.0.26" + version: "3.1.2" flutter_test: dependency: "direct dev" description: flutter @@ -226,54 +266,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.5" - google_maps: - dependency: transitive - description: - name: google_maps - sha256: "6811afff50dc410b860622ac89df125f5be1eada3ad69197e4c24dd18f2ef0e0" - url: "https://pub.dev" - source: hosted - version: "8.3.0" - google_maps_flutter: - dependency: "direct main" - description: - name: google_maps_flutter - sha256: "621125e35e81ca39ef600e45243d2be93167e61def72bc7207b0c4a635c58506" - url: "https://pub.dev" - source: hosted - version: "2.10.1" - google_maps_flutter_android: - dependency: transitive - description: - name: google_maps_flutter_android - sha256: "721ffae2240e957c04b0de19ffd4b68580adb57a8224496b7fb55fad23aec98a" - url: "https://pub.dev" - source: hosted - version: "2.14.13" - google_maps_flutter_ios: - dependency: transitive - description: - name: google_maps_flutter_ios - sha256: c7433645c4c9b61c587938cb06072f3dad601239e596b090c0f8f206c1f2ade7 - url: "https://pub.dev" - source: hosted - version: "2.15.2" - google_maps_flutter_platform_interface: - dependency: transitive - description: - name: google_maps_flutter_platform_interface - sha256: "970c8f766c02909c7be282dea923c971f83a88adaf07f8871d0aacebc3b07bb2" - url: "https://pub.dev" - source: hosted - version: "2.11.1" - google_maps_flutter_web: - dependency: transitive - description: - name: google_maps_flutter_web - sha256: a45786ea6691cc7cdbe2cf3ce2c2daf4f82a885745666b4a36baada3a4e12897 - url: "https://pub.dev" - source: hosted - version: "0.5.12" html: dependency: transitive description: @@ -303,6 +295,38 @@ packages: description: flutter source: sdk version: "0.0.0" + jni: + dependency: transitive + description: + name: jni + sha256: f038e58b4dc2c9037f50e233175086337e0b305e356d28211bf55f21c504cbd3 + url: "https://pub.dev" + source: hosted + version: "1.0.3" + jni_flutter: + dependency: transitive + description: + name: jni_flutter + sha256: b2310cdd4c18c65c081ab141a41efa94aa26c65431803703ece51996f174f351 + url: "https://pub.dev" + source: hosted + version: "1.0.3" + jni_util: + dependency: transitive + description: + name: jni_util + sha256: "1ba86da04a5f2bf18fde2edb235587e70c5b0fc5bd4ba955f46b00942c3fc35f" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" leak_tracker: dependency: transitive description: @@ -375,6 +399,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: ffcf4cf3d6c0b74ac43708d9f56625506e8a68aa935abe9d267a7330f320eb5d + url: "https://pub.dev" + source: hosted + version: "3.0.0" path: dependency: transitive description: @@ -383,6 +415,54 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825 + url: "https://pub.dev" + source: hosted + version: "2.1.6" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd" + url: "https://pub.dev" + source: hosted + version: "2.3.1" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "6d13aece7b3f5c5a9731eaf553ff9dcbc2eff41087fd2df587fd0fed9a3eb0c4" + url: "https://pub.dev" + source: hosted + version: "2.5.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: "58c2005f147315b11e9b4a7bc889cd5203e250cba8e3f012dae259b4972b5c16" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda" + url: "https://pub.dev" + source: hosted + version: "2.1.3" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" petitparser: dependency: transitive description: @@ -415,14 +495,6 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.6" - sanitize_html: - dependency: transitive - description: - name: sanitize_html - sha256: "60257e2b8fd8b13b7c755065c2639a6c800031bb5bd3e30a007fc6739c77b2e1" - url: "https://pub.dev" - source: hosted - version: "2.2.0" serverpod_auth_core_client: dependency: transitive description: @@ -555,14 +627,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" - stream_transform: - dependency: transitive - description: - name: stream_transform - sha256: a00e5f18bffc764f923e7dec1038527f7fe7a1791361a7117f0358193f13d53a - url: "https://pub.dev" - source: hosted - version: "2.1.2" string_scanner: dependency: transitive description: @@ -683,6 +747,22 @@ packages: url: "https://pub.dev" source: hosted version: "3.2.0" + win32: + dependency: transitive + description: + name: win32 + sha256: "329edf97fdd893e0f1e3b9e88d6a0e627128cc17cc316a8d67fda8f1451178ba" + url: "https://pub.dev" + source: hosted + version: "5.13.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" xml: dependency: transitive description: @@ -700,5 +780,5 @@ packages: source: hosted version: "3.1.4" sdks: - dart: ">=3.10.0-0 <4.0.0" - flutter: ">=3.24.0" + dart: ">=3.11.0 <4.0.0" + flutter: ">=3.38.0" diff --git a/apps/acs/pubspec.yaml b/apps/acs/pubspec.yaml index 71f58bf..f6548b8 100644 --- a/apps/acs/pubspec.yaml +++ b/apps/acs/pubspec.yaml @@ -22,10 +22,13 @@ dependencies: sqflite: ^2.3.0 sqflite_common_ffi: ^2.3.0 sqflite_sqlcipher: ^3.2.0 + # Custódia da chave do SQLCipher no Android Keystore / iOS Keychain. + # É invólucro de API de plataforma, não framework novo — materializa o + # "chave gerada pelo TEE do hardware" de spec/test_plan.md. + flutter_secure_storage: ^9.2.2 connectivity_plus: ^7.3.1 mqtt_client: ^10.0.0 geolocator: ^12.0.0 - google_maps_flutter: ^2.6.0 dev_dependencies: diff --git a/apps/acs/test/encrypted_database_test.dart b/apps/acs/test/encrypted_database_test.dart index 66ee9e4..bfba337 100644 --- a/apps/acs/test/encrypted_database_test.dart +++ b/apps/acs/test/encrypted_database_test.dart @@ -1,17 +1,353 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:sinalacs_acs/core/database/encrypted_database.dart'; +import 'package:sinalacs_acs/core/database/sqlcipher_visit_store.dart'; +import 'package:sinalacs_acs/core/security/database_key_store.dart'; +import 'package:sinalacs_acs/core/services/offline_visit_queue.dart'; +import 'package:sqflite_common_ffi/sqflite_ffi.dart'; +import 'support/fakes.dart'; + +/// Estes testes rodam na VM, onde o SQLCipher não existe — por isso passam +/// `allowUnencryptedForTesting`. Eles provam a LÓGICA (chave, schema, round-trip, +/// minimização), **não** a criptografia. +/// +/// A prova de que o arquivo está cifrado só é possível em dispositivo e vive em +/// `integration_test/encrypted_storage_test.dart`. Foi exatamente essa confusão +/// que produziu o defeito original: havia um teste chamado "deve abrir banco +/// criptografado" que, no Linux, exercitava o caminho sem criptografia nenhuma. void main() { TestWidgetsFlutterBinding.ensureInitialized(); - test('deve abrir banco criptografado com senha configurada', () async { - final db = await EncryptedLocalDatabase.open( - databaseName: 'sinalacs_acs_test.db', - passphrase: 'test-passphrase', - ); + group('custódia da chave', () { + test('gera uma chave de 256 bits em hexadecimal', () { + final key = generateDatabaseKey(); + + expect(key, hasLength(64)); + expect(key, matches(RegExp(r'^[0-9a-f]{64}$'))); + }); + + test('duas chaves geradas são diferentes', () { + expect(generateDatabaseKey(), isNot(generateDatabaseKey())); + }); + + test('a chave é estável entre leituras', () async { + final store = InMemoryDatabaseKeyStore(); + + expect(await store.readOrCreate(), await store.readOrCreate()); + }); + + test('apagar a chave faz a próxima leitura gerar outra', () async { + final store = InMemoryDatabaseKeyStore(); + final first = await store.readOrCreate(); + + await store.delete(); + + expect(await store.readOrCreate(), isNot(first)); + }); + }); + + group('INV-04: nunca persistir em texto plano', () { + test('recusa abrir fora de Android/iOS sem a flag explícita', () async { + // Este é o teste que trava a regressão. Antes, o caminho sem criptografia + // era o comportamento PADRÃO fora de mobile. + expect( + () => EncryptedLocalDatabase.open( + databaseName: 'inv04_test.db', + passphrase: generateDatabaseKey(), + ), + throwsA(isA()), + ); + }); + + test('abre com a flag explícita, que só o teste passa', () async { + final db = await EncryptedLocalDatabase.open( + databaseName: 'inv04_allowed_test.db', + passphrase: generateDatabaseKey(), + allowUnencryptedForTesting: true, + ); + + expect(db.isOpen, isTrue); + await db.close(); + }); + + test('recusa passphrase vazia', () async { + expect( + () => EncryptedLocalDatabase.open( + databaseName: 'inv04_empty_test.db', + passphrase: '', + allowUnencryptedForTesting: true, + ), + throwsA(isA()), + ); + }); + }); + + group('migração do schema', () { + // O passo de maior risco da mudança: um app já instalado abre um arquivo + // v1 com o código v2. Sem `onUpgrade`, a tabela que a fila lê simplesmente + // não existe, e o ACS vê "as visitas não estão sendo salvas" para sempre. + // + // Estes testes chamam `EncryptedLocalDatabase.open` DIRETO, nunca através + // do `SqlCipherVisitStore`: o `catch` do store apaga o arquivo ilegível e + // recomeça, então uma migração quebrada passaria despercebida por ele. + const nome = 'migracao_test.db'; + final chave = 'a' * 64; + + Future abrirV1(String create) async { + sqfliteFfiInit(); + return databaseFactoryFfi.openDatabase( + await EncryptedLocalDatabase.pathFor(nome), + options: OpenDatabaseOptions( + version: 1, + onCreate: (db, version) => db.execute(create), + ), + ); + } + + Future> tabelas(Database db) async { + final rows = await db.query('sqlite_master', columns: ['name'], where: "type = 'table'"); + return {for (final row in rows) row['name']! as String}; + } + + setUp(() => EncryptedLocalDatabase.deleteDatabaseFile(nome)); + tearDown(() => EncryptedLocalDatabase.deleteDatabaseFile(nome)); + + test('o esquema antigo local_queue dá lugar a offline_visits', () async { + final legado = await abrirV1( + 'CREATE TABLE IF NOT EXISTS local_queue (id TEXT PRIMARY KEY)', + ); + await legado.insert('local_queue', {'id': 'antigo'}); + await legado.close(); + + final atual = await EncryptedLocalDatabase.open( + databaseName: nome, + passphrase: chave, + allowUnencryptedForTesting: true, + ); + + final nomes = await tabelas(atual); + expect(nomes, contains('offline_visits')); + expect(nomes, isNot(contains('local_queue'))); + await atual.close(); + }); + + test('a offline_visits v1 com patient_name é recriada com patient_id', () async { + // O rótulo antigo era 'Paciente ' + 8 dos 32 dígitos do UUID: não há como + // recuperar o identificador, então preservar a linha só produziria uma + // pendência que o servidor recusa a cada tentativa. + final legado = await abrirV1(''' +CREATE TABLE IF NOT EXISTS offline_visits ( + local_id TEXT PRIMARY KEY, + patient_name TEXT NOT NULL, + risk TEXT NOT NULL, + status TEXT NOT NULL, + outcome TEXT NOT NULL, + created_at TEXT NOT NULL, + version INTEGER NOT NULL +)'''); + await legado.insert('offline_visits', { + 'local_id': '00000000-0000-4000-8000-00000000000a', + 'patient_name': 'Paciente 00000000', + 'risk': 'red', + 'status': 'PENDENTE', + 'outcome': 'realizada', + 'created_at': DateTime.utc(2026).toIso8601String(), + 'version': 1, + }); + await legado.close(); + + final atual = await EncryptedLocalDatabase.open( + databaseName: nome, + passphrase: chave, + allowUnencryptedForTesting: true, + ); + + final colunas = [ + for (final row in await atual.rawQuery('PRAGMA table_info(offline_visits)')) + row['name']! as String, + ]; + expect(colunas, contains('patient_id')); + expect(colunas, isNot(contains('patient_name'))); + expect(await atual.query('offline_visits'), isEmpty); + await atual.close(); + }); + + test('abrir de novo na mesma versão não apaga o que está gravado', () async { + // A migração roda uma vez. Se rodasse a cada abertura, ela apagaria a + // fila do dia toda vez que o app abrisse. + final store = SqlCipherVisitStore( + keyStore: InMemoryDatabaseKeyStore(), + databaseName: nome, + allowUnencryptedForTesting: true, + ); + await store.save([ + OfflineVisitRecord(patientId: seedPatientId, risk: 'red', status: 'PENDENTE'), + ]); + await store.close(); + + expect((await store.load()).single.patientId, seedPatientId); + await store.close(); + }); + }); + + group('SqlCipherVisitStore', () { + late SqlCipherVisitStore store; + + OfflineVisitRecord visit(String patientId) => OfflineVisitRecord( + patientId: patientId, + risk: 'red', + status: 'PENDENTE', + outcome: 'realizada', + ); + + setUp(() async { + await EncryptedLocalDatabase.deleteDatabaseFile('store_test.db'); + store = SqlCipherVisitStore( + keyStore: InMemoryDatabaseKeyStore(), + databaseName: 'store_test.db', + allowUnencryptedForTesting: true, + ); + }); + + tearDown(() => store.close()); - expect(db.isOpen, isTrue); - await db.execute('CREATE TABLE IF NOT EXISTS test_table (id INTEGER PRIMARY KEY)'); - await db.close(); + test('grava e relê preservando os campos', () async { + final original = visit(seedPatientId); + await store.save([original]); + + final loaded = await store.load(); + + expect(loaded, hasLength(1)); + expect(loaded.single.localId, original.localId); + expect(loaded.single.patientId, original.patientId); + expect(loaded.single.risk, 'red'); + expect(loaded.single.outcome, 'realizada'); + expect(loaded.single.version, original.version); + }); + + test('save substitui o conjunto inteiro', () async { + await store.save([visit(syntheticPatientId(1)), visit(syntheticPatientId(2))]); + await store.save([visit(syntheticPatientId(3))]); + + final loaded = await store.load(); + + expect(loaded, hasLength(1)); + expect(loaded.single.patientId, syntheticPatientId(3)); + }); + + test('sobrevive a fechar e reabrir o banco', () async { + await store.save([visit(seedPatientId)]); + await store.close(); + + expect((await store.load()).single.patientId, seedPatientId); + }); }); + + group('fila persistente', () { + late SqlCipherVisitStore store; + + OfflineVisitQueue queueOn(SqlCipherVisitStore store, {VisitSynchronizer? sender}) => + OfflineVisitQueue(store: store, synchronizer: sender); + + setUp(() async { + await EncryptedLocalDatabase.deleteDatabaseFile('queue_test.db'); + store = SqlCipherVisitStore( + keyStore: InMemoryDatabaseKeyStore(), + databaseName: 'queue_test.db', + allowUnencryptedForTesting: true, + ); + }); + + tearDown(() => store.close()); + + test('a visita sobrevive a recriar a fila', () async { + // É o que "offline-first" significa: fechar o app não pode perder o + // trabalho de campo. + final first = queueOn(store); + await first.add(OfflineVisitRecord( + patientId: seedPatientId, + risk: 'red', + status: 'PENDENTE', + )); + + final second = queueOn(store); + await second.restore(); + + expect(second.pendingCount, 1); + expect(second.pendingVisits.single.patientId, seedPatientId); + expect(second.persistenceFailed, isFalse); + }); + + test('a visita confirmada pelo servidor SAI do disco', () async { + // Minimização (LGPD-RF07 / seção 5.6): o que já chegou ao servidor não + // precisa continuar no aparelho que pode ser perdido ou roubado. + final queue = queueOn(store, sender: _SyncedSynchronizer()); + await queue.add(OfflineVisitRecord( + patientId: seedPatientId, + risk: 'red', + status: 'PENDENTE', + )); + expect(await store.load(), hasLength(1)); + + final result = await queue.sync(); + + expect(result.kind, SyncOutcomeKind.synced); + expect(await store.load(), isEmpty); + }); + + test('a visita em conflito PERMANECE no disco', () async { + // Conflito precisa de resolução; descartá-lo perderia o registro. + final queue = queueOn(store, sender: _ConflictSynchronizer()); + await queue.add(OfflineVisitRecord( + patientId: syntheticPatientId(2), + risk: 'yellow', + status: 'PENDENTE', + )); + + final result = await queue.sync(); + + expect(result.kind, SyncOutcomeKind.conflict); + expect(await store.load(), hasLength(1)); + }); + + test('armazenamento indisponível não derruba o registro em campo', () async { + // Travar o registro de visita seria pior do que não persistir. A fila + // segue em memória e acende o sinalizador para a UI avisar. + final queue = OfflineVisitQueue(store: _BrokenVisitStore()); + + await queue.restore(); + await queue.add(OfflineVisitRecord( + patientId: syntheticPatientId(3), + risk: 'green', + status: 'PENDENTE', + )); + + expect(queue.pendingCount, 1); + expect(queue.persistenceFailed, isTrue); + }); + }); +} + +class _SyncedSynchronizer implements VisitSynchronizer { + @override + Future> push(List visits) async => [ + for (final visit in visits) + VisitSyncOutcome(localId: visit.localId, status: 'synced', serverVersion: 1), + ]; +} + +class _ConflictSynchronizer implements VisitSynchronizer { + @override + Future> push(List visits) async => [ + for (final visit in visits) + VisitSyncOutcome(localId: visit.localId, status: 'conflict', serverVersion: 9), + ]; +} + +class _BrokenVisitStore implements VisitStore { + @override + Future> load() async => throw StateError('sem banco'); + + @override + Future save(List visits) async => throw StateError('sem banco'); } diff --git a/apps/acs/test/login_flow_test.dart b/apps/acs/test/login_flow_test.dart index 8264aa4..2cb6268 100644 --- a/apps/acs/test/login_flow_test.dart +++ b/apps/acs/test/login_flow_test.dart @@ -3,7 +3,9 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:sinalacs_acs/app/app.dart'; import 'package:sinalacs_acs/core/network/backend_client.dart'; import 'package:sinalacs_acs/core/services/alert_queue.dart'; +import 'package:sinalacs_acs/core/services/backend_visit_synchronizer.dart'; import 'package:sinalacs_acs/core/services/offline_visit_queue.dart'; +import 'package:sinalacs_acs/core/services/visit_queue_factory.dart'; import 'support/fakes.dart'; @@ -173,8 +175,8 @@ void main() { }); group('fila offline de visitas', () { - OfflineVisitRecord visit(String name) => OfflineVisitRecord( - patientName: name, + OfflineVisitRecord visit(String patientId) => OfflineVisitRecord( + patientId: patientId, risk: 'red', status: 'PENDENTE', ); @@ -183,7 +185,7 @@ void main() { final sender = FakeVisitSynchronizer(); final queue = OfflineVisitQueue(synchronizer: sender); - await queue.add(visit('Maria Souza')); + await queue.add(visit(seedPatientId)); expect(queue.pendingCount, 1); final result = await queue.sync(); @@ -201,7 +203,7 @@ void main() { ); final queue = OfflineVisitQueue(synchronizer: sender); - await queue.add(visit('João Souza')); + await queue.add(visit(syntheticPatientId(2))); final conflict = await queue.sync(); expect(conflict.kind, SyncOutcomeKind.conflict); @@ -222,7 +224,7 @@ void main() { final sender = FakeVisitSynchronizer(throwOnPush: true); final queue = OfflineVisitQueue(synchronizer: sender); - await queue.add(visit('Ana Costa')); + await queue.add(visit(syntheticPatientId(3))); final result = await queue.sync(); expect(result.kind, SyncOutcomeKind.error); @@ -234,20 +236,58 @@ void main() { // Fechar o app não pode perder a fila. final store = InMemoryVisitStore(); final first = OfflineVisitQueue(store: store); - await first.add(visit('Maria Souza')); + await first.add(visit(seedPatientId)); final second = OfflineVisitQueue(store: store); await second.restore(); expect(second.pendingCount, 1); - expect(second.pendingVisits.single.patientName, 'Maria Souza'); + expect(second.pendingVisits.single.patientId, seedPatientId); + }); + + test('erro por visita devolve o motivo e a visita permanece pendente', () async { + // Antes o status 'error' caía no ramo default e a chamada devolvia + // `synced`: a visita recusada ficava presa na fila para sempre, sem que + // ninguém pudesse descobrir por quê. + final sender = FakeVisitSynchronizer( + statusFor: (_) => 'error', + messageFor: (_) => 'identificadores devem ser UUID', + ); + final queue = OfflineVisitQueue(synchronizer: sender); + + await queue.add(visit(seedPatientId)); + final result = await queue.sync(); + + expect(result.kind, SyncOutcomeKind.error); + expect(result.message, 'identificadores devem ser UUID'); + expect(queue.pendingCount, 1); + expect(queue.syncedCount, 0); + }); + + test('conflito tem precedência sobre erro no mesmo lote', () async { + // Conflito exige decisão de quem registrou; erro, no máximo, uma nova + // tentativa. Quem chama precisa ver o que pede ação. + final conflitante = visit(seedPatientId); + final sender = FakeVisitSynchronizer( + statusFor: (v) => v.localId == conflitante.localId ? 'conflict' : 'error', + ); + final queue = OfflineVisitQueue(synchronizer: sender); + + await queue.add(conflitante); + await queue.add(visit(syntheticPatientId(2))); + + final result = await queue.sync(); + + expect(result.kind, SyncOutcomeKind.conflict); + // Nada saiu: as duas continuam no aparelho. + expect(queue.pendingCount, 2); }); test('não deve declarar sincronizado sem sincronizador configurado', () async { // O comportamento antigo carimbava 'SINCRONIZADO' sem nada sair do // dispositivo, o que escondia a ausência de integração. final queue = OfflineVisitQueue(); - await queue.add(visit('Maria Souza')); + await queue.add(visit(seedPatientId)); final result = await queue.sync(); @@ -284,5 +324,125 @@ void main() { expect(visitQueue.pendingCount, 1); }); + + testWidgets('deve enviar o UUID do alerta ao servidor, não o rótulo da tela', (tester) async { + // O caminho inteiro: alerta pelo broker → gravação → visits.sync. Antes a + // tela gravava 'Paciente 00000000' e descartava o UUID, então o servidor + // recusaria a visita por identificador inválido. + final backend = FakeAcsBackend(); + final visitQueue = OfflineVisitQueue( + synchronizer: BackendVisitSynchronizer(backend: backend), + ); + late FakeAlertFeed feed; + + await tester.pumpWidget(SinalAcsApp( + backend: backend, + feedBuilder: (queue) => feed = FakeAlertFeed(queue), + visitQueue: visitQueue, + )); + + await tester.tap(find.byKey(const Key('login_button'))); + await tester.pumpAndSettle(); + + feed.deliver(testAlert(alertId: 'alerta-1', riskLevel: 'yellow')); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Iniciar rota de visita')); + await tester.pumpAndSettle(); + + await tester.tap(find.byKey(const Key('save_visit'))); + await tester.pumpAndSettle(); + expect(find.byKey(const Key('pending_visits_count')), findsOneWidget); + expect(find.text('Pendentes de sincronização: 1'), findsOneWidget); + + // O SnackBar de confirmação ocupa o rodapé por 4 s e o botão fica abaixo + // da dobra nesta viewport: sem avançar o relógio e rolar, o toque bate no + // SnackBar. `pumpAndSettle` sozinho não resolve — ele só roda enquanto há + // quadros agendados, e um SnackBar parado não agenda nenhum. + await tester.pump(const Duration(seconds: 5)); + await tester.pumpAndSettle(); + await tester.ensureVisible(find.byKey(const Key('sync_visits'))); + await tester.pumpAndSettle(); + + await tester.tap(find.byKey(const Key('sync_visits'))); + await tester.pumpAndSettle(); + + expect(backend.syncedVisitBatches.single.single.patientId, seedPatientId); + expect(visitQueue.pendingCount, 0); + expect(find.text('Pendentes de sincronização: 0'), findsOneWidget); + }); + + testWidgets('sem alerta vinculado não deixa gravar', (tester) async { + // `visits.sync` exige UUID de paciente: gravar aqui criaria um registro + // que nunca sobe e, por isso, nunca sai do aparelho. + final visitQueue = OfflineVisitQueue(); + + await tester.pumpWidget(SinalAcsApp( + backend: FakeAcsBackend(), + feedBuilder: (queue) => FakeAlertFeed(queue), + visitQueue: visitQueue, + )); + + await tester.tap(find.byKey(const Key('login_button'))); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Visita')); + await tester.pumpAndSettle(); + + expect(find.byKey(const Key('visit_needs_alert')), findsOneWidget); + final botao = tester.widget(find.byKey(const Key('save_visit'))); + expect(botao.onPressed, isNull); + }); + + testWidgets('falha de rede mostra o motivo e mantém a visita no aparelho', (tester) async { + final backend = FakeAcsBackend() + ..syncFailure = const BackendFailure('Sem conexão com o servidor.'); + final visitQueue = OfflineVisitQueue( + synchronizer: BackendVisitSynchronizer(backend: backend), + ); + late FakeAlertFeed feed; + + await tester.pumpWidget(SinalAcsApp( + backend: backend, + feedBuilder: (queue) => feed = FakeAlertFeed(queue), + visitQueue: visitQueue, + )); + + await tester.tap(find.byKey(const Key('login_button'))); + await tester.pumpAndSettle(); + + feed.deliver(testAlert(alertId: 'alerta-1', riskLevel: 'yellow')); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Iniciar rota de visita')); + await tester.pumpAndSettle(); + + await tester.tap(find.byKey(const Key('save_visit'))); + await tester.pumpAndSettle(); + await tester.pump(const Duration(seconds: 5)); + await tester.pumpAndSettle(); + + await tester.ensureVisible(find.byKey(const Key('sync_visits'))); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('sync_visits'))); + await tester.pumpAndSettle(); + + expect(find.text('Sem conexão com o servidor.'), findsOneWidget); + expect(visitQueue.pendingCount, 1); + }); + }); + + group('fiação de produção', () { + test('a fila do app sai com o sincronizador do backend ligado', () { + // A regressão que motivou tudo: o app montava a fila sem sincronizador, + // `sync()` devolvia erro, e nenhum teste podia ver — a UI só é testável + // com a fila injetada, então a montagem real nunca era exercitada. + final queue = buildVisitQueue( + backend: FakeAcsBackend(), + store: InMemoryVisitStore(), + ); + + expect(queue.synchronizer, isA()); + }); }); } diff --git a/apps/acs/test/support/fakes.dart b/apps/acs/test/support/fakes.dart index c8500b8..440c7ef 100644 --- a/apps/acs/test/support/fakes.dart +++ b/apps/acs/test/support/fakes.dart @@ -7,9 +7,15 @@ import 'package:sinalacs_client/sinalacs_client.dart'; /// UUIDs do seed de desenvolvimento. Dados sintéticos. const seedAcsId = '00000000-0000-4000-8000-000000000002'; +const seedPatientId = '00000000-0000-4000-8000-000000000001'; const seedMicroAreaId = '00000000-0000-4000-8000-000000000003'; const otherMicroAreaId = '00000000-0000-4000-8000-000000000099'; +/// UUIDs sintéticos distintos, para os testes que precisam de mais de um +/// paciente. Nome próprio não entra em teste de repositório de saúde. +String syntheticPatientId(int n) => + '00000000-0000-4000-8000-${n.toString().padLeft(12, '0')}'; + class FakeAcsBackend implements AcsBackend { FakeAcsBackend({this.loginFailure, this.acknowledged = true, this.microAreaId = seedMicroAreaId}); @@ -62,9 +68,21 @@ class FakeAcsBackend implements AcsBackend { final List> syncedVisitBatches = >[]; + /// Falha da chamada inteira, como uma queda de rede. + BackendFailure? syncFailure; + + /// Resultado por visita; ausente significa `synced`. + VisitSyncResult Function(VisitSyncEntry entry)? syncResultFor; + @override Future> syncVisits(List visits) async { syncedVisitBatches.add(List.of(visits)); + final failure = syncFailure; + if (failure != null) throw failure; + + final custom = syncResultFor; + if (custom != null) return [for (final visit in visits) custom(visit)]; + return [ for (final visit in visits) VisitSyncResult( @@ -115,10 +133,13 @@ class FakeAlertFeed implements AlertFeed { /// Sincronizador que devolve o resultado programado pelo teste. class FakeVisitSynchronizer implements VisitSynchronizer { - FakeVisitSynchronizer({this.statusFor, this.throwOnPush = false}); + FakeVisitSynchronizer({this.statusFor, this.messageFor, this.throwOnPush = false}); /// Status por localId; ausente significa `synced`. String Function(OfflineVisitRecord visit)? statusFor; + + /// Motivo devolvido pelo servidor, como em `VisitSyncResult.message`. + String? Function(OfflineVisitRecord visit)? messageFor; bool throwOnPush; final List> batches = >[]; @@ -134,6 +155,7 @@ class FakeVisitSynchronizer implements VisitSynchronizer { localId: visit.localId, status: statusFor?.call(visit) ?? 'synced', serverVersion: visit.version + 1, + message: messageFor?.call(visit), ), ]; } @@ -147,7 +169,7 @@ PrioritizedAlert testAlert({ }) { return PrioritizedAlert( alertId: alertId, - patientId: '00000000-0000-4000-8000-000000000001', + patientId: seedPatientId, microAreaId: microAreaId, riskLevel: riskLevel, locationHash: 'sem-local-00', diff --git a/apps/acs/tool/live_check.dart b/apps/acs/tool/live_check.dart index 3e3d990..dc543b8 100644 --- a/apps/acs/tool/live_check.dart +++ b/apps/acs/tool/live_check.dart @@ -20,7 +20,9 @@ import 'dart:io'; import 'package:sinalacs_acs/core/network/backend_client.dart'; import 'package:sinalacs_acs/core/network/backend_config.dart'; +import 'package:sinalacs_acs/core/services/backend_visit_synchronizer.dart'; import 'package:sinalacs_acs/core/services/mqtt_secure_client.dart'; +import 'package:sinalacs_acs/core/services/offline_visit_queue.dart'; import 'package:sinalacs_client/sinalacs_client.dart' as api; String _arg(List args, String name, String fallback) { @@ -106,23 +108,26 @@ Future main(List args) async { exitCode = 1; } - // Sincronização de visita, pelo endpoint novo. - final localId = '00000000-0000-4000-8000-${DateTime.now().millisecondsSinceEpoch % 1000000000000}'; - final synced = await backend.syncVisits([ - api.VisitSyncEntry( - localId: localId.padRight(36, '0').substring(0, 36), - patientId: alert.patientId, - scheduledAt: DateTime.now().toUtc(), - status: 'realizada', - riskLevelBefore: api.RiskLevel.red, - notes: const {}, - version: 0, - ), - ]); - stdout.writeln(' visita sincronizada ${synced.single.syncStatus.name} ' - 'versão=${synced.single.serverVersion}'); - if (synced.single.syncStatus != api.SyncStatus.synced) { - stderr.writeln(' ERRO: a visita não sincronizou (${synced.single.message}).'); + // Sincronização de visita pela FILA de produção, não por um VisitSyncEntry + // montado à mão: é isto que pega uma fila sem sincronizador ligado, um + // patientId errado ou um localId malformado — tudo o que o app faz sozinho. + // O único pedaço fora do caminho real é o store: SQLCipher não abre na VM. + final queue = OfflineVisitQueue( + store: InMemoryVisitStore(), + synchronizer: BackendVisitSynchronizer(backend: backend), + ); + await queue.add(OfflineVisitRecord( + patientId: alert.patientId, + risk: alert.riskLevel, + status: 'PENDENTE', + outcome: 'realizada', + )); + + final synced = await queue.sync(); + stdout.writeln(' visita sincronizada ${synced.kind.name} ' + 'processadas=${synced.processed} pendentes=${queue.pendingCount}'); + if (synced.kind != SyncOutcomeKind.synced || queue.pendingCount != 0) { + stderr.writeln(' ERRO: a visita não sincronizou (${synced.message}).'); exitCode = 1; } diff --git a/apps/patient/lib/core/database/encrypted_database.dart b/apps/patient/lib/core/database/encrypted_database.dart deleted file mode 100644 index d86c2ce..0000000 --- a/apps/patient/lib/core/database/encrypted_database.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'dart:io'; - -import 'package:sqflite_common_ffi/sqflite_ffi.dart'; -import 'package:sqflite_sqlcipher/sqflite.dart' as sqlcipher; - -class EncryptedLocalDatabase { - EncryptedLocalDatabase._(); - - static Future open({ - required String databaseName, - required String passphrase, - }) async { - if (!Platform.isAndroid && !Platform.isIOS) { - sqfliteFfiInit(); - } - - final databasePath = Platform.isAndroid || Platform.isIOS - ? await sqlcipher.getDatabasesPath() - : await databaseFactoryFfi.getDatabasesPath(); - final dbPath = '$databasePath/$databaseName'; - - if (Platform.isAndroid || Platform.isIOS) { - return sqlcipher.openDatabase( - dbPath, - password: passphrase, - version: 1, - onCreate: (db, version) async { - await db.execute('CREATE TABLE IF NOT EXISTS local_queue (id TEXT PRIMARY KEY)'); - }, - ); - } - - return databaseFactoryFfi.openDatabase( - dbPath, - options: OpenDatabaseOptions( - version: 1, - onCreate: (db, version) async { - await db.execute('CREATE TABLE IF NOT EXISTS local_queue (id TEXT PRIMARY KEY)'); - }, - ), - ); - } -} diff --git a/apps/patient/test/encrypted_database_test.dart b/apps/patient/test/encrypted_database_test.dart deleted file mode 100644 index 57a1715..0000000 --- a/apps/patient/test/encrypted_database_test.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:sinalacs_patient/core/database/encrypted_database.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - test('deve abrir banco criptografado com senha configurada', () async { - final db = await EncryptedLocalDatabase.open( - databaseName: 'sinalacs_patient_test.db', - passphrase: 'test-passphrase', - ); - - expect(db.isOpen, isTrue); - await db.execute('CREATE TABLE IF NOT EXISTS test_table (id INTEGER PRIMARY KEY)'); - await db.close(); - }); -} diff --git a/spec/lgpd_design.md b/spec/lgpd_design.md index 81aa7a8..b8c812d 100644 --- a/spec/lgpd_design.md +++ b/spec/lgpd_design.md @@ -431,6 +431,17 @@ A estratégia de conformidade adota os princípios de **Privacy by Design** e ** | **Implementação** | TLS para todas as comunicações (Flutter ↔ backend), AES-256 para dados locais no `sqflite`, criptografia de campos sensíveis (CPF, condições crônicas) no banco central PostgreSQL. | | **Riscos Mitigados** | Interceptação de dados (MITM), violação de dados por acesso físico ao dispositivo, vazamento em caso de comprometimento do banco de dados. | +#### Decisão de custódia da chave (app do ACS) + +| Propriedade | Descrição | +|-------------|-----------| +| **Decisão** | A chave do SQLCipher é aleatória (256 bits) e fica no Android Keystore / iOS Keychain, via `flutter_secure_storage`. Não é derivada de PIN nem de biometria. | +| **Contexto** | `spec/PRD_system.md` (4.2.3) prescreve "chave derivada do PIN/Biometria (PBKDF2)"; `spec/test_plan.md` (item 1 da matriz de risco) fala em "chave gerada pelo TEE do hardware". Adotamos a segunda leitura. | +| **Motivo** | Não existe nenhum fluxo de PIN nos apps — a autenticação é `auth.developmentLogin`, sem credencial. Derivar de PIN exigiria projetar definição, desbloqueio, política de tentativas e recuperação; e um PIN esquecido significaria perder visitas ainda não sincronizadas. O keystore protege contra o risco que esta seção nomeia (acesso físico ao dispositivo) sem inventar produto. | +| **Reversibilidade** | A interface `DatabaseKeyStore` aceita um segundo fator depois, envelopando a chave, **sem migrar dados**. O caminho do PRD segue aberto. | +| **Limite conhecido** | Num aparelho comprometido (root) com o usuário autenticado, a chave é alcançável. Proteger contra isso exigiria o fator de posse do PIN. | +| **Verificação** | `apps/acs/integration_test/encrypted_storage_test.dart` lê o arquivo do banco e afirma que ele não contém o conteúdo em texto plano. Roda em dispositivo — no CI (Linux) o caminho é o FFI, que não criptografa, e por isso a abertura fora de Android/iOS lança por padrão. | + ### 5.2 Controle de Acesso por Perfil (RBAC) | Propriedade | Descrição | @@ -471,6 +482,17 @@ A estratégia de conformidade adota os princípios de **Privacy by Design** e ** | **Implementação** | Tabela com política de retenção por categoria de dado: alertas (2 anos), visitas (5 anos conforme legislação), logs de acesso (1 ano), dados de consentimento (indeterminado). Processo automatizado de expurgo com anonimização. | | **Riscos Mitigados** | Acumulação excessiva de dados, violação do princípio da necessidade, armazenamento desnecessário de dados sensíveis. | +#### Retenção no aparelho do ACS (implementada) + +| Propriedade | Descrição | +|-------------|-----------| +| **Decisão** | O banco local guarda **apenas** visitas ainda não sincronizadas. `SqlCipherVisitStore.save()` reescreve a tabela inteira a partir da lista de pendentes, então a visita confirmada pelo servidor deixa o dispositivo na gravação seguinte. | +| **Estado** | Passou a valer de fato quando o `BackendVisitSynchronizer` foi ligado em produção. Antes, o app montava a fila sem sincronizador: `sync()` devolvia erro, nada era confirmado e nada era apagado — a retenção estava implementada e testada, mas nunca disparava. | +| **O que é gravado** | `patient_id` (UUID), risco, status, desfecho, data e versão. **Não** é gravado nome, rótulo legível nem endereço. As observações de campo digitadas na tela ainda não são persistidas nem enviadas. | +| **Por que o identificador, e não o rótulo** | A tela antes gravava `'Paciente ' + 8 dos 32 dígitos hex` e descartava o UUID. Era pior nos dois sentidos: texto legível sobre a pessoa no disco, e um identificador irrecuperável — `visits.sync` só aceita UUID, então a visita nunca poderia sair do aparelho e a retenção nunca a alcançaria. Guardar o identificador e montar o rótulo na tela é o desenho de minimização correto (LGPD-RF01). | +| **Consequência de produto** | Sem alerta vinculado não há paciente, logo a tela de visita não grava. Um seletor de pacientes da microárea exigiria um endpoint de listagem que não existe. | +| **Verificação** | `apps/acs/integration_test/encrypted_storage_test.dart` afirma que nem o conteúdo da visita nem o `patient_id` aparecem legíveis no arquivo do banco; `apps/acs/integration_test/red_alert_cycle_test.dart` ("a visita confirmada SAI do disco criptografado") prova a retenção contra o servidor real, em dispositivo. | + ### 5.7 Anonimização/Pseudonimização | Propriedade | Descrição | From 601d5f4519da4ff1d80679fae75c6757724c3ff7 Mon Sep 17 00:00:00 2001 From: Herbert Rocha Date: Sat, 12 Sep 2026 16:04:22 -0400 Subject: [PATCH 04/43] fix(acs): separa os avisos de infraestrutura e faz o app conectar ao broker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .env.example | 31 ++-- CLAUDE.md | 8 +- CONTRIBUTING.md | 6 +- PROGRESS.md | 36 ++++ README.md | 33 ++-- .../red_alert_cycle_test.dart | 21 ++- apps/acs/lib/app/app.dart | 156 +++++++++++++++--- apps/acs/lib/core/network/backend_config.dart | 34 ++-- apps/acs/lib/core/services/alert_feed.dart | 96 ++++++++++- .../lib/core/services/mqtt_secure_client.dart | 46 +++++- apps/acs/test/alert_feed_test.dart | 40 +++++ apps/acs/test/encrypted_database_test.dart | 8 +- apps/acs/test/login_flow_test.dart | 119 +++++++++++++ apps/acs/test/mqtt_secure_client_test.dart | 29 ++++ apps/acs/test/support/fakes.dart | 36 +++- apps/acs/tool/live_check.dart | 23 ++- docs/telas-acs.md | 2 +- scripts/dev/run_acs.sh | 103 ++++++++++++ 18 files changed, 735 insertions(+), 92 deletions(-) create mode 100644 apps/acs/test/alert_feed_test.dart create mode 100755 scripts/dev/run_acs.sh diff --git a/.env.example b/.env.example index a664993..33f7cdb 100644 --- a/.env.example +++ b/.env.example @@ -82,20 +82,29 @@ ENABLE_DEV_LOGIN=true # Apps Flutter · NÃO são lidos do .env # # Os apps resolvem estes valores em tempo de COMPILAÇÃO, via --dart-define, com -# defaults em apps/*/lib/core/network/backend_config.dart. Ficam aqui como -# documentação, já que nenhum outro documento os cobria. +# defaults em apps/*/lib/core/network/backend_config.dart. # -# flutter run \ -# --dart-define=SINALACS_HOST=http://10.0.2.2:8080/ \ -# --dart-define=SINALACS_MQTT_HOST=10.0.2.2 \ -# --dart-define=SINALACS_MQTT_USER=acs-area-12 \ -# --dart-define=SINALACS_MQTT_PASSWORD=... +# Para o ACS, use o script — ele lê MQTT_ACS_PASSWORD daqui, copia a CA do +# broker para os assets e preenche tudo: +# +# ./scripts/dev/run_acs.sh +# ./scripts/dev/run_acs.sh --build +# +# O que ele passa: +# +# --dart-define=SINALACS_HOST=http://10.0.2.2:8080/ +# --dart-define=SINALACS_MQTT_HOST=10.0.2.2 +# --dart-define=SINALACS_MQTT_USER=acs-area-12 +# --dart-define=SINALACS_MQTT_PASSWORD= # # O default de 10.0.2.2 é o host da máquina visto de dentro do emulador Android. # +# SINALACS_MQTT_PASSWORD **não tem default**: o broker usa um segredo por +# máquina, então nenhum valor embutido acertaria — o app compilado sem ele +# avisa que falta a senha, em vez de dizer "sem conexão com a central". +# # AVISO: SINALACS_MQTT_PASSWORD vira uma CONSTANTE NO BINÁRIO — String.fromEnvironment -# é resolvido na compilação, e o valor é extraível de qualquer APK. O default de -# desenvolvimento é uma escolha consciente para manter `flutter run` sem -# argumentos; em produção a credencial do broker não pode viajar no app. A -# correção é credencial por dispositivo / mTLS, registrada como lacuna conhecida. +# é resolvido na compilação, e o valor é extraível de qualquer APK. Em produção a +# credencial do broker não pode viajar no app. A correção é credencial por +# dispositivo / mTLS, registrada como lacuna conhecida. # --------------------------------------------------------------------------- diff --git a/CLAUDE.md b/CLAUDE.md index 984824b..deb1eca 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -107,9 +107,11 @@ flutter pub get flutter analyze flutter test flutter test test/login_flow_test.dart # single test file -flutter run +flutter run # patient only flutter build apk --debug # debug APK, validated with compileSdk/targetSdk 36 ``` +For the ACS app, run `./scripts/dev/run_acs.sh` (or `--build` for the APK) instead of bare `flutter run`: `SINALACS_MQTT_PASSWORD` is a compile-time constant with **no default**, and the broker's password is generated per machine by `bootstrap_env.sh`, so a bare `flutter run` produces an app that never receives an alert. The script reads `.env`, runs `sync_dev_ca.sh` (the CA is a gitignored asset the build requires) and fills in all four `--dart-define`s. + `apps/admin` exists only as a pubspec skeleton (backoffice), no implementation yet. ### Validating the real connection to the backend @@ -124,7 +126,7 @@ flutter build apk --debug # debug APK, validated with compileSdk/targetSdk 36 The script brings up Docker Compose, waits for the healthcheck, applies the seed, runs `scripts/dev/sync_dev_ca.sh` (copies the broker CA into `apps/acs/assets/certs/`, which is gitignored and regenerated), and then runs each app's `tool/live_check.dart`. Those scripts run on the plain Dart VM — no emulator — using the apps' own network code: the patient one covers health/login/triage/alert/idempotency, and the ACS one covers the full cycle including the MQTT/TLS subscription and `visits.sync`. -`integration_test/` in each app holds the on-device version, excluded from `flutter test` by construction. Run it against an already-running emulator with `--dart-define=SINALACS_HOST=http://10.0.2.2:8080/` (and `SINALACS_MQTT_HOST=10.0.2.2` for the ACS). CI's four jobs are unchanged and never need a backend. +`integration_test/` in each app holds the on-device version, excluded from `flutter test` by construction. Run it against an already-running emulator with `--dart-define=SINALACS_HOST=http://10.0.2.2:8080/` (and, for the ACS, `SINALACS_MQTT_HOST=10.0.2.2` plus `SINALACS_MQTT_PASSWORD="$MQTT_ACS_PASSWORD"` — without the password the suite fails in `setUpAll` with the command you need). `scripts/qa/e2e.sh --emulator` already passes all three. CI's four jobs are unchanged and never need a backend. Debug builds of both apps carry `android/app/src/debug/res/xml/network_security_config.xml`, which permits cleartext only to `10.0.2.2` and loopback; release manifests are untouched. @@ -153,7 +155,7 @@ Both follow the same skeleton: `lib/main.dart` → `lib/app/app.dart` (+ `*_them - `database/encrypted_database.dart` (ACS only) — local persistence via `sqflite_sqlcipher`. **INV-04 of the PRD says health data is never persisted in plaintext**, so opening outside Android/iOS throws unless the caller passes `allowUnencryptedForTesting: true`. That flag is deliberately embarrassing to type: `grep` for it and you see every place that gave up the guarantee — today, only VM tests. It used to be the *default* behaviour off-mobile, which is why the old test named "deve abrir banco criptografado" proved nothing on Linux CI. - `security/database_key_store.dart` (ACS only) — the 256-bit key lives in the Android Keystore / iOS Keychain (`flutter_secure_storage`), never in code. **Not** derived from a PIN, though `spec/PRD_system.md` 4.2.3 prescribes PBKDF2-from-PIN: no PIN flow exists in the apps, and the decision plus its reversibility is recorded in `spec/lgpd_design.md` §5.1. `SqlCipherVisitStore` recovers from a lost key (reinstall, backup restore) by discarding the unreadable file instead of leaving the app permanently stuck. - **Encryption is only actually proven on a device**: `apps/acs/integration_test/encrypted_storage_test.dart` reads the raw database file and asserts it neither starts with `SQLite format 3` nor contains the visit content. `flutter test` cannot prove this — on Linux everything goes through the unencrypted FFI path, which is the trap that produced the original gap. -- ACS-only, in `apps/acs/lib/core/services/`: `mqtt_secure_client.dart` (TCP/TLS MQTT client on 8883 — the broker's only published listener; trusts the dev CA from an asset via `setTrustedCertificatesBytes`, keeps hostname verification on, uses a persistent session with a stable client id so the broker re-delivers alerts that arrived while the device was offline), `alert_feed.dart` (builds the MQTT config from the session and feeds `alert_queue.dart`), `offline_visit_queue.dart` (offline-first visit queue backed by a `VisitStore` — in production `SqlCipherVisitStore`, so visits survive closing the app; a store failure sets `persistenceFailed` and the queue keeps working in RAM rather than blocking field work, with the shell surfacing a banner — pushed to `visits.sync` through `backend_visit_synchronizer.dart`, wired in production by `visit_queue_factory.dart`; a conflict returns to the queue instead of being discarded, a per-visit `error` also stays pending but is reported with the server's message, and a network failure keeps the batch pending). An `OfflineVisitRecord` carries the patient's **UUID**, never a display label: the screen builds `Paciente <8 hex>` at render time, `visits.sync` rejects anything that is not a UUID, and the local schema (`offline_visits`, v2) has `patient_id`. Because of that, the "Visita" tab cannot save without a selected alert — there is no patient endpoint to pick from. `EncryptedLocalDatabase._upgrade` drops and recreates the table on v1 → v2; **once the app has a real release, that migration must preserve rows instead**. Sync is triggered by hand from the visit screen ("Sincronizar agora" plus pending/conflict counters), not automatically, `network_chaos_simulator.dart` (injects latency/jitter/packet loss/partition for testing offline resilience — see `apps/acs/test/network_chaos_test.dart`). +- ACS-only, in `apps/acs/lib/core/services/`: `mqtt_secure_client.dart` (TCP/TLS MQTT client on 8883 — the broker's only published listener; trusts the dev CA from an asset via `setTrustedCertificatesBytes`, keeps hostname verification on, uses a persistent session with a stable client id so the broker re-delivers alerts that arrived while the device was offline), `alert_feed.dart` (builds the MQTT config from the session and feeds `alert_queue.dart`), `offline_visit_queue.dart` (offline-first visit queue backed by a `VisitStore` — in production `SqlCipherVisitStore`, so visits survive closing the app; a store failure sets `persistenceFailed` and the queue keeps working in RAM rather than blocking field work, surfaced as its **own** banner (`Key('storage_error')`) alongside — never instead of — the broker one, plus an inline warning on the visit screen; the two used to share one slot through a `??`, so a field outage hid the fact that visits were not being saved — pushed to `visits.sync` through `backend_visit_synchronizer.dart`, wired in production by `visit_queue_factory.dart`; a conflict returns to the queue instead of being discarded, a per-visit `error` also stays pending but is reported with the server's message, and a network failure keeps the batch pending). An `OfflineVisitRecord` carries the patient's **UUID**, never a display label: the screen builds `Paciente <8 hex>` at render time, `visits.sync` rejects anything that is not a UUID, and the local schema (`offline_visits`, v2) has `patient_id`. Because of that, the "Visita" tab cannot save without a selected alert — there is no patient endpoint to pick from. `EncryptedLocalDatabase._upgrade` drops and recreates the table on v1 → v2; **once the app has a real release, that migration must preserve rows instead**. Sync is triggered by hand from the visit screen ("Sincronizar agora" plus pending/conflict counters), not automatically, `network_chaos_simulator.dart` (injects latency/jitter/packet loss/partition for testing offline resilience — see `apps/acs/test/network_chaos_test.dart`). Topic namespace must agree in three places: the server's `AlertDelivery.topicPrefix`, the broker ACL (`infra/docker/mosquitto/aclfile`), and `alertTopicFor()` in `mqtt_secure_client.dart` — all `sinalacs/v1/microareas//alerts`, where `` is the seed UUID, not `area-12`. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 36dbcd1..0925c57 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -115,9 +115,13 @@ flutter test Para executar um aplicativo em dispositivo ou emulador: ```bash -flutter run +./scripts/dev/run_acs.sh # ACS: preenche os dart-defines a partir do .env +cd apps/patient && flutter run ``` +O ACS precisa do script: a senha do broker é constante de compilação, sem valor +padrão, e é gerada por máquina. + ## Alterações no backend O backend é um workspace Serverpod. Mantenha lógica de negócio em `application/` diff --git a/PROGRESS.md b/PROGRESS.md index c050585..853857a 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -211,6 +211,42 @@ devolvia `synced` quando o servidor recusava uma visita (o status `error` caía ramo `default`), o que a prendia na fila em silêncio; e `visits.sync` reportava "este alerta não pertence à sua microárea" para erro de sessão. +#### O app passou a dizer o que está errado + +Dois defeitos vizinhos, com a mesma forma: o app sabia e não contava. + +O painel tinha **um slot de banner para dois estados** (`_feedError ?? _storageError`). +Em campo o broker e o armazenamento caem juntos, e o `??` sempre mostrava o do +broker: o ACS via "sem conexão com a central" e nunca descobria que as visitas +do dia não estavam sendo salvas. O subtítulo era fixo, então quando o banner +exibido era o de disco ele ainda afirmava algo sobre alertas. E o aviso de +persistência era calculado uma vez, no `initState` — falhas posteriores de +gravação ficavam invisíveis. Agora são dois banners independentes, cada um com +seu texto, e o de persistência é lido do estado corrente da fila a cada build. +A tela de visita ganhou o mesmo aviso inline: é onde a pessoa acabou de gravar. + +Os avisos de infraestrutura passaram a usar o azul de destaque. `docs/telas-acs.md` +reserva a cor para a gravidade clínica, e um card vermelho de falha técnica +competia com o alerta vermelho de um paciente na mesma lista. + +O segundo defeito: **`SINALACS_MQTT_PASSWORD` tinha um default que nunca +funcionou**. O broker cria `acs-area-12` com `MQTT_ACS_PASSWORD`, segredo +aleatório por máquina, então nenhum valor embutido no código poderia acertá-lo — +e toda a documentação mandava rodar `flutter run` sem `--dart-define` nenhum. O +resultado era um app que nunca recebia alerta e dizia apenas "sem conexão". + +- O default saiu. Vazio virou estado detectável, e a tela diz que o aplicativo + foi compilado sem a senha. +- `scripts/dev/run_acs.sh` lê o `.env`, roda o `sync_dev_ca.sh` (a CA é asset + gitignored que o build exige) e preenche os quatro dart-defines. +- As falhas do feed viraram `AlertFeedFailure` classificada. Senha ausente, CA + ausente, credencial recusada e broker inalcançável eram a mesma frase; o + `mqtt_client` já trazia o motivo no CONNACK e o código o descartava, junto com + o próprio erro, que agora vai para `dart:developer`. + +Verificado no emulador, os quatro caminhos: compilado sem senha, compilado pelo +script, senha errada e broker parado — cada um com sua mensagem. + ### M2.5 - Testes de Caos Foi adicionada a simulação de degradação de rede em [apps/acs/lib/core/services/network_chaos_simulator.dart](apps/acs/lib/core/services/network_chaos_simulator.dart): diff --git a/README.md b/README.md index 4fbf9bb..aec7b88 100644 --- a/README.md +++ b/README.md @@ -76,31 +76,42 @@ Para encerrar a stack: docker compose down ``` -O Compose atual usa credenciais de desenvolvimento declaradas no -[docker-compose.yml](docker-compose.yml). Não reutilize essas credenciais nem -habilite o dashboard inseguro do Traefik em ambientes públicos. +O Compose lê todas as credenciais do `.env` gerado por +[scripts/dev/bootstrap_env.sh](scripts/dev/bootstrap_env.sh) — cada máquina tem +as suas. Não reutilize credenciais de desenvolvimento nem habilite o dashboard +inseguro do Traefik em ambientes públicos. ### Aplicativo ACS ```bash -cd apps/acs -flutter pub get -flutter run +cd apps/acs && flutter pub get && cd - +./scripts/dev/run_acs.sh ``` -Para selecionar explicitamente um emulador Android disponível: +Use o script, não `flutter run` direto. A senha do broker é resolvida em tempo +de compilação e não tem valor padrão: ela é gerada por máquina pelo +`bootstrap_env.sh`, então um `flutter run` sem `--dart-define` produz um app que +nunca recebe alerta. O script lê o `.env`, copia a CA do broker para os assets e +preenche os quatro `--dart-define`. + +Para escolher o dispositivo, ou gerar o APK: ```bash flutter devices -flutter run -d +./scripts/dev/run_acs.sh -d +./scripts/dev/run_acs.sh --build ``` ### Aplicativo do paciente +O paciente não usa MQTT: o default de `SINALACS_HOST` já serve no emulador. + ```bash cd apps/patient flutter pub get flutter run +# em aparelho físico, apontando para a máquina da stack: +flutter run --dart-define=SINALACS_HOST=http://:8080/ ``` ## Build @@ -110,10 +121,8 @@ flutter run O app ACS foi validado com `compileSdk` e `targetSdk` 36. Para gerar o APK: ```bash -cd apps/acs -flutter clean -flutter pub get -flutter build apk --debug +cd apps/acs && flutter clean && flutter pub get && cd - +./scripts/dev/run_acs.sh --build ``` O artefato é criado em: diff --git a/apps/acs/integration_test/red_alert_cycle_test.dart b/apps/acs/integration_test/red_alert_cycle_test.dart index 4e3db43..5f91d21 100644 --- a/apps/acs/integration_test/red_alert_cycle_test.dart +++ b/apps/acs/integration_test/red_alert_cycle_test.dart @@ -8,9 +8,15 @@ /// · `./scripts/dev/sync_dev_ca.sh` — a CA do broker é asset do app e é /// regerada, não versionada. /// +/// O caminho pronto é `./scripts/qa/e2e.sh --emulator`, que sobe a stack e +/// preenche os dart-defines a partir do `.env`. À mão: +/// /// flutter test integration_test \ /// --dart-define=SINALACS_HOST=http://10.0.2.2:8080/ \ -/// --dart-define=SINALACS_MQTT_HOST=10.0.2.2 +/// --dart-define=SINALACS_MQTT_HOST=10.0.2.2 \ +/// --dart-define=SINALACS_MQTT_PASSWORD="$MQTT_ACS_PASSWORD" +/// +/// A senha é obrigatória: ela não tem default no `BackendConfig`. /// /// PRIVACIDADE: só os UUIDs sintéticos do seed. library; @@ -22,6 +28,7 @@ import 'package:integration_test/integration_test.dart'; import 'package:sinalacs_acs/core/database/encrypted_database.dart'; import 'package:sinalacs_acs/core/database/sqlcipher_visit_store.dart'; import 'package:sinalacs_acs/core/network/backend_client.dart'; +import 'package:sinalacs_acs/core/network/backend_config.dart'; import 'package:sinalacs_acs/core/security/database_key_store.dart'; import 'package:sinalacs_acs/core/services/alert_feed.dart'; import 'package:sinalacs_acs/core/services/alert_queue.dart'; @@ -36,6 +43,18 @@ const otherMicroAreaId = '00000000-0000-4000-8000-000000000099'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + setUpAll(() { + // Sem a senha, a assinatura do tópico falha depois do timeout do broker e + // o motivo real — o comando de compilação — não aparece em lugar nenhum. + if (BackendConfig.mqttPasswordMissing) { + fail( + 'Compile com --dart-define=SINALACS_MQTT_PASSWORD (o valor está em ' + 'MQTT_ACS_PASSWORD no .env). O caminho pronto é ' + './scripts/qa/e2e.sh --emulator.', + ); + } + }); + late BackendClient backend; late api.Client patientClient; diff --git a/apps/acs/lib/app/app.dart b/apps/acs/lib/app/app.dart index 93f37b7..06797f0 100644 --- a/apps/acs/lib/app/app.dart +++ b/apps/acs/lib/app/app.dart @@ -1,3 +1,5 @@ +import 'dart:developer' as developer; + import 'package:flutter/material.dart'; import 'package:sinalacs_acs/app/acs_theme.dart'; import 'package:sinalacs_acs/core/network/backend_client.dart'; @@ -148,6 +150,13 @@ class _LoginScreenState extends State { enum AcsDestination { area, queue, map, visit, escalation, geofencing, notices } +/// Aviso de infraestrutura: o que quebrou e a consequência prática. +/// +/// O subtítulo era fixo no [DashboardScreen] e passou a mentir quando o banner +/// exibido era o de armazenamento — "Novos alertas podem não estar chegando" +/// sobre uma falha de disco. +typedef InfraNotice = ({String title, String detail}); + class AcsHomeShell extends StatefulWidget { const AcsHomeShell({ required this.microAreaId, @@ -178,8 +187,7 @@ class _AcsHomeShellState extends State { (widget.feedBuilder ?? (queue) => MqttAlertFeed(queue: queue))(_queue); bool _brokerConnected = false; - String? _feedError; - String? _storageError; + InfraNotice? _feedError; PrioritizedAlert? _selected; @override @@ -201,13 +209,31 @@ class _AcsHomeShellState extends State { /// *persistir* em texto plano. Future _restoreVisits() async { await widget.visitQueue.restore(); - if (!mounted) return; - setState(() => _storageError = widget.visitQueue.persistenceFailed - ? 'As visitas não estão sendo salvas neste aparelho. ' - 'Sincronize antes de fechar o aplicativo.' - : null); + // `setState` sem estado próprio: o que mudou está DENTRO da fila, e o aviso + // é lido de lá a cada build. Guardar a resposta num campo era o defeito — + // o sinalizador congelava aqui e toda falha posterior de `add()`/`sync()` + // ficava invisível. + if (mounted) setState(() {}); } + /// Aviso de persistência, lido do estado corrente da fila. + /// + /// [OfflineVisitQueue] continua sendo um serviço puro (`import 'dart:math'` + /// apenas): transformá-la em `ChangeNotifier` traria `package:flutter` para + /// dentro dela, o que este repositório já recusou. Ler no `build` basta + /// **enquanto** toda escrita na fila partir da tela de Visita — voltar para a + /// Fila passa obrigatoriamente pelo `setState` da `NavigationBar`. Se um dia + /// existir sincronização automática em segundo plano, o próximo passo é um + /// `void Function(bool)? onPersistenceChanged` na fila, no molde de + /// `MqttAlertFeed.onConnectionChanged` — não um `ChangeNotifier`. + InfraNotice? get _storageNotice => widget.visitQueue.persistenceFailed + ? ( + title: 'As visitas não estão sendo salvas neste aparelho.', + detail: 'O que está na fila só existe na memória. ' + 'Sincronize antes de fechar o aplicativo.', + ) + : null; + /// Assina o tópico da microárea da sessão. /// /// Falhar aqui não derruba o painel, mas **precisa ficar visível**: um ACS que @@ -220,11 +246,34 @@ class _AcsHomeShellState extends State { _brokerConnected = _feed.isConnected; _feedError = null; }); - } catch (error) { + } on AlertFeedFailure catch (failure, stackTrace) { + developer.log( + 'não foi possível assinar o tópico de alertas', + name: 'sinalacs.acs.alert_feed', + error: failure.cause ?? failure, + stackTrace: stackTrace, + ); + if (!mounted) return; + setState(() { + _brokerConnected = false; + _feedError = (title: failure.title, detail: failure.detail); + }); + } catch (error, stackTrace) { + // Defesa: hoje só um AlertFeed de teste chega aqui. Logar em vez de + // descartar — antes o erro era engolido e a tela dizia sempre o mesmo. + developer.log( + 'falha não classificada ao assinar o tópico de alertas', + name: 'sinalacs.acs.alert_feed', + error: error, + stackTrace: stackTrace, + ); if (!mounted) return; setState(() { _brokerConnected = false; - _feedError = 'Sem conexão com a central de alertas.'; + _feedError = ( + title: 'Sem conexão com a central de alertas.', + detail: 'Novos alertas podem não estar chegando.', + ); }); } } @@ -257,7 +306,8 @@ class _AcsHomeShellState extends State { AcsDestination.area => const TerritorializationScreen(), AcsDestination.queue => DashboardScreen( queue: _queue, - feedError: _feedError ?? _storageError, + feedError: _feedError, + storageError: _storageNotice, onAcknowledge: _acknowledge, onEscalate: (alert) => setState(() { _selected = alert; destination = AcsDestination.escalation; }), onVisit: (alert) => setState(() { _selected = alert; destination = AcsDestination.visit; }), @@ -296,13 +346,21 @@ class DashboardScreen extends StatelessWidget { required this.queue, super.key, this.feedError, + this.storageError, this.onAcknowledge, this.onEscalate, this.onVisit, }); final AlertQueue queue; - final String? feedError; + final InfraNotice? feedError; + + /// Armazenamento local recusando gravação. + /// + /// Coexiste com [feedError] em vez de disputar o mesmo espaço: em campo os + /// dois caem juntos, e um `??` entre eles escondia justamente o que ninguém + /// descobre sozinho — que as visitas do dia não estão sendo salvas. + final InfraNotice? storageError; final void Function(PrioritizedAlert alert)? onAcknowledge; final void Function(PrioritizedAlert alert)? onEscalate; final void Function(PrioritizedAlert alert)? onVisit; @@ -316,17 +374,16 @@ class DashboardScreen extends StatelessWidget { const Text('Painel de Priorização', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), const SizedBox(height: 4), const Text('Fila ordenada por risco clínico'), - if (feedError != null) Padding( - padding: const EdgeInsets.only(top: 12), - child: Card( - color: AcsColors.red.withValues(alpha: 0.15), - child: ListTile( - key: const Key('feed_error'), - leading: const Icon(Icons.cloud_off_outlined, color: AcsColors.red), - title: Text(feedError!, style: const TextStyle(fontWeight: FontWeight.bold)), - subtitle: const Text('Novos alertas podem não estar chegando.'), - ), - ), + // Alertas primeiro: é o aviso com risco de vida atrás. + if (feedError != null) _InfraBanner( + key: const Key('feed_error'), + icon: Icons.cloud_off_outlined, + notice: feedError!, + ), + if (storageError != null) _InfraBanner( + key: const Key('storage_error'), + icon: Icons.sd_card_alert_outlined, + notice: storageError!, ), const SizedBox(height: 16), if (alerts.isEmpty) const Card( @@ -353,6 +410,34 @@ class DashboardScreen extends StatelessWidget { ); } +/// Aviso de infraestrutura no painel. +/// +/// Azul, e não vermelho: `docs/telas-acs.md` reserva a cor para a gravidade +/// clínica, e um card vermelho de "sem conexão" competia visualmente com o +/// alerta vermelho de um paciente logo abaixo, na mesma lista. +/// +/// A `Key` vem de fora e é o único identificador do aviso — pô-la também no +/// `ListTile` faria `find.byKey` achar dois widgets. +class _InfraBanner extends StatelessWidget { + const _InfraBanner({required this.icon, required this.notice, super.key}); + + final IconData icon; + final InfraNotice notice; + + @override + Widget build(BuildContext context) => Padding( + padding: const EdgeInsets.only(top: 12), + child: Card( + color: AcsColors.accent.withValues(alpha: 0.15), + child: ListTile( + leading: Icon(icon, color: AcsColors.accent), + title: Text(notice.title, style: const TextStyle(fontWeight: FontWeight.bold)), + subtitle: Text(notice.detail), + ), + ), + ); +} + class _AlertCard extends StatelessWidget { const _AlertCard({required this.alert, this.onAcknowledge, this.onEscalate, this.onVisit}); @@ -496,7 +581,9 @@ class _VisitRegistrationScreenState extends State { SyncOutcomeKind.conflict => '${result.processed} visita(s) em conflito. Continuam na fila para resolução.', SyncOutcomeKind.empty => 'Não há visitas pendentes.', SyncOutcomeKind.error => result.message ?? - 'Não foi possível sincronizar. As visitas continuam salvas no aparelho.', + (widget.queue.persistenceFailed + ? 'Não foi possível sincronizar. As visitas estão apenas na memória deste aparelho.' + : 'Não foi possível sincronizar. As visitas continuam salvas no aparelho.'), }); } @@ -512,7 +599,19 @@ class _VisitRegistrationScreenState extends State { child: Text( key: Key('visit_needs_alert'), 'Selecione um alerta na fila para registrar a visita.', - style: TextStyle(color: AcsColors.yellow), + style: TextStyle(color: AcsColors.accent), + ), + ), + // Aqui, e não só no painel: é nesta tela que a pessoa acabou de gravar. + // Obrigá-la a voltar para a Fila para descobrir que não salvou seria o + // mesmo defeito de outra forma. + if (queue.persistenceFailed) const Padding( + padding: EdgeInsets.only(top: 8), + child: Text( + key: Key('visit_storage_error'), + 'As visitas não estão sendo salvas neste aparelho — elas só existem ' + 'na memória até sincronizar.', + style: TextStyle(color: AcsColors.accent, fontWeight: FontWeight.bold), ), ), const SizedBox(height: 16), @@ -533,13 +632,18 @@ class _VisitRegistrationScreenState extends State { const SizedBox(height: 20), FilledButton.icon(key: const Key('save_visit'), onPressed: semAlerta ? null : _save, icon: const Icon(Icons.save_outlined), label: const Text('Salvar e enfileirar sincronização')), const Divider(height: 32), - Text(key: const Key('pending_visits_count'), 'Pendentes de sincronização: ${queue.pendingCount}'), + Text( + key: const Key('pending_visits_count'), + 'Pendentes de sincronização: ${queue.pendingCount}' + '${queue.persistenceFailed ? ' (em memória)' : ''}', + ), if (queue.conflictCount > 0) Padding( padding: const EdgeInsets.only(top: 4), child: Text( key: const Key('conflict_visits_count'), 'Em conflito: ${queue.conflictCount}', - style: const TextStyle(color: AcsColors.yellow, fontWeight: FontWeight.bold), + // Conflito de sincronização é operacional, não gravidade clínica. + style: const TextStyle(color: AcsColors.accent, fontWeight: FontWeight.bold), ), ), const SizedBox(height: 12), diff --git a/apps/acs/lib/core/network/backend_config.dart b/apps/acs/lib/core/network/backend_config.dart index 1561fcb..557a7ac 100644 --- a/apps/acs/lib/core/network/backend_config.dart +++ b/apps/acs/lib/core/network/backend_config.dart @@ -6,9 +6,10 @@ /// flutter run --dart-define=SINALACS_HOST=http://localhost:8080/ \ /// --dart-define=SINALACS_MQTT_HOST=localhost /// -/// As credenciais abaixo são as do `docker-compose.yml` de desenvolvimento e -/// não valem fora dele. Em produção o ACS não deveria carregar segredo do -/// broker embutido no binário — ver a lacuna de mTLS registrada em spec/. +/// A senha do broker **não tem default**: ela é um segredo por máquina. Use +/// `scripts/dev/run_acs.sh`, que lê o `.env` e preenche os dart-defines. Em +/// produção o ACS não deveria carregar segredo do broker embutido no binário — +/// ver a lacuna de mTLS registrada em spec/. class BackendConfig { const BackendConfig._(); @@ -34,22 +35,29 @@ class BackendConfig { defaultValue: 'acs-area-12', ); - /// Senha do broker. + /// Senha do broker. **Sem valor padrão.** /// /// **`String.fromEnvironment` é resolvido em tempo de COMPILAÇÃO**: este valor - /// vira uma constante dentro do binário e é extraível de qualquer APK. O - /// default existe para que `flutter run` e scripts/qa/e2e.sh funcionem sem - /// argumentos em desenvolvimento — é uma escolha consciente, não um descuido, - /// e a senha correspondente do broker local é gerada por - /// scripts/dev/bootstrap_env.sh, não é esta. + /// vira uma constante dentro do binário e é extraível de qualquer APK. + /// + /// O default anterior (`'development-acs-password'`) nunca funcionou: o + /// broker local cria o usuário `acs-area-12` com `MQTT_ACS_PASSWORD`, um + /// segredo aleatório **por máquina** gerado por scripts/dev/bootstrap_env.sh + /// e aplicado por infra/docker/mosquitto/init.sh. Nenhum valor embutido no + /// código poderia acertá-lo, então um `flutter run` sem `--dart-define` + /// produzia um app que nunca recebia alerta e dizia apenas "sem conexão". + /// + /// Vazio é agora um estado legítimo e detectável — ver [mqttPasswordMissing]. + /// Para rodar contra a stack local, use scripts/dev/run_acs.sh. /// /// Em produção credencial de broker não pode viajar dentro do app. A correção /// é credencial por dispositivo / mTLS, lacuna registrada em spec/ — o /// mosquitto.conf atual nem sequer tem `require_certificate`. - static const String mqttPassword = String.fromEnvironment( - 'SINALACS_MQTT_PASSWORD', - defaultValue: 'development-acs-password', - ); + static const String mqttPassword = + String.fromEnvironment('SINALACS_MQTT_PASSWORD'); + + /// `true` quando o binário foi compilado sem a senha do broker. + static bool get mqttPasswordMissing => mqttPassword.isEmpty; /// CA que assina o certificado do broker local. /// diff --git a/apps/acs/lib/core/services/alert_feed.dart b/apps/acs/lib/core/services/alert_feed.dart index cd5823e..12065eb 100644 --- a/apps/acs/lib/core/services/alert_feed.dart +++ b/apps/acs/lib/core/services/alert_feed.dart @@ -1,4 +1,4 @@ -import 'package:flutter/services.dart' show rootBundle; +import 'package:flutter/services.dart' show ByteData, rootBundle; import 'package:sinalacs_acs/core/network/backend_config.dart'; import 'package:sinalacs_acs/core/services/alert_queue.dart'; import 'package:sinalacs_acs/core/services/mqtt_secure_client.dart'; @@ -16,6 +16,46 @@ abstract class AlertFeed { bool get isConnected; } +/// Por que o ACS não está recebendo alertas. +enum AlertFeedFailureKind { + /// O binário foi compilado sem `SINALACS_MQTT_PASSWORD`. + missingPassword, + + /// Falta o certificado da CA nos assets (`scripts/dev/sync_dev_ca.sh`). + missingCaAsset, + + /// O broker respondeu e recusou (credencial, identificador, indisponível). + refused, + + /// Não foi possível chegar ao broker. + unreachable, +} + +/// Falha ao assinar o tópico de alertas, já classificada. +/// +/// [title] e [detail] vão direto para o banner do painel; [cause] existe só +/// para o log e **nunca** chega à tela. Nenhum dos três carrega credencial. +/// +/// Antes tudo isto virava a mesma frase — "Sem conexão com a central de +/// alertas" —, então senha errada, certificado ausente e broker fora do ar +/// eram indistinguíveis, inclusive para quem depura. +class AlertFeedFailure implements Exception { + const AlertFeedFailure( + this.kind, { + required this.title, + required this.detail, + this.cause, + }); + + final AlertFeedFailureKind kind; + final String title; + final String detail; + final Object? cause; + + @override + String toString() => '${kind.name}: $title'; +} + class MqttAlertFeed implements AlertFeed { MqttAlertFeed({ required this.queue, @@ -36,7 +76,29 @@ class MqttAlertFeed implements AlertFeed { required String microAreaId, required String acsId, }) async { - final caCertificate = await rootBundle.load(BackendConfig.mqttCaAsset); + // Antes de abrir qualquer socket: este binário tem senha? Sem isto, um app + // compilado sem `--dart-define` gastava o timeout do broker para depois + // dizer "sem conexão", escondendo que o problema estava na compilação. + if (BackendConfig.mqttPasswordMissing) { + throw const AlertFeedFailure( + AlertFeedFailureKind.missingPassword, + title: 'Este aplicativo foi compilado sem a senha do broker de alertas.', + detail: 'Nenhum alerta será recebido até o aplicativo ser recompilado.', + ); + } + + // A CA é asset gitignored, copiado por scripts/dev/sync_dev_ca.sh. + final ByteData caCertificate; + try { + caCertificate = await rootBundle.load(BackendConfig.mqttCaAsset); + } catch (error) { + throw AlertFeedFailure( + AlertFeedFailureKind.missingCaAsset, + title: 'Falta o certificado da central neste aplicativo.', + detail: 'Sem ele a conexão segura não pode ser verificada.', + cause: error, + ); + } final client = MqttSecureClient( config: SecureMqttConfig( @@ -53,13 +115,29 @@ class MqttAlertFeed implements AlertFeed { ), ); - await client.connect( - onAlert: (alert) => queue.upsert(PrioritizedAlert.fromMqtt(alert)), - onConnectionChanged: (connected) { - _connected = connected; - onConnectionChanged?.call(connected); - }, - ); + try { + await client.connect( + onAlert: (alert) => queue.upsert(PrioritizedAlert.fromMqtt(alert)), + onConnectionChanged: (connected) { + _connected = connected; + onConnectionChanged?.call(connected); + }, + ); + } on MqttConnectionRefused catch (error) { + throw AlertFeedFailure( + AlertFeedFailureKind.refused, + title: error.message, + detail: 'Novos alertas não estão chegando.', + cause: error, + ); + } catch (error) { + throw AlertFeedFailure( + AlertFeedFailureKind.unreachable, + title: 'Sem conexão com a central de alertas.', + detail: 'Novos alertas podem não estar chegando.', + cause: error, + ); + } _client = client; _connected = client.isConnected; diff --git a/apps/acs/lib/core/services/mqtt_secure_client.dart b/apps/acs/lib/core/services/mqtt_secure_client.dart index 80bfa45..c129e8f 100644 --- a/apps/acs/lib/core/services/mqtt_secure_client.dart +++ b/apps/acs/lib/core/services/mqtt_secure_client.dart @@ -19,6 +19,42 @@ String alertTopicFor(String microAreaId) => /// Tópico onde o ACS confirma o recebimento de um alerta. String ackTopicFor(String alertId) => 'sinalacs/v1/alerts/$alertId/acks'; +/// O broker respondeu, e recusou. +/// +/// Distinguir isso de "não deu para chegar ao broker" é o que separa uma senha +/// errada de uma central fora do ar — que antes chegavam à tela como a mesma +/// frase. +class MqttConnectionRefused implements Exception { + const MqttConnectionRefused(this.message); + + /// Já em português e pronta para a tela. **Nunca** contém credencial. + final String message; + + @override + String toString() => message; +} + +/// Traduz o código de recusa do CONNACK. +/// +/// O `mqtt_client` grava o motivo em `connectionStatus.returnCode` antes de +/// derrubar a conexão; o código antigo lançava um `StateError` genérico e +/// jogava fora a única informação que separava os casos. Devolve `null` quando +/// não houve recusa — aí a falha foi de transporte, não de autorização. +/// +/// Nunca cita usuário nem senha: a mensagem vai para a tela do ACS. +String? mqttRefusalReason(MqttConnectReturnCode? code) => switch (code) { + MqttConnectReturnCode.badUsernameOrPassword || + MqttConnectReturnCode.notAuthorized => + 'A central recusou as credenciais deste aplicativo.', + MqttConnectReturnCode.identifierRejected => + 'A central recusou o identificador deste aplicativo.', + MqttConnectReturnCode.brokerUnavailable => + 'A central de alertas está indisponível.', + MqttConnectReturnCode.unacceptedProtocolVersion => + 'A central não aceita a versão de protocolo deste aplicativo.', + _ => null, + }; + class SecureMqttConfig { const SecureMqttConfig({ required this.brokerHost, @@ -236,14 +272,20 @@ class MqttSecureClient { try { await client.connect(config.username, config.password); - } catch (_) { + } catch (error) { + // Lido ANTES do disconnect, que zera o connectionStatus. + final refusal = mqttRefusalReason(client.connectionStatus?.returnCode); disconnect(); + if (refusal != null) throw MqttConnectionRefused(refusal); rethrow; } if (client.connectionStatus?.state != MqttConnectionState.connected) { + final refusal = mqttRefusalReason(client.connectionStatus?.returnCode); disconnect(); - throw StateError('Não foi possível conectar ao broker MQTT.'); + throw MqttConnectionRefused( + refusal ?? 'Não foi possível conectar ao broker MQTT.', + ); } client.subscribe(config.topic, MqttQos.atLeastOnce); diff --git a/apps/acs/test/alert_feed_test.dart b/apps/acs/test/alert_feed_test.dart new file mode 100644 index 0000000..f331b06 --- /dev/null +++ b/apps/acs/test/alert_feed_test.dart @@ -0,0 +1,40 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:sinalacs_acs/core/network/backend_config.dart'; +import 'package:sinalacs_acs/core/services/alert_feed.dart'; +import 'package:sinalacs_acs/core/services/alert_queue.dart'; + +/// Estes testes rodam **sem** `--dart-define`, que é exatamente a condição do +/// app compilado à mão — o defeito que eles travam. +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('senha do broker', () { + test('não tem valor padrão embutido no binário', () { + // O default anterior ('development-acs-password') nunca bateu com o + // broker, que usa um segredo por máquina: ele só servia para transformar + // um erro de compilação em "sem conexão com a central". + expect( + BackendConfig.mqttPassword, + isEmpty, + reason: 'a suíte roda sem --dart-define; um valor aqui significa que ' + 'alguém reintroduziu o defaultValue', + ); + expect(BackendConfig.mqttPasswordMissing, isTrue); + }); + + test('o feed falha antes de abrir qualquer socket quando falta a senha', () async { + // Se chegasse a tentar conectar, este teste gastaria o timeout do broker + // em vez de responder na hora. + final feed = MqttAlertFeed(queue: AlertQueue(microAreaId: 'micro-area')); + + await expectLater( + feed.start(microAreaId: 'micro-area', acsId: 'acs'), + throwsA(isA().having( + (failure) => failure.kind, + 'kind', + AlertFeedFailureKind.missingPassword, + )), + ); + }); + }); +} diff --git a/apps/acs/test/encrypted_database_test.dart b/apps/acs/test/encrypted_database_test.dart index bfba337..6fc4f44 100644 --- a/apps/acs/test/encrypted_database_test.dart +++ b/apps/acs/test/encrypted_database_test.dart @@ -313,7 +313,7 @@ CREATE TABLE IF NOT EXISTS offline_visits ( test('armazenamento indisponível não derruba o registro em campo', () async { // Travar o registro de visita seria pior do que não persistir. A fila // segue em memória e acende o sinalizador para a UI avisar. - final queue = OfflineVisitQueue(store: _BrokenVisitStore()); + final queue = OfflineVisitQueue(store: FailingVisitStore()); await queue.restore(); await queue.add(OfflineVisitRecord( @@ -344,10 +344,4 @@ class _ConflictSynchronizer implements VisitSynchronizer { ]; } -class _BrokenVisitStore implements VisitStore { - @override - Future> load() async => throw StateError('sem banco'); - @override - Future save(List visits) async => throw StateError('sem banco'); -} diff --git a/apps/acs/test/login_flow_test.dart b/apps/acs/test/login_flow_test.dart index 2cb6268..d5d9864 100644 --- a/apps/acs/test/login_flow_test.dart +++ b/apps/acs/test/login_flow_test.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:sinalacs_acs/app/app.dart'; import 'package:sinalacs_acs/core/network/backend_client.dart'; +import 'package:sinalacs_acs/core/services/alert_feed.dart'; import 'package:sinalacs_acs/core/services/alert_queue.dart'; import 'package:sinalacs_acs/core/services/backend_visit_synchronizer.dart'; import 'package:sinalacs_acs/core/services/offline_visit_queue.dart'; @@ -432,6 +433,124 @@ void main() { }); }); + group('avisos de infraestrutura', () { + /// Leva até o painel, com o feed e a fila que o teste quiser. + Future abrirPainel( + WidgetTester tester, { + required OfflineVisitQueue visitQueue, + AlertFeed Function(AlertQueue queue)? feedBuilder, + }) async { + await tester.pumpWidget(SinalAcsApp( + backend: FakeAcsBackend(), + feedBuilder: feedBuilder ?? (queue) => FakeAlertFeed(queue), + visitQueue: visitQueue, + )); + await tester.tap(find.byKey(const Key('login_button'))); + await tester.pumpAndSettle(); + } + + testWidgets('os dois avisos coexistem quando broker e armazenamento caem juntos', (tester) async { + // Em campo os dois caem juntos, e o `??` de antes mostrava só o do + // broker: o ACS nunca descobria que as visitas do dia não eram salvas. + await abrirPainel( + tester, + visitQueue: OfflineVisitQueue(store: FailingVisitStore()), + feedBuilder: (queue) => FakeAlertFeed(queue, failOnStart: true), + ); + + expect(find.byKey(const Key('feed_error')), findsOneWidget); + expect(find.byKey(const Key('storage_error')), findsOneWidget); + // Cada um com o seu subtítulo: o texto sobre alertas era fixo e passava a + // mentir quando o aviso exibido era o de disco. + expect(find.text('Novos alertas podem não estar chegando.'), findsOneWidget); + expect( + find.textContaining('só existe na memória'), + findsOneWidget, + ); + }); + + testWidgets('o aviso de armazenamento aparece sozinho, com o feed saudável', (tester) async { + await abrirPainel( + tester, + visitQueue: OfflineVisitQueue(store: FailingVisitStore()), + ); + + expect(find.byKey(const Key('feed_error')), findsNothing); + expect(find.byKey(const Key('storage_error')), findsOneWidget); + }); + + testWidgets('o aviso de armazenamento acende depois de uma gravação que falha', (tester) async { + // O banco abre e só falha ao gravar. Um campo calculado no initState + // nunca veria isto — era o segundo defeito. + final visitQueue = OfflineVisitQueue( + store: FailingVisitStore(failOnLoad: false, failOnSave: true), + ); + late FakeAlertFeed feed; + + await abrirPainel( + tester, + visitQueue: visitQueue, + feedBuilder: (queue) => feed = FakeAlertFeed(queue), + ); + expect(find.byKey(const Key('storage_error')), findsNothing); + + feed.deliver(testAlert(alertId: 'alerta-1', riskLevel: 'yellow')); + await tester.pumpAndSettle(); + await tester.tap(find.text('Iniciar rota de visita')); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('save_visit'))); + await tester.pumpAndSettle(); + + // Na própria tela de visita, onde a pessoa acabou de gravar. + expect(find.byKey(const Key('visit_storage_error')), findsOneWidget); + expect(find.textContaining('(em memória)'), findsOneWidget); + + await tester.tap(find.text('Fila')); + await tester.pumpAndSettle(); + + expect(find.byKey(const Key('storage_error')), findsOneWidget); + }); + + testWidgets('sem senha compilada o painel diz que falta a senha, não que falta conexão', (tester) async { + await abrirPainel( + tester, + visitQueue: OfflineVisitQueue(), + feedBuilder: (queue) => FakeAlertFeed( + queue, + failure: const AlertFeedFailure( + AlertFeedFailureKind.missingPassword, + title: 'Este aplicativo foi compilado sem a senha do broker de alertas.', + detail: 'Nenhum alerta será recebido até o aplicativo ser recompilado.', + ), + ), + ); + + expect( + find.text('Este aplicativo foi compilado sem a senha do broker de alertas.'), + findsOneWidget, + ); + expect(find.text('Sem conexão com a central de alertas.'), findsNothing); + }); + + testWidgets('credencial recusada não vira "sem conexão"', (tester) async { + await abrirPainel( + tester, + visitQueue: OfflineVisitQueue(), + feedBuilder: (queue) => FakeAlertFeed( + queue, + failure: const AlertFeedFailure( + AlertFeedFailureKind.refused, + title: 'A central recusou as credenciais deste aplicativo.', + detail: 'Novos alertas não estão chegando.', + ), + ), + ); + + expect(find.text('A central recusou as credenciais deste aplicativo.'), findsOneWidget); + expect(find.text('Sem conexão com a central de alertas.'), findsNothing); + }); + }); + group('fiação de produção', () { test('a fila do app sai com o sincronizador do backend ligado', () { // A regressão que motivou tudo: o app montava a fila sem sincronizador, diff --git a/apps/acs/test/mqtt_secure_client_test.dart b/apps/acs/test/mqtt_secure_client_test.dart index 1cde4fe..5e1aa75 100644 --- a/apps/acs/test/mqtt_secure_client_test.dart +++ b/apps/acs/test/mqtt_secure_client_test.dart @@ -1,4 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; +import 'package:mqtt_client/mqtt_client.dart'; import 'package:sinalacs_acs/core/services/mqtt_secure_client.dart'; void main() { @@ -84,4 +85,32 @@ void main() { expect(alert?.microAreaId, 'area-12'); expect(ReceivedMqttAlert.tryParse('{"version":2}'), isNull); }); + + group('recusa do broker', () { + test('traduz cada código de CONNACK sem citar credencial', () { + expect( + mqttRefusalReason(MqttConnectReturnCode.badUsernameOrPassword), + 'A central recusou as credenciais deste aplicativo.', + ); + expect( + mqttRefusalReason(MqttConnectReturnCode.notAuthorized), + 'A central recusou as credenciais deste aplicativo.', + ); + expect( + mqttRefusalReason(MqttConnectReturnCode.identifierRejected), + 'A central recusou o identificador deste aplicativo.', + ); + expect( + mqttRefusalReason(MqttConnectReturnCode.brokerUnavailable), + 'A central de alertas está indisponível.', + ); + }); + + test('devolve null quando não houve recusa', () { + // Aí a falha foi de transporte, não de autorização — e a mensagem da tela + // precisa ser outra: "sem conexão" em vez de "credenciais recusadas". + expect(mqttRefusalReason(null), isNull); + expect(mqttRefusalReason(MqttConnectReturnCode.connectionAccepted), isNull); + }); + }); } diff --git a/apps/acs/test/support/fakes.dart b/apps/acs/test/support/fakes.dart index 440c7ef..e997955 100644 --- a/apps/acs/test/support/fakes.dart +++ b/apps/acs/test/support/fakes.dart @@ -102,11 +102,17 @@ class FakeAcsBackend implements AcsBackend { /// Expõe a [AlertQueue] para que o teste empurre alertas como se tivessem /// chegado pelo broker, sem precisar de rede nem de emulador. class FakeAlertFeed implements AlertFeed { - FakeAlertFeed(this.queue, {this.failOnStart = false}); + FakeAlertFeed(this.queue, {this.failOnStart = false, this.failure}); final AlertQueue queue; final bool failOnStart; + /// Falha já classificada, para exercitar cada banner do painel. + /// + /// `failOnStart` continua lançando um erro genérico de propósito: é o ramo de + /// defesa do shell, o que nenhum `AlertFeedFailure` cobre. + final AlertFeedFailure? failure; + bool started = false; bool stopped = false; String? startedTopicMicroArea; @@ -116,6 +122,8 @@ class FakeAlertFeed implements AlertFeed { @override Future start({required String microAreaId, required String acsId}) async { + final classified = failure; + if (classified != null) throw classified; if (failOnStart) throw StateError('broker indisponível'); started = true; startedTopicMicroArea = microAreaId; @@ -131,6 +139,32 @@ class FakeAlertFeed implements AlertFeed { void deliver(PrioritizedAlert alert) => queue.upsert(alert); } +/// Armazenamento que recusa gravar, para acender `persistenceFailed`. +/// +/// Separar `load` de `save` importa: um banco que abre e depois falha ao gravar +/// é o caso em que o sinalizador precisa acender **depois** do `initState` — +/// exatamente o que um campo congelado ali não enxergava. +class FailingVisitStore implements VisitStore { + FailingVisitStore({this.failOnLoad = true, this.failOnSave = true}); + + final bool failOnLoad; + final bool failOnSave; + + List _visits = []; + + @override + Future> load() async { + if (failOnLoad) throw StateError('sem banco'); + return List.of(_visits); + } + + @override + Future save(List visits) async { + if (failOnSave) throw StateError('sem banco'); + _visits = List.of(visits); + } +} + /// Sincronizador que devolve o resultado programado pelo teste. class FakeVisitSynchronizer implements VisitSynchronizer { FakeVisitSynchronizer({this.statusFor, this.messageFor, this.throwOnPush = false}); diff --git a/apps/acs/tool/live_check.dart b/apps/acs/tool/live_check.dart index dc543b8..2f3d8e4 100644 --- a/apps/acs/tool/live_check.dart +++ b/apps/acs/tool/live_check.dart @@ -13,6 +13,8 @@ /// dart run tool/live_check.dart /// dart run tool/live_check.dart --host http://10.0.2.2:8080/ --broker 10.0.2.2 /// dart run tool/live_check.dart --mqtt-password "$MQTT_ACS_PASSWORD" +/// +/// `--mqtt-password` é obrigatório: a senha do broker não tem default. library; import 'dart:async'; @@ -35,6 +37,18 @@ Future main(List args) async { final brokerHost = _arg(args, 'broker', 'localhost'); final caPath = _arg(args, 'ca', '../../infra/docker/mosquitto/runtime/certs/ca.crt'); + // A senha do broker não tem default no BackendConfig: é um segredo por + // máquina. Sem esta guarda, quem rodasse à mão ganharia um timeout mudo em + // vez de saber o que faltou. + final mqttPassword = _arg(args, 'mqtt-password', BackendConfig.mqttPassword); + if (mqttPassword.isEmpty) { + stderr.writeln('erro: a senha do broker não foi informada.'); + stderr.writeln(r'Use: dart run tool/live_check.dart --mqtt-password "$MQTT_ACS_PASSWORD"'); + stderr.writeln('O valor vem do .env; scripts/qa/e2e.sh já faz isso por você.'); + exitCode = 2; + return; + } + final backend = BackendClient(host: host); // O paciente usa o cliente gerado diretamente: aqui ele só encena o disparo // para que o ACS tenha o que receber. @@ -65,12 +79,11 @@ Future main(List args) async { port: 8883, clientId: 'sinalacs-acs-livecheck-${session.userId}', topic: alertTopicFor(microAreaId), - // Default vem do BackendConfig (não de literais soltos aqui), mas a - // senha do broker local é gerada por scripts/dev/bootstrap_env.sh e é - // diferente a cada máquina — por isso o --mqtt-password, que - // scripts/qa/e2e.sh preenche a partir do .env. + // A senha do broker local é gerada por scripts/dev/bootstrap_env.sh e + // é diferente a cada máquina — por isso o --mqtt-password obrigatório, + // que scripts/qa/e2e.sh preenche a partir do .env. username: _arg(args, 'mqtt-user', BackendConfig.mqttUsername), - password: _arg(args, 'mqtt-password', BackendConfig.mqttPassword), + password: mqttPassword, caCertificate: File(caPath).readAsBytesSync(), ), ); diff --git a/docs/telas-acs.md b/docs/telas-acs.md index 33ab384..a7f1f74 100644 --- a/docs/telas-acs.md +++ b/docs/telas-acs.md @@ -22,7 +22,7 @@ Exibe a microárea, quantidade de pacientes e status do cache. A ação de atual ![Fila ordenada por risco clínico](screenshots/acs/02-dashboard.png) -A fila apresenta os riscos vermelho, amarelo e verde com borda e ação contextual. A ordem mantém a prioridade clínica e a cor é reservada para a gravidade. +A fila apresenta os riscos vermelho, amarelo e verde com borda e ação contextual. A ordem mantém a prioridade clínica e a cor é reservada para a gravidade. Por isso avisos de infraestrutura — sem conexão com a central, visitas não sendo salvas — usam o azul de destaque, e não vermelho ou amarelo: um card vermelho de falha técnica competiria com o alerta vermelho de um paciente na mesma lista. ## Mapa de pacientes diff --git a/scripts/dev/run_acs.sh b/scripts/dev/run_acs.sh new file mode 100755 index 0000000..684f0c5 --- /dev/null +++ b/scripts/dev/run_acs.sh @@ -0,0 +1,103 @@ +#!/usr/bin/env bash +# +# Executa (ou compila) o app do ACS contra a stack local, com as credenciais +# desta máquina. +# +# ./scripts/dev/run_acs.sh # flutter run no dispositivo padrão +# ./scripts/dev/run_acs.sh -d emulator-5554 # argumentos extras vão para o flutter +# ./scripts/dev/run_acs.sh --build # flutter build apk --debug +# ./scripts/dev/run_acs.sh --host http://192.168.0.10:8080/ --mqtt-host 192.168.0.10 +# ./scripts/dev/run_acs.sh --skip-ca # não recopia a CA do broker +# +# Por que existe: +# · SINALACS_MQTT_PASSWORD é resolvido em tempo de COMPILAÇÃO e não tem +# default. O broker cria o usuário acs-area-12 com MQTT_ACS_PASSWORD, um +# segredo aleatório POR MÁQUINA gerado por scripts/dev/bootstrap_env.sh: +# nenhum valor embutido no código poderia acertá-lo. Sem este wrapper, +# `flutter run` produz um app que nunca recebe alerta nenhum. +# · A CA do broker é asset do APK (apps/acs/assets/certs/), regerada pelo +# mosquitto-init e não versionada. Sem ela o build falha antes do TLS — +# apps/acs/pubspec.yaml declara o diretório — e não haveria em quem confiar. +# +# A senha nunca é impressa. Ela viaja na linha de comando do flutter e fica +# visível em `ps` para outros usuários da MESMA máquina: aceitável em +# desenvolvimento, e é o que scripts/qa/e2e.sh já faz. Para endurecer, o +# caminho é --dart-define-from-file, que exige um arquivo temporário com trap +# de limpeza. +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +app_dir="$repo_root/apps/acs" +env_file="$repo_root/.env" + +# Defaults do emulador Android, que enxerga o host da máquina em 10.0.2.2. +host='http://10.0.2.2:8080/' +mqtt_host='10.0.2.2' +action='run' +skip_ca=0 +flutter_args=() + +while [[ $# -gt 0 ]]; do + case "$1" in + --host) host="${2:?--host exige um valor}"; shift 2 ;; + --mqtt-host) mqtt_host="${2:?--mqtt-host exige um valor}"; shift 2 ;; + --build) action='build'; shift ;; + --skip-ca) skip_ca=1; shift ;; + -h|--help) sed -n '3,25p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 0 ;; + --) shift; flutter_args+=("$@"); break ;; + *) flutter_args+=("$1"); shift ;; + esac +done + +if ! command -v flutter >/dev/null 2>&1; then + echo 'erro: flutter não encontrado no PATH.' >&2 + exit 1 +fi + +if [[ ! -f "$env_file" ]]; then + echo 'erro: .env não existe.' >&2 + echo 'Rode ./scripts/dev/bootstrap_env.sh para gerar a configuração local.' >&2 + exit 1 +fi + +# Lê só a chave necessária, em vez de `source .env`: não há motivo para empurrar +# POSTGRES_PASSWORD e JWT_SECRET para dentro do Gradle e do daemon do Flutter. +# Uma variável já exportada tem precedência — é como se testa o caminho de +# credencial recusada sem editar o .env. +mqtt_password="${MQTT_ACS_PASSWORD:-$(grep -E '^MQTT_ACS_PASSWORD=' "$env_file" | head -1 | cut -d= -f2-)}" +# Usuário criado por infra/docker/mosquitto/init.sh. +mqtt_user="${MQTT_ACS_USER:-acs-area-12}" + +if [[ -z "$mqtt_password" ]]; then + echo 'erro: MQTT_ACS_PASSWORD está vazio no .env.' >&2 + echo 'Rode ./scripts/dev/bootstrap_env.sh --force para regerar os segredos.' >&2 + exit 1 +fi + +if [[ "$skip_ca" -eq 0 ]]; then + if ! "$repo_root/scripts/dev/sync_dev_ca.sh"; then + if [[ -f "$app_dir/assets/certs/dev_ca.crt" ]]; then + echo 'aviso: a stack parece estar fora do ar; mantendo a CA já copiada.' >&2 + else + echo 'erro: sem a CA do broker o app nem compila (pubspec declara assets/certs/).' >&2 + echo 'Suba a stack com `docker compose up` e rode de novo.' >&2 + exit 1 + fi + fi +fi + +defines=( + "--dart-define=SINALACS_HOST=$host" + "--dart-define=SINALACS_MQTT_HOST=$mqtt_host" + "--dart-define=SINALACS_MQTT_USER=$mqtt_user" + "--dart-define=SINALACS_MQTT_PASSWORD=$mqtt_password" +) + +echo "ACS → $host broker → $mqtt_host:8883 (usuário $mqtt_user)" +echo 'senha do broker .... lida do .env, não exibida' + +cd "$app_dir" +if [[ "$action" == 'build' ]]; then + exec flutter build apk --debug "${defines[@]}" "${flutter_args[@]}" +fi +exec flutter run "${defines[@]}" "${flutter_args[@]}" From 42e904ba70eec30c52301f81aae6badeca3ab6cd Mon Sep 17 00:00:00 2001 From: Herbert Rocha Date: Sat, 12 Sep 2026 16:42:01 -0400 Subject: [PATCH 05/43] =?UTF-8?q?fix(acs):=20retenta=20a=20primeira=20cone?= =?UTF-8?q?x=C3=A3o=20ao=20broker=20em=20vez=20de=20desistir?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `_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 --- CLAUDE.md | 2 +- PROGRESS.md | 30 ++++ apps/acs/lib/app/app.dart | 108 +++++++++++- apps/acs/lib/core/services/alert_feed.dart | 28 ++- .../lib/core/services/mqtt_secure_client.dart | 28 ++- .../lib/core/services/reconnect_schedule.dart | 35 ++++ apps/acs/test/login_flow_test.dart | 160 +++++++++++++++++- apps/acs/test/mqtt_secure_client_test.dart | 12 ++ apps/acs/test/reconnect_schedule_test.dart | 30 ++++ apps/acs/test/support/fakes.dart | 25 ++- docs/telas-acs.md | 2 +- 11 files changed, 445 insertions(+), 15 deletions(-) create mode 100644 apps/acs/lib/core/services/reconnect_schedule.dart create mode 100644 apps/acs/test/reconnect_schedule_test.dart diff --git a/CLAUDE.md b/CLAUDE.md index deb1eca..3466ae8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -155,7 +155,7 @@ Both follow the same skeleton: `lib/main.dart` → `lib/app/app.dart` (+ `*_them - `database/encrypted_database.dart` (ACS only) — local persistence via `sqflite_sqlcipher`. **INV-04 of the PRD says health data is never persisted in plaintext**, so opening outside Android/iOS throws unless the caller passes `allowUnencryptedForTesting: true`. That flag is deliberately embarrassing to type: `grep` for it and you see every place that gave up the guarantee — today, only VM tests. It used to be the *default* behaviour off-mobile, which is why the old test named "deve abrir banco criptografado" proved nothing on Linux CI. - `security/database_key_store.dart` (ACS only) — the 256-bit key lives in the Android Keystore / iOS Keychain (`flutter_secure_storage`), never in code. **Not** derived from a PIN, though `spec/PRD_system.md` 4.2.3 prescribes PBKDF2-from-PIN: no PIN flow exists in the apps, and the decision plus its reversibility is recorded in `spec/lgpd_design.md` §5.1. `SqlCipherVisitStore` recovers from a lost key (reinstall, backup restore) by discarding the unreadable file instead of leaving the app permanently stuck. - **Encryption is only actually proven on a device**: `apps/acs/integration_test/encrypted_storage_test.dart` reads the raw database file and asserts it neither starts with `SQLite format 3` nor contains the visit content. `flutter test` cannot prove this — on Linux everything goes through the unencrypted FFI path, which is the trap that produced the original gap. -- ACS-only, in `apps/acs/lib/core/services/`: `mqtt_secure_client.dart` (TCP/TLS MQTT client on 8883 — the broker's only published listener; trusts the dev CA from an asset via `setTrustedCertificatesBytes`, keeps hostname verification on, uses a persistent session with a stable client id so the broker re-delivers alerts that arrived while the device was offline), `alert_feed.dart` (builds the MQTT config from the session and feeds `alert_queue.dart`), `offline_visit_queue.dart` (offline-first visit queue backed by a `VisitStore` — in production `SqlCipherVisitStore`, so visits survive closing the app; a store failure sets `persistenceFailed` and the queue keeps working in RAM rather than blocking field work, surfaced as its **own** banner (`Key('storage_error')`) alongside — never instead of — the broker one, plus an inline warning on the visit screen; the two used to share one slot through a `??`, so a field outage hid the fact that visits were not being saved — pushed to `visits.sync` through `backend_visit_synchronizer.dart`, wired in production by `visit_queue_factory.dart`; a conflict returns to the queue instead of being discarded, a per-visit `error` also stays pending but is reported with the server's message, and a network failure keeps the batch pending). An `OfflineVisitRecord` carries the patient's **UUID**, never a display label: the screen builds `Paciente <8 hex>` at render time, `visits.sync` rejects anything that is not a UUID, and the local schema (`offline_visits`, v2) has `patient_id`. Because of that, the "Visita" tab cannot save without a selected alert — there is no patient endpoint to pick from. `EncryptedLocalDatabase._upgrade` drops and recreates the table on v1 → v2; **once the app has a real release, that migration must preserve rows instead**. Sync is triggered by hand from the visit screen ("Sincronizar agora" plus pending/conflict counters), not automatically, `network_chaos_simulator.dart` (injects latency/jitter/packet loss/partition for testing offline resilience — see `apps/acs/test/network_chaos_test.dart`). +- ACS-only, in `apps/acs/lib/core/services/`: `mqtt_secure_client.dart` (TCP/TLS MQTT client on 8883 — the broker's only published listener; trusts the dev CA from an asset via `setTrustedCertificatesBytes`, keeps hostname verification on, uses a persistent session with a stable client id so the broker re-delivers alerts that arrived while the device was offline), `alert_feed.dart` (builds the MQTT config from the session and feeds `alert_queue.dart`; classifies failures into `AlertFeedFailure.transient` — a missing password/CA asset or a refused credential is not, an unreachable broker is), `reconnect_schedule.dart` (pure 2s→60s exponential backoff, same scale as the server's `MqttAlertDispatcher`, with no `Timer` of its own so it is testable without a fake clock — used by `AcsHomeShell` in `app.dart` to retry the *first* connection to the broker on its own when the failure is transient, with a "Tentar agora" button on the banner and an immediate retry on returning from the background; a permanent failure, such as a missing password, is not retried. `mqtt_client`'s own `autoReconnect` only takes over *after* a first successful connection — before that there is nothing to reconnect, which is why this existed as a real gap: a device that opened the app without signal used to need a restart to ever receive the alerts the broker was already holding for it with QoS 1), `offline_visit_queue.dart` (offline-first visit queue backed by a `VisitStore` — in production `SqlCipherVisitStore`, so visits survive closing the app; a store failure sets `persistenceFailed` and the queue keeps working in RAM rather than blocking field work, surfaced as its **own** banner (`Key('storage_error')`) alongside — never instead of — the broker one, plus an inline warning on the visit screen; the two used to share one slot through a `??`, so a field outage hid the fact that visits were not being saved — pushed to `visits.sync` through `backend_visit_synchronizer.dart`, wired in production by `visit_queue_factory.dart`; a conflict returns to the queue instead of being discarded, a per-visit `error` also stays pending but is reported with the server's message, and a network failure keeps the batch pending). An `OfflineVisitRecord` carries the patient's **UUID**, never a display label: the screen builds `Paciente <8 hex>` at render time, `visits.sync` rejects anything that is not a UUID, and the local schema (`offline_visits`, v2) has `patient_id`. Because of that, the "Visita" tab cannot save without a selected alert — there is no patient endpoint to pick from. `EncryptedLocalDatabase._upgrade` drops and recreates the table on v1 → v2; **once the app has a real release, that migration must preserve rows instead**. Sync is triggered by hand from the visit screen ("Sincronizar agora" plus pending/conflict counters), not automatically, `network_chaos_simulator.dart` (injects latency/jitter/packet loss/partition for testing offline resilience — see `apps/acs/test/network_chaos_test.dart`). Topic namespace must agree in three places: the server's `AlertDelivery.topicPrefix`, the broker ACL (`infra/docker/mosquitto/aclfile`), and `alertTopicFor()` in `mqtt_secure_client.dart` — all `sinalacs/v1/microareas//alerts`, where `` is the seed UUID, not `area-12`. diff --git a/PROGRESS.md b/PROGRESS.md index 853857a..82cf2c8 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -247,6 +247,36 @@ resultado era um app que nunca recebia alerta e dizia apenas "sem conexão". Verificado no emulador, os quatro caminhos: compilado sem senha, compilado pelo script, senha errada e broker parado — cada um com sua mensagem. +#### O app desistia do broker na primeira tentativa + +`_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 cobria isso: 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 o cliente não ficar +órfão tentando para sempre. + +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 cinco 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. + +`AcsHomeShell` passou a retentar sozinho quando a falha é **transitória** +(broker inalcançável, ou `brokerUnavailable` do CONNACK — nunca senha ausente, +CA ausente, ou credencial/identificador recusado, que não mudam sozinhos): +backoff de 2s a 60s em [reconnect_schedule.dart](apps/acs/lib/core/services/reconnect_schedule.dart), +os mesmos valores do `MqttAlertDispatcher` do backend. O banner ganhou "Tentar +agora" para quem já vê o sinal voltar, e voltar do segundo plano dispara uma +tentativa imediata — é o gatilho que mais importa, 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. 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 para o shell poder assinar mudanças +de estado a qualquer momento, não só no retorno do `start()`. + ### M2.5 - Testes de Caos Foi adicionada a simulação de degradação de rede em [apps/acs/lib/core/services/network_chaos_simulator.dart](apps/acs/lib/core/services/network_chaos_simulator.dart): diff --git a/apps/acs/lib/app/app.dart b/apps/acs/lib/app/app.dart index 06797f0..11fc37f 100644 --- a/apps/acs/lib/app/app.dart +++ b/apps/acs/lib/app/app.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:developer' as developer; import 'package:flutter/material.dart'; @@ -7,6 +8,7 @@ import 'package:sinalacs_acs/core/network/backend_scope.dart'; import 'package:sinalacs_acs/core/services/alert_feed.dart'; import 'package:sinalacs_acs/core/services/alert_queue.dart'; import 'package:sinalacs_acs/core/services/offline_visit_queue.dart'; +import 'package:sinalacs_acs/core/services/reconnect_schedule.dart'; import 'package:sinalacs_acs/core/services/visit_queue_factory.dart'; class SinalAcsApp extends StatefulWidget { @@ -179,7 +181,7 @@ class AcsHomeShell extends StatefulWidget { State createState() => _AcsHomeShellState(); } -class _AcsHomeShellState extends State { +class _AcsHomeShellState extends State with WidgetsBindingObserver { AcsDestination destination = AcsDestination.queue; late final AlertQueue _queue = AlertQueue(microAreaId: widget.microAreaId); @@ -188,11 +190,23 @@ class _AcsHomeShellState extends State { bool _brokerConnected = false; InfraNotice? _feedError; + bool _feedErrorIsTransient = false; PrioritizedAlert? _selected; + /// Só existe enquanto uma falha transitória está sendo retentada. `null` + /// quer dizer "nada agendado" — nem depois de um sucesso, nem depois de uma + /// falha permanente. + Timer? _reconnectTimer; + final ReconnectSchedule _reconnectDelay = ReconnectSchedule(); + @override void initState() { super.initState(); + WidgetsBinding.instance.addObserver(this); + // Assinado antes de start(): uma queda depois de conectar chega aqui + // sozinha, via autoReconnect do mqtt_client — é o que mantém o chip do + // cabeçalho honesto sem o shell abrir um segundo laço de retentativa. + _feed.onConnectionChanged = _onBrokerConnectionChanged; _connectFeed(); _restoreVisits(); } @@ -238,13 +252,24 @@ class _AcsHomeShellState extends State { /// /// Falhar aqui não derruba o painel, mas **precisa ficar visível**: um ACS que /// não sabe que parou de receber alertas é o pior modo de falha do produto. + /// + /// Antes rodava só uma vez, no `initState`: se falhasse, o app nunca mais + /// tentava, e reabrir era o único jeito de recuperar um alerta que o broker + /// já estava guardando com QoS 1. Falhas transitórias agora são retentadas + /// sozinhas por [_scheduleReconnect]; falhas permanentes (senha ausente, CA + /// ausente, credencial recusada) não são — insistir nelas não muda nada e só + /// gastaria bateria. Future _connectFeed() async { try { await _feed.start(microAreaId: widget.microAreaId, acsId: widget.acsId); + _reconnectTimer?.cancel(); + _reconnectTimer = null; + _reconnectDelay.reset(); if (!mounted) return; setState(() { _brokerConnected = _feed.isConnected; _feedError = null; + _feedErrorIsTransient = false; }); } on AlertFeedFailure catch (failure, stackTrace) { developer.log( @@ -256,11 +281,20 @@ class _AcsHomeShellState extends State { if (!mounted) return; setState(() { _brokerConnected = false; - _feedError = (title: failure.title, detail: failure.detail); + _feedErrorIsTransient = failure.transient; + _feedError = ( + title: failure.title, + detail: failure.transient + ? '${failure.detail} Tentando reconectar automaticamente.' + : failure.detail, + ); }); + if (failure.transient) _scheduleReconnect(); } catch (error, stackTrace) { // Defesa: hoje só um AlertFeed de teste chega aqui. Logar em vez de // descartar — antes o erro era engolido e a tela dizia sempre o mesmo. + // Na dúvida sobre a natureza da falha, tratar como transitória: silêncio + // de alertas é o pior modo de falha do produto. developer.log( 'falha não classificada ao assinar o tópico de alertas', name: 'sinalacs.acs.alert_feed', @@ -270,11 +304,58 @@ class _AcsHomeShellState extends State { if (!mounted) return; setState(() { _brokerConnected = false; + _feedErrorIsTransient = true; _feedError = ( title: 'Sem conexão com a central de alertas.', - detail: 'Novos alertas podem não estar chegando.', + detail: 'Novos alertas podem não estar chegando. ' + 'Tentando reconectar automaticamente.', ); }); + _scheduleReconnect(); + } + } + + /// Agenda a próxima tentativa, cancelando qualquer uma já pendente. + /// + /// O cancelamento prévio é o que impede "Tentar agora" durante a espera de + /// deixar dois timers vivos. + void _scheduleReconnect() { + _reconnectTimer?.cancel(); + _reconnectTimer = Timer(_reconnectDelay.next(), () { + if (mounted) _connectFeed(); + }); + } + + /// O botão "Tentar agora" do banner: pula a fila do backoff. + void _retryFeedNow() { + _reconnectTimer?.cancel(); + _reconnectTimer = null; + _reconnectDelay.reset(); + _connectFeed(); + } + + /// Notificado pelo [AlertFeed] a cada mudança de conexão — inclusive depois + /// do `start()` já ter retornado. Sem isto, uma queda que acontecesse depois + /// de uma conexão bem-sucedida nunca chegaria ao cabeçalho, que continuaria + /// dizendo "em linha" indefinidamente enquanto o `autoReconnect` do + /// `mqtt_client` trabalhava por baixo em silêncio. + void _onBrokerConnectionChanged(bool connected) { + if (!mounted) return; + setState(() => _brokerConnected = connected); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.paused) { + // Um Timer em segundo plano no Android não é confiável e só gastaria + // bateria; a tentativa volta ao primeiro plano. + _reconnectTimer?.cancel(); + _reconnectTimer = null; + } else if (state == AppLifecycleState.resumed && _feedErrorIsTransient) { + // O gatilho que mais importa na prática: o sinal costuma voltar com a + // tela apagada, e o ACS tira o aparelho do bolso já esperando o alerta. + _reconnectDelay.reset(); + _connectFeed(); } } @@ -294,6 +375,8 @@ class _AcsHomeShellState extends State { @override void dispose() { + _reconnectTimer?.cancel(); + WidgetsBinding.instance.removeObserver(this); _feed.stop(); _queue.dispose(); super.dispose(); @@ -307,6 +390,7 @@ class _AcsHomeShellState extends State { AcsDestination.queue => DashboardScreen( queue: _queue, feedError: _feedError, + onRetryFeed: _feedErrorIsTransient ? _retryFeedNow : null, storageError: _storageNotice, onAcknowledge: _acknowledge, onEscalate: (alert) => setState(() { _selected = alert; destination = AcsDestination.escalation; }), @@ -346,6 +430,7 @@ class DashboardScreen extends StatelessWidget { required this.queue, super.key, this.feedError, + this.onRetryFeed, this.storageError, this.onAcknowledge, this.onEscalate, @@ -355,6 +440,12 @@ class DashboardScreen extends StatelessWidget { final AlertQueue queue; final InfraNotice? feedError; + /// Presente só quando [feedError] é uma falha transitória — dá ao ACS que já + /// vê o sinal voltar a chance de não esperar o backoff automático. `null` + /// numa falha permanente (senha ausente, CA ausente) evita oferecer um botão + /// que nunca vai funcionar. + final VoidCallback? onRetryFeed; + /// Armazenamento local recusando gravação. /// /// Coexiste com [feedError] em vez de disputar o mesmo espaço: em campo os @@ -379,6 +470,7 @@ class DashboardScreen extends StatelessWidget { key: const Key('feed_error'), icon: Icons.cloud_off_outlined, notice: feedError!, + onRetry: onRetryFeed, ), if (storageError != null) _InfraBanner( key: const Key('storage_error'), @@ -419,11 +511,14 @@ class DashboardScreen extends StatelessWidget { /// A `Key` vem de fora e é o único identificador do aviso — pô-la também no /// `ListTile` faria `find.byKey` achar dois widgets. class _InfraBanner extends StatelessWidget { - const _InfraBanner({required this.icon, required this.notice, super.key}); + const _InfraBanner({required this.icon, required this.notice, super.key, this.onRetry}); final IconData icon; final InfraNotice notice; + /// Presente só nas falhas transitórias — ver [DashboardScreen.onRetryFeed]. + final VoidCallback? onRetry; + @override Widget build(BuildContext context) => Padding( padding: const EdgeInsets.only(top: 12), @@ -433,6 +528,11 @@ class _InfraBanner extends StatelessWidget { leading: Icon(icon, color: AcsColors.accent), title: Text(notice.title, style: const TextStyle(fontWeight: FontWeight.bold)), subtitle: Text(notice.detail), + trailing: onRetry == null ? null : TextButton( + key: const Key('retry_feed'), + onPressed: onRetry, + child: const Text('Tentar agora'), + ), ), ), ); diff --git a/apps/acs/lib/core/services/alert_feed.dart b/apps/acs/lib/core/services/alert_feed.dart index 12065eb..b3ae6c6 100644 --- a/apps/acs/lib/core/services/alert_feed.dart +++ b/apps/acs/lib/core/services/alert_feed.dart @@ -14,6 +14,15 @@ abstract class AlertFeed { void stop(); bool get isConnected; + + /// Notificado a cada mudança de estado da conexão, inclusive depois de + /// [start] já ter retornado — é o que mantém o chip do cabeçalho honesto + /// quando a conexão cai e volta sozinha (`autoReconnect`). + /// + /// Campo mutável, não parâmetro de construtor: quem monta o [AlertFeed] + /// (os `feedBuilder` de teste) não precisa mudar para o dono do widget + /// poder assinar depois. + abstract void Function(bool connected)? onConnectionChanged; } /// Por que o ACS não está recebendo alertas. @@ -45,6 +54,7 @@ class AlertFeedFailure implements Exception { required this.title, required this.detail, this.cause, + this.transient = false, }); final AlertFeedFailureKind kind; @@ -52,6 +62,13 @@ class AlertFeedFailure implements Exception { final String detail; final Object? cause; + /// Se vale a pena tentar de novo sozinho, sem intervenção humana. + /// + /// `missingPassword`/`missingCaAsset` exigem recompilar; `refused` herda o + /// veredito do CONNACK; `unreachable` é sempre transitória — falta de rede + /// é o caso comum de um ACS em campo. + final bool transient; + @override String toString() => '${kind.name}: $title'; } @@ -63,7 +80,9 @@ class MqttAlertFeed implements AlertFeed { }); final AlertQueue queue; - final void Function(bool connected)? onConnectionChanged; + + @override + void Function(bool connected)? onConnectionChanged; MqttSecureClient? _client; bool _connected = false; @@ -76,6 +95,11 @@ class MqttAlertFeed implements AlertFeed { required String microAreaId, required String acsId, }) async { + // Desliga qualquer conexão anterior antes de abrir outra. Sem isto, uma + // segunda chamada a start() (a nova retentativa) sobrescreveria _client e + // deixaria o cliente antigo órfão, fora do alcance de stop()/dispose(). + stop(); + // Antes de abrir qualquer socket: este binário tem senha? Sem isto, um app // compilado sem `--dart-define` gastava o timeout do broker para depois // dizer "sem conexão", escondendo que o problema estava na compilação. @@ -129,6 +153,7 @@ class MqttAlertFeed implements AlertFeed { title: error.message, detail: 'Novos alertas não estão chegando.', cause: error, + transient: error.transient, ); } catch (error) { throw AlertFeedFailure( @@ -136,6 +161,7 @@ class MqttAlertFeed implements AlertFeed { title: 'Sem conexão com a central de alertas.', detail: 'Novos alertas podem não estar chegando.', cause: error, + transient: true, ); } diff --git a/apps/acs/lib/core/services/mqtt_secure_client.dart b/apps/acs/lib/core/services/mqtt_secure_client.dart index c129e8f..86e95e4 100644 --- a/apps/acs/lib/core/services/mqtt_secure_client.dart +++ b/apps/acs/lib/core/services/mqtt_secure_client.dart @@ -25,11 +25,16 @@ String ackTopicFor(String alertId) => 'sinalacs/v1/alerts/$alertId/acks'; /// errada de uma central fora do ar — que antes chegavam à tela como a mesma /// frase. class MqttConnectionRefused implements Exception { - const MqttConnectionRefused(this.message); + const MqttConnectionRefused(this.message, {this.transient = false}); /// Já em português e pronta para a tela. **Nunca** contém credencial. final String message; + /// Se vale a pena tentar de novo sem intervenção humana. + /// + /// Ver [mqttRefusalIsTransient] para o critério. + final bool transient; + @override String toString() => message; } @@ -55,6 +60,14 @@ String? mqttRefusalReason(MqttConnectReturnCode? code) => switch (code) { _ => null, }; +/// Se vale tentar de novo depois de uma recusa. +/// +/// Só `brokerUnavailable`: a central pode estar reiniciando ou sobrecarregada. +/// Credencial e identificador recusados não mudam sozinhos — insistir neles +/// só gastaria bateria sem chance de sucesso. +bool mqttRefusalIsTransient(MqttConnectReturnCode? code) => + code == MqttConnectReturnCode.brokerUnavailable; + class SecureMqttConfig { const SecureMqttConfig({ required this.brokerHost, @@ -273,18 +286,23 @@ class MqttSecureClient { try { await client.connect(config.username, config.password); } catch (error) { - // Lido ANTES do disconnect, que zera o connectionStatus. - final refusal = mqttRefusalReason(client.connectionStatus?.returnCode); + // Lidos ANTES do disconnect, que zera o connectionStatus. + final returnCode = client.connectionStatus?.returnCode; + final refusal = mqttRefusalReason(returnCode); disconnect(); - if (refusal != null) throw MqttConnectionRefused(refusal); + if (refusal != null) { + throw MqttConnectionRefused(refusal, transient: mqttRefusalIsTransient(returnCode)); + } rethrow; } if (client.connectionStatus?.state != MqttConnectionState.connected) { - final refusal = mqttRefusalReason(client.connectionStatus?.returnCode); + final returnCode = client.connectionStatus?.returnCode; + final refusal = mqttRefusalReason(returnCode); disconnect(); throw MqttConnectionRefused( refusal ?? 'Não foi possível conectar ao broker MQTT.', + transient: mqttRefusalIsTransient(returnCode), ); } diff --git a/apps/acs/lib/core/services/reconnect_schedule.dart b/apps/acs/lib/core/services/reconnect_schedule.dart new file mode 100644 index 0000000..968740e --- /dev/null +++ b/apps/acs/lib/core/services/reconnect_schedule.dart @@ -0,0 +1,35 @@ +/// Atrasos entre tentativas de conectar ao broker. +/// +/// Existe separada do widget por dois motivos: o cálculo é testável sem relógio +/// falso nem árvore de widgets, e a escala fica num lugar só. Os valores são os +/// mesmos de `MqttAlertDispatcher` no backend — não há razão para as duas pontas +/// divergirem. +/// +/// Só aritmética: nenhum `Timer` mora aqui. Quem agenda é o dono do ciclo de +/// vida, que sabe cancelar. +class ReconnectSchedule { + /// Curto o bastante para o ACS que abre o app logo depois de recuperar sinal + /// não esperar de pé olhando a tela. + static const initial = Duration(seconds: 2); + + /// Teto: numa zona sem cobertura, insistir a cada 2 segundos pelo turno + /// inteiro gastaria bateria sem chance de sucesso. + static const maximum = Duration(seconds: 60); + + Duration _current = initial; + + /// O atraso desta tentativa. Dobra para a próxima, até [maximum]. + Duration next() { + final delay = _current; + final doubled = delay.inSeconds * 2; + _current = doubled >= maximum.inSeconds ? maximum : Duration(seconds: doubled); + return delay; + } + + /// Volta ao início. + /// + /// Chamada quando a conexão dá certo e quando o app retorna do segundo plano: + /// nesse instante a rede é outra, e herdar o atraso de 60s da tentativa + /// anterior faria o ACS esperar um minuto com sinal na mão. + void reset() => _current = initial; +} diff --git a/apps/acs/test/login_flow_test.dart b/apps/acs/test/login_flow_test.dart index d5d9864..ddad15b 100644 --- a/apps/acs/test/login_flow_test.dart +++ b/apps/acs/test/login_flow_test.dart @@ -461,8 +461,12 @@ void main() { expect(find.byKey(const Key('feed_error')), findsOneWidget); expect(find.byKey(const Key('storage_error')), findsOneWidget); // Cada um com o seu subtítulo: o texto sobre alertas era fixo e passava a - // mentir quando o aviso exibido era o de disco. - expect(find.text('Novos alertas podem não estar chegando.'), findsOneWidget); + // mentir quando o aviso exibido era o de disco. A falha é transitória + // (StateError genérico), por isso o aviso de tentativa automática. + expect( + find.text('Novos alertas podem não estar chegando. Tentando reconectar automaticamente.'), + findsOneWidget, + ); expect( find.textContaining('só existe na memória'), findsOneWidget, @@ -551,6 +555,158 @@ void main() { }); }); + group('reconexão do feed', () { + /// Leva até o painel, com o feed e a fila que o teste quiser. + /// + /// Cópia local de propósito: o helper do grupo `avisos de infraestrutura` + /// é privado ao seu próprio corpo. + Future abrirPainel( + WidgetTester tester, { + required OfflineVisitQueue visitQueue, + AlertFeed Function(AlertQueue queue)? feedBuilder, + }) async { + await tester.pumpWidget(SinalAcsApp( + backend: FakeAcsBackend(), + feedBuilder: feedBuilder ?? (queue) => FakeAlertFeed(queue), + visitQueue: visitQueue, + )); + await tester.tap(find.byKey(const Key('login_button'))); + await tester.pumpAndSettle(); + } + + testWidgets('o painel volta a receber alertas quando a conexão dá certo na segunda tentativa', (tester) async { + // O defeito literal: antes disto, uma falha na primeira conexão exigia + // fechar e reabrir o app. O broker guarda os alertas com QoS 1 — basta + // conectar para recebê-los. + late FakeAlertFeed feed; + + await abrirPainel( + tester, + visitQueue: OfflineVisitQueue(), + feedBuilder: (queue) => feed = FakeAlertFeed(queue, failuresBeforeSuccess: 1), + ); + + expect(feed.startCount, 1); + expect(find.byKey(const Key('feed_error')), findsOneWidget); + expect(find.text('Sem central'), findsOneWidget); + + // O atraso inicial do ReconnectSchedule é de 2s. + await tester.pump(const Duration(seconds: 2)); + await tester.pumpAndSettle(); + + expect(feed.startCount, 2); + expect(find.byKey(const Key('feed_error')), findsNothing); + expect(find.text('Alertas em tempo real'), findsOneWidget); + + feed.deliver(testAlert(alertId: 'alerta-pos-reconexao')); + await tester.pumpAndSettle(); + // A fila cresceu: o cartão de "nenhum alerta" precisa ter sumido. + expect(find.text('Nenhum alerta na sua microárea agora.'), findsNothing); + }); + + testWidgets('o atraso entre tentativas dobra a cada falha', (tester) async { + late FakeAlertFeed feed; + + await abrirPainel( + tester, + visitQueue: OfflineVisitQueue(), + feedBuilder: (queue) => feed = FakeAlertFeed(queue, failOnStart: true), + ); + expect(feed.startCount, 1); + + await tester.pump(const Duration(seconds: 2)); + expect(feed.startCount, 2, reason: 'primeiro atraso: 2s'); + + await tester.pump(const Duration(seconds: 2)); + expect(feed.startCount, 2, reason: 'segundo atraso é 4s, ainda não decorreu'); + + await tester.pump(const Duration(seconds: 2)); + expect(feed.startCount, 3, reason: 'agora os 4s completaram'); + }); + + testWidgets('"Tentar agora" reconecta sem esperar o backoff', (tester) async { + late FakeAlertFeed feed; + + await abrirPainel( + tester, + visitQueue: OfflineVisitQueue(), + feedBuilder: (queue) => feed = FakeAlertFeed(queue, failuresBeforeSuccess: 1), + ); + expect(feed.startCount, 1); + + await tester.tap(find.byKey(const Key('retry_feed'))); + await tester.pumpAndSettle(); + + expect(feed.startCount, 2); + expect(find.byKey(const Key('feed_error')), findsNothing); + expect(find.text('Alertas em tempo real'), findsOneWidget); + }); + + testWidgets('falta de senha não oferece nem agenda nova tentativa', (tester) async { + late FakeAlertFeed feed; + + await abrirPainel( + tester, + visitQueue: OfflineVisitQueue(), + feedBuilder: (queue) => feed = FakeAlertFeed( + queue, + failure: const AlertFeedFailure( + AlertFeedFailureKind.missingPassword, + title: 'Este aplicativo foi compilado sem a senha do broker de alertas.', + detail: 'Nenhum alerta será recebido até o aplicativo ser recompilado.', + ), + ), + ); + + expect(find.byKey(const Key('retry_feed')), findsNothing); + + // Bem além de qualquer atraso possível (teto de 60s) — se algo estivesse + // agendado, teria disparado. + await tester.pump(const Duration(seconds: 70)); + expect(feed.startCount, 1); + }); + + testWidgets('voltar do segundo plano tenta reconectar na hora', (tester) async { + late FakeAlertFeed feed; + + await abrirPainel( + tester, + visitQueue: OfflineVisitQueue(), + feedBuilder: (queue) => feed = FakeAlertFeed(queue, failuresBeforeSuccess: 1), + ); + expect(feed.startCount, 1); + + // Sem avançar o relógio: o sinal costuma voltar com a tela apagada, e o + // ACS reabre o app já esperando o alerta. + tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.resumed); + await tester.pumpAndSettle(); + + expect(feed.startCount, 2); + expect(find.byKey(const Key('feed_error')), findsNothing); + }); + + testWidgets('o cabeçalho para de dizer "em linha" quando a conexão cai depois de conectar', (tester) async { + late FakeAlertFeed feed; + + await abrirPainel( + tester, + visitQueue: OfflineVisitQueue(), + feedBuilder: (queue) => feed = FakeAlertFeed(queue), + ); + expect(find.text('Alertas em tempo real'), findsOneWidget); + + // Simula o autoReconnect do mqtt_client notificando uma queda depois de + // já ter conectado — o caso que o campo escrito uma única vez não via. + feed.onConnectionChanged?.call(false); + await tester.pump(); + + expect(find.text('Sem central'), findsOneWidget); + // Nenhum banner novo: quem trata essa reconexão é o autoReconnect do + // mqtt_client, não o shell. + expect(find.byKey(const Key('feed_error')), findsNothing); + }); + }); + group('fiação de produção', () { test('a fila do app sai com o sincronizador do backend ligado', () { // A regressão que motivou tudo: o app montava a fila sem sincronizador, diff --git a/apps/acs/test/mqtt_secure_client_test.dart b/apps/acs/test/mqtt_secure_client_test.dart index 5e1aa75..639f25d 100644 --- a/apps/acs/test/mqtt_secure_client_test.dart +++ b/apps/acs/test/mqtt_secure_client_test.dart @@ -112,5 +112,17 @@ void main() { expect(mqttRefusalReason(null), isNull); expect(mqttRefusalReason(MqttConnectReturnCode.connectionAccepted), isNull); }); + + test('só brokerUnavailable vale retentar sozinho', () { + // Credencial e identificador recusados não mudam sozinhos — insistir + // neles só gastaria bateria sem chance de sucesso. É justamente o erro + // que mais tentaria induzir a retentar, por isso o teste explícito. + expect(mqttRefusalIsTransient(MqttConnectReturnCode.brokerUnavailable), isTrue); + expect(mqttRefusalIsTransient(MqttConnectReturnCode.badUsernameOrPassword), isFalse); + expect(mqttRefusalIsTransient(MqttConnectReturnCode.notAuthorized), isFalse); + expect(mqttRefusalIsTransient(MqttConnectReturnCode.identifierRejected), isFalse); + expect(mqttRefusalIsTransient(MqttConnectReturnCode.unacceptedProtocolVersion), isFalse); + expect(mqttRefusalIsTransient(null), isFalse); + }); }); } diff --git a/apps/acs/test/reconnect_schedule_test.dart b/apps/acs/test/reconnect_schedule_test.dart new file mode 100644 index 0000000..8dbaf4d --- /dev/null +++ b/apps/acs/test/reconnect_schedule_test.dart @@ -0,0 +1,30 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:sinalacs_acs/core/services/reconnect_schedule.dart'; + +void main() { + group('ReconnectSchedule', () { + test('dobra a cada tentativa até o teto de 60s', () { + final schedule = ReconnectSchedule(); + + expect(schedule.next(), const Duration(seconds: 2)); + expect(schedule.next(), const Duration(seconds: 4)); + expect(schedule.next(), const Duration(seconds: 8)); + expect(schedule.next(), const Duration(seconds: 16)); + expect(schedule.next(), const Duration(seconds: 32)); + expect(schedule.next(), const Duration(seconds: 60)); + // Fica no teto, não ultrapassa. + expect(schedule.next(), const Duration(seconds: 60)); + }); + + test('reset volta ao atraso inicial depois de já ter subido', () { + final schedule = ReconnectSchedule(); + schedule.next(); + schedule.next(); + schedule.next(); + + schedule.reset(); + + expect(schedule.next(), const Duration(seconds: 2)); + }); + }); +} diff --git a/apps/acs/test/support/fakes.dart b/apps/acs/test/support/fakes.dart index e997955..01edeed 100644 --- a/apps/acs/test/support/fakes.dart +++ b/apps/acs/test/support/fakes.dart @@ -102,7 +102,7 @@ class FakeAcsBackend implements AcsBackend { /// Expõe a [AlertQueue] para que o teste empurre alertas como se tivessem /// chegado pelo broker, sem precisar de rede nem de emulador. class FakeAlertFeed implements AlertFeed { - FakeAlertFeed(this.queue, {this.failOnStart = false, this.failure}); + FakeAlertFeed(this.queue, {this.failOnStart = false, this.failure, this.failuresBeforeSuccess = 0}); final AlertQueue queue; final bool failOnStart; @@ -113,20 +113,43 @@ class FakeAlertFeed implements AlertFeed { /// defesa do shell, o que nenhum `AlertFeedFailure` cobre. final AlertFeedFailure? failure; + /// Quantas chamadas a [start] devem falhar antes de uma que conecta. + /// + /// É o que prova que a retentativa do shell não só acontece, mas **dá + /// certo**: sem isto, todo teste de reconexão ficaria preso numa falha para + /// sempre. + final int failuresBeforeSuccess; + bool started = false; bool stopped = false; String? startedTopicMicroArea; + /// Quantas vezes [start] foi chamado — inclusive as que falharam. + int startCount = 0; + @override bool get isConnected => started; + @override + void Function(bool connected)? onConnectionChanged; + @override Future start({required String microAreaId, required String acsId}) async { + startCount++; + if (startCount <= failuresBeforeSuccess) { + throw failure ?? const AlertFeedFailure( + AlertFeedFailureKind.unreachable, + title: 'Sem conexão com a central de alertas.', + detail: 'Novos alertas podem não estar chegando.', + transient: true, + ); + } final classified = failure; if (classified != null) throw classified; if (failOnStart) throw StateError('broker indisponível'); started = true; startedTopicMicroArea = microAreaId; + onConnectionChanged?.call(true); } @override diff --git a/docs/telas-acs.md b/docs/telas-acs.md index a7f1f74..1c902ef 100644 --- a/docs/telas-acs.md +++ b/docs/telas-acs.md @@ -22,7 +22,7 @@ Exibe a microárea, quantidade de pacientes e status do cache. A ação de atual ![Fila ordenada por risco clínico](screenshots/acs/02-dashboard.png) -A fila apresenta os riscos vermelho, amarelo e verde com borda e ação contextual. A ordem mantém a prioridade clínica e a cor é reservada para a gravidade. Por isso avisos de infraestrutura — sem conexão com a central, visitas não sendo salvas — usam o azul de destaque, e não vermelho ou amarelo: um card vermelho de falha técnica competiria com o alerta vermelho de um paciente na mesma lista. +A fila apresenta os riscos vermelho, amarelo e verde com borda e ação contextual. A ordem mantém a prioridade clínica e a cor é reservada para a gravidade. Por isso avisos de infraestrutura — sem conexão com a central, visitas não sendo salvas — usam o azul de destaque, e não vermelho ou amarelo: um card vermelho de falha técnica competiria com o alerta vermelho de um paciente na mesma lista. Quando a falha de conexão é transitória, o card ainda ganha um botão "Tentar agora" — a reconexão automática já está tentando sozinha em segundo plano, mas quem vê o sinal voltar não precisa esperar o intervalo. ## Mapa de pacientes From da74d84cabbda17f8595252243d43d007dc77fae Mon Sep 17 00:00:00 2001 From: Herbert Rocha Date: Sat, 12 Sep 2026 17:10:41 -0400 Subject: [PATCH 06/43] =?UTF-8?q?fix(acs):=20flutter=20build=20apk=20falha?= =?UTF-8?q?=20sem=20a=20senha=20do=20broker,=20em=20vez=20de=20compilar=20?= =?UTF-8?q?em=20sil=C3=AAncio?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- CLAUDE.md | 2 +- CONTRIBUTING.md | 4 +- PROGRESS.md | 26 +++++++++++ README.md | 9 ++-- apps/acs/android/app/build.gradle.kts | 62 +++++++++++++++++++++++++++ scripts/dev/run_acs.sh | 51 +++++++++++++++------- 6 files changed, 134 insertions(+), 20 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 3466ae8..ad528d2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -110,7 +110,7 @@ flutter test test/login_flow_test.dart # single test file flutter run # patient only flutter build apk --debug # debug APK, validated with compileSdk/targetSdk 36 ``` -For the ACS app, run `./scripts/dev/run_acs.sh` (or `--build` for the APK) instead of bare `flutter run`: `SINALACS_MQTT_PASSWORD` is a compile-time constant with **no default**, and the broker's password is generated per machine by `bootstrap_env.sh`, so a bare `flutter run` produces an app that never receives an alert. The script reads `.env`, runs `sync_dev_ca.sh` (the CA is a gitignored asset the build requires) and fills in all four `--dart-define`s. +For the ACS app, run `./scripts/dev/run_acs.sh` (or `--build` for the APK) instead of bare `flutter run`: `SINALACS_MQTT_PASSWORD` is a compile-time constant with **no default**, and the broker's password is generated per machine by `bootstrap_env.sh`. The script reads `.env`, runs `sync_dev_ca.sh` (the CA is a gitignored asset the build requires) and passes all four defines via `--dart-define-from-file` (a temp file it creates and deletes, so the password never sits in `flutter`'s argv). `apps/acs/android/app/build.gradle.kts` makes a bare `flutter build apk` **fail** with the right command instead of silently producing an APK that never connects; the escape hatch for a deliberately-passwordless build (e.g. to see the "compiled without the password" banner) is `-Psinalacs.allowMissingMqttPassword=true`. `apps/admin` exists only as a pubspec skeleton (backoffice), no implementation yet. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0925c57..b5e4e28 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -120,7 +120,9 @@ cd apps/patient && flutter run ``` O ACS precisa do script: a senha do broker é constante de compilação, sem valor -padrão, e é gerada por máquina. +padrão, e é gerada por máquina. Um `flutter build apk` puro **falha** — a +guarda vive em `apps/acs/android/app/build.gradle.kts` — em vez de compilar em +silêncio um APK que nunca recebe alerta. ## Alterações no backend diff --git a/PROGRESS.md b/PROGRESS.md index 82cf2c8..bd819ef 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -277,6 +277,32 @@ nunca chegava a ele, que continuava dizendo "em linha" para sempre enquanto o subiu para a interface como campo mutável para o shell poder assinar mudanças de estado a qualquer momento, não só no retorno do `start()`. +#### `flutter build apk` puro ainda entregava um APK que nunca conectava + +Os dois defeitos acima foram fechados, mas sobrava uma lacuna: mesmo sem +`SINALACS_MQTT_PASSWORD`, `flutter build apk` compilava normalmente. O defeito +só se denunciava em tempo de execução, pelo banner "compilado sem a senha" — +tarde demais para quem já distribuiu o APK. + +[apps/acs/android/app/build.gradle.kts](apps/acs/android/app/build.gradle.kts) +ganhou uma guarda em `doFirst` das tarefas `compileFlutterBuild*`: decodifica a +propriedade `dart-defines` (o Flutter Gradle Plugin já lê essa mesma +propriedade) e falha, com o comando certo, se `SINALACS_MQTT_PASSWORD` não +estiver lá. Precisou ser `doFirst` de tarefa, e não bloco de configuração — +senão dispararia em todo `gradlew`, inclusive o sync do Android Studio, que não +passa define nenhum. 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`. + +Aproveitado para tirar a senha do `argv`: `run_acs.sh` passou de `--dart-define` +para `--dart-define-from-file`, com um arquivo temporário (`mktemp`, 0600) que +um `trap` apaga ao sair. A troca exigiu remover o `exec` das duas chamadas ao +`flutter` — `exec` substitui o processo do shell, e o `trap` nunca rodaria, +deixando o arquivo com a senha esquecido em `/tmp` depois de cada execução. +`scripts/qa/e2e.sh` continua passando a senha por `argv`: aquele caminho roda +`flutter test`/`dart run`, não `flutter build`, e não passa pela guarda. + ### M2.5 - Testes de Caos Foi adicionada a simulação de degradação de rede em [apps/acs/lib/core/services/network_chaos_simulator.dart](apps/acs/lib/core/services/network_chaos_simulator.dart): diff --git a/README.md b/README.md index aec7b88..e88bd3c 100644 --- a/README.md +++ b/README.md @@ -90,9 +90,12 @@ cd apps/acs && flutter pub get && cd - Use o script, não `flutter run` direto. A senha do broker é resolvida em tempo de compilação e não tem valor padrão: ela é gerada por máquina pelo -`bootstrap_env.sh`, então um `flutter run` sem `--dart-define` produz um app que -nunca recebe alerta. O script lê o `.env`, copia a CA do broker para os assets e -preenche os quatro `--dart-define`. +`bootstrap_env.sh`. O script lê o `.env`, copia a CA do broker para os assets e +passa os quatro `--dart-define` por um arquivo temporário (`--dart-define-from-file`, +apagado ao sair), para a senha não trafegar na linha de comando do `flutter`. Um +`flutter build apk` sem essas variáveis **falha** — a guarda vive em +`apps/acs/android/app/build.gradle.kts` — em vez de compilar em silêncio um APK +que nunca recebe alerta. Para escolher o dispositivo, ou gerar o APK: diff --git a/apps/acs/android/app/build.gradle.kts b/apps/acs/android/app/build.gradle.kts index 6689f48..20dab9e 100644 --- a/apps/acs/android/app/build.gradle.kts +++ b/apps/acs/android/app/build.gradle.kts @@ -1,3 +1,5 @@ +import java.util.Base64 + plugins { id("com.android.application") // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. @@ -43,3 +45,63 @@ kotlin { flutter { source = "../.." } + +// Guarda contra o APK silenciosamente inútil: SINALACS_MQTT_PASSWORD é +// constante de compilação sem default (ver backend_config.dart) porque o +// broker usa um segredo por máquina — nenhum valor embutido no código +// poderia acertá-lo. Sem esta guarda, `flutter build apk` puro compila com +// sucesso um APK que nunca recebe alerta nenhum, e isso só se denuncia em +// tempo de execução, pelo banner "compilado sem a senha". +// +// Acoplada a dois detalhes internos do Flutter Gradle Plugin: a propriedade +// "dart-defines" (uma lista de pares CHAVE=VALOR em base64, separados por +// vírgula — FlutterPlugin.kt lê a mesma propriedade) e o prefixo de nome +// "compileFlutterBuild" das tarefas de compilação Dart. Se um upgrade do +// Flutter renomear qualquer um dos dois, esta guarda falha ABERTA — para de +// bloquear, sem avisar. `flutter build apk` sem senha voltando a passar é o +// sintoma; conferir isto faz parte de todo upgrade do Flutter. +fun hasMqttPassword(): Boolean { + val encoded = project.findProperty("dart-defines")?.toString() ?: return false + return encoded.split(",").any { item -> + runCatching { + String(Base64.getDecoder().decode(item), Charsets.UTF_8) + }.getOrNull()?.let { + it.startsWith("SINALACS_MQTT_PASSWORD=") && it.substringAfter('=').isNotEmpty() + } ?: false + } +} + +// doFirst de TAREFA, nunca bloco de configuração: na configuração, isto +// dispararia em todo `gradlew`, inclusive o sync do Android Studio (que não +// passa define nenhum) — tornando o projeto impossível de abrir na IDE. No +// doFirst só roda quando o Dart vai de fato ser compilado. +tasks.configureEach { + if (!name.startsWith("compileFlutterBuild")) return@configureEach + doFirst { + val allowMissing = project.hasProperty("sinalacs.allowMissingMqttPassword") + if (hasMqttPassword()) return@doFirst + if (allowMissing) { + logger.warn("aviso: compilando sem SINALACS_MQTT_PASSWORD — este APK não vai receber alerta nenhum.") + return@doFirst + } + // Nunca imprimir "dart-defines" aqui: a lista carrega a própria senha + // quando ela FOI passada com outro nome de chave por engano. + throw GradleException( + """ + |Este APK não receberia alerta nenhum: falta --dart-define=SINALACS_MQTT_PASSWORD. + | + |O broker cria o usuário acs-area-12 com MQTT_ACS_PASSWORD, um segredo por + |máquina gerado por scripts/dev/bootstrap_env.sh — nenhum valor embutido no + |código poderia acertá-lo. + | + | ./scripts/dev/run_acs.sh # flutter run + | ./scripts/dev/run_acs.sh --build # APK de depuração + | ./scripts/qa/e2e.sh --emulator # integration_test + | + |Para compilar de propósito sem a senha — por exemplo, para reproduzir na tela + |o aviso "compilado sem a senha" —, acrescente: + | -Psinalacs.allowMissingMqttPassword=true + """.trimMargin() + ) + } +} diff --git a/scripts/dev/run_acs.sh b/scripts/dev/run_acs.sh index 684f0c5..ca11022 100755 --- a/scripts/dev/run_acs.sh +++ b/scripts/dev/run_acs.sh @@ -14,16 +14,19 @@ # default. O broker cria o usuário acs-area-12 com MQTT_ACS_PASSWORD, um # segredo aleatório POR MÁQUINA gerado por scripts/dev/bootstrap_env.sh: # nenhum valor embutido no código poderia acertá-lo. Sem este wrapper, -# `flutter run` produz um app que nunca recebe alerta nenhum. +# `flutter run` produz um app que nunca recebe alerta nenhum — e desde +# que apps/acs/android/app/build.gradle.kts ganhou a guarda, um +# `flutter build apk` puro nem chega a compilar. # · A CA do broker é asset do APK (apps/acs/assets/certs/), regerada pelo # mosquitto-init e não versionada. Sem ela o build falha antes do TLS — # apps/acs/pubspec.yaml declara o diretório — e não haveria em quem confiar. # -# A senha nunca é impressa. Ela viaja na linha de comando do flutter e fica -# visível em `ps` para outros usuários da MESMA máquina: aceitável em -# desenvolvimento, e é o que scripts/qa/e2e.sh já faz. Para endurecer, o -# caminho é --dart-define-from-file, que exige um arquivo temporário com trap -# de limpeza. +# A senha nunca é impressa e não trafega na linha de comando do flutter: vai +# num arquivo temporário lido por --dart-define-from-file, criado com 0600 +# (mktemp) fora da árvore do repositório e apagado pelo trap abaixo — inclusive +# se o script for interrompido. scripts/qa/e2e.sh ainda passa a senha por +# --dart-define porque aquele caminho roda `flutter test`/`dart run`, não +# `flutter build`, e não passa pela guarda do Gradle. set -euo pipefail repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" @@ -43,7 +46,7 @@ while [[ $# -gt 0 ]]; do --mqtt-host) mqtt_host="${2:?--mqtt-host exige um valor}"; shift 2 ;; --build) action='build'; shift ;; --skip-ca) skip_ca=1; shift ;; - -h|--help) sed -n '3,25p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 0 ;; + -h|--help) sed -n '3,26p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 0 ;; --) shift; flutter_args+=("$@"); break ;; *) flutter_args+=("$1"); shift ;; esac @@ -86,18 +89,36 @@ if [[ "$skip_ca" -eq 0 ]]; then fi fi -defines=( - "--dart-define=SINALACS_HOST=$host" - "--dart-define=SINALACS_MQTT_HOST=$mqtt_host" - "--dart-define=SINALACS_MQTT_USER=$mqtt_user" - "--dart-define=SINALACS_MQTT_PASSWORD=$mqtt_password" -) +# json_escape: escapa \ e " para o valor caber dentro de uma string JSON. +# Os valores hoje são hex (senha, gerada por bootstrap_env.sh) e host/URL sem +# aspas — não deveria haver o que escapar na prática, mas a função existe para +# não produzir um JSON inválido silenciosamente se isso mudar. +json_escape() { printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'; } + +# --dart-define-from-file em vez de --dart-define: a senha não pode aparecer +# no argv do flutter, visível em `ps` para outros usuários da mesma máquina. +# mktemp já cria com 0600 e fora da árvore do repositório. SEM `exec` nas +# chamadas de flutter abaixo: `exec` substitui o processo do shell e o trap +# nunca rodaria, deixando o arquivo com a senha esquecido em /tmp — trocaria +# uma exposição temporária por uma persistente, o oposto do que este passo +# quer. +defines_file="$(mktemp)" +trap 'rm -f "$defines_file"' EXIT INT TERM +cat > "$defines_file" < Date: Sat, 12 Sep 2026 17:26:49 -0400 Subject: [PATCH 07/43] doc: update AGENTS.md Signed-off-by: Herbert Rocha --- AGENTS.md | 179 ++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 127 insertions(+), 52 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index a54d5b0..b15eb2d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,74 +2,149 @@ ## Visão geral do projeto -Este repositório contém a definição do produto, arquitetura e protótipos do SinalACS, uma plataforma para priorização de atendimentos em Atenção Primária à Saúde. +O SinalACS é uma plataforma para priorização de atendimentos na Atenção Primária à Saúde, transformando sinais clínicos estruturados em uma fila de trabalho para o ACS (Agente Comunitário de Saúde), ordenada por risco e preparada para operar em conectividade instável. -A intenção principal do projeto é apoiar dois fluxos centrais: +O repositório já contém um protótipo funcional com: +- backend em Dart/Serverpod; +- apps Flutter para paciente e ACS; +- PostgreSQL como persistência central; +- broker MQTT para entrega de alertas em tempo real; +- fluxo de sincronização offline para visitas do ACS. + +Os dois fluxos centrais continuam sendo: - paciente: autenticação simples, alerta de urgência, triagem estruturada e acompanhamento de status; -- ACS: priorização dinâmica, territorialização, registro offline e acompanhamento de microárea. - -## Documentos fundamentais - -Antes de implementar qualquer funcionalidade, consulte primeiro estes arquivos: -- [README.md](README.md) – visão geral do produto e contexto de negócio; -- [spec/idea.md](spec/idea.md) – conceito e objetivo do produto; -- [spec/PRD_system.md](spec/PRD_system.md) – requisitos, JTBD, invariantes e métricas; -- [spec/stack.md](spec/stack.md) – arquitetura de stack, infraestrutura e decisões técnicas; -- [spec/ui_design.md](spec/ui_design.md) – linguagem visual e comportamento de UX; -- [spec/ui_acs](spec/ui_acs) – protótipos das telas do ACS; -- [spec/ui_paciente](spec/ui_paciente) – protótipos das telas do paciente; -- [CLAUDE.md](CLAUDE.md) – guia de arquitetura e comandos para agentes de IA, mais atualizado que este arquivo quanto ao estado de implementação; -- [PROGRESS.md](PROGRESS.md) – status real dos milestones já implementados; -- [backend/](backend) – backend Serverpod (workspace Dart com `sinalacs_server` e o cliente gerado `sinalacs_client`); -- [apps/acs](apps/acs) e [apps/patient](apps/patient) – apps Flutter implementados; -- [docs/](docs) – documentação visual das telas dos apps. - -## Regras de trabalho para agentes de IA +- ACS: priorização dinâmica, territorialização por microárea, registro offline e acompanhamento do território. + +## Leitura obrigatória antes de implementar + +Antes de mexer em produto, arquitetura ou comportamento, consulte primeiro: +- [README.md](README.md) — visão geral do produto e estado atual; +- [spec/PRD_system.md](spec/PRD_system.md) — requisitos, JTBD, invariantes e métricas; +- [spec/stack.md](spec/stack.md) — decisões de stack e infraestrutura; +- [spec/ui_design.md](spec/ui_design.md) — linguagem visual e UX; +- [spec/lgpd_design.md](spec/lgpd_design.md) — privacidade e LGPD; +- [CLAUDE.md](CLAUDE.md) — guia técnico e comandos para IA; é a referência mais atualizada do repositório; +- [PROGRESS.md](PROGRESS.md) — status dos milestones e histórico de migração; +- [backend/](backend) — workspace Dart com `sinalacs_server` e `sinalacs_client`; +- [apps/acs](apps/acs) e [apps/patient](apps/patient) — aplicativos Flutter reais; +- [spec/ui_acs](spec/ui_acs) e [spec/ui_paciente](spec/ui_paciente) — protótipos visuais e fluxos do produto. + +Quando houver conflito entre convenções gerais e documentação do projeto, a documentação do projeto vence. + +## Invariantes de negócio e segurança + +Não violar estes pontos sob qualquer hipótese: +- a microárea do ACS restringe o acesso apenas ao território correspondente; +- a classificação de risco é determinística e não pode ser alterada por intervenção manual no fluxo de triagem; +- alertas vermelhos nunca podem ser descartados silenciosamente; +- dados de saúde devem seguir os padrões de privacidade e LGPD; +- o cliente ACS deve operar mesmo com rede instável; +- a sincronização local/central é uma área crítica de risco arquitetural. + +## Arquitetura atual do repositório + +### Backend +- O backend está em um workspace Dart em [backend/](backend), com os pacotes `sinalacs_server` e `sinalacs_client`. +- A stack atual é Serverpod, não um servidor hand-rolled em `dart:io`. +- O servidor usa PostgreSQL e MQTT; a autenticação de desenvolvimento é opcional e não substitui autenticação institucional real. +- A camada de domínio fica em `backend/sinalacs_server/lib/src/application/` e a infraestrutura em `.../infrastructure/`. +- Os modelos e endpoints são definidos com Serverpod; não se deve editar manualmente arquivos gerados em `lib/src/generated/` ou migrações sem regenerar via `serverpod generate` e `serverpod create-migration`. +- O fluxo principal inclui: `auth.developmentLogin`, `triage.evaluate`, `alerts.createRedAlert`, `alerts.acknowledge` e `visits.sync`. +- MQTT conecta em background após boot, com reconexão exponencial e sem bloquear a API. + +### Apps Flutter +- A app do paciente e a app do ACS vivem em [apps/patient](apps/patient) e [apps/acs](apps/acs). +- Ambos usam o cliente gerado `sinalacs_client` por dependência local e não devem depender diretamente de detalhes de implementação do servidor em widgets. +- O app ACS é o mais crítico em termos de offline-first e sincronização: guarda visitas locais, tenta sincronizar em fila e trata conflitos sem perder registros. +- O tema visual é dark mode, com foco em legibilidade e uso de cor restrito a sinal clínico (vermelho, amarelo, verde). +- O app do paciente não deve reintroduzir regras de risco no cliente; a classificação deve vir do backend via `triage.evaluate`. + +### Infraestrutura local +- O ambiente de desenvolvimento usa Docker Compose com PostgreSQL, Mosquitto, backend e Traefik. +- Há geração local de segredos via [scripts/dev/bootstrap_env.sh](scripts/dev/bootstrap_env.sh); o projeto não possui `.env` versionado. +- O servidor e o broker exigem valores configurados por ambiente; não reaproveitar credenciais do ambiente local em produção. + +## Regras de desenvolvimento ### 1) Preserve o contexto do produto -- A solução é orientada por risco clínico, não por roteiros geográficos fixos. +- A solução é orientada por risco clínico e não por roteiros geográficos fixos. - O foco do MVP é priorização e resposta rápida a urgências. -- O idioma principal do projeto e da documentação é o português. +- O idioma principal da documentação, comentários e textos de UI é o português. -### 2) Respeite a arquitetura definida -- O produto combina Flutter no cliente, um backend Serverpod em Dart e PostgreSQL como persistência central. O Serverpod era a decisão original de stack, ficou por um tempo não implementado — o servidor era um `dart:io` roteado à mão — e foi adotado depois, substituindo-o. -- O modelo offline-first é crítico; o ACS deve operar mesmo com rede instável. -- O MQTT é usado para entrega de alertas de urgência em tempo real. -- A sincronização local/central deve ser tratada como risco arquitetural principal. +### 2) Respeite a arquitetura escolhida +- Prefira soluções simples e previsíveis alinhadas ao stack já decidido: Flutter + Dart + PostgreSQL + MQTT. +- Não introduzir frameworks ou serviços novos sem justificativa no PRD, stack ou arquitetura do projeto. +- Ao alterar sincronização ou fila offline, preservar retry, deduplicação, conflito e persistência local. ### 3) Preserve invariantes de negócio e segurança -- A microárea da pessoa ACS deve restringir o acesso somente ao seu território. -- A classificação de risco deve ser determinística e não alterável por intervenção humana na triagem. -- Alertas vermelhos não podem ser descartados silenciosamente. -- Dados sensíveis de saúde devem seguir o padrão de privacidade e LGPD. +- Microárea deve restringir dados ao território do ACS. +- Red alert não pode ser perdido em silêncio. +- Triagem precisa ser determinística e consistente com o modelo do Protocolo de Manchester. +- Nenhum dado sensível de paciente deve entrar em logs, screenshots, testes, seeds ou configurações compartilhadas. ### 4) Ao criar ou alterar código -- Prefira soluções simples, previsíveis e alinhadas com a arquitetura já definida. -- Evite inventar recursos que não estejam no PRD ou no stack do projeto. -- Quando houver conflito entre convenções gerais e documentação do projeto, a documentação do projeto vence. -- Mantenha a lógica de priorização e triagem consistente com o modelo do Protocolo de Manchester e regras determinísticas. +- Manter a lógica de domínio separada da infraestrutura. +- Preferir interfaces e casos de uso testáveis, como no padrão de `application/` + `infrastructure/`. +- Evitar acoplamento de widgets e UI com o cliente gerado do backend. +- Quando houver mudança em modelos ou endpoints, gerar a migração correspondente em vez de editar arquivos gerados manualmente. ### 5) Quando o trabalho for de UI -- Siga a linguagem visual do dark mode, alta legibilidade e baixo ruído visual. -- Use a lógica de cores somente para sinalizar gravidade: vermelho, amarelo e verde são sinais clínicos, não elementos decorativos. -- Mantenha foco em mobile-first e acessibilidade. +- Manter dark mode, alta legibilidade e baixo ruído visual. +- Usar cores apenas para sinal clínico; não decorar interfaces com vermelho/amarelo/verde sem relação com risco. +- Manter foco em mobile-first e acessibilidade. ### 6) Quando o trabalho for de backend ou dados -- Considere cache local em SQLite/sqflite para uso offline. -- Planeje sincronização com retry, fila e resolução de conflitos. -- Preserve auditabilidade e rastreabilidade dos eventos de triagem e sincronização. +- Considerar uso de SQLite/SQLCipher e filas locais para operação offline. +- Planejar retry, reconciliação de conflitos e rastreabilidade de eventos. +- Manter a lógica de sincronização e persistência consistentes com o fluxo de visitas do ACS. + +## Comandos e validações importantes + +Antes do primeiro ambiente local: +```bash +./scripts/dev/bootstrap_env.sh +``` + +Subir a stack local: +```bash +docker compose up --build +``` + +Rodar testes do backend: +```bash +cd backend +dart pub get +cd sinalacs_server +dart test +``` + +Rodar análise do Flutter nos apps: +```bash +cd apps/patient && flutter pub get && flutter analyze && flutter test +cd apps/acs && flutter pub get && flutter analyze && flutter test +``` + +Importante: +- `flutter test` é hermético e não substitui validações com stack local real; +- os testes de integração e validação de conexão vivem fora do `flutter test` e utilizam a stack Docker/VM; +- o CI do projeto valida jobs separados para backend e apps. + +## Observações finais + +Este repositório já não é apenas um conjunto de especificações. Ele contém um protótipo funcional validado localmente em stack Docker, com backend, apps reais e infraestrutura mínima operável para desenvolvimento. + +O estado atual não é produção: não há autenticação institucional real, não há mTLS no broker, e não existe deploy de produção concluído. Mesmo assim, qualquer mudança deve preservar a direção arquitetural do projeto e os invariantes de negócio definidos no PRD. -## Estrutura relevante do repositório +A documentação do produto, a arquitetura e os comandos de execução já estão no repositório; a implementação deve seguir o que está ali registrado e não inventar novos padrões sem alinhamento técnico e funcional. -- [spec/](spec) — artefatos de produto, arquitetura, requisitos e protótipos; -- [spec/ui_acs](spec/ui_acs) — fluxos do ACS; -- [spec/ui_paciente](spec/ui_paciente) — fluxos do paciente; -- [backend/](backend) — backend Serverpod; -- [apps/acs](apps/acs), [apps/patient](apps/patient) — apps Flutter reais; -- [docs/](docs) — documentação visual das telas. +## Graphify -## Observações para agentes +Este projeto possui um grafo de conhecimento em [graphify-out](graphify-out), com nós e relações entre arquivos e conceitos. -Este repositório já tem um protótipo funcional implementado (backend Serverpod, apps Flutter de paciente e ACS, CI validando os três), validado localmente via Docker Compose — não é mais só especificação/prototipação. Qualquer código adicionado deve refletir as decisões capturadas em [spec/stack.md](spec/stack.md), [spec/PRD_system.md](spec/PRD_system.md) e [spec/ui_design.md](spec/ui_design.md) quando ainda válidas. Atenção ao ler o [PROGRESS.md](PROGRESS.md): suas entradas M1.x e M2.x descrevem o servidor `dart:io` que existiu antes da migração para Serverpod, e seus links apontam para código que só existe no histórico do git — a seção "Migração para Serverpod", ao final daquele documento, registra o estado atual. O [CLAUDE.md](CLAUDE.md) é a referência de arquitetura e comandos. +Quando estiver explorando o código e precisar entender ligações entre módulos, prefira: +- `graphify query ""` quando o grafo existir; +- `graphify path "" ""` para traçar relações; +- `graphify explain ""` para foco em um tema específico; +- `graphify update .` após alterações de código para manter o grafo atualizado. -Se a tarefa solicitar implementação, priorize a consistência com os documentos acima e mantenha o comportamento alinhado ao MVP definido no PRD. +Se o diretório [graphify-out/wiki](graphify-out/wiki) existir, use-o para navegação ampla antes de navegar por arquivos isolados. From 7db32c5fcb4074df530c54903851ee5d97d16095 Mon Sep 17 00:00:00 2001 From: Herbert Rocha Date: Sat, 12 Sep 2026 17:37:59 -0400 Subject: [PATCH 08/43] fix(acs): preserve visit notes through offline queue and sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- apps/acs/lib/app/app.dart | 6 ++---- .../lib/core/database/encrypted_database.dart | 18 ++++++++++++++++-- .../core/database/sqlcipher_visit_store.dart | 2 ++ .../services/backend_visit_synchronizer.dart | 4 +++- .../core/services/offline_visit_queue.dart | 6 ++++++ apps/acs/test/login_flow_test.dart | 19 +++++++++++++++++++ 6 files changed, 48 insertions(+), 7 deletions(-) diff --git a/apps/acs/lib/app/app.dart b/apps/acs/lib/app/app.dart index 11fc37f..c1a38da 100644 --- a/apps/acs/lib/app/app.dart +++ b/apps/acs/lib/app/app.dart @@ -657,6 +657,7 @@ class _VisitRegistrationScreenState extends State { risk: alert.riskLevel, status: 'PENDENTE', outcome: outcome, + notes: notes.text.trim(), )); if (!mounted) return; setState(() {}); @@ -723,10 +724,7 @@ class _VisitRegistrationScreenState extends State { enabled: !semAlerta, decoration: const InputDecoration( labelText: 'Observações de campo', - // A tela coleta o texto e o descarta. Dizer isso é o mínimo enquanto - // o envio não existe; persistir texto livre num aparelho que pode ser - // roubado é decisão de LGPD que merece o próprio PR. - helperText: 'Ainda não é enviado ao servidor.', + helperText: 'São salvas localmente e enviadas com a visita quando sincronizar.', ), ), const SizedBox(height: 20), diff --git a/apps/acs/lib/core/database/encrypted_database.dart b/apps/acs/lib/core/database/encrypted_database.dart index 49202dc..9da5f4c 100644 --- a/apps/acs/lib/core/database/encrypted_database.dart +++ b/apps/acs/lib/core/database/encrypted_database.dart @@ -18,7 +18,7 @@ class EncryptedLocalDatabase { /// v1 existiu em duas formas em campo: `local_queue(id)` — o que um aparelho /// com a versão anterior instalada tem — e uma `offline_visits` com /// `patient_name`. v2 grava `patient_id`. - static const schemaVersion = 2; + static const schemaVersion = 3; /// Visitas registradas offline, aguardando sincronização. /// @@ -32,11 +32,12 @@ CREATE TABLE IF NOT EXISTS offline_visits ( risk TEXT NOT NULL, status TEXT NOT NULL, outcome TEXT NOT NULL, + notes TEXT NOT NULL DEFAULT '', created_at TEXT NOT NULL, version INTEGER NOT NULL )'''; - /// Migração v1 → v2. + /// Migração v1 → v2 e v2 → v3. /// /// Nenhuma das duas formas de v1 guarda o UUID do paciente: `patient_name` /// era `'Paciente ' + 8 dos 32 dígitos hex`, irreversível. Sem UUID, @@ -52,6 +53,19 @@ CREATE TABLE IF NOT EXISTS offline_visits ( await db.execute('DROP TABLE IF EXISTS local_queue'); await db.execute('DROP TABLE IF EXISTS offline_visits'); await db.execute(createOfflineVisits); + return; + } + + if (from < 3) { + final columns = await db.rawQuery( + "PRAGMA table_info('offline_visits')", + ); + final hasNotes = columns.any((column) => column['name'] == 'notes'); + if (!hasNotes) { + await db.execute( + "ALTER TABLE offline_visits ADD COLUMN notes TEXT NOT NULL DEFAULT '';", + ); + } } } diff --git a/apps/acs/lib/core/database/sqlcipher_visit_store.dart b/apps/acs/lib/core/database/sqlcipher_visit_store.dart index 052e0f8..75afce7 100644 --- a/apps/acs/lib/core/database/sqlcipher_visit_store.dart +++ b/apps/acs/lib/core/database/sqlcipher_visit_store.dart @@ -74,6 +74,7 @@ class SqlCipherVisitStore implements VisitStore { risk: row['risk']! as String, status: row['status']! as String, outcome: row['outcome']! as String, + notes: (row['notes'] as String?) ?? '', localId: row['local_id']! as String, createdAt: DateTime.parse(row['created_at']! as String), version: row['version']! as int, @@ -99,6 +100,7 @@ class SqlCipherVisitStore implements VisitStore { 'risk': visit.risk, 'status': visit.status, 'outcome': visit.outcome, + 'notes': visit.notes, 'created_at': visit.createdAt.toIso8601String(), 'version': visit.version, }); diff --git a/apps/acs/lib/core/services/backend_visit_synchronizer.dart b/apps/acs/lib/core/services/backend_visit_synchronizer.dart index 9746641..efab97b 100644 --- a/apps/acs/lib/core/services/backend_visit_synchronizer.dart +++ b/apps/acs/lib/core/services/backend_visit_synchronizer.dart @@ -23,7 +23,9 @@ class BackendVisitSynchronizer implements VisitSynchronizer { completedAt: visit.createdAt.toUtc(), status: visit.outcome.isEmpty ? visit.status : visit.outcome, riskLevelBefore: _riskLevel(visit.risk), - notes: const {}, + notes: visit.notes.trim().isEmpty + ? const {} + : {'campo': visit.notes.trim()}, version: visit.version, ), ]); diff --git a/apps/acs/lib/core/services/offline_visit_queue.dart b/apps/acs/lib/core/services/offline_visit_queue.dart index 59f3679..6ba90dd 100644 --- a/apps/acs/lib/core/services/offline_visit_queue.dart +++ b/apps/acs/lib/core/services/offline_visit_queue.dart @@ -30,6 +30,7 @@ class OfflineVisitRecord { required this.risk, required this.status, this.outcome = '', + this.notes = '', String? localId, DateTime? createdAt, this.version = 1, @@ -49,6 +50,9 @@ class OfflineVisitRecord { final String status; final String outcome; + /// Observações de campo registradas na visita antes da sincronização. + final String notes; + /// Identificador gerado no dispositivo. /// /// É a chave de deduplicação da sincronização: a tabela `visits` do servidor @@ -66,6 +70,7 @@ class OfflineVisitRecord { String? risk, String? status, String? outcome, + String? notes, String? localId, DateTime? createdAt, int? version, @@ -75,6 +80,7 @@ class OfflineVisitRecord { risk: risk ?? this.risk, status: status ?? this.status, outcome: outcome ?? this.outcome, + notes: notes ?? this.notes, localId: localId ?? this.localId, createdAt: createdAt ?? this.createdAt, version: version ?? this.version, diff --git a/apps/acs/test/login_flow_test.dart b/apps/acs/test/login_flow_test.dart index ddad15b..e1180e4 100644 --- a/apps/acs/test/login_flow_test.dart +++ b/apps/acs/test/login_flow_test.dart @@ -233,6 +233,25 @@ void main() { expect(queue.syncedCount, 0); }); + test('deve preservar observações no ciclo offline e ao sincronizar', () async { + final sender = FakeVisitSynchronizer(); + final queue = OfflineVisitQueue(synchronizer: sender); + + await queue.add(OfflineVisitRecord( + patientId: seedPatientId, + risk: 'yellow', + status: 'PENDENTE', + notes: 'Paciente com febre e náusea na última consulta.', + )); + + expect(queue.pendingVisits.single.notes, contains('febre')); + + final result = await queue.sync(); + + expect(result.kind, SyncOutcomeKind.synced); + expect(sender.batches.single.single.notes, contains('febre')); + }); + test('deve restaurar as visitas gravadas de uma execução anterior', () async { // Fechar o app não pode perder a fila. final store = InMemoryVisitStore(); From 29b3e0a684fe99f5ac63560830335b64be8cf07e Mon Sep 17 00:00:00 2001 From: Herbert Rocha Date: Sat, 12 Sep 2026 17:50:27 -0400 Subject: [PATCH 09/43] feat(acs): torna o mapa operacional e preserva notas no fluxo offline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- apps/acs/.flutter-plugins-dependencies | 2 +- apps/acs/lib/app/app.dart | 279 ++++++++++++++++++++++++- apps/acs/pubspec.lock | 76 ++++++- apps/acs/pubspec.yaml | 1 + apps/acs/test/map_screen_test.dart | 27 +++ 5 files changed, 380 insertions(+), 5 deletions(-) create mode 100644 apps/acs/test/map_screen_test.dart diff --git a/apps/acs/.flutter-plugins-dependencies b/apps/acs/.flutter-plugins-dependencies index abf95d4..45ffef8 100644 --- a/apps/acs/.flutter-plugins-dependencies +++ b/apps/acs/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"connectivity_plus","path":"/home/rock/.pub-cache/hosted/pub.dev/connectivity_plus-7.3.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_secure_storage","path":"/home/rock/.pub-cache/hosted/pub.dev/flutter_secure_storage-9.2.4/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"geolocator_apple","path":"/home/rock/.pub-cache/hosted/pub.dev/geolocator_apple-2.3.14/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"integration_test","path":"/home/rock/flutter/packages/integration_test/","native_build":true,"dependencies":[],"dev_dependency":true},{"name":"path_provider_foundation","path":"/home/rock/.pub-cache/hosted/pub.dev/path_provider_foundation-2.5.1/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqflite_darwin","path":"/home/rock/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqflite_sqlcipher","path":"/home/rock/.pub-cache/hosted/pub.dev/sqflite_sqlcipher-3.2.0/","native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"connectivity_plus","path":"/home/rock/.pub-cache/hosted/pub.dev/connectivity_plus-7.3.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_secure_storage","path":"/home/rock/.pub-cache/hosted/pub.dev/flutter_secure_storage-9.2.4/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"geolocator_android","path":"/home/rock/.pub-cache/hosted/pub.dev/geolocator_android-4.6.2/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"integration_test","path":"/home/rock/flutter/packages/integration_test/","native_build":true,"dependencies":[],"dev_dependency":true},{"name":"jni","path":"/home/rock/.pub-cache/hosted/pub.dev/jni-1.0.3/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"jni_flutter","path":"/home/rock/.pub-cache/hosted/pub.dev/jni_flutter-1.0.3/","native_build":true,"dependencies":["jni"],"dev_dependency":false},{"name":"path_provider_android","path":"/home/rock/.pub-cache/hosted/pub.dev/path_provider_android-2.3.1/","native_build":false,"dependencies":["jni","jni_flutter"],"dev_dependency":false},{"name":"sqflite_android","path":"/home/rock/.pub-cache/hosted/pub.dev/sqflite_android-2.4.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqflite_sqlcipher","path":"/home/rock/.pub-cache/hosted/pub.dev/sqflite_sqlcipher-3.2.0/","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"connectivity_plus","path":"/home/rock/.pub-cache/hosted/pub.dev/connectivity_plus-7.3.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_secure_storage_macos","path":"/home/rock/.pub-cache/hosted/pub.dev/flutter_secure_storage_macos-3.1.3/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"geolocator_apple","path":"/home/rock/.pub-cache/hosted/pub.dev/geolocator_apple-2.3.14/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/home/rock/.pub-cache/hosted/pub.dev/path_provider_foundation-2.5.1/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqflite_darwin","path":"/home/rock/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqflite_sqlcipher","path":"/home/rock/.pub-cache/hosted/pub.dev/sqflite_sqlcipher-3.2.0/","native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"connectivity_plus","path":"/home/rock/.pub-cache/hosted/pub.dev/connectivity_plus-7.3.1/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"flutter_secure_storage_linux","path":"/home/rock/.pub-cache/hosted/pub.dev/flutter_secure_storage_linux-1.2.3/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"jni","path":"/home/rock/.pub-cache/hosted/pub.dev/jni-1.0.3/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_linux","path":"/home/rock/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.2/","native_build":false,"dependencies":[],"dev_dependency":false}],"windows":[{"name":"connectivity_plus","path":"/home/rock/.pub-cache/hosted/pub.dev/connectivity_plus-7.3.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_secure_storage_windows","path":"/home/rock/.pub-cache/hosted/pub.dev/flutter_secure_storage_windows-3.1.2/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"geolocator_windows","path":"/home/rock/.pub-cache/hosted/pub.dev/geolocator_windows-0.2.5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"jni","path":"/home/rock/.pub-cache/hosted/pub.dev/jni-1.0.3/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_windows","path":"/home/rock/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[],"dev_dependency":false}],"web":[{"name":"connectivity_plus","path":"/home/rock/.pub-cache/hosted/pub.dev/connectivity_plus-7.3.1/","dependencies":[],"dev_dependency":false},{"name":"flutter_secure_storage_web","path":"/home/rock/.pub-cache/hosted/pub.dev/flutter_secure_storage_web-1.2.1/","dependencies":[],"dev_dependency":false},{"name":"geolocator_web","path":"/home/rock/.pub-cache/hosted/pub.dev/geolocator_web-4.1.4/","dependencies":[],"dev_dependency":false}]},"dependencyGraph":[{"name":"connectivity_plus","dependencies":[]},{"name":"flutter_secure_storage","dependencies":["flutter_secure_storage_linux","flutter_secure_storage_macos","flutter_secure_storage_web","flutter_secure_storage_windows"]},{"name":"flutter_secure_storage_linux","dependencies":[]},{"name":"flutter_secure_storage_macos","dependencies":[]},{"name":"flutter_secure_storage_web","dependencies":[]},{"name":"flutter_secure_storage_windows","dependencies":["path_provider"]},{"name":"geolocator","dependencies":["geolocator_android","geolocator_apple","geolocator_web","geolocator_windows"]},{"name":"geolocator_android","dependencies":[]},{"name":"geolocator_apple","dependencies":[]},{"name":"geolocator_web","dependencies":[]},{"name":"geolocator_windows","dependencies":[]},{"name":"integration_test","dependencies":[]},{"name":"jni","dependencies":[]},{"name":"jni_flutter","dependencies":["jni"]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":["jni","jni_flutter"]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"sqflite","dependencies":["sqflite_android","sqflite_darwin"]},{"name":"sqflite_android","dependencies":[]},{"name":"sqflite_darwin","dependencies":[]},{"name":"sqflite_sqlcipher","dependencies":[]}],"date_created":"2026-09-12 11:08:11.848787","version":"3.44.8","swift_package_manager_enabled":{"ios":false,"macos":false}} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"connectivity_plus","path":"/home/rock/.pub-cache/hosted/pub.dev/connectivity_plus-7.3.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_secure_storage","path":"/home/rock/.pub-cache/hosted/pub.dev/flutter_secure_storage-9.2.4/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"geolocator_apple","path":"/home/rock/.pub-cache/hosted/pub.dev/geolocator_apple-2.3.14/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"google_maps_flutter_ios","path":"/home/rock/.pub-cache/hosted/pub.dev/google_maps_flutter_ios-2.18.6/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"integration_test","path":"/home/rock/flutter/packages/integration_test/","native_build":true,"dependencies":[],"dev_dependency":true},{"name":"path_provider_foundation","path":"/home/rock/.pub-cache/hosted/pub.dev/path_provider_foundation-2.5.1/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqflite_darwin","path":"/home/rock/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqflite_sqlcipher","path":"/home/rock/.pub-cache/hosted/pub.dev/sqflite_sqlcipher-3.2.0/","native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"connectivity_plus","path":"/home/rock/.pub-cache/hosted/pub.dev/connectivity_plus-7.3.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_plugin_android_lifecycle","path":"/home/rock/.pub-cache/hosted/pub.dev/flutter_plugin_android_lifecycle-2.0.35/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_secure_storage","path":"/home/rock/.pub-cache/hosted/pub.dev/flutter_secure_storage-9.2.4/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"geolocator_android","path":"/home/rock/.pub-cache/hosted/pub.dev/geolocator_android-4.6.2/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"google_maps_flutter_android","path":"/home/rock/.pub-cache/hosted/pub.dev/google_maps_flutter_android-2.19.13/","native_build":true,"dependencies":["flutter_plugin_android_lifecycle"],"dev_dependency":false},{"name":"integration_test","path":"/home/rock/flutter/packages/integration_test/","native_build":true,"dependencies":[],"dev_dependency":true},{"name":"jni","path":"/home/rock/.pub-cache/hosted/pub.dev/jni-1.0.3/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"jni_flutter","path":"/home/rock/.pub-cache/hosted/pub.dev/jni_flutter-1.0.3/","native_build":true,"dependencies":["jni"],"dev_dependency":false},{"name":"path_provider_android","path":"/home/rock/.pub-cache/hosted/pub.dev/path_provider_android-2.3.1/","native_build":false,"dependencies":["jni","jni_flutter"],"dev_dependency":false},{"name":"sqflite_android","path":"/home/rock/.pub-cache/hosted/pub.dev/sqflite_android-2.4.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqflite_sqlcipher","path":"/home/rock/.pub-cache/hosted/pub.dev/sqflite_sqlcipher-3.2.0/","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"connectivity_plus","path":"/home/rock/.pub-cache/hosted/pub.dev/connectivity_plus-7.3.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_secure_storage_macos","path":"/home/rock/.pub-cache/hosted/pub.dev/flutter_secure_storage_macos-3.1.3/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"geolocator_apple","path":"/home/rock/.pub-cache/hosted/pub.dev/geolocator_apple-2.3.14/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/home/rock/.pub-cache/hosted/pub.dev/path_provider_foundation-2.5.1/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqflite_darwin","path":"/home/rock/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqflite_sqlcipher","path":"/home/rock/.pub-cache/hosted/pub.dev/sqflite_sqlcipher-3.2.0/","native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"connectivity_plus","path":"/home/rock/.pub-cache/hosted/pub.dev/connectivity_plus-7.3.1/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"flutter_secure_storage_linux","path":"/home/rock/.pub-cache/hosted/pub.dev/flutter_secure_storage_linux-1.2.3/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"jni","path":"/home/rock/.pub-cache/hosted/pub.dev/jni-1.0.3/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_linux","path":"/home/rock/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.2/","native_build":false,"dependencies":[],"dev_dependency":false}],"windows":[{"name":"connectivity_plus","path":"/home/rock/.pub-cache/hosted/pub.dev/connectivity_plus-7.3.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_secure_storage_windows","path":"/home/rock/.pub-cache/hosted/pub.dev/flutter_secure_storage_windows-3.1.2/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"geolocator_windows","path":"/home/rock/.pub-cache/hosted/pub.dev/geolocator_windows-0.2.5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"jni","path":"/home/rock/.pub-cache/hosted/pub.dev/jni-1.0.3/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_windows","path":"/home/rock/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[],"dev_dependency":false}],"web":[{"name":"connectivity_plus","path":"/home/rock/.pub-cache/hosted/pub.dev/connectivity_plus-7.3.1/","dependencies":[],"dev_dependency":false},{"name":"flutter_secure_storage_web","path":"/home/rock/.pub-cache/hosted/pub.dev/flutter_secure_storage_web-1.2.1/","dependencies":[],"dev_dependency":false},{"name":"geolocator_web","path":"/home/rock/.pub-cache/hosted/pub.dev/geolocator_web-4.1.4/","dependencies":[],"dev_dependency":false},{"name":"google_maps_flutter_web","path":"/home/rock/.pub-cache/hosted/pub.dev/google_maps_flutter_web-0.6.3+1/","dependencies":[],"dev_dependency":false}]},"dependencyGraph":[{"name":"connectivity_plus","dependencies":[]},{"name":"flutter_plugin_android_lifecycle","dependencies":[]},{"name":"flutter_secure_storage","dependencies":["flutter_secure_storage_linux","flutter_secure_storage_macos","flutter_secure_storage_web","flutter_secure_storage_windows"]},{"name":"flutter_secure_storage_linux","dependencies":[]},{"name":"flutter_secure_storage_macos","dependencies":[]},{"name":"flutter_secure_storage_web","dependencies":[]},{"name":"flutter_secure_storage_windows","dependencies":["path_provider"]},{"name":"geolocator","dependencies":["geolocator_android","geolocator_apple","geolocator_web","geolocator_windows"]},{"name":"geolocator_android","dependencies":[]},{"name":"geolocator_apple","dependencies":[]},{"name":"geolocator_web","dependencies":[]},{"name":"geolocator_windows","dependencies":[]},{"name":"google_maps_flutter","dependencies":["google_maps_flutter_android","google_maps_flutter_ios","google_maps_flutter_web"]},{"name":"google_maps_flutter_android","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"google_maps_flutter_ios","dependencies":[]},{"name":"google_maps_flutter_web","dependencies":[]},{"name":"integration_test","dependencies":[]},{"name":"jni","dependencies":[]},{"name":"jni_flutter","dependencies":["jni"]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":["jni","jni_flutter"]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"sqflite","dependencies":["sqflite_android","sqflite_darwin"]},{"name":"sqflite_android","dependencies":[]},{"name":"sqflite_darwin","dependencies":[]},{"name":"sqflite_sqlcipher","dependencies":[]}],"date_created":"2026-09-12 17:43:15.622135","version":"3.44.8","swift_package_manager_enabled":{"ios":false,"macos":false}} \ No newline at end of file diff --git a/apps/acs/lib/app/app.dart b/apps/acs/lib/app/app.dart index c1a38da..5e7cafb 100644 --- a/apps/acs/lib/app/app.dart +++ b/apps/acs/lib/app/app.dart @@ -2,6 +2,8 @@ import 'dart:async'; import 'dart:developer' as developer; import 'package:flutter/material.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:sinalacs_acs/app/acs_theme.dart'; import 'package:sinalacs_acs/core/network/backend_client.dart'; import 'package:sinalacs_acs/core/network/backend_scope.dart'; @@ -192,6 +194,7 @@ class _AcsHomeShellState extends State with WidgetsBindingObserver InfraNotice? _feedError; bool _feedErrorIsTransient = false; PrioritizedAlert? _selected; + LatLng? _currentPosition; /// Só existe enquanto uma falha transitória está sendo retentada. `null` /// quer dizer "nada agendado" — nem depois de um sucesso, nem depois de uma @@ -209,6 +212,7 @@ class _AcsHomeShellState extends State with WidgetsBindingObserver _feed.onConnectionChanged = _onBrokerConnectionChanged; _connectFeed(); _restoreVisits(); + _loadCurrentPosition(); } /// Recarrega as visitas gravadas em execuções anteriores. @@ -230,6 +234,27 @@ class _AcsHomeShellState extends State with WidgetsBindingObserver if (mounted) setState(() {}); } + Future _loadCurrentPosition() async { + try { + LocationPermission permission = await Geolocator.checkPermission(); + if (permission == LocationPermission.denied || permission == LocationPermission.deniedForever) { + permission = await Geolocator.requestPermission(); + if (permission == LocationPermission.denied || permission == LocationPermission.deniedForever) { + return; + } + } + + final position = await Geolocator.getCurrentPosition(); + if (!mounted) return; + setState(() { + _currentPosition = LatLng(position.latitude, position.longitude); + }); + } catch (_) { + // Localização não é crítica para a fila; sem GPS ou permissão, o mapa + // continua funcional com o centro do território em vez de quebrar. + } + } + /// Aviso de persistência, lido do estado corrente da fila. /// /// [OfflineVisitQueue] continua sendo um serviço puro (`import 'dart:math'` @@ -396,7 +421,11 @@ class _AcsHomeShellState extends State with WidgetsBindingObserver onEscalate: (alert) => setState(() { _selected = alert; destination = AcsDestination.escalation; }), onVisit: (alert) => setState(() { _selected = alert; destination = AcsDestination.visit; }), ), - AcsDestination.map => const MapScreen(), + AcsDestination.map => MapScreen( + queue: _queue, + currentPosition: _currentPosition, + apiKey: const String.fromEnvironment('GOOGLE_MAPS_API_KEY', defaultValue: ''), + ), AcsDestination.visit => VisitRegistrationScreen( alert: _selected, queue: widget.visitQueue, @@ -613,7 +642,253 @@ class _AlertCard extends StatelessWidget { } } -class MapScreen extends StatelessWidget { const MapScreen({super.key}); @override Widget build(BuildContext context) => _page([Container(height: 220, decoration: BoxDecoration(color: AcsColors.surface, borderRadius: BorderRadius.circular(8)), child: const Stack(children: [Align(alignment: Alignment(-.55, -.4), child: Icon(Icons.location_on, color: AcsColors.red, size: 38)), Align(alignment: Alignment(.55, .1), child: Icon(Icons.location_on, color: AcsColors.yellow, size: 38)), Align(alignment: Alignment(-.1, .65), child: Icon(Icons.location_on, color: AcsColors.green, size: 38)), Center(child: Text('Mapa demonstrativo'))])), const SizedBox(height: 16), const Text('Pinos representam a prioridade clínica no território.', textAlign: TextAlign.center), const SizedBox(height: 16), FilledButton(onPressed: () => _message(context, 'Traçado de rota depende da integração de mapas.'), child: const Text('Traçar rota eficiente'))]); } +class MapScreen extends StatefulWidget { + const MapScreen({ + required this.queue, + this.currentPosition, + this.apiKey = '', + super.key, + }); + + final AlertQueue queue; + final LatLng? currentPosition; + final String apiKey; + + static const LatLng _fallbackCenter = LatLng(-15.7942, -47.8828); + + @override + State createState() => _MapScreenState(); +} + +class _MapScreenState extends State { + PrioritizedAlert? _selectedAlert; + + @override + Widget build(BuildContext context) => AnimatedBuilder( + animation: widget.queue, + builder: (context, _) { + final hasApiKey = widget.apiKey.trim().isNotEmpty; + final center = widget.currentPosition ?? MapScreen._fallbackCenter; + final markers = { + if (widget.currentPosition != null) + Marker( + markerId: const MarkerId('acs_location'), + position: widget.currentPosition!, + icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueAzure), + infoWindow: const InfoWindow(title: 'Localização atual'), + ), + for (final alert in widget.queue.alerts) + Marker( + markerId: MarkerId('alert_${alert.alertId}'), + position: _markerPosition(alert), + icon: _markerColor(alert.riskLevel), + consumeTapEvents: true, + onTap: () { + setState(() => _selectedAlert = alert); + _showAlertDetail(context, alert); + }, + infoWindow: InfoWindow( + title: 'Paciente ${alert.patientId.substring(0, 8)}', + snippet: '${_riskLabel(alert.riskLevel)} • ${_time(alert.triggeredAt)}', + ), + ), + }; + + final map = hasApiKey + ? GoogleMap( + initialCameraPosition: CameraPosition(target: center, zoom: 13), + markers: markers, + myLocationEnabled: widget.currentPosition != null, + myLocationButtonEnabled: false, + mapToolbarEnabled: false, + onTap: (_) => setState(() => _selectedAlert = null), + ) + : Container( + height: 260, + decoration: const BoxDecoration( + color: AcsColors.surface, + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + child: const Center( + child: Padding( + padding: EdgeInsets.all(24), + child: Text( + 'Mapa operacional indisponível sem chave de API do Google Maps.', + textAlign: TextAlign.center, + ), + ), + ), + ); + + return _page([ + const Text('Mapa operacional', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), + const SizedBox(height: 8), + Text( + widget.currentPosition == null + ? 'Localização atual indisponível. Exibindo centro do território.' + : 'Localização atual', + ), + if (_selectedAlert != null) ...[ + const SizedBox(height: 12), + _AlertMapSummary(alert: _selectedAlert!), + ], + const SizedBox(height: 12), + SizedBox(height: 260, child: map), + const SizedBox(height: 12), + const Wrap( + spacing: 8, + runSpacing: 8, + children: [ + _LegendChip(color: AcsColors.red, label: 'Vermelho'), + _LegendChip(color: AcsColors.yellow, label: 'Amarelo'), + _LegendChip(color: AcsColors.green, label: 'Verde'), + ], + ), + const SizedBox(height: 16), + FilledButton( + onPressed: () => _message(context, 'Rota e geofencing serão integrados ao mapa real.'), + child: const Text('Traçar rota eficiente'), + ), + ]); + }, + ); + + void _showAlertDetail(BuildContext context, PrioritizedAlert alert) { + final riskLabel = _riskLabel(alert.riskLevel); + showModalBottomSheet( + context: context, + builder: (sheetContext) => SafeArea( + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Paciente ${alert.patientId.substring(0, 8)}', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18)), + const SizedBox(height: 8), + Text('$riskLabel • ${_time(alert.triggeredAt)}', style: TextStyle(color: _riskColor(alert.riskLevel))), + const SizedBox(height: 8), + Text('Microárea: ${alert.microAreaId.substring(0, 8)}'), + const SizedBox(height: 16), + SizedBox( + width: double.infinity, + child: FilledButton( + onPressed: () => Navigator.pop(sheetContext), + child: const Text('Fechar'), + ), + ), + ], + ), + ), + ), + ); + } + + BitmapDescriptor _markerColor(String riskLevel) { + final normalized = riskLevel.toLowerCase(); + switch (normalized) { + case 'red': + case 'vermelho': + return BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueRed); + case 'yellow': + case 'amarelo': + return BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueYellow); + case 'green': + case 'verde': + return BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueGreen); + default: + return BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueAzure); + } + } + + Color _riskColor(String riskLevel) => switch (riskLevel.toLowerCase()) { + 'red' || 'vermelho' => AcsColors.red, + 'yellow' || 'amarelo' => AcsColors.yellow, + 'green' || 'verde' => AcsColors.green, + _ => AcsColors.accent, + }; + + String _riskLabel(String riskLevel) => switch (riskLevel.toLowerCase()) { + 'red' || 'vermelho' => 'Vermelho', + 'yellow' || 'amarelo' => 'Amarelo', + 'green' || 'verde' => 'Verde', + _ => 'Não classificado', + }; + + LatLng _markerPosition(PrioritizedAlert alert) { + final hash = alert.locationHash; + final seed = hash.codeUnits.fold(0, (sum, code) => sum + code) % 1000; + final lat = -15.7942 + ((seed % 7) * 0.0025); + final lng = -47.8828 + (((seed / 7).floor() % 9) * 0.0035); + return LatLng(lat, lng); + } + + String _time(DateTime value) { + final local = value.toLocal(); + return '${local.hour.toString().padLeft(2, '0')}:${local.minute.toString().padLeft(2, '0')}'; + } +} + +class _AlertMapSummary extends StatelessWidget { + const _AlertMapSummary({required this.alert}); + + final PrioritizedAlert alert; + + @override + Widget build(BuildContext context) { + final color = switch (alert.riskLevel.toLowerCase()) { + 'red' || 'vermelho' => AcsColors.red, + 'yellow' || 'amarelo' => AcsColors.yellow, + 'green' || 'verde' => AcsColors.green, + _ => AcsColors.accent, + }; + + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: color.withValues(alpha: 0.6)), + ), + child: Row( + children: [ + Icon(Icons.location_on, color: color), + const SizedBox(width: 8), + Expanded( + child: Text( + 'Alerta selecionado: ${alert.patientId.substring(0, 8)} • ${switch (alert.riskLevel.toLowerCase()) { 'red' || 'vermelho' => 'Vermelho', 'yellow' || 'amarelo' => 'Amarelo', 'green' || 'verde' => 'Verde', _ => 'Não classificado' }}', + ), + ), + ], + ), + ); + } +} + +class _LegendChip extends StatelessWidget { + const _LegendChip({required this.color, required this.label}); + + final Color color; + final String label; + + @override + Widget build(BuildContext context) => Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(999), + border: Border.all(color: color.withValues(alpha: 0.6)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container(width: 10, height: 10, decoration: BoxDecoration(color: color, shape: BoxShape.circle)), + const SizedBox(width: 6), + Text(label), + ], + ), + ); +} class VisitRegistrationScreen extends StatefulWidget { const VisitRegistrationScreen({required this.queue, super.key, this.alert}); diff --git a/apps/acs/pubspec.lock b/apps/acs/pubspec.lock index 5a1a8b0..0f1089b 100644 --- a/apps/acs/pubspec.lock +++ b/apps/acs/pubspec.lock @@ -155,6 +155,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.2" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "3854fe5e3bff0b113c658f260b90c95dea17c92db0f2addeac2e343dd9969785" + url: "https://pub.dev" + source: hosted + version: "2.0.35" flutter_secure_storage: dependency: "direct main" description: @@ -266,6 +274,54 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.5" + google_maps: + dependency: transitive + description: + name: google_maps + sha256: "6811afff50dc410b860622ac89df125f5be1eada3ad69197e4c24dd18f2ef0e0" + url: "https://pub.dev" + source: hosted + version: "8.3.0" + google_maps_flutter: + dependency: "direct main" + description: + name: google_maps_flutter + sha256: "90f9d01eb282f996769defff95f15ebd8650a79c7e105c42a51a23f1376df612" + url: "https://pub.dev" + source: hosted + version: "2.18.0" + google_maps_flutter_android: + dependency: transitive + description: + name: google_maps_flutter_android + sha256: "50bf2abde65d1abbb4505c709b20a6d5e36436b1c4830d1f36bfc20fbef091bd" + url: "https://pub.dev" + source: hosted + version: "2.19.13" + google_maps_flutter_ios: + dependency: transitive + description: + name: google_maps_flutter_ios + sha256: "9638557fa31d451d64c815d415b685f9959e314606bfa5daf2a77bc63f1eba04" + url: "https://pub.dev" + source: hosted + version: "2.18.6" + google_maps_flutter_platform_interface: + dependency: transitive + description: + name: google_maps_flutter_platform_interface + sha256: "574d35a89847731b10cd93e18e9374c8fcd30c6a868255f70bf7196ad71cdfb9" + url: "https://pub.dev" + source: hosted + version: "2.16.1" + google_maps_flutter_web: + dependency: transitive + description: + name: google_maps_flutter_web + sha256: "903db0e985e831d3883cf0971dd5b4dbee83468b748c3753dc94da0e9a269b93" + url: "https://pub.dev" + source: hosted + version: "0.6.3+1" html: dependency: transitive description: @@ -495,6 +551,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.6" + sanitize_html: + dependency: transitive + description: + name: sanitize_html + sha256: "60257e2b8fd8b13b7c755065c2639a6c800031bb5bd3e30a007fc6739c77b2e1" + url: "https://pub.dev" + source: hosted + version: "2.2.0" serverpod_auth_core_client: dependency: transitive description: @@ -627,6 +691,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: a00e5f18bffc764f923e7dec1038527f7fe7a1791361a7117f0358193f13d53a + url: "https://pub.dev" + source: hosted + version: "2.1.2" string_scanner: dependency: transitive description: @@ -780,5 +852,5 @@ packages: source: hosted version: "3.1.4" sdks: - dart: ">=3.11.0 <4.0.0" - flutter: ">=3.38.0" + dart: ">=3.12.0 <4.0.0" + flutter: ">=3.44.0" diff --git a/apps/acs/pubspec.yaml b/apps/acs/pubspec.yaml index f6548b8..fed1877 100644 --- a/apps/acs/pubspec.yaml +++ b/apps/acs/pubspec.yaml @@ -29,6 +29,7 @@ dependencies: connectivity_plus: ^7.3.1 mqtt_client: ^10.0.0 geolocator: ^12.0.0 + google_maps_flutter: ^2.10.1 dev_dependencies: diff --git a/apps/acs/test/map_screen_test.dart b/apps/acs/test/map_screen_test.dart new file mode 100644 index 0000000..b6c8c2c --- /dev/null +++ b/apps/acs/test/map_screen_test.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:sinalacs_acs/app/app.dart'; +import 'package:sinalacs_acs/core/services/alert_queue.dart'; + +import 'support/fakes.dart'; + +void main() { + testWidgets('deve mostrar a localização atual do ACS e alertas no mapa', (tester) async { + final queue = AlertQueue(microAreaId: seedMicroAreaId); + queue.upsert(testAlert(alertId: 'alert-map-1', riskLevel: 'red')); + queue.upsert(testAlert(alertId: 'alert-map-2', riskLevel: 'yellow')); + + await tester.pumpWidget(MaterialApp( + home: MapScreen( + queue: queue, + currentPosition: const LatLng(0, 0), + apiKey: 'test-api-key', + ), + )); + + expect(find.text('Mapa operacional'), findsOneWidget); + expect(find.text('Localização atual'), findsOneWidget); + expect(find.textContaining('Vermelho'), findsOneWidget); + }); +} From d366b8bd095c809723650c5679ed0f8a7630e919 Mon Sep 17 00:00:00 2001 From: Herbert Rocha Date: Sat, 12 Sep 2026 18:22:07 -0400 Subject: [PATCH 10/43] feat(acs): tornar rota e geofencing funcionais no fluxo de visitas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- apps/acs/lib/app/app.dart | 283 ++++++++++++++++-- apps/acs/lib/core/services/route_service.dart | 78 +++++ apps/acs/test/login_flow_test.dart | 56 ++++ apps/acs/test/map_screen_test.dart | 81 +++++ apps/acs/test/route_service_test.dart | 43 +++ 5 files changed, 519 insertions(+), 22 deletions(-) create mode 100644 apps/acs/lib/core/services/route_service.dart create mode 100644 apps/acs/test/route_service_test.dart diff --git a/apps/acs/lib/app/app.dart b/apps/acs/lib/app/app.dart index 5e7cafb..b9211df 100644 --- a/apps/acs/lib/app/app.dart +++ b/apps/acs/lib/app/app.dart @@ -11,15 +11,25 @@ import 'package:sinalacs_acs/core/services/alert_feed.dart'; import 'package:sinalacs_acs/core/services/alert_queue.dart'; import 'package:sinalacs_acs/core/services/offline_visit_queue.dart'; import 'package:sinalacs_acs/core/services/reconnect_schedule.dart'; +import 'package:sinalacs_acs/core/services/route_service.dart'; import 'package:sinalacs_acs/core/services/visit_queue_factory.dart'; class SinalAcsApp extends StatefulWidget { - const SinalAcsApp({super.key, this.backend, this.feedBuilder, this.visitQueue}); + const SinalAcsApp({ + super.key, + this.backend, + this.feedBuilder, + this.visitQueue, + this.initialAlert, + this.currentPosition, + }); /// Injetáveis para teste. Em execução normal são as implementações reais. final AcsBackend? backend; final AlertFeed Function(AlertQueue queue)? feedBuilder; final OfflineVisitQueue? visitQueue; + final PrioritizedAlert? initialAlert; + final LatLng? currentPosition; @override State createState() => _SinalAcsAppState(); @@ -59,16 +69,26 @@ class _SinalAcsAppState extends State { home: LoginScreen( feedBuilder: widget.feedBuilder, visitQueue: _visitQueue, + initialAlert: widget.initialAlert, + initialPosition: widget.currentPosition, ), ), ); } class LoginScreen extends StatefulWidget { - const LoginScreen({required this.visitQueue, super.key, this.feedBuilder}); + const LoginScreen({ + required this.visitQueue, + super.key, + this.feedBuilder, + this.initialAlert, + this.initialPosition, + }); final AlertFeed Function(AlertQueue queue)? feedBuilder; final OfflineVisitQueue visitQueue; + final PrioritizedAlert? initialAlert; + final LatLng? initialPosition; @override State createState() => _LoginScreenState(); @@ -109,6 +129,8 @@ class _LoginScreenState extends State { acsId: session.userId, feedBuilder: widget.feedBuilder, visitQueue: widget.visitQueue, + initialAlert: widget.initialAlert, + initialPosition: widget.initialPosition, ), )); } on BackendFailure catch (failure) { @@ -168,11 +190,15 @@ class AcsHomeShell extends StatefulWidget { required this.visitQueue, super.key, this.feedBuilder, + this.initialAlert, + this.initialPosition, }); final String microAreaId; final String acsId; final AlertFeed Function(AlertQueue queue)? feedBuilder; + final PrioritizedAlert? initialAlert; + final LatLng? initialPosition; /// Obrigatória: a tela de visita usava `widget.visitQueue ?? OfflineVisitQueue()`, /// e um dia em que o shell fosse construído sem fila voltaria a descartar a @@ -205,6 +231,8 @@ class _AcsHomeShellState extends State with WidgetsBindingObserver @override void initState() { super.initState(); + _selected = widget.initialAlert; + _currentPosition ??= widget.initialPosition; WidgetsBinding.instance.addObserver(this); // Assinado antes de start(): uma queda depois de conectar chega aqui // sozinha, via autoReconnect do mqtt_client — é o que mantém o chip do @@ -425,13 +453,25 @@ class _AcsHomeShellState extends State with WidgetsBindingObserver queue: _queue, currentPosition: _currentPosition, apiKey: const String.fromEnvironment('GOOGLE_MAPS_API_KEY', defaultValue: ''), + onVisit: (alert) => setState(() { + _selected = alert; + destination = AcsDestination.visit; + }), ), AcsDestination.visit => VisitRegistrationScreen( alert: _selected, queue: widget.visitQueue, + currentPosition: _currentPosition, ), AcsDestination.escalation => EscalationScreen(alert: _selected), - AcsDestination.geofencing => const GeofencingScreen(), + AcsDestination.geofencing => GeofencingScreen( + queue: _queue, + currentPosition: _currentPosition, + onVisit: (alert) => setState(() { + _selected = alert; + destination = AcsDestination.visit; + }), + ), AcsDestination.notices => const NoticesScreen(), }), bottomNavigationBar: NavigationBar( @@ -647,21 +687,55 @@ class MapScreen extends StatefulWidget { required this.queue, this.currentPosition, this.apiKey = '', + this.onVisit, super.key, }); final AlertQueue queue; final LatLng? currentPosition; final String apiKey; + final void Function(PrioritizedAlert alert)? onVisit; static const LatLng _fallbackCenter = LatLng(-15.7942, -47.8828); + static LatLng alertPositionFor(PrioritizedAlert alert) { + final hash = alert.locationHash; + final seed = hash.codeUnits.fold(0, (sum, code) => sum + code) % 1000; + final lat = -15.7942 + ((seed % 7) * 0.0025); + final lng = -47.8828 + (((seed ~/ 7) % 9) * 0.0035); + return LatLng(lat, lng); + } + @override State createState() => _MapScreenState(); } class _MapScreenState extends State { PrioritizedAlert? _selectedAlert; + List _routePoints = const []; + RoutePlan? _routePlan; + final RouteService _routeService = const RouteService(speedKmh: 20); + + void _buildRouteFor([PrioritizedAlert? alert]) { + final target = alert ?? _selectedAlert ?? (widget.queue.alerts.isEmpty ? null : widget.queue.alerts.first); + final origin = widget.currentPosition; + if (target == null || origin == null) { + setState(() { + _routePoints = const []; + _routePlan = null; + }); + _message(context, 'Sem posição atual ou alerta disponível para traçar a rota.'); + return; + } + + final destination = _markerPosition(target); + final plan = _routeService.plan(origin: origin, destination: destination); + setState(() { + _selectedAlert = target; + _routePoints = plan.points; + _routePlan = plan; + }); + } @override Widget build(BuildContext context) => AnimatedBuilder( @@ -684,7 +758,7 @@ class _MapScreenState extends State { icon: _markerColor(alert.riskLevel), consumeTapEvents: true, onTap: () { - setState(() => _selectedAlert = alert); + _buildRouteFor(alert); _showAlertDetail(context, alert); }, infoWindow: InfoWindow( @@ -694,10 +768,21 @@ class _MapScreenState extends State { ), }; + final polylines = { + if (_routePoints.length >= 2) + Polyline( + polylineId: const PolylineId('route_active'), + points: _routePoints, + color: AcsColors.accent, + width: 6, + ), + }; + final map = hasApiKey ? GoogleMap( initialCameraPosition: CameraPosition(target: center, zoom: 13), markers: markers, + polylines: polylines, myLocationEnabled: widget.currentPosition != null, myLocationButtonEnabled: false, mapToolbarEnabled: false, @@ -732,9 +817,35 @@ class _MapScreenState extends State { const SizedBox(height: 12), _AlertMapSummary(alert: _selectedAlert!), ], + if (_routePlan != null) ...[ + const SizedBox(height: 8), + Text( + switch (_routePlan!.status) { + RouteStatus.complete => 'Rota concluída • ${_routePlan!.distanceKm.toStringAsFixed(1)} km • ETA ${_routePlan!.etaMinutes} min', + _ => 'Rota ativa • ${_routePlan!.distanceKm.toStringAsFixed(1)} km • ETA ${_routePlan!.etaMinutes} min', + }, + style: const TextStyle(color: AcsColors.accent, fontWeight: FontWeight.bold), + ), + if (_routePlan!.status == RouteStatus.complete && _selectedAlert != null) ...[ + const SizedBox(height: 8), + const Text( + 'Local alcançado. Você pode abrir o registro de visita agora.', + style: TextStyle(color: AcsColors.green, fontWeight: FontWeight.bold), + ), + ], + ], const SizedBox(height: 12), SizedBox(height: 260, child: map), const SizedBox(height: 12), + if (_routePlan != null && _routePlan!.status == RouteStatus.complete && _selectedAlert != null) ...[ + FilledButton.icon( + key: const Key('visit_at_destination'), + onPressed: () => widget.onVisit?.call(_selectedAlert!), + icon: const Icon(Icons.assignment_turned_in_outlined), + label: const Text('Registrar visita no local'), + ), + const SizedBox(height: 12), + ], const Wrap( spacing: 8, runSpacing: 8, @@ -746,7 +857,7 @@ class _MapScreenState extends State { ), const SizedBox(height: 16), FilledButton( - onPressed: () => _message(context, 'Rota e geofencing serão integrados ao mapa real.'), + onPressed: () => _buildRouteFor(), child: const Text('Traçar rota eficiente'), ), ]); @@ -770,9 +881,33 @@ class _MapScreenState extends State { const SizedBox(height: 8), Text('Microárea: ${alert.microAreaId.substring(0, 8)}'), const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: FilledButton( + onPressed: () { + Navigator.pop(sheetContext); + _buildRouteFor(alert); + }, + child: const Text('Traçar rota'), + ), + ), + const SizedBox(width: 8), + Expanded( + child: OutlinedButton( + onPressed: () { + Navigator.pop(sheetContext); + widget.onVisit?.call(alert); + }, + child: const Text('Ir para visita'), + ), + ), + ], + ), + const SizedBox(height: 12), SizedBox( width: double.infinity, - child: FilledButton( + child: TextButton( onPressed: () => Navigator.pop(sheetContext), child: const Text('Fechar'), ), @@ -815,13 +950,7 @@ class _MapScreenState extends State { _ => 'Não classificado', }; - LatLng _markerPosition(PrioritizedAlert alert) { - final hash = alert.locationHash; - final seed = hash.codeUnits.fold(0, (sum, code) => sum + code) % 1000; - final lat = -15.7942 + ((seed % 7) * 0.0025); - final lng = -47.8828 + (((seed / 7).floor() % 9) * 0.0035); - return LatLng(lat, lng); - } + LatLng _markerPosition(PrioritizedAlert alert) => MapScreen.alertPositionFor(alert); String _time(DateTime value) { final local = value.toLocal(); @@ -891,12 +1020,13 @@ class _LegendChip extends StatelessWidget { } class VisitRegistrationScreen extends StatefulWidget { - const VisitRegistrationScreen({required this.queue, super.key, this.alert}); + const VisitRegistrationScreen({required this.queue, super.key, this.alert, this.currentPosition}); /// A fila é recebida pronta, não construída aqui: uma instância por gravação /// descartava a visita assim que o callback retornava. final OfflineVisitQueue queue; final PrioritizedAlert? alert; + final LatLng? currentPosition; @override State createState() => _VisitRegistrationScreenState(); @@ -906,10 +1036,35 @@ class _VisitRegistrationScreenState extends State { String outcome = 'Realizada com sucesso'; final notes = TextEditingController(); bool _syncing = false; + bool _arrivalConfirmed = false; + + @override + void initState() { + super.initState(); + _arrivalConfirmed = _gpsArrivalMatches(); + } + + @override + void didUpdateWidget(covariant VisitRegistrationScreen oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.currentPosition != widget.currentPosition || oldWidget.alert != widget.alert) { + _arrivalConfirmed = _gpsArrivalMatches(); + } + } @override void dispose() { notes.dispose(); super.dispose(); } + bool _gpsArrivalMatches() { + final alert = widget.alert; + final position = widget.currentPosition; + if (alert == null || position == null) return false; + + final destination = MapScreen.alertPositionFor(alert); + const routeService = RouteService(); + return routeService.arrivalStatus(origin: position, destination: destination) == ArrivalStatus.arrived; + } + /// Rótulo montado a cada build a partir do alerta que está em memória. /// /// Nunca é gravado: o que vai para a fila é `alert.patientId` inteiro. Antes @@ -991,19 +1146,34 @@ class _VisitRegistrationScreenState extends State { ), ), const SizedBox(height: 16), - DropdownButtonFormField(initialValue: outcome, decoration: const InputDecoration(labelText: 'Status do atendimento'), items: const ['Realizada com sucesso', 'Paciente ausente', 'Recusou atendimento'].map((v) => DropdownMenuItem(value: v, child: Text(v))).toList(), onChanged: semAlerta ? null : (v) => setState(() => outcome = v!)), + if (!semAlerta) CheckboxListTile( + key: const Key('arrival_confirmation'), + value: _arrivalConfirmed, + onChanged: (value) => setState(() => _arrivalConfirmed = value ?? false), + title: const Text('Cheguei ao local e confirmei a presença do paciente.'), + contentPadding: EdgeInsets.zero, + ), + const SizedBox(height: 8), + DropdownButtonFormField(initialValue: outcome, decoration: const InputDecoration(labelText: 'Status do atendimento'), items: const ['Realizada com sucesso', 'Paciente ausente', 'Recusou atendimento'].map((v) => DropdownMenuItem(value: v, child: Text(v))).toList(), onChanged: semAlerta || !_arrivalConfirmed ? null : (v) => setState(() => outcome = v!)), const SizedBox(height: 16), TextField( controller: notes, maxLines: 4, - enabled: !semAlerta, + enabled: !semAlerta && _arrivalConfirmed, decoration: const InputDecoration( labelText: 'Observações de campo', helperText: 'São salvas localmente e enviadas com a visita quando sincronizar.', ), ), const SizedBox(height: 20), - FilledButton.icon(key: const Key('save_visit'), onPressed: semAlerta ? null : _save, icon: const Icon(Icons.save_outlined), label: const Text('Salvar e enfileirar sincronização')), + FilledButton.icon(key: const Key('save_visit'), onPressed: semAlerta || !_arrivalConfirmed ? null : _save, icon: const Icon(Icons.save_outlined), label: const Text('Salvar e enfileirar sincronização')), + if (!semAlerta && _arrivalConfirmed) ...[ + const SizedBox(height: 12), + const Text( + 'Local alcançado. Você pode registrar a visita agora.', + style: TextStyle(color: AcsColors.green, fontWeight: FontWeight.bold), + ), + ], const Divider(height: 32), Text( key: const Key('pending_visits_count'), @@ -1055,7 +1225,72 @@ class EscalationScreen extends StatelessWidget { ]); } } -class GeofencingScreen extends StatelessWidget { const GeofencingScreen({super.key}); @override Widget build(BuildContext context) => _page([const Icon(Icons.location_searching, size: 44), const SizedBox(height: 16), const Text('Check-in passivo', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), const SizedBox(height: 8), const Text('Proximidade demonstrativa: residência de Maria Oliveira, a 18 metros.'), const SizedBox(height: 20), FilledButton(onPressed: () => _message(context, 'Use a tela Visita para registrar o atendimento.'), child: const Text('Abrir formulário da visita'))]); } +class GeofencingScreen extends StatelessWidget { + const GeofencingScreen({ + required this.queue, + this.currentPosition, + this.onVisit, + super.key, + }); + + final AlertQueue queue; + final LatLng? currentPosition; + final void Function(PrioritizedAlert alert)? onVisit; + + @override + Widget build(BuildContext context) => AnimatedBuilder( + animation: queue, + builder: (context, _) { + final alert = queue.alerts.firstOrNull; + if (alert == null) { + return _page([ + const Icon(Icons.location_searching, size: 44), + const SizedBox(height: 16), + const Text('Check-in passivo', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), + const SizedBox(height: 8), + const Text('Nenhum alerta disponível para monitorar proximidade.'), + ]); + } + + final destination = MapScreen.alertPositionFor(alert); + const routeService = RouteService(); + final status = routeService.arrivalStatus(origin: currentPosition, destination: destination); + final distance = currentPosition == null + ? null + : routeService.distanceToDestination(origin: currentPosition!, destination: destination); + + return _page([ + const Icon(Icons.location_searching, size: 44), + const SizedBox(height: 16), + const Text('Check-in passivo', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), + const SizedBox(height: 12), + _InfoRow('Paciente', 'Paciente ${alert.patientId.substring(0, 8)}'), + _InfoRow('Risco', alert.riskLevel), + _InfoRow('Raio de chegada', '${(routeService.arrivalThresholdKm * 1000).round()} m'), + const SizedBox(height: 12), + Text( + switch (status) { + ArrivalStatus.unavailable => 'Localização atual indisponível. O check-in não pode ser confirmado.', + ArrivalStatus.approaching => 'A caminho do local${distance == null ? '' : ' • ${(distance * 1000).round()} m restantes'}.', + ArrivalStatus.arrived => 'Local alcançado. O registro da visita está liberado.', + }, + key: const Key('geofence_status'), + style: TextStyle( + color: status == ArrivalStatus.arrived ? AcsColors.green : AcsColors.accent, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 20), + FilledButton.icon( + key: const Key('geofence_visit'), + onPressed: status == ArrivalStatus.arrived ? () => onVisit?.call(alert) : null, + icon: const Icon(Icons.assignment_turned_in_outlined), + label: const Text('Abrir registro da visita'), + ), + ]); + }, + ); +} class NoticesScreen extends StatefulWidget { const NoticesScreen({super.key}); @override State createState() => _NoticesScreenState(); } class _NoticesScreenState extends State { final notice = TextEditingController(); @override void dispose() { notice.dispose(); super.dispose(); } @override Widget build(BuildContext context) => _page([const Text('Aviso comunitário', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), const SizedBox(height: 16), const TextField(decoration: InputDecoration(labelText: 'Público-alvo', hintText: 'Pacientes com condições crônicas')), const SizedBox(height: 16), TextField(controller: notice, maxLines: 4, decoration: const InputDecoration(labelText: 'Mensagem')), const SizedBox(height: 20), FilledButton(onPressed: () => _message(context, 'Envio depende da integração de notificações push.'), child: const Text('Preparar aviso'))]); } @@ -1107,7 +1342,11 @@ class _Header extends StatelessWidget implements PreferredSizeWidget { /// Sem `hideCurrentSnackBar`, a confirmação da gravação ficava 4 s na tela e o /// resultado da sincronização entrava na fila atrás dela: a pessoa tocava em /// "Sincronizar agora" e continuava lendo "visita salva". -void _message(BuildContext context, String message) => - ScaffoldMessenger.of(context) - ..hideCurrentSnackBar() - ..showSnackBar(SnackBar(content: Text(message))); \ No newline at end of file +void _message(BuildContext context, String message) { + final messenger = ScaffoldMessenger.maybeOf(context); + if (messenger == null) return; + + messenger + ..hideCurrentSnackBar() + ..showSnackBar(SnackBar(content: Text(message))); +} \ No newline at end of file diff --git a/apps/acs/lib/core/services/route_service.dart b/apps/acs/lib/core/services/route_service.dart new file mode 100644 index 0000000..6b4497e --- /dev/null +++ b/apps/acs/lib/core/services/route_service.dart @@ -0,0 +1,78 @@ +import 'dart:math'; + +import 'package:google_maps_flutter/google_maps_flutter.dart'; + +/// Estado do percurso calculado para o ACS. +enum RouteStatus { unavailable, active, complete } + +/// Estado de proximidade do ACS em relação ao destino da visita. +enum ArrivalStatus { unavailable, approaching, arrived } + +class RoutePlan { + const RoutePlan({ + required this.points, + required this.distanceKm, + required this.etaMinutes, + this.status = RouteStatus.active, + }); + + final List points; + final double distanceKm; + final int etaMinutes; + final RouteStatus status; +} + +class RouteService { + const RouteService({this.speedKmh = 20, this.arrivalThresholdKm = 0.15}); + + final double speedKmh; + final double arrivalThresholdKm; + + double distanceToDestination({required LatLng origin, required LatLng destination}) => + _distanceBetween(origin, destination); + + ArrivalStatus arrivalStatus({required LatLng? origin, required LatLng? destination}) { + if (origin == null || destination == null) return ArrivalStatus.unavailable; + return isAtDestination(origin: origin, destination: destination) + ? ArrivalStatus.arrived + : ArrivalStatus.approaching; + } + + bool isAtDestination({required LatLng origin, required LatLng destination}) => + distanceToDestination(origin: origin, destination: destination) <= arrivalThresholdKm; + + RoutePlan plan({required LatLng origin, required LatLng destination}) { + final points = []; + const segments = 6; + for (var i = 0; i <= segments; i++) { + final progress = i / segments; + final lat = origin.latitude + (destination.latitude - origin.latitude) * progress; + final lng = origin.longitude + (destination.longitude - origin.longitude) * progress; + points.add(LatLng(lat, lng)); + } + + final distanceKm = distanceToDestination(origin: origin, destination: destination); + final etaMinutes = distanceKm <= 0 ? 0 : (distanceKm / speedKmh * 60).round(); + final status = distanceKm <= arrivalThresholdKm ? RouteStatus.complete : RouteStatus.active; + + return RoutePlan( + points: points, + distanceKm: distanceKm, + etaMinutes: etaMinutes, + status: status, + ); + } + + double _distanceBetween(LatLng origin, LatLng destination) { + const earthRadiusKm = 6371.0; + final lat1 = origin.latitude * pi / 180; + final lat2 = destination.latitude * pi / 180; + final deltaLat = (destination.latitude - origin.latitude) * pi / 180; + final deltaLng = (destination.longitude - origin.longitude) * pi / 180; + + final a = (sin(deltaLat / 2) * sin(deltaLat / 2)) + + (cos(lat1) * cos(lat2) * sin(deltaLng / 2) * sin(deltaLng / 2)); + final c = 2 * atan2(sqrt(a), sqrt(1 - a)); + return earthRadiusKm * c; + } +} diff --git a/apps/acs/test/login_flow_test.dart b/apps/acs/test/login_flow_test.dart index e1180e4..9e066e1 100644 --- a/apps/acs/test/login_flow_test.dart +++ b/apps/acs/test/login_flow_test.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:sinalacs_acs/app/app.dart'; import 'package:sinalacs_acs/core/network/backend_client.dart'; import 'package:sinalacs_acs/core/services/alert_feed.dart'; @@ -339,6 +340,8 @@ void main() { await tester.tap(find.text('Iniciar rota de visita')); await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('arrival_confirmation'))); + await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('save_visit'))); await tester.pumpAndSettle(); @@ -370,6 +373,8 @@ void main() { await tester.tap(find.text('Iniciar rota de visita')); await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('arrival_confirmation'))); + await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('save_visit'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('pending_visits_count')), findsOneWidget); @@ -392,6 +397,53 @@ void main() { expect(find.text('Pendentes de sincronização: 0'), findsOneWidget); }); + testWidgets('deve exigir confirmação de chegada antes de salvar a visita', (tester) async { + final visitQueue = OfflineVisitQueue(); + late FakeAlertFeed feed; + + await tester.pumpWidget(SinalAcsApp( + backend: FakeAcsBackend(), + feedBuilder: (queue) => feed = FakeAlertFeed(queue), + visitQueue: visitQueue, + )); + + await tester.tap(find.byKey(const Key('login_button'))); + await tester.pumpAndSettle(); + + feed.deliver(testAlert(alertId: 'alerta-chegada', riskLevel: 'yellow')); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Iniciar rota de visita')); + await tester.pumpAndSettle(); + + expect(find.byKey(const Key('arrival_confirmation')), findsOneWidget); + final botao = tester.widget(find.byKey(const Key('save_visit'))); + expect(botao.onPressed, isNull); + }); + + testWidgets('deve informar explicitamente quando o ACS alcançou o local do paciente', (tester) async { + final visitQueue = OfflineVisitQueue(); + final alert = testAlert(alertId: 'alerta-geofence', riskLevel: 'yellow'); + final hash = alert.locationHash; + final seed = hash.codeUnits.fold(0, (sum, code) => sum + code) % 1000; + final destination = LatLng( + -15.7942 + ((seed % 7) * 0.0025), + -47.8828 + (((seed ~/ 7) % 9) * 0.0035), + ); + + await tester.pumpWidget(MaterialApp( + home: VisitRegistrationScreen( + queue: visitQueue, + alert: alert, + currentPosition: destination, + ), + )); + await tester.pumpAndSettle(); + + expect(find.text('Local alcançado. Você pode registrar a visita agora.'), findsOneWidget); + expect(find.byKey(const Key('arrival_confirmation')), findsOneWidget); + }); + testWidgets('sem alerta vinculado não deixa gravar', (tester) async { // `visits.sync` exige UUID de paciente: gravar aqui criaria um registro // que nunca sobe e, por isso, nunca sai do aparelho. @@ -437,6 +489,8 @@ void main() { await tester.tap(find.text('Iniciar rota de visita')); await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('arrival_confirmation'))); + await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('save_visit'))); await tester.pumpAndSettle(); await tester.pump(const Duration(seconds: 5)); @@ -521,6 +575,8 @@ void main() { await tester.pumpAndSettle(); await tester.tap(find.text('Iniciar rota de visita')); await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('arrival_confirmation'))); + await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('save_visit'))); await tester.pumpAndSettle(); diff --git a/apps/acs/test/map_screen_test.dart b/apps/acs/test/map_screen_test.dart index b6c8c2c..c1bef30 100644 --- a/apps/acs/test/map_screen_test.dart +++ b/apps/acs/test/map_screen_test.dart @@ -24,4 +24,85 @@ void main() { expect(find.text('Localização atual'), findsOneWidget); expect(find.textContaining('Vermelho'), findsOneWidget); }); + + testWidgets('deve renderizar rota ativa quando há posição atual e alerta selecionado', (tester) async { + final queue = AlertQueue(microAreaId: seedMicroAreaId); + final alert = testAlert(alertId: 'alert-map-route', riskLevel: 'red'); + queue.upsert(alert); + + await tester.pumpWidget(MaterialApp( + home: MapScreen( + queue: queue, + currentPosition: const LatLng(-15.79, -47.88), + apiKey: 'test-api-key', + ), + )); + + await tester.tap(find.text('Traçar rota eficiente')); + await tester.pump(); + + expect(find.textContaining('Rota ativa'), findsOneWidget); + }); + + testWidgets('deve indicar rota concluída quando o ACS chegou ao destino', (tester) async { + final queue = AlertQueue(microAreaId: seedMicroAreaId); + final alert = testAlert(alertId: 'alert-map-arrival', riskLevel: 'yellow'); + queue.upsert(alert); + + final hash = alert.locationHash; + final seed = hash.codeUnits.fold(0, (sum, code) => sum + code) % 1000; + final destination = LatLng( + -15.7942 + ((seed % 7) * 0.0025), + -47.8828 + (((seed ~/ 7) % 9) * 0.0035), + ); + + await tester.pumpWidget(MaterialApp( + home: MapScreen( + queue: queue, + currentPosition: destination, + apiKey: 'test-api-key', + ), + )); + + await tester.tap(find.text('Traçar rota eficiente')); + await tester.pump(); + + expect(find.textContaining('Rota concluída'), findsOneWidget); + expect(find.text('Registrar visita no local'), findsOneWidget); + }); + + testWidgets('geofencing bloqueia o registro sem localização atual', (tester) async { + final queue = AlertQueue(microAreaId: seedMicroAreaId); + queue.upsert(testAlert(alertId: 'alert-geofence-unavailable', riskLevel: 'yellow')); + + await tester.pumpWidget(MaterialApp( + home: GeofencingScreen(queue: queue), + )); + + expect(find.textContaining('Localização atual indisponível'), findsOneWidget); + expect(tester.widget(find.byKey(const Key('geofence_visit'))).onPressed, isNull); + }); + + testWidgets('geofencing libera o registro quando o ACS chega ao destino', (tester) async { + final queue = AlertQueue(microAreaId: seedMicroAreaId); + final alert = testAlert(alertId: 'alert-geofence-arrived', riskLevel: 'green'); + queue.upsert(alert); + + final destination = MapScreen.alertPositionFor(alert); + PrioritizedAlert? selected; + + await tester.pumpWidget(MaterialApp( + home: GeofencingScreen( + queue: queue, + currentPosition: destination, + onVisit: (value) => selected = value, + ), + )); + + expect(find.text('Local alcançado. O registro da visita está liberado.'), findsOneWidget); + expect(tester.widget(find.byKey(const Key('geofence_visit'))).onPressed, isNotNull); + + await tester.tap(find.byKey(const Key('geofence_visit'))); + expect(selected?.alertId, alert.alertId); + }); } diff --git a/apps/acs/test/route_service_test.dart b/apps/acs/test/route_service_test.dart new file mode 100644 index 0000000..54066a5 --- /dev/null +++ b/apps/acs/test/route_service_test.dart @@ -0,0 +1,43 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:sinalacs_acs/core/services/route_service.dart'; + +void main() { + const service = RouteService(arrivalThresholdKm: 0.15); + const destination = LatLng(-15.7942, -47.8828); + + test('sem origem ou destino informa que o geofence está indisponível', () { + expect( + service.arrivalStatus(origin: null, destination: destination), + ArrivalStatus.unavailable, + ); + expect( + service.arrivalStatus(origin: destination, destination: null), + ArrivalStatus.unavailable, + ); + }); + + test('fora do raio mantém o ACS a caminho', () { + const origin = LatLng(-15.79, -47.88); + + expect( + service.arrivalStatus(origin: origin, destination: destination), + ArrivalStatus.approaching, + ); + expect( + service.distanceToDestination(origin: origin, destination: destination), + greaterThan(service.arrivalThresholdKm), + ); + }); + + test('dentro do raio marca o ACS como chegado', () { + expect( + service.arrivalStatus(origin: destination, destination: destination), + ArrivalStatus.arrived, + ); + expect( + service.distanceToDestination(origin: destination, destination: destination), + 0, + ); + }); +} From b6067a3ae0d2edbc789e304a89e4685fd0bb8a12 Mon Sep 17 00:00:00 2001 From: Herbert Rocha Date: Sat, 12 Sep 2026 20:12:30 -0400 Subject: [PATCH 11/43] test(acs): adicionar testes de mapa e geofencing no emulator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- apps/acs/integration_test/map_flow_test.dart | 81 ++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 apps/acs/integration_test/map_flow_test.dart diff --git a/apps/acs/integration_test/map_flow_test.dart b/apps/acs/integration_test/map_flow_test.dart new file mode 100644 index 0000000..f0b5aa0 --- /dev/null +++ b/apps/acs/integration_test/map_flow_test.dart @@ -0,0 +1,81 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:sinalacs_acs/app/app.dart'; +import 'package:sinalacs_acs/core/services/alert_queue.dart'; + +const _microAreaId = '00000000-0000-4000-8000-000000000003'; +const _patientId = '00000000-0000-4000-8000-000000000001'; + +PrioritizedAlert _alert(String id) => PrioritizedAlert( + alertId: id, + patientId: _patientId, + microAreaId: _microAreaId, + riskLevel: 'yellow', + locationHash: 'sem-local-$id', + triggeredAt: DateTime.utc(2026, 9, 12, 8), + ); + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('mapa operacional mantém rota ativa no emulador', (tester) async { + final queue = AlertQueue(microAreaId: _microAreaId); + queue.upsert(_alert('map-active')); + + await tester.pumpWidget(MaterialApp( + home: MapScreen( + queue: queue, + currentPosition: const LatLng(-15.79, -47.88), + apiKey: '', + ), + )); + await tester.pumpAndSettle(); + + expect(find.text('Mapa operacional'), findsOneWidget); + expect(find.textContaining('Mapa operacional indisponível sem chave'), findsOneWidget); + + await tester.tap(find.text('Traçar rota eficiente')); + await tester.pumpAndSettle(); + + expect(find.textContaining('Rota ativa'), findsOneWidget); + queue.dispose(); + }); + + testWidgets('mapa e geofencing liberam visita quando o ACS chega', (tester) async { + final queue = AlertQueue(microAreaId: _microAreaId); + final alert = _alert('map-arrived'); + queue.upsert(alert); + final destination = MapScreen.alertPositionFor(alert); + PrioritizedAlert? selected; + + await tester.pumpWidget(MaterialApp( + home: MapScreen( + queue: queue, + currentPosition: destination, + apiKey: '', + ), + )); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Traçar rota eficiente')); + await tester.pumpAndSettle(); + expect(find.textContaining('Rota concluída'), findsOneWidget); + expect(find.byKey(const Key('visit_at_destination')), findsOneWidget); + + await tester.pumpWidget(MaterialApp( + home: GeofencingScreen( + queue: queue, + currentPosition: destination, + onVisit: (value) => selected = value, + ), + )); + await tester.pumpAndSettle(); + expect(find.text('Local alcançado. O registro da visita está liberado.'), findsOneWidget); + + await tester.tap(find.byKey(const Key('geofence_visit'))); + expect(selected?.alertId, alert.alertId); + queue.dispose(); + }); +} From edb8bab2a26244040c44800f132a0f2c906376be Mon Sep 17 00:00:00 2001 From: Herbert Rocha Date: Sat, 12 Sep 2026 20:23:46 -0400 Subject: [PATCH 12/43] =?UTF-8?q?feat(acs):=20permitir=20visita=20ap=C3=B3?= =?UTF-8?q?s=20escalonamento=20de=20alerta=20vermelho?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- apps/acs/integration_test/map_flow_test.dart | 29 ++++++++++++++++++++ apps/acs/lib/app/app.dart | 25 +++++++++++++++-- apps/acs/test/login_flow_test.dart | 23 ++++++++++++++++ 3 files changed, 75 insertions(+), 2 deletions(-) diff --git a/apps/acs/integration_test/map_flow_test.dart b/apps/acs/integration_test/map_flow_test.dart index f0b5aa0..e3d317a 100644 --- a/apps/acs/integration_test/map_flow_test.dart +++ b/apps/acs/integration_test/map_flow_test.dart @@ -17,6 +17,15 @@ PrioritizedAlert _alert(String id) => PrioritizedAlert( triggeredAt: DateTime.utc(2026, 9, 12, 8), ); +PrioritizedAlert _redAlert(String id) => PrioritizedAlert( + alertId: id, + patientId: _patientId, + microAreaId: _microAreaId, + riskLevel: 'red', + locationHash: 'sem-local-$id', + triggeredAt: DateTime.utc(2026, 9, 12, 8), + ); + void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); @@ -78,4 +87,24 @@ void main() { expect(selected?.alertId, alert.alertId); queue.dispose(); }); + + testWidgets('escalonamento mantém SAMU primário e encaminha alerta vermelho para visita', (tester) async { + final alert = _redAlert('red-escalation-visit'); + PrioritizedAlert? selected; + + await tester.pumpWidget(MaterialApp( + home: EscalationScreen( + alert: alert, + onVisit: (value) => selected = value, + ), + )); + await tester.pumpAndSettle(); + + expect(find.text('Ligar para o SAMU (192)'), findsOneWidget); + expect(find.text('Iniciar rota de visita'), findsOneWidget); + expect(find.textContaining('não substitui o acionamento do SAMU'), findsOneWidget); + + await tester.tap(find.byKey(const Key('escalation_visit'))); + expect(selected?.alertId, alert.alertId); + }); } diff --git a/apps/acs/lib/app/app.dart b/apps/acs/lib/app/app.dart index b9211df..3488aa0 100644 --- a/apps/acs/lib/app/app.dart +++ b/apps/acs/lib/app/app.dart @@ -463,7 +463,13 @@ class _AcsHomeShellState extends State with WidgetsBindingObserver queue: widget.visitQueue, currentPosition: _currentPosition, ), - AcsDestination.escalation => EscalationScreen(alert: _selected), + AcsDestination.escalation => EscalationScreen( + alert: _selected, + onVisit: (alert) => setState(() { + _selected = alert; + destination = AcsDestination.visit; + }), + ), AcsDestination.geofencing => GeofencingScreen( queue: _queue, currentPosition: _currentPosition, @@ -1204,9 +1210,10 @@ class _VisitRegistrationScreenState extends State { } class EscalationScreen extends StatelessWidget { - const EscalationScreen({super.key, this.alert}); + const EscalationScreen({super.key, this.alert, this.onVisit}); final PrioritizedAlert? alert; + final void Function(PrioritizedAlert alert)? onVisit; @override Widget build(BuildContext context) { @@ -1222,6 +1229,20 @@ class EscalationScreen extends StatelessWidget { FilledButton.icon(onPressed: () => _message(context, 'Discagem não está integrada neste protótipo.'), style: FilledButton.styleFrom(backgroundColor: AcsColors.red), icon: const Icon(Icons.call), label: const Text('Ligar para o SAMU (192)')), const SizedBox(height: 12), OutlinedButton(onPressed: () => _message(context, 'Encaminhamento será integrado à UBS.'), child: const Text('Encaminhar para UBS Central')), + if (current != null) ...[ + const SizedBox(height: 12), + OutlinedButton.icon( + key: const Key('escalation_visit'), + onPressed: onVisit == null ? null : () => onVisit!(current), + icon: const Icon(Icons.alt_route_outlined), + label: const Text('Iniciar rota de visita'), + ), + const SizedBox(height: 8), + const Text( + 'A visita é acompanhamento do caso e não substitui o acionamento do SAMU.', + style: TextStyle(color: AcsColors.accent), + ), + ], ]); } } diff --git a/apps/acs/test/login_flow_test.dart b/apps/acs/test/login_flow_test.dart index 9e066e1..a835914 100644 --- a/apps/acs/test/login_flow_test.dart +++ b/apps/acs/test/login_flow_test.dart @@ -421,6 +421,29 @@ void main() { expect(botao.onPressed, isNull); }); + testWidgets('deve iniciar a rota de visita do alerta vermelho após o escalonamento', (tester) async { + final alert = testAlert(alertId: 'alerta-vermelho-visita', riskLevel: 'red'); + PrioritizedAlert? selected; + + await tester.pumpWidget(MaterialApp( + home: EscalationScreen( + alert: alert, + onVisit: (value) => selected = value, + ), + )); + + expect(find.text('Ligar para o SAMU (192)'), findsOneWidget); + expect(find.text('Iniciar rota de visita'), findsOneWidget); + expect( + find.text('A visita é acompanhamento do caso e não substitui o acionamento do SAMU.'), + findsOneWidget, + ); + + await tester.tap(find.byKey(const Key('escalation_visit'))); + + expect(selected?.alertId, alert.alertId); + }); + testWidgets('deve informar explicitamente quando o ACS alcançou o local do paciente', (tester) async { final visitQueue = OfflineVisitQueue(); final alert = testAlert(alertId: 'alerta-geofence', riskLevel: 'yellow'); From b856585087891547117307c65b38e09c2f806ea5 Mon Sep 17 00:00:00 2001 From: Herbert Rocha Date: Sat, 12 Sep 2026 20:42:19 -0400 Subject: [PATCH 13/43] =?UTF-8?q?build(acs):=20configurar=20inje=C3=A7?= =?UTF-8?q?=C3=A3o=20segura=20da=20chave=20do=20Google=20Maps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .env.example | 10 ++++++++++ apps/acs/android/app/build.gradle.kts | 10 ++++++++++ apps/acs/android/app/src/main/AndroidManifest.xml | 3 +++ scripts/dev/run_acs.sh | 4 +++- 4 files changed, 26 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 33f7cdb..7290eaa 100644 --- a/.env.example +++ b/.env.example @@ -18,6 +18,16 @@ # · As senhas MQTT são regravadas a cada boot pelo mosquitto-init, então # trocá-las aqui basta. +# --------------------------------------------------------------------------- +# Google Maps Android SDK · consumido por: apps/acs +# +# Crie uma chave restrita no Google Cloud Console, habilite Maps SDK for +# Android, restrinja por package name/SHA-1 e mantenha o valor apenas no `.env`. +# O script de execução injeta a chave por dart-define e no manifest Android. +# Nunca versione uma chave real neste arquivo. +# --------------------------------------------------------------------------- +GOOGLE_MAPS_API_KEY= + # --------------------------------------------------------------------------- # Postgres da stack local · consumido por: postgres, serverpod, database-seed # --------------------------------------------------------------------------- diff --git a/apps/acs/android/app/build.gradle.kts b/apps/acs/android/app/build.gradle.kts index 20dab9e..8a469bf 100644 --- a/apps/acs/android/app/build.gradle.kts +++ b/apps/acs/android/app/build.gradle.kts @@ -1,5 +1,14 @@ import java.util.Base64 +fun dartDefineValue(name: String): String? { + val encoded = project.findProperty("dart-defines")?.toString() ?: return null + return encoded.split(",").firstNotNullOfOrNull { item -> + runCatching { + String(Base64.getDecoder().decode(item), Charsets.UTF_8) + }.getOrNull()?.takeIf { it.startsWith("$name=") }?.substringAfter('=') + } +} + plugins { id("com.android.application") // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. @@ -25,6 +34,7 @@ android { targetSdk = 36 versionCode = flutter.versionCode versionName = flutter.versionName + manifestPlaceholders["GOOGLE_MAPS_API_KEY"] = dartDefineValue("GOOGLE_MAPS_API_KEY") ?: "" } buildTypes { diff --git a/apps/acs/android/app/src/main/AndroidManifest.xml b/apps/acs/android/app/src/main/AndroidManifest.xml index 922c1a1..84c2c3a 100644 --- a/apps/acs/android/app/src/main/AndroidManifest.xml +++ b/apps/acs/android/app/src/main/AndroidManifest.xml @@ -30,6 +30,9 @@ + + + + + + + + + + + + + + + + + sinalacs_admin + + + + + + + diff --git a/apps/admin/web/manifest.json b/apps/admin/web/manifest.json new file mode 100644 index 0000000..660d789 --- /dev/null +++ b/apps/admin/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "sinalacs_admin", + "short_name": "sinalacs_admin", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} From 244099a402248c0d209618e96ff5dc46d9254714 Mon Sep 17 00:00:00 2001 From: vinimartinsufrr Date: Fri, 11 Sep 2026 11:14:21 -0400 Subject: [PATCH 26/43] fix(admin): remover uso decorativo de amarelo/verde (releitura da issue) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01WyMMKXSksyUUNXPieFpSfX --- apps/admin/lib/app/app.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/admin/lib/app/app.dart b/apps/admin/lib/app/app.dart index d02a5df..d78fbbd 100644 --- a/apps/admin/lib/app/app.dart +++ b/apps/admin/lib/app/app.dart @@ -59,11 +59,11 @@ class _LoginScreenState extends State { decoration: BoxDecoration( color: AdminColors.surfaceRaised, borderRadius: BorderRadius.circular(8), - border: Border.all(color: AdminColors.yellow), + border: Border.all(color: AdminColors.accent), ), child: const Row( children: [ - Icon(Icons.science_outlined, color: AdminColors.yellow), + Icon(Icons.science_outlined, color: AdminColors.accent), SizedBox(width: 12), Expanded( child: Text( @@ -331,8 +331,9 @@ class _MicroAreasScreenState extends State { title: Text(area.name), subtitle: Text('ACS: ${area.acsName} (${area.acsEnrollmentId})'), trailing: Chip( + avatar: Icon(area.acsActive ? Icons.check_circle_outline : Icons.remove_circle_outline, size: 18), label: Text(area.acsActive ? 'Ativo' : 'Sem ACS ativo'), - backgroundColor: area.acsActive ? AdminColors.green.withValues(alpha: 0.2) : AdminColors.surface, + backgroundColor: AdminColors.surface, ), ), ), From 0dc16510d3db425135b17d7f24df68cd5493dfe9 Mon Sep 17 00:00:00 2001 From: vinimartinsufrr Date: Sun, 13 Sep 2026 10:25:15 -0400 Subject: [PATCH 27/43] =?UTF-8?q?docs(admin):=20documentar=20por=20que=20o?= =?UTF-8?q?=20login=20n=C3=A3o=20chama=20auth.developmentLogin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01WyMMKXSksyUUNXPieFpSfX --- apps/admin/lib/app/app.dart | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/apps/admin/lib/app/app.dart b/apps/admin/lib/app/app.dart index d78fbbd..8b7b025 100644 --- a/apps/admin/lib/app/app.dart +++ b/apps/admin/lib/app/app.dart @@ -17,6 +17,13 @@ class SinalAdminApp extends StatelessWidget { ); } +/// Login local (não chama `auth.developmentLogin`). +/// +/// Investigado antes de decidir: `backend/sinalacs_server/lib/src/endpoints/auth_endpoint.dart` +/// só aceita `role: 'patient'` ou `role: 'acs'` — não existe usuário fixo de +/// desenvolvimento para `admin`, então a chamada real falharia com +/// AlertValidationException. Ligar isso de verdade exige uma mudança no +/// backend (fora do escopo desta issue); ver descrição do PR. class LoginScreen extends StatefulWidget { const LoginScreen({required this.dataSource, super.key}); From 457b1920a5b52d2e2adf95369227310bb37efe2d Mon Sep 17 00:00:00 2001 From: vinimartinsufrr Date: Sun, 13 Sep 2026 10:28:58 -0400 Subject: [PATCH 28/43] =?UTF-8?q?docs(admin):=20atualizar=20telas-admin.md?= =?UTF-8?q?=20sobre=20login=20(n=C3=A3o=20valida=20campos)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01WyMMKXSksyUUNXPieFpSfX --- docs/telas-admin.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/telas-admin.md b/docs/telas-admin.md index 8775de1..b0b5d65 100644 --- a/docs/telas-admin.md +++ b/docs/telas-admin.md @@ -10,7 +10,9 @@ O backoffice é desktop-first (`spec/PRD_system.md` §2.1): em telas ≥640px de ![Login do backoffice](screenshots/admin/01-login.png) -Formulário local com matrícula/CNS e senha, no mesmo padrão visual do login do ACS. Um banner fixo deixa explícito que não há autenticação institucional real (SSO/gov.br) nesta etapa — mesmo padrão dos apps de paciente e ACS, que também não chamam o backend hoje. +Formulário local (matrícula/CNS e senha pré-preenchidos, não validados — o botão avança independente do que está digitado) no mesmo padrão visual do login do ACS. Um banner fixo deixa explícito que não há autenticação institucional real (SSO/gov.br) nesta etapa. + +Paciente e ACS já autenticam de verdade contra `auth.developmentLogin`; o admin não, porque esse endpoint hoje só aceita `role: patient` ou `role: acs` — não existe usuário fixo de desenvolvimento para `admin` (ver `backend/sinalacs_server/lib/src/endpoints/auth_endpoint.dart`). Ligar isso de verdade exige uma mudança no backend, fora do escopo desta issue. ## Painel de indicadores From 42b63773731519efb06551797917e0150402e5a0 Mon Sep 17 00:00:00 2001 From: vinimartinsufrr Date: Sun, 13 Sep 2026 10:46:13 -0400 Subject: [PATCH 29/43] docs(admin): adicionar screenshots reais das 5 telas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01WyMMKXSksyUUNXPieFpSfX --- docs/screenshots/admin/01-login.png | Bin 0 -> 78475 bytes docs/screenshots/admin/02-indicadores.png | Bin 0 -> 86441 bytes docs/screenshots/admin/03-microareas.png | Bin 0 -> 102975 bytes docs/screenshots/admin/04-alertas.png | Bin 0 -> 125074 bytes docs/screenshots/admin/05-auditoria.png | Bin 0 -> 133462 bytes docs/telas-admin.md | 6 +----- 6 files changed, 1 insertion(+), 5 deletions(-) create mode 100644 docs/screenshots/admin/01-login.png create mode 100644 docs/screenshots/admin/02-indicadores.png create mode 100644 docs/screenshots/admin/03-microareas.png create mode 100644 docs/screenshots/admin/04-alertas.png create mode 100644 docs/screenshots/admin/05-auditoria.png diff --git a/docs/screenshots/admin/01-login.png b/docs/screenshots/admin/01-login.png new file mode 100644 index 0000000000000000000000000000000000000000..4a714dfe42cc1ad103cf287abc0f7ec9ec6473d6 GIT binary patch literal 78475 zcmb@ucTiK?8#am}76d#fsB}?Kihxo=7X$$*p@kw{q?gc(bWs5X5u^&C_ZC76F@yvK zDT4GGI?@TDhTiVxcs#%JeSh4UJ9GC;V3O>;_Fn64&-=d5%6m<9C2C4WN-{DsYUQU- zw8_XW1dx%DZ(KYNeDV_62m=0{ao1LoBP;D-UIgBpvv~}DOh#57b@|Yo0(gJP<*9)? z8QE2j)7P1CYqpnUWGA`GPaf;MG+iF^_J{PJG_UV~d$6|LP`sbKR;XIz8Qr+dUt|Vm z6gPNy8l+t!UXfGWyQLbP{w`|*LHUK&HRCMJxpQ&d-?+wje;gfsx+|P0NXb{V;rocy zCpyJ_1&bE7D+-u*|EN_LXaNBp;h*Cb&kt+ppEtmN9hSGa|Gm`=q1`|G?^{`V#f8iN zz9ZB9d^%R|ubQ7jZM^h$+SP*9tl{o8=`pRZr_suvbIMKbnVRWbmd7U&e|MpS^#{P{Y? zi{?^*yqp~UUw=J^o`0p%{D7+Q`x7JBzbB~khP}nn(w4+t%2>KcN%`MP^?*xRXu^<} z$awe~{&G0@vo^1Klg$>G=;)qpu#@Rr()rKlNxwAjau{R!a|s#Q*CIJNGw0_HH`6H1enfL?oLvTii9cK&rUMfPh^}^HrisO$6+))EzgZHh+rSVm{a+6a(@oG^@i6lv-@BU{-DSHlpC|U)de2-A)R`Iaj_| zd?@|$%gsUP&#H)pM>2`p9qwZKG9JfjJIjYs^$x&CJlpt04MgEry;ep&)xND}DO~1I zo^GCi5+&GYlfS=wW7td5A*jS)UY5?yIHD3(yzIJJ6B#9_Os-k?-l1M*?D<+s7ADHK zt$xX5dofk|%pz)O03HVDL1uoMui?Xo4`Xr|giQ5NPSV1zzwQjQxAuR3e}z|4(g+L# zA-$A2U*l6Hz4y{=fD_{4<7Y3MMR~9)^`JKmT01)QUc4}J&<{Z$Zt%v%d3-`LtU<88 zAI4u!dm?SSM~69;YlziGBDP)q{}b7oKvy2vVa&Dp^PTa~D{MT1pcD8 z>(i@`?AHj?{uihjUl1kCXBqc>UmHR~&0C1uYKx97T{1~dSZr_Z?13?5eCD$-4<8nc zMB^}^rEaj8_6L?X8;x#Wke{}gqBc?utzq+D%WV399&E4*-rSUPnqi9@w<BYjXcxJDjI0k#%BRAx%C^*?(wg<^;kefb zoKfm!G%y~R�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�Ia_ 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
  • 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�D>N$Y;*Q&7gd4Jk5XV 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

    C06+X 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�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

      $}5RrWSnu+yhe=5NI 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 LZ>C?@eevG_Ll^mK literal 0 HcmV?d00001 diff --git a/docs/telas-admin.md b/docs/telas-admin.md index b0b5d65..a490d6b 100644 --- a/docs/telas-admin.md +++ b/docs/telas-admin.md @@ -1,6 +1,6 @@ # Telas do Backoffice Admin -Documentação visual do protótipo Flutter do backoffice administrativo (`apps/admin`). Assim como `docs/telas-acs.md` e `docs/telas-paciente.md`, as imagens abaixo devem ser capturadas rodando o app (`flutter run -d chrome`, já que o alvo é Flutter Web) com dados sintéticos — **pendente nesta entrega**, ver observação no final. +Documentação visual do protótipo Flutter do backoffice administrativo (`apps/admin`). Assim como `docs/telas-acs.md` e `docs/telas-paciente.md`, as imagens abaixo foram capturadas rodando o app (`flutter run -d web-server`, já que o alvo é Flutter Web) com dados sintéticos. ## Navegação @@ -38,10 +38,6 @@ Lista de alertas filtrável por microárea e status. Não há nenhum controle de Log somente leitura. Toda visita às telas de Microáreas, Alertas e Auditoria registra uma entrada própria via `AdminDataSource.recordAccess`, simulando o requisito do PRD §4.2.2 de que o acesso do Administrador também é auditado. -## Pendência desta entrega - -As screenshots reais ainda não foram capturadas nesta máquina de desenvolvimento — ver descrição do PR para o motivo e o plano de captura. - ## Referências - [Implementação Flutter](../apps/admin/lib/app/app.dart) From 40fbe27534fffdf4fd5cb780ca1d6ad0ff75a0f3 Mon Sep 17 00:00:00 2001 From: sarah Date: Mon, 14 Sep 2026 10:13:51 -0400 Subject: [PATCH 30/43] docs: add UX and accessibility assessment report and evidence assets --- spec/assets/contrast_red_fail.png | Bin 0 -> 105809 bytes spec/assets/contrast_teal_pass.png | Bin 0 -> 81291 bytes spec/assets/contrast_white_pass.png | Bin 0 -> 106622 bytes spec/ux_accessibility_assessment.md | 107 ++++++++++++++++++++++++++++ 4 files changed, 107 insertions(+) create mode 100644 spec/assets/contrast_red_fail.png create mode 100644 spec/assets/contrast_teal_pass.png create mode 100644 spec/assets/contrast_white_pass.png create mode 100644 spec/ux_accessibility_assessment.md diff --git a/spec/assets/contrast_red_fail.png b/spec/assets/contrast_red_fail.png new file mode 100644 index 0000000000000000000000000000000000000000..aec3d2b27bd965cc23a3a3d0a67aa82af48f410e GIT binary patch literal 105809 zcmdSBWm}bD*EI@)pi(NRGzKY1cY`3^lF|qWNOz-%G$%O1o zJ@yaSANKpfxdX6eKWBMc{tWa!MHOxq!gE8TrPp}w`I7wUG2O|c)xr$QU#y=3=;ba8 z>ppcFtdcM9##HPKDJ99>aej9|BxG9UcpN&UJxGl_-z37O&qO)7Q9^n4)C%RkdS}I^ zg!XNtEv-^a;cJdn^L5&%eMsf8y0p#90hZoXC}mEo*Ie%f1i3=^Ht{Ut7>^#hPTdY7 z{Ut+tJheIKz;q@*Z8p¨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 Z;(>eLWS2zf8;FZF!2c+Nya%)M+ z_bzI(5>Qb1DSmM^3cW_yp0JiJxbaZl0M_E4ejSR$ zGw-h5J|p=AR70i_WA}EXLvZu!de7Zwelt-q{#fBdZ2K&>D@>n7*l5xoCC2N;!zp+6 zyZ1|<=DV_rlFDY9cmuK7#tD&`f=`&3-LW!=QQE_deQ@DazPezC8KxP}x4haWIL@lv z&rsEbz9)uXtupTWd?qV*@;J?leRa2oJ@f+Q!=&uC@JI0+EE%GsoQ?|=6j~qT4|ED6 z8WDIA#Z_9{)z-n%+SJ%q8;XO2m+^OP$^Y`Y4ipzBS0Zz`!GB(NF?BXKw} de6*` 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 literal 0 HcmV?d00001 diff --git a/spec/ux_accessibility_assessment.md b/spec/ux_accessibility_assessment.md new file mode 100644 index 0000000..9bfaf93 --- /dev/null +++ b/spec/ux_accessibility_assessment.md @@ -0,0 +1,107 @@ +# Relatório de Avaliação de UI/UX e Acessibilidade + +**Aplicativos Avaliados:** `apps/patient` e `apps/acs` +**Referência Baseline:** PRD de Sistemas (Seção 4.3 — WCAG 2.1 Nível AA), `spec/ui_design.md` +**Metodologia e Ferramentas:** WebAIM Contrast Checker, Auditoria Estática de Código (Dart AST/grep), Validação Prática via Android TalkBack em Dispositivo Físico USB e Especificação de Testes Remotos no UXtweak. + +--- + +## 1. Comparativo com a Baseline WCAG 2.1 AA (Seção 4.3 do PRD) + +| Critério WCAG 2.1 | Descrição do Critério | Requisito do PRD / Issue | Status | Resumo do Diagnóstico | +| :--- | :--- | :--- | :---: | :--- | +| **1.4.1 Color Use** | A cor não deve ser o único indicador visual de estado/risco | Duplo canal (Texto/Ícone + Cor) em sinais clínicos de risco | **Conforme** | Aplicado no paciente: rótulos explícitos `'Risco: Vermelho'`, `'Risco: Amarelo'` e `'Risco: Verde'` acompanhados de ícones. | +| **1.4.3 Contrast (Minimum)** | Razão de contraste min. de 4.5:1 (texto normal) e 3:1 (texto grande/UI) | ≥ 4.5:1 para texto sobre fundo escuro nos temas | **Não Conforme (Parcial)** | O vermelho (`#DC2626`) em texto atinge 4.16:1 sobre fundo `#030712`. O texto `Colors.white54` no Card da triagem atinge apenas ~3.2:1 (falha). | +| **2.4.7 Focus Visible** | Indicador claro de foco visual ao navegar por campos interativos | Foco visível em todos os elementos selecionáveis | **Conforme** | Testado via TalkBack em dispositivo Android: moldura de seleção envolve 100% dos elementos sem cortes visuais. | +| **2.5.5 Target Size** | Alvo de toque adequado para interatividade | ≥ 48x48 dp (padrão), ≥ 60x60 dp (botão de emergência/pânico) | **Conforme** | Botão de pânico mede `208x208 dp`. Botões de formulário e login medem `48x52 dp`. | +| **4.1.2 Name, Role, Value** | Árvore semântica exposta para leitores de tela nativos | Rótulos e papeis em 100% dos fluxos críticos | **Conforme** | Árvore semântica nativa do Flutter expõe abas (**Área, Fila, Mapa, Visita, Mais**) e formulários com clareza. | + +--- + +## 2. Evidências Técnicas e Achados por Critério + +### 2.1 Contraste e Uso de Cor (WCAG 1.4.3 e 1.4.1) + +Auditoria realizada comparando as cores declaradas em `acs_theme.dart` e `patient_theme.dart` com as superfícies reais de renderização utilizando o WebAIM Contrast Checker: + +#### Tabela de Razão de Contraste Renderizado + +| Aplicativo | Par de Cores (Texto vs Fundo) | Contexto de Uso | Razão Real (WebAIM) | Exigência WCAG | Status | +| :--- | :--- | :--- | :---: | :---: | :---: | +| **Ambos** | Branco (`#FFFFFF`) / Background (`#030712`) | Texto principal / AppBar | **20.13:1** | 4.5:1 | Conforme | +| **ACS** | Amarelo (`#F59E0B`) / Background (`#030712`) | Risco Médio / Status | **9.37:1** | 4.5:1 | Conforme | +| **ACS** | Verde (`#10B981`) / Background (`#030712`) | Risco Baixo / Status | **7.93:1** | 4.5:1 | Conforme | +| **Paciente** | Teal Accent (`#0D9488`) / Background (`#030712`) | Elementos primários | **5.37:1** | 4.5:1 | Conforme | +| **Ambos** | Vermelho (`#DC2626`) / Background (`#030712`) | Alertas e textos críticos | **4.16:1** | 4.5:1 | Falha em Texto Normal | +| **Paciente** | Branco 54% opaco (`Colors.white54`) / Card (`#1E293B`) | Rodapé explicativo da triagem | **~3.2:1** | 4.5:1 | Falha (Texto pequeno) | + +#### Evidências Visuais de Validação no WebAIM + +![Validação de Contraste da Cor Vermelha no WebAIM](assets/contrast_red_fail.png) +*Figura 2.1: Teste de contraste entre a cor vermelha (#DC2626) e o fundo escuro (#030712) no WebAIM, indicando razão de 4.16:1 (Falha para Normal Text no WCAG AA).* + +![Validação de Contraste da Cor Teal no WebAIM](assets/contrast_teal_pass.png) +*Figura 2.2: Teste de contraste entre o tom Teal (#0D9488) e o fundo escuro (#030712) no WebAIM, aprovado com razão de 5.37:1.* + +![Validação de Contraste da Cor Branca no WebAIM](assets/contrast_white_pass.png) +*Figura 2.3: Teste de contraste entre a cor branca (#FFFFFF) e o fundo escuro (#030712) no WebAIM, aprovado com razão de 20.13:1.* + +#### Validação de Duplo Canal (WCAG 1.4.1) +No arquivo `apps/patient/lib/app/app.dart` (linhas 503-560), o componente `_TriageResult` utiliza rótulos de texto explícitos concatenados ao sinal visual: `'Risco: Vermelho'`, `'Risco: Amarelo'` e `'Risco: Verde'`. + +--- + +### 2.2 Alvos de Toque (WCAG 2.5.5) + +| App | Componente / Fluxo | Dimensão Renderizada | Mínimo Requerido | Status | +| :--- | :--- | :---: | :---: | :---: | +| **Paciente** | Botão de Emergência/Pânico (`panic_button`) | `208 x 208 dp` | 60 x 60 dp | Conforme (Amplo) | +| **Paciente** | Botões de Login e Envio de Triagem | `48 x 52 dp` | 48 x 48 dp | Conforme | +| **ACS** | Ações de Login, Confirmar Recebimento e Sincronizar | `48 x 52 dp` | 48 x 48 dp | Conforme | +| **ACS** | Ações de Emergência do SAMU ("Acionar/Ligar SAMU") | `48 x 52 dp` | 60 x 60 dp | Sugestão de Destaque | + +--- + +### 2.3 Rótulos Semânticos e Navegação por Leitor de Tela (WCAG 4.1.2) + +#### Inspeção Estática de Código +* **Instâncias de `Semantics()`:** Mapeadas nos fluxos de Login ACS (`app.dart:159`), Login Paciente (`app.dart:127`) e Botão de Pânico (`app.dart:327`). +* **Instâncias de `tooltip:`:** Mapeados em `apps/patient/lib/app/app.dart` nas linhas 606 (`'Enviar mensagem'`) e 696 (`'Novo alarme'`). + +#### Teste Prático com Android TalkBack (Dispositivo Físico USB - ACS) +* **Navegação por Varredura nas Abas:** O leitor anunciou perfeitamente o nome e o papel semântico de todas as guias da barra inferior: **"Área"**, **"Fila"**, **"Mapa"**, **"Visita"** e **"Mais"** (ex: *"Mapa, guia 3 de 5, selecionado"*). +* **Fluxo de Visita e Formulários:** O motor semântico nativo do Flutter converteu os widgets `Text`, `Icon` e `IconButton` em elementos acessíveis sem interrupções. + +--- + +### 2.4 Indicador de Foco Visível (WCAG 2.4.7) + +* **Validação Prática:** Realizada via varredura com TalkBack habilitado em dispositivo Android físico conectado via USB no aplicativo ACS. +* **Resultado:** O indicador de foco nativo do sistema (moldura verde de destaque) acompanhou com precisão todos os alvos tocáveis, sem truncamento ou ocultação visual. + +--- + +## 3. Matriz de Priorização de Achados e Sugestões de Ajustes + +| Prioridade | Critério | Localização | Problema Identificado | Sugestão Concreta de UI / Código | +| :---: | :---: | :--- | :--- | :--- | +| **Alta** | 1.4.3 | `patient_app.dart` (linha 542) | `Colors.white54` sobre Card resulta em contraste de **~3.2:1** (falha WCAG AA para 12pt). | Substituir `color: Colors.white54` por `color: Colors.white70` ou `color: Color(0xFFE2E8F0)` (~5.2:1). | +| **Média** | 1.4.3 | `acs_theme.dart` e `patient_theme.dart` | Vermelho `danger` (`#DC2626`) como cor de texto sobre fundo escuro resulta em **4.16:1**. | Ajustar a constante da cor de perigo para `Color(0xFFEF4444)` (eleva a razão de contraste para **~5.7:1**). | +| **Média** | 2.5.5 | `apps/acs/lib/app/app.dart` | Botões de acionamento crítico do SAMU usam tamanho padrão (`48x52 dp`). | Aplicar `minSize` estendido de `60x60 dp` nas ações do SAMU para corresponder ao padrão tátil de emergência. | +| **Baixa** | 4.1.2 | `apps/acs/lib/app/app.dart` | Cartões de pacientes e badges de risco dependem da árvore automática. | Envolver os cartões do dashboard em widgets `Semantics(label: "Paciente X, Risco Vermelho Alto", ...)` explícitos. | + +--- + +## 4. Planejamento de Testes de Usabilidade Remota (UXtweak) + +Para complementar a auditoria técnica de acessibilidade com testes de fricção de UX com usuários reais, foram estruturados os seguintes cenários no **UXtweak**: + +### Cenário 1: Disparo de Alerta de Urgência (App Paciente) +* **Objetivo:** Avaliar o tempo de reação e a taxa de sucesso no acionamento do botão de pânico. +* **Métrica:** Tempo até o primeiro clique (*First-Click*) e taxa de conclusão sem erros. +* **Instrução dada ao usuário:** *"Você está se sentindo mal e precisa acionar o atendimento de emergência imediatamente. Qual botão você pressiona?"* + +### Cenário 2: Priorização na Fila de Atendimento (App ACS) +* **Objetivo:** Avaliar a clareza da visualização dos sinais de risco em ambiente com baixo ruído visual. +* **Métrica:** Taxa de cliques corretos no paciente de maior risco da lista. +* **Instrução dada ao usuário:** *"No seu painel de atendimento, identifique o paciente que exige visita prioritária imediata e acesse os detalhes dele."* From 8c5215ac50d8d30e62f463886154ab1c7c92a705 Mon Sep 17 00:00:00 2001 From: Herbert Rocha Date: Mon, 14 Sep 2026 12:11:20 -0400 Subject: [PATCH 31/43] feat: cadeia de hash em audit_logs e estado terminal para visita recusada MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .env.example | 16 + CLAUDE.md | 14 +- PROGRESS.md | 65 +- apps/acs/lib/app/app.dart | 50 + .../lib/core/database/encrypted_database.dart | 36 +- .../core/database/sqlcipher_visit_store.dart | 11 +- .../core/services/offline_visit_queue.dart | 96 +- apps/acs/test/encrypted_database_test.dart | 98 + apps/acs/test/login_flow_test.dart | 139 +- .../lib/src/protocol/audit_log.dart | 49 + .../lib/src/protocol/enums/sync_status.dart | 11 +- .../bin/audit_chain_check.dart | 52 + .../src/application/audit/audit_chain.dart | 88 + .../audit/audit_chain_verifier.dart | 108 + .../lib/src/application/sync/sync_fsm.dart | 10 + .../visits/visit_sync_service.dart | 23 +- .../lib/src/config/app_config.dart | 59 +- .../lib/src/generated/audit_log.dart | 94 + .../lib/src/generated/enums/sync_status.dart | 11 +- .../lib/src/generated/protocol.dart | 31 + .../database/orm_audit_chain_reader.dart | 37 + .../database/orm_audit_trail.dart | 73 +- .../lib/src/models/audit_log.spy.yaml | 23 + .../lib/src/models/enums/sync_status.spy.yaml | 7 + .../lib/src/runtime/alert_runtime.dart | 5 +- .../20260914154730381/definition.json | 2750 +++++++++++++++++ .../20260914154730381/definition.sql | 565 ++++ .../20260914154730381/definition_project.json | 1414 +++++++++ .../20260914154730381/migration.json | 171 + .../20260914154730381/migration.sql | 56 + .../migrations/migration_registry.txt | 1 + .../patient_directory_and_territory_test.dart | 46 +- .../integration/red_alert_cycle_test.dart | 1 + .../test/unit/app_config_test.dart | 92 +- .../test/unit/audit_chain_test.dart | 292 ++ .../test/unit/sync_fsm_test.dart | 29 + .../test/unit/visit_sync_service_test.dart | 15 +- docker-compose.yml | 3 + scripts/dev/bootstrap_env.sh | 4 +- spec/PRD_system.md | 7 +- spec/lgpd_design.md | 4 +- 41 files changed, 6572 insertions(+), 84 deletions(-) create mode 100644 backend/sinalacs_server/bin/audit_chain_check.dart create mode 100644 backend/sinalacs_server/lib/src/application/audit/audit_chain.dart create mode 100644 backend/sinalacs_server/lib/src/application/audit/audit_chain_verifier.dart create mode 100644 backend/sinalacs_server/lib/src/infrastructure/database/orm_audit_chain_reader.dart create mode 100644 backend/sinalacs_server/migrations/20260914154730381/definition.json create mode 100644 backend/sinalacs_server/migrations/20260914154730381/definition.sql create mode 100644 backend/sinalacs_server/migrations/20260914154730381/definition_project.json create mode 100644 backend/sinalacs_server/migrations/20260914154730381/migration.json create mode 100644 backend/sinalacs_server/migrations/20260914154730381/migration.sql create mode 100644 backend/sinalacs_server/test/unit/audit_chain_test.dart diff --git a/.env.example b/.env.example index 7290eaa..6302626 100644 --- a/.env.example +++ b/.env.example @@ -76,6 +76,22 @@ MQTT_CERT_SAN_EXTRA= # openssl rand -hex 32 JWT_SECRET= +# --------------------------------------------------------------------------- +# Cadeia de hash de audit_logs · consumido por: serverpod +# +# Chave HMAC que encadeia cada linha de audit_logs à anterior (ver +# backend/sinalacs_server/lib/src/application/audit/). Detecta edição, remoção +# ou reordenação de linhas — inclusive por quem tem acesso de escrita direto ao +# Postgres, que é por isso que este segredo NÃO pode ser derivado do +# JWT_SECRET nem reaproveitado dele. +# +# ATENÇÃO: rotacionar este valor invalida a verificação de TUDO que já foi +# gravado na trilha até aqui — ao contrário do JWT_SECRET, que pode ser trocado +# livremente. Trocar exige aceitar que a cadeia anterior à troca não verifica +# mais contra o segredo novo. +# openssl rand -hex 32 +AUDIT_CHAIN_SECRET= + # --------------------------------------------------------------------------- # Comportamento · consumido por: serverpod # --------------------------------------------------------------------------- diff --git a/CLAUDE.md b/CLAUDE.md index 7191cbd..d48353b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -41,8 +41,8 @@ baked into the versioned file. [.env.example](.env.example) is the full reference for every variable. The script generates random per-machine values for `POSTGRES_PASSWORD`, -`TEST_DATABASE_PASSWORD`, `MQTT_BACKEND_PASSWORD`, `MQTT_ACS_PASSWORD` and -`JWT_SECRET`, writes `backend/sinalacs_server/config/passwords.yaml` with the +`TEST_DATABASE_PASSWORD`, `MQTT_BACKEND_PASSWORD`, `MQTT_ACS_PASSWORD`, +`JWT_SECRET` and `AUDIT_CHAIN_SECRET`, writes `backend/sinalacs_server/config/passwords.yaml` with the same test-database password, and `chmod 600` on both. It never overwrites existing files without `--force`, and warns when `pg_data/` predates a rotation (`POSTGRES_PASSWORD` only takes effect on the volume's first init — to adopt a @@ -67,7 +67,7 @@ cd backend dart pub get dart analyze cd sinalacs_server -dart test # 48 tests: 37 unit + 11 integration +dart test # 83 tests: 69 unit + 14 integration dart test test/unit # hermetic only, no database needed dart test test/unit/red_alert_service_test.dart # single test file ``` @@ -94,9 +94,9 @@ serverpod create-migration Run the server directly: `dart run backend/sinalacs_server/bin/main.dart`. Serverpod reads `config/*.yaml` plus `config/passwords.yaml`, and every setting can be overridden by environment variable: `SERVERPOD_DATABASE_HOST`/`_PORT`/`_NAME`/`_USER`/`_PASSWORD`/`_REQUIRE_SSL`, `SERVERPOD_APPLY_MIGRATIONS` (applies migrations at boot — this is what replaced the manual `psql` steps), `SERVERPOD_REDIS_ENABLED` (Redis is optional and off), and `SERVERPOD_INSIGHTS_SERVER_PORT` (remapped to 8083 in `docker-compose.yml`, because Serverpod's default 8081 is the Traefik dashboard here). -MQTT is not part of Serverpod and keeps its own env vars, read by `AppConfig.fromEnvironment()` in `backend/sinalacs_server/lib/src/config/app_config.dart`: `MQTT_BROKER`/`MQTT_USERNAME`/`MQTT_PASSWORD`/`MQTT_USE_TLS`/`MQTT_CA_CERT_PATH`, plus `JWT_SECRET`, `APP_ENV` and `ENABLE_DEV_LOGIN` (default `false` — gates `auth.developmentLogin`, which fails as if the route did not exist when off). +MQTT is not part of Serverpod and keeps its own env vars, read by `AppConfig.fromEnvironment()` in `backend/sinalacs_server/lib/src/config/app_config.dart`: `MQTT_BROKER`/`MQTT_USERNAME`/`MQTT_PASSWORD`/`MQTT_USE_TLS`/`MQTT_CA_CERT_PATH`, plus `JWT_SECRET`, `AUDIT_CHAIN_SECRET`, `APP_ENV` and `ENABLE_DEV_LOGIN` (default `false` — gates `auth.developmentLogin`, which fails as if the route did not exist when off). -Outside `development`, boot fails fast when `JWT_SECRET` is absent, empty, blank, or equal to `AppConfig.developmentJwtSecret` — that fallback lives in versioned code, so it is public, and the token it signs carries the role and micro-area. The rules are in `_resolveJwtSecret` and covered by `test/unit/app_config_test.dart`; `AppConfig.fromMap()` exists so they are testable without touching `Platform.environment`. +Outside `development`, boot fails fast when `JWT_SECRET` or `AUDIT_CHAIN_SECRET` is absent, empty, blank, or equal to its own development fallback (`AppConfig.developmentJwtSecret` / `AppConfig.developmentAuditChainSecret`) — those fallbacks live in versioned code, so they're public, and `JWT_SECRET` signs tokens that carry role and micro-area while `AUDIT_CHAIN_SECRET` keys the `audit_logs` hash chain. The two are deliberately independent secrets — rotating one must not affect the other. The shared validation logic is `_resolveSecret`, covered by `test/unit/app_config_test.dart`; `AppConfig.fromMap()` exists so the rules are testable without touching `Platform.environment`. The dev seed (`sinalacs_server/lib/src/infrastructure/database/seeds/development.sql`) is not optional for a running stack: `auth.developmentLogin` issues tokens for fixed UUIDs, and `alerts.patientId` has a foreign key to `patients` — without the seed, `alerts.createRedAlert` fails with a foreign-key violation. In `docker-compose.yml` the `database-seed` service applies it after the server is healthy. @@ -148,14 +148,14 @@ Never hand-edit anything under `lib/src/generated/` or `migrations/` — run `se Key pattern: application services depend on abstract interfaces (`AlertPublisher`, `AlertStore`) defined alongside them in `application/`, implemented by `infrastructure/`. Follow this when adding new use cases — keep `application/` testable without real Postgres/MQTT (see how `test/unit/red_alert_service_test.dart` fakes both). -**Serverpod is RPC, not REST**, so there are no URL routes to match: the generated client calls methods. Endpoints: `health.check` (returns `{status, mqttConnected, dbConnected}`; answers as soon as the server is up, independent of MQTT/DB state), `auth.developmentLogin` (throws `EndpointDisabledException` unless `ENABLE_DEV_LOGIN=true`, preserving the old 404-not-403 semantics), `alerts.createRedAlert` (idempotency key is a method parameter, not a header; throws `AlertDispatchUnavailableException` if the MQTT dispatcher isn't connected), `alerts.acknowledge`, `triage.evaluate`, `visits.sync` (batch upload of visits registered offline by the ACS; deduplicated by the device-generated `localId`, which has a unique index on `visits`, version-checked — a mismatched `version` returns `SyncStatus.conflict` and never overwrites — and territory-checked against the patient's own micro-area, not just the caller's), and `patients.listMicroArea` (the ACS's routine-visit patient picker; the micro-area comes from the caller's token, never a parameter, and every call is written to `audit_logs`). Errors are typed exceptions declared in `.spy.yaml` and serialized to the client, replacing HTTP status codes. MQTT connects in the background after boot (non-blocking) with exponential-backoff auto-reconnect, so the server stays responsive even if the broker is unreachable — this matters on free-tier hosts that sleep/hibernate. +**Serverpod is RPC, not REST**, so there are no URL routes to match: the generated client calls methods. Endpoints: `health.check` (returns `{status, mqttConnected, dbConnected}`; answers as soon as the server is up, independent of MQTT/DB state), `auth.developmentLogin` (throws `EndpointDisabledException` unless `ENABLE_DEV_LOGIN=true`, preserving the old 404-not-403 semantics), `alerts.createRedAlert` (idempotency key is a method parameter, not a header; throws `AlertDispatchUnavailableException` if the MQTT dispatcher isn't connected), `alerts.acknowledge`, `triage.evaluate`, `visits.sync` (batch upload of visits registered offline by the ACS; deduplicated by the device-generated `localId`, which has a unique index on `visits`, version-checked — a mismatched `version` returns `SyncStatus.conflict` and never overwrites — and territory-checked against the patient's own micro-area, not just the caller's; a malformed identifier, a territory mismatch, or a visit owned by another ACS all return the terminal `SyncStatus.rejected`, distinct from the retryable `SyncStatus.error` used for things like an unknown patient, so the device queue knows which failures are worth retrying), and `patients.listMicroArea` (the ACS's routine-visit patient picker; the micro-area comes from the caller's token, never a parameter, and every call is written to `audit_logs`, whose rows are hash-chained — `AuditChain`/`AuditChainVerifier` in `application/audit/`, keyed by `AUDIT_CHAIN_SECRET` — so tampering with a row is detectable even by someone with direct Postgres write access; `bin/audit_chain_check.dart` verifies the chain on demand). Errors are typed exceptions declared in `.spy.yaml` and serialized to the client, replacing HTTP status codes. MQTT connects in the background after boot (non-blocking) with exponential-backoff auto-reconnect, so the server stays responsive even if the broker is unreachable — this matters on free-tier hosts that sleep/hibernate. ### Flutter apps (`apps/acs/`, `apps/patient/`) Both follow the same skeleton: `lib/main.dart` → `lib/app/app.dart` (+ `*_theme.dart` for the dark, high-legibility, low-noise visual language — see `spec/ui_design.md`) → `lib/core/`. Shared `core/` concerns: - `database/encrypted_database.dart` (ACS only) — local persistence via `sqflite_sqlcipher`. **INV-04 of the PRD says health data is never persisted in plaintext**, so opening outside Android/iOS throws unless the caller passes `allowUnencryptedForTesting: true`. That flag is deliberately embarrassing to type: `grep` for it and you see every place that gave up the guarantee — today, only VM tests. It used to be the *default* behaviour off-mobile, which is why the old test named "deve abrir banco criptografado" proved nothing on Linux CI. - `security/database_key_store.dart` (ACS only) — the 256-bit key lives in the Android Keystore / iOS Keychain (`flutter_secure_storage`), never in code. **Not** derived from a PIN, though `spec/PRD_system.md` 4.2.3 prescribes PBKDF2-from-PIN: no PIN flow exists in the apps, and the decision plus its reversibility is recorded in `spec/lgpd_design.md` §5.1. `SqlCipherVisitStore` recovers from a lost key (reinstall, backup restore) by discarding the unreadable file instead of leaving the app permanently stuck. - **Encryption is only actually proven on a device**: `apps/acs/integration_test/encrypted_storage_test.dart` reads the raw database file and asserts it neither starts with `SQLite format 3` nor contains the visit content. `flutter test` cannot prove this — on Linux everything goes through the unencrypted FFI path, which is the trap that produced the original gap. -- ACS-only, in `apps/acs/lib/core/services/`: `mqtt_secure_client.dart` (TCP/TLS MQTT client on 8883 — the broker's only published listener; trusts the dev CA from an asset via `setTrustedCertificatesBytes`, keeps hostname verification on, uses a persistent session with a stable client id so the broker re-delivers alerts that arrived while the device was offline), `alert_feed.dart` (builds the MQTT config from the session and feeds `alert_queue.dart`; classifies failures into `AlertFeedFailure.transient` — a missing password/CA asset or a refused credential is not, an unreachable broker is), `reconnect_schedule.dart` (pure 2s→60s exponential backoff, same scale as the server's `MqttAlertDispatcher`, with no `Timer` of its own so it is testable without a fake clock — used by `AcsHomeShell` in `app.dart` to retry the *first* connection to the broker on its own when the failure is transient, with a "Tentar agora" button on the banner and an immediate retry on returning from the background; a permanent failure, such as a missing password, is not retried. `mqtt_client`'s own `autoReconnect` only takes over *after* a first successful connection — before that there is nothing to reconnect, which is why this existed as a real gap: a device that opened the app without signal used to need a restart to ever receive the alerts the broker was already holding for it with QoS 1), `offline_visit_queue.dart` (offline-first visit queue backed by a `VisitStore` — in production `SqlCipherVisitStore`, so visits survive closing the app; a store failure sets `persistenceFailed` and the queue keeps working in RAM rather than blocking field work, surfaced as its **own** banner (`Key('storage_error')`) alongside — never instead of — the broker one, plus an inline warning on the visit screen; the two used to share one slot through a `??`, so a field outage hid the fact that visits were not being saved — pushed to `visits.sync` through `backend_visit_synchronizer.dart`, wired in production by `visit_queue_factory.dart`; a conflict returns to the queue instead of being discarded, a per-visit `error` also stays pending but is reported with the server's message, and a network failure keeps the batch pending). An `OfflineVisitRecord` carries the patient's **UUID**, never a display label: when the visit comes from an alert the screen builds `Paciente <8 hex>` at render time, `visits.sync` rejects anything that is not a UUID, and the local schema (`offline_visits`, v2) has `patient_id`. The backend only ever publishes `riskLevel: 'red'` (emergency, routed to escalation), so a routine visit — the PRD's actual usage pattern, ≥8/ACS/day — cannot start from an alert at all; `patients.listMicroArea` (backend, territorialized by the caller's token, never a client-supplied micro-area) and the picker it feeds in `VisitRegistrationScreen` (`Key('patient_picker')`) exist for that path, showing name and chronic conditions per `spec/lgpd_design.md`'s routine-visit minimization rule — the picked name lives only in memory for the label, never written to `offline_visits` or logged. The red-alert path keeps its own way in too: the escalation screen's "Iniciar rota de visita" (`Key('escalation_visit')`) — the ACS visits after triggering SAMU, not instead of it. `VisitSyncService` also checks that the patient's micro-area matches the ACS's, not just that the ACS is territorialized; a mismatch is `SyncStatus.error` per-visit (never discards the rest of the batch) and writes an `audit_logs` row (`result: denied_territory`) per `spec/lgpd_design.md`'s access-monitoring requirement — the first writer that table ever had. `EncryptedLocalDatabase._upgrade` drops and recreates the table on v1 → v2; **once the app has a real release, that migration must preserve rows instead**. Sync is triggered by hand from the visit screen ("Sincronizar agora" plus pending/conflict counters), not automatically, `network_chaos_simulator.dart` (injects latency/jitter/packet loss/partition for testing offline resilience — see `apps/acs/test/network_chaos_test.dart`). +- ACS-only, in `apps/acs/lib/core/services/`: `mqtt_secure_client.dart` (TCP/TLS MQTT client on 8883 — the broker's only published listener; trusts the dev CA from an asset via `setTrustedCertificatesBytes`, keeps hostname verification on, uses a persistent session with a stable client id so the broker re-delivers alerts that arrived while the device was offline), `alert_feed.dart` (builds the MQTT config from the session and feeds `alert_queue.dart`; classifies failures into `AlertFeedFailure.transient` — a missing password/CA asset or a refused credential is not, an unreachable broker is), `reconnect_schedule.dart` (pure 2s→60s exponential backoff, same scale as the server's `MqttAlertDispatcher`, with no `Timer` of its own so it is testable without a fake clock — used by `AcsHomeShell` in `app.dart` to retry the *first* connection to the broker on its own when the failure is transient, with a "Tentar agora" button on the banner and an immediate retry on returning from the background; a permanent failure, such as a missing password, is not retried. `mqtt_client`'s own `autoReconnect` only takes over *after* a first successful connection — before that there is nothing to reconnect, which is why this existed as a real gap: a device that opened the app without signal used to need a restart to ever receive the alerts the broker was already holding for it with QoS 1), `offline_visit_queue.dart` (offline-first visit queue backed by a `VisitStore` — in production `SqlCipherVisitStore`, so visits survive closing the app; a store failure sets `persistenceFailed` and the queue keeps working in RAM rather than blocking field work, surfaced as its **own** banner (`Key('storage_error')`) alongside — never instead of — the broker one, plus an inline warning on the visit screen; the two used to share one slot through a `??`, so a field outage hid the fact that visits were not being saved — pushed to `visits.sync` through `backend_visit_synchronizer.dart`, wired in production by `visit_queue_factory.dart`; a conflict returns to the queue instead of being discarded, a per-visit retryable `error` also stays pending and is reported with the server's message, a network failure keeps the batch pending, and a per-visit terminal `rejected` leaves the retry queue but stays visible on the device (`rejectedCount`/`rejectedVisits`, `Key('rejected_visits_count')`) with the server's reason until the ACS explicitly discards it (`discardRejected()`, `Key('discard_rejected')`, behind a confirmation dialog) — nothing removes a rejected visit from disk on its own). An `OfflineVisitRecord` carries the patient's **UUID**, never a display label: when the visit comes from an alert the screen builds `Paciente <8 hex>` at render time, `visits.sync` rejects anything that is not a UUID, and the local schema (`offline_visits`, v4) has `patient_id` plus a nullable `rejection_reason`. The backend only ever publishes `riskLevel: 'red'` (emergency, routed to escalation), so a routine visit — the PRD's actual usage pattern, ≥8/ACS/day — cannot start from an alert at all; `patients.listMicroArea` (backend, territorialized by the caller's token, never a client-supplied micro-area) and the picker it feeds in `VisitRegistrationScreen` (`Key('patient_picker')`) exist for that path, showing name and chronic conditions per `spec/lgpd_design.md`'s routine-visit minimization rule — the picked name lives only in memory for the label, never written to `offline_visits` or logged. The red-alert path keeps its own way in too: the escalation screen's "Iniciar rota de visita" (`Key('escalation_visit')`) — the ACS visits after triggering SAMU, not instead of it. `VisitSyncService` also checks that the patient's micro-area matches the ACS's, not just that the ACS is territorialized; a mismatch is the terminal `SyncStatus.rejected` per-visit (never discards the rest of the batch) and writes an `audit_logs` row (`result: denied_territory`) per `spec/lgpd_design.md`'s access-monitoring requirement — the first writer that table ever had. `EncryptedLocalDatabase._upgrade` drops and recreates the table on v1 → v2 — acceptable only because the app had not shipped a release yet; from v3 → v4 onward the migration is additive (`ALTER TABLE ... ADD COLUMN`) and preserves existing rows, which is what any migration after a real release must do. Sync is triggered by hand from the visit screen ("Sincronizar agora" plus pending/conflict counters), not automatically, `network_chaos_simulator.dart` (injects latency/jitter/packet loss/partition for testing offline resilience — see `apps/acs/test/network_chaos_test.dart`). Topic namespace must agree in three places: the server's `AlertDelivery.topicPrefix`, the broker ACL (`infra/docker/mosquitto/aclfile`), and `alertTopicFor()` in `mqtt_secure_client.dart` — all `sinalacs/v1/microareas//alerts`, where `` is the seed UUID, not `area-12`. diff --git a/PROGRESS.md b/PROGRESS.md index 0369e7f..8780417 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -341,7 +341,8 @@ Dois furos adjacentes fechados no caminho, achados ao implementar o diretório: 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. + (na época, `SyncStatus.error` — virou `SyncStatus.rejected`, terminal, numa + mudança posterior, ver abaixo), não descarta o resto do lote. - **`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 @@ -368,9 +369,65 @@ lista os pacientes reais do Postgres; escolher um e sincronizar grava `syncStatus: synced` no servidor; o arquivo do banco cifrado, puxado do aparelho depois de gravar e sincronizar pelo seletor, não contém o nome em nenhum ponto (nem o logcat); uma visita para paciente de outra microárea volta -como `error` e grava a linha `denied_territory` em `audit_logs`, sem tocar -`visits`; o caminho por alerta (vermelho → SAMU → escalonamento → visita) -continua idêntico ao de antes desta mudança. +como `error` (na época — ver abaixo) e grava a linha `denied_territory` em +`audit_logs`, sem tocar `visits`; o caminho por alerta (vermelho → SAMU → +escalonamento → visita) continua idêntico ao de antes desta mudança. + +#### A cadeia de hash da trilha de auditoria, e a recusa que ficava presa na fila para sempre + +Duas dívidas registradas explicitamente no PR anterior, fechadas nesta mudança. + +**A cadeia de hash de `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, o que não serve ao +não-repúdio que a spec promete. `audit_logs` ganhou três colunas: `sequence` +(posição, contígua, índice único), `previousHash` (o `entryHash` da linha +anterior, ou `AuditChain.genesisHash` — 64 zeros — na primeira) e `entryHash` +(HMAC-SHA256 do conteúdo da linha). A chave é um segredo PRÓPRIO +(`AUDIT_CHAIN_SECRET`), nunca derivado do `JWT_SECRET`: os dois precisam poder +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 que a §458 de `spec/lgpd_design.md` descreve. +`OrmAuditTrail.record` agora lê a última linha e insere a próxima dentro da +MESMA transação, sob `pg_advisory_xact_lock`, para duas gravações concorrentes +não lerem a mesma linha anterior e bifurcarem a cadeia. `AuditChainVerifier` (e +o `bin/audit_chain_check.dart` que o expõe como script) reconstrói a cadeia +inteira e detecta edição, remoção ou reordenação de qualquer linha — verificado +ao vivo: adulterar uma linha por `UPDATE` direto no Postgres faz o verificador +falhar exatamente na `sequence` afetada. 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. + +**Recusa definitiva presa na fila para sempre.** `VisitSyncService._syncOne` +colapsava seis motivos de falha distintos no mesmo `SyncStatus.error`, e +`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 por quê. `SyncStatus` ganhou +`rejected`, terminal: localId vazio, UUID malformado, território incompatível e +visita de outro agente agora retornam `rejected` (só "paciente não encontrado" +continua `error` — pode ser cadastrado depois, é legitimamente retentável). +`SyncFsm` ganhou o estado espelhado, sem transição de saída. No aparelho, +`OfflineVisitRecord` ganhou `rejectionReason`; a fila ganhou uma quarta lista +(`_rejected`, ao lado de pendente/sincronizada/conflito) que sai da retentativa +mas continua no disco — `VisitStore.save` passou a persistir pendentes MAIS +recusadas, não só pendentes, senão a recusada sumiria do aparelho no instante +da recusa, antes de o ACS decidir. A tela ganhou o contador +(`Key('rejected_visits_count')`) e um botão de descarte com confirmação +(`Key('discard_rejected')`) — nada remove a recusada do aparelho sem essa +confirmação explícita. `encrypted_database.dart` foi 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. + +Verificação: 83 testes no backend (69 unit + 14 integração, incluindo a +gravação real de duas linhas encadeadas contra Postgres e a checagem do +`pg_advisory_xact_lock` — a cadeia de hash tem cobertura própria em +`test/unit/audit_chain_test.dart`, incluindo detecção de edição, remoção, +renumeração e segredo errado), 89 testes herméticos no app ACS (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, e o +fluxo completo de descarte com confirmação na tela). ### M2.5 - Testes de Caos diff --git a/apps/acs/lib/app/app.dart b/apps/acs/lib/app/app.dart index 490f5f2..586f2a9 100644 --- a/apps/acs/lib/app/app.dart +++ b/apps/acs/lib/app/app.dart @@ -1177,9 +1177,39 @@ class _VisitRegistrationScreenState extends State { (widget.queue.persistenceFailed ? 'Não foi possível sincronizar. As visitas estão apenas na memória deste aparelho.' : 'Não foi possível sincronizar. As visitas continuam salvas no aparelho.'), + // Terminal: ao contrário de error, não vai retentar sozinha — dizer isso + // aqui é o que evita o ACS ficar tocando "Sincronizar agora" à toa. + SyncOutcomeKind.rejected => '${result.processed} visita(s) recusada(s) pelo ' + 'servidor e não serão reenviadas. ' + '${result.message ?? 'Veja o motivo na lista abaixo.'}', }); } + /// Descarta as visitas recusadas, com confirmação — o registro sai do + /// aparelho para sempre, e ele nunca chegou ao servidor. + Future _discardRejected() async { + final confirmed = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Descartar visita(s) recusada(s)?'), + content: const Text( + 'O servidor recusou esta visita em definitivo e ela nunca chegou a ' + 'ser sincronizada. Descartar apaga o registro deste aparelho — não ' + 'há como recuperá-lo depois.', + ), + actions: [ + TextButton(onPressed: () => Navigator.of(context).pop(false), child: const Text('Cancelar')), + FilledButton(onPressed: () => Navigator.of(context).pop(true), child: const Text('Descartar')), + ], + ), + ); + if (confirmed != true) return; + + await widget.queue.discardRejected(); + if (!mounted) return; + setState(() {}); + } + /// Seletor de pacientes da microárea, para quando a visita não vem de um /// alerta. Substitui o antigo aviso estático `visit_needs_alert`: antes a /// aba simplesmente dizia "selecione um alerta" e não havia outro caminho — @@ -1340,6 +1370,26 @@ class _VisitRegistrationScreenState extends State { style: const TextStyle(color: AcsColors.accent, fontWeight: FontWeight.bold), ), ), + if (queue.rejectedCount > 0) ...[ + Padding( + padding: const EdgeInsets.only(top: 4), + child: Text( + key: const Key('rejected_visits_count'), + 'Recusada(s) pelo servidor, não serão reenviadas: ${queue.rejectedCount}\n' + '${queue.rejectedVisits.map((v) => v.rejectionReason).whereType().toSet().join('; ')}', + // Recusa de sync é operacional, não gravidade clínica — mesma cor + // do contador de conflito. + style: const TextStyle(color: AcsColors.accent, fontWeight: FontWeight.bold), + ), + ), + const SizedBox(height: 8), + OutlinedButton.icon( + key: const Key('discard_rejected'), + onPressed: _discardRejected, + icon: const Icon(Icons.delete_outline), + label: const Text('Descartar recusada(s)'), + ), + ], const SizedBox(height: 12), OutlinedButton.icon( key: const Key('sync_visits'), diff --git a/apps/acs/lib/core/database/encrypted_database.dart b/apps/acs/lib/core/database/encrypted_database.dart index 9da5f4c..e103222 100644 --- a/apps/acs/lib/core/database/encrypted_database.dart +++ b/apps/acs/lib/core/database/encrypted_database.dart @@ -17,14 +17,18 @@ class EncryptedLocalDatabase { /// /// v1 existiu em duas formas em campo: `local_queue(id)` — o que um aparelho /// com a versão anterior instalada tem — e uma `offline_visits` com - /// `patient_name`. v2 grava `patient_id`. - static const schemaVersion = 3; + /// `patient_name`. v2 grava `patient_id`. v4 acrescenta `rejection_reason`, + /// para a visita recusada em definitivo pelo servidor (`SyncStatus.rejected`) + /// ficar visível no aparelho em vez de retentar para sempre. + static const schemaVersion = 4; /// Visitas registradas offline, aguardando sincronização. /// /// Só o que ainda precisa sair do dispositivo é gravado: a visita confirmada /// pelo servidor é removida no `save` seguinte, o que atende o princípio da - /// minimização (LGPD-RF07 / seção 5.6 de spec/lgpd_design.md). + /// minimização (LGPD-RF07 / seção 5.6 de spec/lgpd_design.md). Desde a v4, + /// isso inclui a visita recusada em definitivo — ela fica no disco, com o + /// motivo, até o ACS descartá-la explicitamente (ver `OfflineVisitQueue`). static const createOfflineVisits = ''' CREATE TABLE IF NOT EXISTS offline_visits ( local_id TEXT PRIMARY KEY, @@ -34,10 +38,11 @@ CREATE TABLE IF NOT EXISTS offline_visits ( outcome TEXT NOT NULL, notes TEXT NOT NULL DEFAULT '', created_at TEXT NOT NULL, - version INTEGER NOT NULL + version INTEGER NOT NULL, + rejection_reason TEXT )'''; - /// Migração v1 → v2 e v2 → v3. + /// Migração v1 → v2, v2 → v3 e v3 → v4. /// /// Nenhuma das duas formas de v1 guarda o UUID do paciente: `patient_name` /// era `'Paciente ' + 8 dos 32 dígitos hex`, irreversível. Sem UUID, @@ -45,9 +50,11 @@ CREATE TABLE IF NOT EXISTS offline_visits ( /// preservar essas linhas só encheria a fila de pendências que nunca sobem e, /// por isso mesmo, nunca saem do disco. /// - /// Recriar perde as visitas pendentes gravadas antes desta versão. É - /// aceitável **apenas** porque o app ainda não teve release; a partir do - /// primeiro, esta migração precisa preservar os dados. + /// Recriar (v1 → v2) perde as visitas pendentes gravadas antes desta versão. + /// Era aceitável **apenas** porque o app ainda não tinha tido release. A + /// partir da v3 → v4, a migração passa a ser só `ALTER TABLE ... ADD COLUMN` + /// — aditiva, preserva as linhas existentes, que é o que uma migração depois + /// do primeiro release precisa fazer. static Future _upgrade(Database db, int from, int to) async { if (from < 2) { await db.execute('DROP TABLE IF EXISTS local_queue'); @@ -67,6 +74,19 @@ CREATE TABLE IF NOT EXISTS offline_visits ( ); } } + + if (from < 4) { + final columns = await db.rawQuery( + "PRAGMA table_info('offline_visits')", + ); + final hasRejectionReason = + columns.any((column) => column['name'] == 'rejection_reason'); + if (!hasRejectionReason) { + await db.execute( + 'ALTER TABLE offline_visits ADD COLUMN rejection_reason TEXT;', + ); + } + } } /// Caminho do arquivo do banco neste dispositivo. diff --git a/apps/acs/lib/core/database/sqlcipher_visit_store.dart b/apps/acs/lib/core/database/sqlcipher_visit_store.dart index 75afce7..d709a90 100644 --- a/apps/acs/lib/core/database/sqlcipher_visit_store.dart +++ b/apps/acs/lib/core/database/sqlcipher_visit_store.dart @@ -75,6 +75,7 @@ class SqlCipherVisitStore implements VisitStore { status: row['status']! as String, outcome: row['outcome']! as String, notes: (row['notes'] as String?) ?? '', + rejectionReason: row['rejection_reason'] as String?, localId: row['local_id']! as String, createdAt: DateTime.parse(row['created_at']! as String), version: row['version']! as int, @@ -84,9 +85,12 @@ class SqlCipherVisitStore implements VisitStore { /// Substitui o conjunto inteiro, em transação. /// - /// A fila chama `save(_pending)`, então o que sai da lista sai do disco: uma - /// visita confirmada pelo servidor deixa o dispositivo aqui. É a minimização - /// de dados acontecendo, e há teste que trava essa propriedade. + /// A fila chama `save([..._pending, ..._rejected])`, então o que sai da + /// lista sai do disco: uma visita confirmada pelo servidor deixa o + /// dispositivo aqui. É a minimização de dados acontecendo, e há teste que + /// trava essa propriedade. Uma visita recusada em definitivo continua no + /// disco — com `rejection_reason` preenchido — até o ACS descartá-la + /// explicitamente; só então ela deixa de fazer parte do que é gravado. @override Future save(List visits) async { final database = await _open(); @@ -103,6 +107,7 @@ class SqlCipherVisitStore implements VisitStore { 'notes': visit.notes, 'created_at': visit.createdAt.toIso8601String(), 'version': visit.version, + 'rejection_reason': visit.rejectionReason, }); } }); diff --git a/apps/acs/lib/core/services/offline_visit_queue.dart b/apps/acs/lib/core/services/offline_visit_queue.dart index 6ba90dd..d52c9bc 100644 --- a/apps/acs/lib/core/services/offline_visit_queue.dart +++ b/apps/acs/lib/core/services/offline_visit_queue.dart @@ -5,6 +5,10 @@ enum SyncOutcomeKind { conflict, empty, error, + + /// O servidor recusou em definitivo — ver [OfflineVisitRecord.rejectionReason]. + /// Ao contrário de [error], que é retentável, nada muda tentando de novo. + rejected, } class SyncOutcome { @@ -17,7 +21,8 @@ class SyncOutcome { final SyncOutcomeKind kind; final int processed; - /// Motivo, já em português, quando [kind] é [SyncOutcomeKind.error]. + /// Motivo, já em português, quando [kind] é [SyncOutcomeKind.error] ou + /// [SyncOutcomeKind.rejected]. /// /// Sem isto a tela só conseguia dizer "não deu certo": a mensagem real do /// servidor (identificador inválido, sessão expirada) ficava presa aqui. @@ -31,6 +36,7 @@ class OfflineVisitRecord { required this.status, this.outcome = '', this.notes = '', + this.rejectionReason, String? localId, DateTime? createdAt, this.version = 1, @@ -65,12 +71,24 @@ class OfflineVisitRecord { /// Versão conhecida no servidor, para detecção de conflito. final int version; + /// Não-nulo significa: o servidor recusou esta visita EM DEFINITIVO — o + /// texto é o motivo, em português, vindo de `VisitSyncResult.message`. + /// + /// Nunca cita nome de paciente nem microárea alheia — a mesma mensagem que + /// já chegava do servidor para o caso retentável (`error`), agora também + /// para o terminal (`rejected`). Uma vez definido, nada nesta classe o + /// desfaz: sair do estado de recusa é uma decisão explícita do ACS + /// (`OfflineVisitQueue.discardRejected`), não um efeito colateral de + /// `copyWith`. + final String? rejectionReason; + OfflineVisitRecord copyWith({ String? patientId, String? risk, String? status, String? outcome, String? notes, + String? rejectionReason, String? localId, DateTime? createdAt, int? version, @@ -81,6 +99,7 @@ class OfflineVisitRecord { status: status ?? this.status, outcome: outcome ?? this.outcome, notes: notes ?? this.notes, + rejectionReason: rejectionReason ?? this.rejectionReason, localId: localId ?? this.localId, createdAt: createdAt ?? this.createdAt, version: version ?? this.version, @@ -109,12 +128,13 @@ class VisitSyncOutcome { final String localId; - /// `synced`, `conflict` ou `error`, como devolvido por `visits.sync`. + /// `synced`, `conflict`, `error` ou `rejected`, como devolvido por + /// `visits.sync` (`SyncStatus.name`). final String status; final int? serverVersion; - /// Motivo devolvido pelo servidor quando [status] é `error`. + /// Motivo devolvido pelo servidor quando [status] é `error` ou `rejected`. /// /// Ele já vinha no contrato (`VisitSyncResult.message`) e era descartado no /// caminho até aqui, o que deixava um `patientId` inválido parecendo silêncio. @@ -130,6 +150,12 @@ abstract class VisitSynchronizer { } /// Onde as visitas ficam enquanto não sincronizam. +/// +/// Desde que existe [OfflineVisitRecord.rejectionReason], o que [save] recebe +/// não é mais só "a fila" — é a fila MAIS as visitas recusadas em definitivo +/// que o ACS ainda não descartou (ver `OfflineVisitQueue._persist`). Sem isso, +/// uma visita recusada sumiria do disco no instante em que saísse de +/// `_pending`, silenciosamente — o oposto do que a recusa terminal pede. abstract class VisitStore { Future> load(); @@ -152,9 +178,11 @@ class InMemoryVisitStore implements VisitStore { /// Fila offline-first de visitas domiciliares. /// /// Preserva a semântica da `SyncFsm` do backend -/// (`idle → localWrite → queued → syncing → {synced|conflict|error}`): uma -/// visita em conflito **volta para a fila** em vez de ser descartada, e uma -/// falha de rede não perde o lote. +/// (`idle → localWrite → queued → syncing → {synced|conflict|error|rejected}`): +/// uma visita em conflito **volta para a fila** em vez de ser descartada, uma +/// falha de rede não perde o lote, e uma recusa definitiva (`rejected`) SAI da +/// fila de retentativa mas fica visível no aparelho até o ACS descartá-la — +/// diferente de `error`, que continua pendente e retenta. class OfflineVisitQueue { OfflineVisitQueue({VisitStore? store, this.synchronizer}) : _store = store ?? InMemoryVisitStore(); @@ -167,6 +195,7 @@ class OfflineVisitQueue { final List _pending = []; final List _synced = []; final List _conflicts = []; + final List _rejected = []; bool _restored = false; bool _persistenceFailed = false; @@ -182,23 +211,29 @@ class OfflineVisitQueue { int get pendingCount => _pending.length; int get syncedCount => _synced.length; int get conflictCount => _conflicts.length; + int get rejectedCount => _rejected.length; List get pendingVisits => List.unmodifiable(_pending); List get syncedVisits => List.unmodifiable(_synced); List get conflictVisits => List.unmodifiable(_conflicts); + List get rejectedVisits => List.unmodifiable(_rejected); /// Recarrega o que ficou gravado de execuções anteriores. /// - /// Sem isso, fechar o app perde a fila — o oposto de offline-first. + /// Sem isso, fechar o app perde a fila — o oposto de offline-first. O que + /// veio do disco é separado entre pendente e recusado pelo próprio + /// [OfflineVisitRecord.rejectionReason] — não há uma segunda tabela. Future restore() async { if (_restored) return; _restored = true; try { final stored = await _store.load(); - _pending - ..clear() - ..addAll(stored); + _pending.clear(); + _rejected.clear(); + for (final visit in stored) { + (visit.rejectionReason == null ? _pending : _rejected).add(visit); + } _persistenceFailed = false; } catch (_) { // Banco indisponível (chave perdida, plataforma sem SQLCipher). A fila @@ -212,10 +247,26 @@ class OfflineVisitQueue { await _persist(); } + /// Descarta as visitas recusadas em definitivo. + /// + /// Ação explícita do ACS, nunca automática: a recusa terminal já tirou a + /// visita da retentativa, mas apagar o registro do trabalho de campo sem + /// confirmação apagaria evidência de uma visita feita que nunca chegou ao + /// servidor. + Future discardRejected() async { + if (_rejected.isEmpty) return; + _rejected.clear(); + await _persist(); + } + /// Grava o estado corrente, sem deixar a falha derrubar quem chamou. + /// + /// Persiste pendentes E recusadas: uma recusada que saísse do disco junto + /// com a fila desapareceria do aparelho no instante em que fosse recusada, + /// silenciosamente — antes mesmo de o ACS decidir descartá-la. Future _persist() async { try { - await _store.save(_pending); + await _store.save([..._pending, ..._rejected]); _persistenceFailed = false; } catch (_) { _persistenceFailed = true; @@ -280,7 +331,9 @@ class OfflineVisitQueue { var conflicts = 0; var synced = 0; var errors = 0; + var rejected = 0; String? firstError; + String? firstRejection; final stillPending = []; for (final visit in batch) { @@ -298,6 +351,14 @@ class OfflineVisitQueue { // Volta para a fila: conflito precisa de resolução, não de descarte. stillPending.add(conflicted); conflicts++; + case 'rejected': + // Terminal: SAI da retentativa, mas fica visível até o ACS + // descartar — não some, e não volta para `stillPending`. + final reason = outcome?.message ?? + 'O servidor recusou esta visita em definitivo.'; + _rejected.add(visit.copyWith(rejectionReason: reason)); + rejected++; + firstRejection ??= reason; case 'error': // O servidor recusou ESTA visita. Continua pendente — descartar // perderia o registro feito em campo —, mas precisa ser contada: @@ -318,8 +379,17 @@ class OfflineVisitQueue { ..addAll(stillPending); await _persist(); - // Precedência: conflito > erro > sincronizado. O conflito é o que exige - // decisão de quem registrou; o erro, no máximo, uma nova tentativa. + // Precedência: recusado > conflito > erro > sincronizado. Recusado é + // trabalho de campo que NUNCA vai ao servidor — o que mais precisa + // chegar aos olhos do ACS. Conflito exige decisão de quem registrou; erro, + // no máximo, uma nova tentativa. + if (rejected > 0) { + return SyncOutcome( + kind: SyncOutcomeKind.rejected, + processed: rejected, + message: firstRejection, + ); + } if (conflicts > 0) { return SyncOutcome(kind: SyncOutcomeKind.conflict, processed: conflicts); } diff --git a/apps/acs/test/encrypted_database_test.dart b/apps/acs/test/encrypted_database_test.dart index 6fc4f44..4fd8daa 100644 --- a/apps/acs/test/encrypted_database_test.dart +++ b/apps/acs/test/encrypted_database_test.dart @@ -109,6 +109,17 @@ void main() { return {for (final row in rows) row['name']! as String}; } + Future abrirV3(String create) async { + sqfliteFfiInit(); + return databaseFactoryFfi.openDatabase( + await EncryptedLocalDatabase.pathFor(nome), + options: OpenDatabaseOptions( + version: 3, + onCreate: (db, version) => db.execute(create), + ), + ); + } + setUp(() => EncryptedLocalDatabase.deleteDatabaseFile(nome)); tearDown(() => EncryptedLocalDatabase.deleteDatabaseFile(nome)); @@ -188,6 +199,52 @@ CREATE TABLE IF NOT EXISTS offline_visits ( expect((await store.load()).single.patientId, seedPatientId); await store.close(); }); + + test('a offline_visits v3 ganha rejection_reason sem perder linhas', () async { + // Diferente da migração v1 → v2 (que recria e perde dados, aceitável só + // porque o app não tinha tido release ainda), esta é aditiva: uma visita + // pendente gravada antes desta versão precisa sobreviver à abertura v4. + final legado = await abrirV3(''' +CREATE TABLE IF NOT EXISTS offline_visits ( + local_id TEXT PRIMARY KEY, + patient_id TEXT NOT NULL, + risk TEXT NOT NULL, + status TEXT NOT NULL, + outcome TEXT NOT NULL, + notes TEXT NOT NULL DEFAULT '', + created_at TEXT NOT NULL, + version INTEGER NOT NULL +)'''); + await legado.insert('offline_visits', { + 'local_id': '00000000-0000-4000-8000-00000000000b', + 'patient_id': seedPatientId, + 'risk': 'red', + 'status': 'PENDENTE', + 'outcome': '', + 'notes': '', + 'created_at': DateTime.utc(2026).toIso8601String(), + 'version': 1, + }); + await legado.close(); + + final atual = await EncryptedLocalDatabase.open( + databaseName: nome, + passphrase: chave, + allowUnencryptedForTesting: true, + ); + + final colunas = [ + for (final row in await atual.rawQuery('PRAGMA table_info(offline_visits)')) + row['name']! as String, + ]; + expect(colunas, contains('rejection_reason')); + + final linhas = await atual.query('offline_visits'); + expect(linhas, hasLength(1)); + expect(linhas.single['patient_id'], seedPatientId); + expect(linhas.single['rejection_reason'], isNull); + await atual.close(); + }); }); group('SqlCipherVisitStore', () { @@ -310,6 +367,35 @@ CREATE TABLE IF NOT EXISTS offline_visits ( expect(await store.load(), hasLength(1)); }); + test('a visita recusada em definitivo PERMANECE no disco, com o motivo', () async { + // Descartar do disco no instante da recusa apagaria trabalho de campo + // sem o ACS decidir isso — a saída do disco só acontece com + // `discardRejected()` explícito. + final queue = queueOn(store, sender: _RejectedSynchronizer()); + await queue.add(OfflineVisitRecord( + patientId: syntheticPatientId(9), + risk: 'rotina', + status: 'PENDENTE', + )); + + final result = await queue.sync(); + + expect(result.kind, SyncOutcomeKind.rejected); + final onDisk = await store.load(); + expect(onDisk, hasLength(1)); + expect(onDisk.single.rejectionReason, 'paciente fora da sua microárea'); + + // Reabrir a fila do zero também vê a recusada — ela não é perdida ao + // fechar o app. + final reopened = queueOn(store); + await reopened.restore(); + expect(reopened.rejectedCount, 1); + expect(reopened.pendingCount, 0); + + await reopened.discardRejected(); + expect(await store.load(), isEmpty); + }); + test('armazenamento indisponível não derruba o registro em campo', () async { // Travar o registro de visita seria pior do que não persistir. A fila // segue em memória e acende o sinalizador para a UI avisar. @@ -344,4 +430,16 @@ class _ConflictSynchronizer implements VisitSynchronizer { ]; } +class _RejectedSynchronizer implements VisitSynchronizer { + @override + Future> push(List visits) async => [ + for (final visit in visits) + VisitSyncOutcome( + localId: visit.localId, + status: 'rejected', + message: 'paciente fora da sua microárea', + ), + ]; +} + diff --git a/apps/acs/test/login_flow_test.dart b/apps/acs/test/login_flow_test.dart index 0cffc59..352435a 100644 --- a/apps/acs/test/login_flow_test.dart +++ b/apps/acs/test/login_flow_test.dart @@ -8,7 +8,8 @@ import 'package:sinalacs_acs/core/services/alert_queue.dart'; import 'package:sinalacs_acs/core/services/backend_visit_synchronizer.dart'; import 'package:sinalacs_acs/core/services/offline_visit_queue.dart'; import 'package:sinalacs_acs/core/services/visit_queue_factory.dart'; -import 'package:sinalacs_client/sinalacs_client.dart' show MicroAreaPatient; +import 'package:sinalacs_client/sinalacs_client.dart' + show MicroAreaPatient, SyncStatus, VisitSyncResult; import 'support/fakes.dart'; @@ -305,6 +306,70 @@ void main() { expect(queue.pendingCount, 2); }); + test('visita recusada em definitivo sai da fila e não é reenviada', () async { + // É o teste que reprova o comportamento antigo: `case 'error'` + // reenfileirava incondicionalmente, então uma recusa territorial (que + // nunca vai dar certo numa próxima tentativa) retentava para sempre. + final sender = FakeVisitSynchronizer( + statusFor: (_) => 'rejected', + messageFor: (_) => 'paciente fora da sua microárea', + ); + final queue = OfflineVisitQueue(synchronizer: sender); + + await queue.add(visit(seedPatientId)); + final result = await queue.sync(); + + expect(result.kind, SyncOutcomeKind.rejected); + expect(result.message, 'paciente fora da sua microárea'); + expect(queue.pendingCount, 0); + expect(queue.rejectedCount, 1); + expect(queue.rejectedVisits.single.rejectionReason, 'paciente fora da sua microárea'); + + // Um segundo sync não reenvia a recusada: nada de novo sai pela rede. + final secondSync = await queue.sync(); + expect(secondSync.kind, SyncOutcomeKind.empty); + expect(sender.batches, hasLength(1)); + }); + + test('recusa tem precedência sobre conflito no mesmo lote', () async { + final rejeitada = visit(seedPatientId); + final sender = FakeVisitSynchronizer( + statusFor: (v) => v.localId == rejeitada.localId ? 'rejected' : 'conflict', + ); + final queue = OfflineVisitQueue(synchronizer: sender); + + await queue.add(rejeitada); + await queue.add(visit(syntheticPatientId(2))); + + final result = await queue.sync(); + + expect(result.kind, SyncOutcomeKind.rejected); + expect(queue.rejectedCount, 1); + // O conflito continua pendente para resolução — só a recusada saiu. + expect(queue.pendingCount, 1); + expect(queue.conflictCount, 1); + }); + + test('discardRejected esvazia as recusadas e persiste', () async { + final sender = FakeVisitSynchronizer(statusFor: (_) => 'rejected'); + final store = InMemoryVisitStore(); + final queue = OfflineVisitQueue(store: store, synchronizer: sender); + + await queue.add(visit(seedPatientId)); + await queue.sync(); + expect(queue.rejectedCount, 1); + + await queue.discardRejected(); + expect(queue.rejectedCount, 0); + + // A fila nova, lendo do mesmo store, não vê a recusada: ela saiu do + // disco quando foi descartada. + final reloaded = OfflineVisitQueue(store: store); + await reloaded.restore(); + expect(reloaded.rejectedCount, 0); + expect(reloaded.pendingCount, 0); + }); + test('não deve declarar sincronizado sem sincronizador configurado', () async { // O comportamento antigo carimbava 'SINCRONIZADO' sem nada sair do // dispositivo, o que escondia a ausência de integração. @@ -666,6 +731,78 @@ void main() { expect(find.text('Sem conexão com o servidor.'), findsOneWidget); expect(visitQueue.pendingCount, 1); }); + + testWidgets('visita recusada mostra o motivo, some da fila e sai só com confirmação', + (tester) async { + final backend = FakeAcsBackend() + ..syncResultFor = (entry) => VisitSyncResult( + localId: entry.localId, + syncStatus: SyncStatus.rejected, + message: 'paciente fora da sua microárea', + ); + final visitQueue = OfflineVisitQueue( + synchronizer: BackendVisitSynchronizer(backend: backend), + ); + late FakeAlertFeed feed; + + await tester.pumpWidget(SinalAcsApp( + backend: backend, + feedBuilder: (queue) => feed = FakeAlertFeed(queue), + visitQueue: visitQueue, + )); + + await tester.tap(find.byKey(const Key('login_button'))); + await tester.pumpAndSettle(); + + feed.deliver(testAlert(alertId: 'alerta-1', riskLevel: 'yellow')); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Iniciar rota de visita')); + await tester.pumpAndSettle(); + + await tester.tap(find.byKey(const Key('arrival_confirmation'))); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('save_visit'))); + await tester.pumpAndSettle(); + await tester.pump(const Duration(seconds: 5)); + await tester.pumpAndSettle(); + + await tester.ensureVisible(find.byKey(const Key('sync_visits'))); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('sync_visits'))); + await tester.pumpAndSettle(); + + // Saiu da retentativa: o contador de pendentes zera, e o botão de + // sincronizar desabilita sozinho — sem isso o ACS ficaria tocando + // "Sincronizar agora" para sempre, à toa. + expect(find.text('Pendentes de sincronização: 0'), findsOneWidget); + expect(visitQueue.pendingCount, 0); + expect(visitQueue.rejectedCount, 1); + await tester.ensureVisible(find.byKey(const Key('rejected_visits_count'))); + await tester.pumpAndSettle(); + expect(find.textContaining('paciente fora da sua microárea'), findsWidgets); + + final syncButton = tester.widget(find.byKey(const Key('sync_visits'))); + expect(syncButton.onPressed, isNull); + + // Cancelar a confirmação não descarta nada. + await tester.ensureVisible(find.byKey(const Key('discard_rejected'))); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('discard_rejected'))); + await tester.pumpAndSettle(); + await tester.tap(find.text('Cancelar')); + await tester.pumpAndSettle(); + expect(visitQueue.rejectedCount, 1); + + // Confirmar descarta — e some do aparelho para sempre. + await tester.tap(find.byKey(const Key('discard_rejected'))); + await tester.pumpAndSettle(); + await tester.tap(find.text('Descartar')); + await tester.pumpAndSettle(); + + expect(visitQueue.rejectedCount, 0); + expect(find.byKey(const Key('rejected_visits_count')), findsNothing); + }); }); group('avisos de infraestrutura', () { diff --git a/backend/sinalacs_client/lib/src/protocol/audit_log.dart b/backend/sinalacs_client/lib/src/protocol/audit_log.dart index 4a20a73..902ab71 100644 --- a/backend/sinalacs_client/lib/src/protocol/audit_log.dart +++ b/backend/sinalacs_client/lib/src/protocol/audit_log.dart @@ -14,6 +14,16 @@ import 'package:serverpod_client/serverpod_client.dart' as _i1; /// Trilha de auditoria de acesso a dados sensíveis. Append-only. +/// +/// `sequence`/`previousHash`/`entryHash` formam uma cadeia de hash (LGPD-RT03): +/// cada linha encadeia à anterior via `previousHash == entryHash` da linha de +/// `sequence - 1`, e `entryHash` é um HMAC-SHA256 sobre o conteúdo da própria +/// linha (ver `application/audit/audit_chain.dart`). Isso torna qualquer +/// edição, remoção ou reordenação de linha detectável — inclusive por quem tem +/// acesso de escrita direto ao Postgres, que é o adversário que a §458 de +/// spec/lgpd_design.md descreve. O índice único em `sequence` é o segundo +/// cinto: uma bifurcação da cadeia por concorrência estoura na hora em vez de +/// corromper em silêncio. abstract class AuditLog implements _i1.SerializableModel { AuditLog._({ this.id, @@ -24,6 +34,9 @@ abstract class AuditLog implements _i1.SerializableModel { required this.timestamp, required this.ipHash, required this.result, + required this.sequence, + required this.previousHash, + required this.entryHash, }); factory AuditLog({ @@ -35,6 +48,9 @@ abstract class AuditLog implements _i1.SerializableModel { required DateTime timestamp, required String ipHash, required String result, + required int sequence, + required String previousHash, + required String entryHash, }) = _AuditLogImpl; factory AuditLog.fromJson(Map jsonSerialization) { @@ -55,6 +71,9 @@ abstract class AuditLog implements _i1.SerializableModel { ), ipHash: jsonSerialization['ipHash'] as String, result: jsonSerialization['result'] as String, + sequence: jsonSerialization['sequence'] as int, + previousHash: jsonSerialization['previousHash'] as String, + entryHash: jsonSerialization['entryHash'] as String, ); } @@ -77,6 +96,18 @@ abstract class AuditLog implements _i1.SerializableModel { String result; + /// Posição na cadeia, começando em 1. Contígua por construção — um buraco + /// aqui é uma linha apagada. + int sequence; + + /// `entryHash` da linha anterior, ou `AuditChain.genesisHash` (64 zeros) na + /// primeira linha. Nunca nulo: gênese e "escrito antes da cadeia existir" + /// não podem ter a mesma representação. + String previousHash; + + /// HMAC-SHA256(AUDIT_CHAIN_SECRET, conteúdo da linha). Ver AuditChain.compute. + String entryHash; + /// Returns a shallow copy of this [AuditLog] /// with some or all fields replaced by the given arguments. @_i1.useResult @@ -89,6 +120,9 @@ abstract class AuditLog implements _i1.SerializableModel { DateTime? timestamp, String? ipHash, String? result, + int? sequence, + String? previousHash, + String? entryHash, }); @override Map toJson() { @@ -102,6 +136,9 @@ abstract class AuditLog implements _i1.SerializableModel { 'timestamp': timestamp.toJson(), 'ipHash': ipHash, 'result': result, + 'sequence': sequence, + 'previousHash': previousHash, + 'entryHash': entryHash, }; } @@ -123,6 +160,9 @@ class _AuditLogImpl extends AuditLog { required DateTime timestamp, required String ipHash, required String result, + required int sequence, + required String previousHash, + required String entryHash, }) : super._( id: id, userId: userId, @@ -132,6 +172,9 @@ class _AuditLogImpl extends AuditLog { timestamp: timestamp, ipHash: ipHash, result: result, + sequence: sequence, + previousHash: previousHash, + entryHash: entryHash, ); /// Returns a shallow copy of this [AuditLog] @@ -147,6 +190,9 @@ class _AuditLogImpl extends AuditLog { DateTime? timestamp, String? ipHash, String? result, + int? sequence, + String? previousHash, + String? entryHash, }) { return AuditLog( id: id is _i1.UuidValue? ? id : this.id, @@ -157,6 +203,9 @@ class _AuditLogImpl extends AuditLog { timestamp: timestamp ?? this.timestamp, ipHash: ipHash ?? this.ipHash, result: result ?? this.result, + sequence: sequence ?? this.sequence, + previousHash: previousHash ?? this.previousHash, + entryHash: entryHash ?? this.entryHash, ); } } diff --git a/backend/sinalacs_client/lib/src/protocol/enums/sync_status.dart b/backend/sinalacs_client/lib/src/protocol/enums/sync_status.dart index ddf7506..dc7070b 100644 --- a/backend/sinalacs_client/lib/src/protocol/enums/sync_status.dart +++ b/backend/sinalacs_client/lib/src/protocol/enums/sync_status.dart @@ -14,11 +14,18 @@ import 'package:serverpod_client/serverpod_client.dart' as _i1; /// Estado de sincronização offline-first, espelha a SyncFsm. +/// +/// `rejected` é TERMINAL: ao contrário de `error`, que é retentável (o +/// dispositivo tenta de novo mais tarde), uma visita `rejected` nunca vai dar +/// certo numa próxima tentativa — o motivo não muda com o tempo (ex.: paciente +/// fora da microárea do ACS, identificador malformado). `SyncFsm.rejected` não +/// tem transição de saída; `networkUp`/`syncStart` não o alcançam. enum SyncStatus implements _i1.SerializableModel { pending, synced, conflict, - error; + error, + rejected; static SyncStatus fromJson(String name) { switch (name) { @@ -30,6 +37,8 @@ enum SyncStatus implements _i1.SerializableModel { return SyncStatus.conflict; case 'error': return SyncStatus.error; + case 'rejected': + return SyncStatus.rejected; default: throw ArgumentError( 'Value "$name" cannot be converted to "SyncStatus"', diff --git a/backend/sinalacs_server/bin/audit_chain_check.dart b/backend/sinalacs_server/bin/audit_chain_check.dart new file mode 100644 index 0000000..a53d470 --- /dev/null +++ b/backend/sinalacs_server/bin/audit_chain_check.dart @@ -0,0 +1,52 @@ +import 'dart:io'; + +import 'package:serverpod/serverpod.dart'; +import 'package:sinalacs_server/src/application/audit/audit_chain_verifier.dart'; +import 'package:sinalacs_server/src/generated/endpoints.dart'; +import 'package:sinalacs_server/src/generated/protocol.dart'; +import 'package:sinalacs_server/src/infrastructure/database/orm_audit_chain_reader.dart'; +import 'package:sinalacs_server/src/runtime/alert_runtime.dart'; + +/// Verifica a cadeia de hash de `audit_logs` (LGPD-RT03) contra o banco +/// configurado em `config/*.yaml` — a mesma configuração que o servidor usa. +/// +/// Uso: `dart run bin/audit_chain_check.dart` +/// +/// Sai `0` quando a cadeia está íntegra, `1` quando encontra uma quebra +/// (edição, remoção ou reordenação de linha), imprimindo em que `sequence` e +/// por quê. Não sobe nenhum listener HTTP — só abre uma sessão interna para +/// ler o banco, no mesmo padrão de script avulso que o Serverpod documenta +/// para tarefas de manutenção. +Future main(List args) async { + final pod = Serverpod(args, Protocol(), Endpoints()); + final session = await pod.createSession(); + + try { + final verifier = AuditChainVerifier( + reader: OrmAuditChainReader(session: () => session), + secret: AlertRuntime.instance.config.auditChainSecret, + ); + final result = await verifier.verify(); + + if (result.ok) { + stdout.writeln( + 'OK — ${result.checked} linha(s) verificada(s), cadeia íntegra.', + ); + exitCode = 0; + } else { + stderr.writeln( + 'FALHA na sequence ${result.brokenAtSequence}: ${result.reason} ' + '(${result.checked} linha(s) íntegra(s) antes da quebra)', + ); + exitCode = 1; + } + } finally { + await session.close(); + // `exitProcess: true` (o padrão) chamaria `exit()` internamente com seu + // próprio código, descartando o `exitCode` que acabamos de definir acima — + // é assim que o script perderia o `1` de uma cadeia quebrada. + await pod.shutdown(exitProcess: false); + } + + exit(exitCode); +} diff --git a/backend/sinalacs_server/lib/src/application/audit/audit_chain.dart b/backend/sinalacs_server/lib/src/application/audit/audit_chain.dart new file mode 100644 index 0000000..6062a49 --- /dev/null +++ b/backend/sinalacs_server/lib/src/application/audit/audit_chain.dart @@ -0,0 +1,88 @@ +import 'dart:convert'; + +import 'package:crypto/crypto.dart'; + +/// Os campos de uma linha de `audit_logs` que entram no cálculo da cadeia de +/// hash, na forma que `application/` conhece — não o `AuditLog` gerado, para +/// não acoplar este cálculo ao ORM. +class AuditChainFields { + const AuditChainFields({ + required this.sequence, + required this.previousHash, + required this.userId, + required this.actionType, + required this.resourceType, + required this.resourceId, + required this.timestamp, + required this.ipHash, + required this.result, + }); + + final int sequence; + final String previousHash; + final String userId; + final String actionType; + final String resourceType; + final String? resourceId; + final DateTime timestamp; + final String ipHash; + final String result; +} + +/// Calcula o elo de uma cadeia de hash sobre `audit_logs` (LGPD-RT03). +/// +/// Cada linha encadeia à anterior por `previousHash`, e `entryHash` é um +/// HMAC-SHA256 do conteúdo da linha — HMAC, não SHA-256 puro, porque a chave +/// fica fora do Postgres (`AUDIT_CHAIN_SECRET`): quem tem acesso de escrita ao +/// banco consegue recalcular um hash simples depois de adulterar uma linha, +/// mas não consegue forjar uma assinatura sem o segredo. `OrmAuditTrail` +/// escreve com esta classe, e `AuditChainVerifier` lê com ela — o mesmo +/// código dos dois lados, para uma segunda implementação do cálculo não virar +/// a forma mais provável de a verificação mentir. +class AuditChain { + AuditChain({required String secret}) : _secret = utf8.encode(secret); + + final List _secret; + + /// `previousHash` da primeira linha da cadeia. Nunca nulo — gênese e "linha + /// escrita antes de a cadeia existir" não podem ter a mesma representação. + static const genesisHash = '0000000000000000000000000000000000000000000000000000000000000000'; + + /// HMAC-SHA256, em hexadecimal — mesmo formato de `ipHash`, que já usa + /// `.toString()` sobre um `Digest`. Não é base64Url: aquele é o formato de + /// token JWT, um contexto diferente. + String computeEntryHash(AuditChainFields fields) => + Hmac(sha256, _secret).convert(utf8.encode(_payloadOf(fields))).toString(); + + /// Compara em tempo constante, no mesmo molde de + /// `DevelopmentAuthService._matchesSignature` — o campo é uma assinatura, + /// então comparar com `==` vazaria timing. + bool matches(AuditChainFields fields, String entryHash) { + final expected = computeEntryHash(fields); + if (expected.length != entryHash.length) return false; + var difference = 0; + for (var index = 0; index < expected.length; index++) { + difference |= expected.codeUnitAt(index) ^ entryHash.codeUnitAt(index); + } + return difference == 0; + } + + /// U+001F (INFORMATION SEPARATOR ONE / "unit separator"), o caractere de + /// controle — não o símbolo visível ␟ (U+241F), que é outro código: não + /// aparece em UUID, em hex, em ISO-8601 nem nos valores de + /// `actionType`/`result` já em uso, então concatenar sem ele não faz dois + /// conjuntos de campos diferentes colidirem no mesmo payload. + static const _separator = '\u001F'; + + String _payloadOf(AuditChainFields fields) => [ + fields.sequence.toString(), + fields.previousHash, + fields.userId, + fields.actionType, + fields.resourceType, + fields.resourceId ?? '', + fields.timestamp.toUtc().toIso8601String(), + fields.ipHash, + fields.result, + ].join(_separator); +} diff --git a/backend/sinalacs_server/lib/src/application/audit/audit_chain_verifier.dart b/backend/sinalacs_server/lib/src/application/audit/audit_chain_verifier.dart new file mode 100644 index 0000000..ceffecc --- /dev/null +++ b/backend/sinalacs_server/lib/src/application/audit/audit_chain_verifier.dart @@ -0,0 +1,108 @@ +import 'package:sinalacs_server/src/application/audit/audit_chain.dart'; + +/// Uma linha de `audit_logs` como a cadeia a vê: os campos que entram no +/// cálculo, mais o `entryHash` gravado — para o verificador comparar contra o +/// que `AuditChain.computeEntryHash` recalcula. +class AuditChainEntry { + const AuditChainEntry({required this.fields, required this.entryHash}); + + final AuditChainFields fields; + final String entryHash; +} + +/// Fonte das linhas de `audit_logs`, em ordem de `sequence` — implementada +/// sobre o ORM em `infrastructure/database/`, no mesmo molde de `AlertStore`/ +/// `VisitStore`: a interface fica em `application/` para o verificador ser +/// testável sem Postgres. +abstract interface class AuditChainReader { + Future> readInOrder(); +} + +/// Resultado de uma verificação da cadeia. +/// +/// `checked` conta quantas linhas confirmaram elo e hash antes de uma quebra +/// (ou o total, quando `ok` é `true`) — útil para dizer "as primeiras N linhas +/// estão intactas" mesmo quando a N+1 não está. +class AuditChainVerification { + const AuditChainVerification({ + required this.ok, + required this.checked, + this.brokenAtSequence, + this.reason, + }); + + final bool ok; + final int checked; + final int? brokenAtSequence; + final String? reason; +} + +/// Verifica a integridade da cadeia de hash de `audit_logs` (LGPD-RT03). +/// +/// Usa a MESMA [AuditChain] que grava as linhas: uma segunda implementação do +/// cálculo de hash aqui seria a forma mais provável de a verificação mentir. +/// +/// Três checagens, nesta ordem — cada uma cobre um jeito diferente de +/// adulterar a trilha: +/// 1. `sequence` contígua a partir de 1 — detecta linha **apagada**; +/// 2. `previousHash` da linha bate com o `entryHash` da anterior (e a +/// primeira usa [AuditChain.genesisHash]) — detecta **reordenação** e +/// **inserção**; +/// 3. `entryHash` recalculado bate com o gravado — detecta **edição de +/// conteúdo**, e forjar um hash que bata exige o segredo. +class AuditChainVerifier { + AuditChainVerifier({ + required AuditChainReader reader, + required String secret, + }) : _reader = reader, + _chain = AuditChain(secret: secret); + + final AuditChainReader _reader; + final AuditChain _chain; + + Future verify() async { + final entries = await _reader.readInOrder(); + + var expectedSequence = 1; + var expectedPreviousHash = AuditChain.genesisHash; + + for (final entry in entries) { + final fields = entry.fields; + + if (fields.sequence != expectedSequence) { + return AuditChainVerification( + ok: false, + checked: expectedSequence - 1, + brokenAtSequence: fields.sequence, + reason: 'sequência descontínua: esperava $expectedSequence, ' + 'encontrou ${fields.sequence} (linha apagada ou fora de ordem)', + ); + } + + if (fields.previousHash != expectedPreviousHash) { + return AuditChainVerification( + ok: false, + checked: expectedSequence - 1, + brokenAtSequence: fields.sequence, + reason: 'elo quebrado: previousHash não confere com a linha ' + 'anterior (possível reordenação ou inserção)', + ); + } + + if (!_chain.matches(fields, entry.entryHash)) { + return AuditChainVerification( + ok: false, + checked: expectedSequence - 1, + brokenAtSequence: fields.sequence, + reason: 'hash não confere com o conteúdo da linha ' + '(possível adulteração)', + ); + } + + expectedPreviousHash = entry.entryHash; + expectedSequence++; + } + + return AuditChainVerification(ok: true, checked: entries.length); + } +} diff --git a/backend/sinalacs_server/lib/src/application/sync/sync_fsm.dart b/backend/sinalacs_server/lib/src/application/sync/sync_fsm.dart index 361b274..fe63ad6 100644 --- a/backend/sinalacs_server/lib/src/application/sync/sync_fsm.dart +++ b/backend/sinalacs_server/lib/src/application/sync/sync_fsm.dart @@ -6,6 +6,12 @@ enum SyncState { conflict, synced, error, + + /// Terminal: o servidor recusou em definitivo (ver `VisitSyncService`, + /// `SyncStatus.rejected`). Ao contrário de `error`, nenhum evento de rede + /// tira uma visita deste estado — `networkUp`/`syncStart` não o alcançam, + /// porque o motivo da recusa não muda com uma próxima tentativa. + rejected, } enum SyncEvent { @@ -17,6 +23,7 @@ enum SyncEvent { syncAck, syncConflict, syncError, + syncRejected, } class SyncFsm { @@ -57,6 +64,9 @@ class SyncFsm { case SyncEvent.syncError: state = SyncState.error; break; + case SyncEvent.syncRejected: + state = SyncState.rejected; + break; case SyncEvent.networkDown: if (state == SyncState.syncing || state == SyncState.queued) { state = SyncState.queued; diff --git a/backend/sinalacs_server/lib/src/application/visits/visit_sync_service.dart b/backend/sinalacs_server/lib/src/application/visits/visit_sync_service.dart index 4d00eb4..8ea8f45 100644 --- a/backend/sinalacs_server/lib/src/application/visits/visit_sync_service.dart +++ b/backend/sinalacs_server/lib/src/application/visits/visit_sync_service.dart @@ -28,8 +28,14 @@ abstract interface class VisitStore { /// Sincronização das visitas registradas offline pelo ACS. /// /// Espelha a `SyncFsm` do lado do dispositivo: cada visita termina em -/// `synced`, `conflict` ou `error`, e conflito **nunca** sobrescreve o que está -/// no servidor — devolve a versão atual para o dispositivo reconciliar. +/// `synced`, `conflict`, `error` ou `rejected`, e conflito **nunca** +/// sobrescreve o que está no servidor — devolve a versão atual para o +/// dispositivo reconciliar. +/// +/// `error` é retentável (rede, paciente ainda não cadastrado); `rejected` é +/// terminal — o motivo não muda com uma próxima tentativa (identificador +/// malformado, território, dono do registro) — e o dispositivo não deve +/// reenviar. class VisitSyncService { VisitSyncService({ required VisitStore store, @@ -63,9 +69,10 @@ class VisitSyncService { required VisitSyncEntry entry, }) async { if (entry.localId.trim().isEmpty) { + // Terminal: um localId vazio não vira válido reenviando o mesmo lote. return VisitSyncResult( localId: entry.localId, - syncStatus: SyncStatus.error, + syncStatus: SyncStatus.rejected, message: 'localId é obrigatório', ); } @@ -80,9 +87,11 @@ class VisitSyncService { localId = UuidValue.withValidation(entry.localId); patientId = UuidValue.withValidation(entry.patientId); } on FormatException { + // Terminal: um identificador malformado na origem não vira válido + // reenviando. return VisitSyncResult( localId: entry.localId, - syncStatus: SyncStatus.error, + syncStatus: SyncStatus.rejected, message: 'identificadores devem ser UUID', ); } @@ -111,9 +120,10 @@ class VisitSyncService { resourceId: patientId.uuid, result: 'denied_territory', )); + // Terminal: o território não muda por retentar. return VisitSyncResult( localId: entry.localId, - syncStatus: SyncStatus.error, + syncStatus: SyncStatus.rejected, message: 'paciente fora da sua microárea', ); } @@ -146,9 +156,10 @@ class VisitSyncService { // A visita pertence ao ACS que a registrou. Um ACS não sincroniza a visita // de outro, mesmo conhecendo o localId. if (existing.acsId != acsId) { + // Terminal: o dono do registro não muda por retentar. return VisitSyncResult( localId: entry.localId, - syncStatus: SyncStatus.error, + syncStatus: SyncStatus.rejected, message: 'visita registrada por outro agente', ); } diff --git a/backend/sinalacs_server/lib/src/config/app_config.dart b/backend/sinalacs_server/lib/src/config/app_config.dart index 46449cc..4df3a5d 100644 --- a/backend/sinalacs_server/lib/src/config/app_config.dart +++ b/backend/sinalacs_server/lib/src/config/app_config.dart @@ -3,11 +3,13 @@ import 'dart:io'; /// Configuração que **não** vem do Serverpod. /// /// Banco e servidor são configurados por `config/*.yaml` mais as variáveis -/// `SERVERPOD_*`. O que sobra — MQTT e o segredo de autenticação — é lido aqui. +/// `SERVERPOD_*`. O que sobra — MQTT e os segredos de autenticação/auditoria — +/// é lido aqui. class AppConfig { const AppConfig({ required this.mqttBroker, required this.jwtSecret, + required this.auditChainSecret, required this.mqttUsername, required this.mqttPassword, required this.mqttUseTls, @@ -24,6 +26,14 @@ class AppConfig { /// um acesso de ACS para qualquer território. Ver [_resolveJwtSecret]. final String jwtSecret; + /// Chave HMAC da cadeia de hash de `audit_logs` (ver `application/audit/`). + /// + /// Deliberadamente um segredo PRÓPRIO, não derivado de [jwtSecret]: rotacionar + /// o JWT é rotina esperada, mas rotacionar este invalida silenciosamente a + /// verificação de tudo que já foi gravado na trilha. Ver + /// [_resolveAuditChainSecret]. + final String auditChainSecret; + final String? mqttUsername; final String? mqttPassword; final bool mqttUseTls; @@ -39,6 +49,11 @@ class AppConfig { /// `development`. [_resolveJwtSecret] recusa promovê-lo a outro ambiente. static const developmentJwtSecret = 'development-secret'; + /// Segredo usado quando `AUDIT_CHAIN_SECRET` não é informado em + /// desenvolvimento. Mesma regra de [developmentJwtSecret]: público, e por + /// isso restrito a `development` por [_resolveAuditChainSecret]. + static const developmentAuditChainSecret = 'development-audit-chain-secret'; + factory AppConfig.fromEnvironment() => AppConfig.fromMap(Platform.environment); @@ -52,7 +67,18 @@ class AppConfig { return AppConfig( mqttBroker: environment['MQTT_BROKER'] ?? 'localhost:1883', - jwtSecret: _resolveJwtSecret(environment['JWT_SECRET'], appEnv), + jwtSecret: _resolveSecret( + value: environment['JWT_SECRET'], + appEnv: appEnv, + envVarName: 'JWT_SECRET', + developmentFallback: developmentJwtSecret, + ), + auditChainSecret: _resolveSecret( + value: environment['AUDIT_CHAIN_SECRET'], + appEnv: appEnv, + envVarName: 'AUDIT_CHAIN_SECRET', + developmentFallback: developmentAuditChainSecret, + ), mqttUsername: environment['MQTT_USERNAME'], mqttPassword: environment['MQTT_PASSWORD'], mqttUseTls: environment['MQTT_USE_TLS'] == 'true', @@ -62,29 +88,36 @@ class AppConfig { ); } - /// Decide o segredo de assinatura, recusando subir com um segredo fraco. + /// Decide um segredo de assinatura, recusando subir com um valor fraco. /// - /// Só `development` aceita ausência de `JWT_SECRET`. Fora dele a falha é no - /// boot, e não na primeira requisição: um servidor que sobe assinando com um - /// segredo público é pior do que um servidor que não sobe. + /// Regra comum a `JWT_SECRET` e `AUDIT_CHAIN_SECRET`: só `development` aceita + /// ausência da variável. Fora dele a falha é no boot, e não na primeira + /// requisição — um servidor que sobe assinando com um segredo público é pior + /// do que um servidor que não sobe. /// - /// A checagem anterior cobria apenas `appEnv == 'production'` e testava só - /// `== null`, então `JWT_SECRET=""` e `APP_ENV=staging` passavam direto. - static String _resolveJwtSecret(String? value, String appEnv) { + /// A checagem anterior (antes de existir mais de um segredo) cobria apenas + /// `appEnv == 'production'` e testava só `== null`, então `JWT_SECRET=""` e + /// `APP_ENV=staging` passavam direto. + static String _resolveSecret({ + required String? value, + required String appEnv, + required String envVarName, + required String developmentFallback, + }) { final secret = value?.trim(); final isDevelopment = appEnv == 'development'; if (secret == null || secret.isEmpty) { - if (isDevelopment) return developmentJwtSecret; + if (isDevelopment) return developmentFallback; throw StateError( - 'JWT_SECRET é obrigatório quando APP_ENV=$appEnv. ' + '$envVarName é obrigatório quando APP_ENV=$appEnv. ' 'Gere um com: openssl rand -hex 32', ); } - if (secret == developmentJwtSecret && !isDevelopment) { + if (secret == developmentFallback && !isDevelopment) { throw StateError( - 'JWT_SECRET está usando o valor de desenvolvimento, que é público, ' + '$envVarName está usando o valor de desenvolvimento, que é público, ' 'com APP_ENV=$appEnv. Gere um próprio com: openssl rand -hex 32', ); } diff --git a/backend/sinalacs_server/lib/src/generated/audit_log.dart b/backend/sinalacs_server/lib/src/generated/audit_log.dart index 5bcc67b..41c17cb 100644 --- a/backend/sinalacs_server/lib/src/generated/audit_log.dart +++ b/backend/sinalacs_server/lib/src/generated/audit_log.dart @@ -14,6 +14,16 @@ import 'package:serverpod/serverpod.dart' as _i1; /// Trilha de auditoria de acesso a dados sensíveis. Append-only. +/// +/// `sequence`/`previousHash`/`entryHash` formam uma cadeia de hash (LGPD-RT03): +/// cada linha encadeia à anterior via `previousHash == entryHash` da linha de +/// `sequence - 1`, e `entryHash` é um HMAC-SHA256 sobre o conteúdo da própria +/// linha (ver `application/audit/audit_chain.dart`). Isso torna qualquer +/// edição, remoção ou reordenação de linha detectável — inclusive por quem tem +/// acesso de escrita direto ao Postgres, que é o adversário que a §458 de +/// spec/lgpd_design.md descreve. O índice único em `sequence` é o segundo +/// cinto: uma bifurcação da cadeia por concorrência estoura na hora em vez de +/// corromper em silêncio. abstract class AuditLog implements _i1.TableRow<_i1.UuidValue?>, _i1.ProtocolSerialization { AuditLog._({ @@ -25,6 +35,9 @@ abstract class AuditLog required this.timestamp, required this.ipHash, required this.result, + required this.sequence, + required this.previousHash, + required this.entryHash, }); factory AuditLog({ @@ -36,6 +49,9 @@ abstract class AuditLog required DateTime timestamp, required String ipHash, required String result, + required int sequence, + required String previousHash, + required String entryHash, }) = _AuditLogImpl; factory AuditLog.fromJson(Map jsonSerialization) { @@ -56,6 +72,9 @@ abstract class AuditLog ), ipHash: jsonSerialization['ipHash'] as String, result: jsonSerialization['result'] as String, + sequence: jsonSerialization['sequence'] as int, + previousHash: jsonSerialization['previousHash'] as String, + entryHash: jsonSerialization['entryHash'] as String, ); } @@ -80,6 +99,18 @@ abstract class AuditLog String result; + /// Posição na cadeia, começando em 1. Contígua por construção — um buraco + /// aqui é uma linha apagada. + int sequence; + + /// `entryHash` da linha anterior, ou `AuditChain.genesisHash` (64 zeros) na + /// primeira linha. Nunca nulo: gênese e "escrito antes da cadeia existir" + /// não podem ter a mesma representação. + String previousHash; + + /// HMAC-SHA256(AUDIT_CHAIN_SECRET, conteúdo da linha). Ver AuditChain.compute. + String entryHash; + @override _i1.Table<_i1.UuidValue?> get table => t; @@ -95,6 +126,9 @@ abstract class AuditLog DateTime? timestamp, String? ipHash, String? result, + int? sequence, + String? previousHash, + String? entryHash, }); @override Map toJson() { @@ -108,6 +142,9 @@ abstract class AuditLog 'timestamp': timestamp.toJson(), 'ipHash': ipHash, 'result': result, + 'sequence': sequence, + 'previousHash': previousHash, + 'entryHash': entryHash, }; } @@ -123,6 +160,9 @@ abstract class AuditLog 'timestamp': timestamp.toJson(), 'ipHash': ipHash, 'result': result, + 'sequence': sequence, + 'previousHash': previousHash, + 'entryHash': entryHash, }; } @@ -168,6 +208,9 @@ class _AuditLogImpl extends AuditLog { required DateTime timestamp, required String ipHash, required String result, + required int sequence, + required String previousHash, + required String entryHash, }) : super._( id: id, userId: userId, @@ -177,6 +220,9 @@ class _AuditLogImpl extends AuditLog { timestamp: timestamp, ipHash: ipHash, result: result, + sequence: sequence, + previousHash: previousHash, + entryHash: entryHash, ); /// Returns a shallow copy of this [AuditLog] @@ -192,6 +238,9 @@ class _AuditLogImpl extends AuditLog { DateTime? timestamp, String? ipHash, String? result, + int? sequence, + String? previousHash, + String? entryHash, }) { return AuditLog( id: id is _i1.UuidValue? ? id : this.id, @@ -202,6 +251,9 @@ class _AuditLogImpl extends AuditLog { timestamp: timestamp ?? this.timestamp, ipHash: ipHash ?? this.ipHash, result: result ?? this.result, + sequence: sequence ?? this.sequence, + previousHash: previousHash ?? this.previousHash, + entryHash: entryHash ?? this.entryHash, ); } } @@ -247,6 +299,21 @@ class AuditLogUpdateTable extends _i1.UpdateTable { table.result, value, ); + + _i1.ColumnValue sequence(int value) => _i1.ColumnValue( + table.sequence, + value, + ); + + _i1.ColumnValue previousHash(String value) => _i1.ColumnValue( + table.previousHash, + value, + ); + + _i1.ColumnValue entryHash(String value) => _i1.ColumnValue( + table.entryHash, + value, + ); } class AuditLogTable extends _i1.Table<_i1.UuidValue?> { @@ -280,6 +347,18 @@ class AuditLogTable extends _i1.Table<_i1.UuidValue?> { 'result', this, ); + sequence = _i1.ColumnInt( + 'sequence', + this, + ); + previousHash = _i1.ColumnString( + 'previousHash', + this, + ); + entryHash = _i1.ColumnString( + 'entryHash', + this, + ); } late final AuditLogUpdateTable updateTable; @@ -298,6 +377,18 @@ class AuditLogTable extends _i1.Table<_i1.UuidValue?> { late final _i1.ColumnString result; + /// Posição na cadeia, começando em 1. Contígua por construção — um buraco + /// aqui é uma linha apagada. + late final _i1.ColumnInt sequence; + + /// `entryHash` da linha anterior, ou `AuditChain.genesisHash` (64 zeros) na + /// primeira linha. Nunca nulo: gênese e "escrito antes da cadeia existir" + /// não podem ter a mesma representação. + late final _i1.ColumnString previousHash; + + /// HMAC-SHA256(AUDIT_CHAIN_SECRET, conteúdo da linha). Ver AuditChain.compute. + late final _i1.ColumnString entryHash; + @override List<_i1.Column> get columns => [ id, @@ -308,6 +399,9 @@ class AuditLogTable extends _i1.Table<_i1.UuidValue?> { timestamp, ipHash, result, + sequence, + previousHash, + entryHash, ]; } diff --git a/backend/sinalacs_server/lib/src/generated/enums/sync_status.dart b/backend/sinalacs_server/lib/src/generated/enums/sync_status.dart index 53f2f07..7f94ea4 100644 --- a/backend/sinalacs_server/lib/src/generated/enums/sync_status.dart +++ b/backend/sinalacs_server/lib/src/generated/enums/sync_status.dart @@ -14,11 +14,18 @@ import 'package:serverpod/serverpod.dart' as _i1; /// Estado de sincronização offline-first, espelha a SyncFsm. +/// +/// `rejected` é TERMINAL: ao contrário de `error`, que é retentável (o +/// dispositivo tenta de novo mais tarde), uma visita `rejected` nunca vai dar +/// certo numa próxima tentativa — o motivo não muda com o tempo (ex.: paciente +/// fora da microárea do ACS, identificador malformado). `SyncFsm.rejected` não +/// tem transição de saída; `networkUp`/`syncStart` não o alcançam. enum SyncStatus implements _i1.SerializableModel { pending, synced, conflict, - error; + error, + rejected; static SyncStatus fromJson(String name) { switch (name) { @@ -30,6 +37,8 @@ enum SyncStatus implements _i1.SerializableModel { return SyncStatus.conflict; case 'error': return SyncStatus.error; + case 'rejected': + return SyncStatus.rejected; default: throw ArgumentError( 'Value "$name" cannot be converted to "SyncStatus"', diff --git a/backend/sinalacs_server/lib/src/generated/protocol.dart b/backend/sinalacs_server/lib/src/generated/protocol.dart index d53b1aa..509ba81 100644 --- a/backend/sinalacs_server/lib/src/generated/protocol.dart +++ b/backend/sinalacs_server/lib/src/generated/protocol.dart @@ -646,6 +646,24 @@ class Protocol extends _i1.SerializationManagerServer { isNullable: false, dartType: 'String', ), + _i2.ColumnDefinition( + name: 'sequence', + columnType: _i2.ColumnType.bigint, + isNullable: false, + dartType: 'int', + ), + _i2.ColumnDefinition( + name: 'previousHash', + columnType: _i2.ColumnType.text, + isNullable: false, + dartType: 'String', + ), + _i2.ColumnDefinition( + name: 'entryHash', + columnType: _i2.ColumnType.text, + isNullable: false, + dartType: 'String', + ), ], foreignKeys: [ _i2.ForeignKeyDefinition( @@ -673,6 +691,19 @@ class Protocol extends _i1.SerializationManagerServer { isUnique: true, isPrimary: true, ), + _i2.IndexDefinition( + indexName: 'audit_logs_sequence_idx', + tableSpace: null, + elements: [ + _i2.IndexElementDefinition( + type: _i2.IndexElementDefinitionType.column, + definition: 'sequence', + ), + ], + type: 'btree', + isUnique: true, + isPrimary: false, + ), ], managed: true, ), diff --git a/backend/sinalacs_server/lib/src/infrastructure/database/orm_audit_chain_reader.dart b/backend/sinalacs_server/lib/src/infrastructure/database/orm_audit_chain_reader.dart new file mode 100644 index 0000000..28bf7fb --- /dev/null +++ b/backend/sinalacs_server/lib/src/infrastructure/database/orm_audit_chain_reader.dart @@ -0,0 +1,37 @@ +import 'package:serverpod/serverpod.dart'; +import 'package:sinalacs_server/src/application/audit/audit_chain.dart'; +import 'package:sinalacs_server/src/application/audit/audit_chain_verifier.dart'; +import 'package:sinalacs_server/src/generated/protocol.dart'; + +/// Implementação de [AuditChainReader] sobre o ORM do Serverpod. +class OrmAuditChainReader implements AuditChainReader { + OrmAuditChainReader({required Session Function() session}) : _session = session; + + final Session Function() _session; + + @override + Future> readInOrder() async { + final rows = await AuditLog.db.find( + _session(), + orderBy: (t) => t.sequence, + ); + + return [ + for (final row in rows) + AuditChainEntry( + fields: AuditChainFields( + sequence: row.sequence, + previousHash: row.previousHash, + userId: row.userId.uuid, + actionType: row.actionType, + resourceType: row.resourceType, + resourceId: row.resourceId?.uuid, + timestamp: row.timestamp, + ipHash: row.ipHash, + result: row.result, + ), + entryHash: row.entryHash, + ), + ]; + } +} diff --git a/backend/sinalacs_server/lib/src/infrastructure/database/orm_audit_trail.dart b/backend/sinalacs_server/lib/src/infrastructure/database/orm_audit_trail.dart index 97fa24a..4f46e6c 100644 --- a/backend/sinalacs_server/lib/src/infrastructure/database/orm_audit_trail.dart +++ b/backend/sinalacs_server/lib/src/infrastructure/database/orm_audit_trail.dart @@ -1,5 +1,6 @@ import 'package:crypto/crypto.dart'; import 'package:serverpod/serverpod.dart'; +import 'package:sinalacs_server/src/application/audit/audit_chain.dart'; import 'package:sinalacs_server/src/application/audit/audit_trail.dart'; import 'package:sinalacs_server/src/generated/protocol.dart'; @@ -11,10 +12,26 @@ import 'package:sinalacs_server/src/generated/protocol.dart'; /// de proxy (prefere `Forwarded`/`X-Forwarded-For` antes do endereço da /// conexão) — decisivo aqui porque a stack tem Traefik na frente, e o endereço /// da conexão seria sempre o do proxy, não o do ACS. +/// +/// Cada gravação também estende a cadeia de hash de `audit_logs` (LGPD-RT03): +/// lê a última linha e insere a próxima dentro da MESMA transação, sob um +/// advisory lock do Postgres — sem isso, duas gravações concorrentes leriam a +/// mesma última linha e encadeariam nela ao mesmo tempo, bifurcando a cadeia. +/// `pg_advisory_xact_lock` solta sozinho no fim da transação (commit ou +/// rollback), então não existe caminho que deixe o lock preso. class OrmAuditTrail extends AuditTrail { - OrmAuditTrail({required Session Function() session}) : _session = session; + OrmAuditTrail({ + required Session Function() session, + required String chainSecret, + }) : _session = session, + _chain = AuditChain(secret: chainSecret); final Session Function() _session; + final AuditChain _chain; + + /// Chave arbitrária e fixa do advisory lock que serializa o apêndice à + /// cadeia. Só precisa ser estável entre chamadas — não é derivada de nada. + static const _chainLockKey = 725100823; @override Future record(AuditEvent event) async { @@ -23,18 +40,54 @@ class OrmAuditTrail extends AuditTrail { // para string vazia, que se confundiria com "IP resolvido, mas vazio" — // um marcador explícito deixa a ausência de request auditável também. final remoteInfo = session.request?.remoteInfo ?? 'sem-requisicao-http'; + final ipHash = sha256.convert(remoteInfo.codeUnits).toString(); + final resourceId = event.resourceId; + final timestamp = DateTime.now().toUtc(); + + await session.db.transaction((transaction) async { + await session.db.unsafeExecute( + 'SELECT pg_advisory_xact_lock(@key);', + parameters: QueryParameters.named({'key': _chainLockKey}), + transaction: transaction, + ); - await AuditLog.db.insertRow( - session, - AuditLog( - userId: UuidValue.fromString(event.userId), + final last = await AuditLog.db.findFirstRow( + session, + orderBy: (t) => t.sequence, + orderDescending: true, + transaction: transaction, + ); + + final sequence = (last?.sequence ?? 0) + 1; + final previousHash = last?.entryHash ?? AuditChain.genesisHash; + final entryHash = _chain.computeEntryHash(AuditChainFields( + sequence: sequence, + previousHash: previousHash, + userId: event.userId, actionType: event.actionType, resourceType: event.resourceType, - resourceId: event.resourceId == null ? null : UuidValue.fromString(event.resourceId!), - timestamp: DateTime.now().toUtc(), - ipHash: sha256.convert(remoteInfo.codeUnits).toString(), + resourceId: resourceId, + timestamp: timestamp, + ipHash: ipHash, result: event.result, - ), - ); + )); + + await AuditLog.db.insertRow( + session, + AuditLog( + userId: UuidValue.fromString(event.userId), + actionType: event.actionType, + resourceType: event.resourceType, + resourceId: resourceId == null ? null : UuidValue.fromString(resourceId), + timestamp: timestamp, + ipHash: ipHash, + result: event.result, + sequence: sequence, + previousHash: previousHash, + entryHash: entryHash, + ), + transaction: transaction, + ); + }); } } diff --git a/backend/sinalacs_server/lib/src/models/audit_log.spy.yaml b/backend/sinalacs_server/lib/src/models/audit_log.spy.yaml index 1d2894d..8518187 100644 --- a/backend/sinalacs_server/lib/src/models/audit_log.spy.yaml +++ b/backend/sinalacs_server/lib/src/models/audit_log.spy.yaml @@ -1,4 +1,14 @@ ### Trilha de auditoria de acesso a dados sensíveis. Append-only. +### +### `sequence`/`previousHash`/`entryHash` formam uma cadeia de hash (LGPD-RT03): +### cada linha encadeia à anterior via `previousHash == entryHash` da linha de +### `sequence - 1`, e `entryHash` é um HMAC-SHA256 sobre o conteúdo da própria +### linha (ver `application/audit/audit_chain.dart`). Isso torna qualquer +### edição, remoção ou reordenação de linha detectável — inclusive por quem tem +### acesso de escrita direto ao Postgres, que é o adversário que a §458 de +### spec/lgpd_design.md descreve. O índice único em `sequence` é o segundo +### cinto: uma bifurcação da cadeia por concorrência estoura na hora em vez de +### corromper em silêncio. class: AuditLog table: audit_logs fields: @@ -10,3 +20,16 @@ fields: timestamp: DateTime ipHash: String result: String + ### Posição na cadeia, começando em 1. Contígua por construção — um buraco + ### aqui é uma linha apagada. + sequence: int + ### `entryHash` da linha anterior, ou `AuditChain.genesisHash` (64 zeros) na + ### primeira linha. Nunca nulo: gênese e "escrito antes da cadeia existir" + ### não podem ter a mesma representação. + previousHash: String + ### HMAC-SHA256(AUDIT_CHAIN_SECRET, conteúdo da linha). Ver AuditChain.compute. + entryHash: String +indexes: + audit_logs_sequence_idx: + fields: sequence + unique: true diff --git a/backend/sinalacs_server/lib/src/models/enums/sync_status.spy.yaml b/backend/sinalacs_server/lib/src/models/enums/sync_status.spy.yaml index 4a628fc..9aea5af 100644 --- a/backend/sinalacs_server/lib/src/models/enums/sync_status.spy.yaml +++ b/backend/sinalacs_server/lib/src/models/enums/sync_status.spy.yaml @@ -1,4 +1,10 @@ ### Estado de sincronização offline-first, espelha a SyncFsm. +### +### `rejected` é TERMINAL: ao contrário de `error`, que é retentável (o +### dispositivo tenta de novo mais tarde), uma visita `rejected` nunca vai dar +### certo numa próxima tentativa — o motivo não muda com o tempo (ex.: paciente +### fora da microárea do ACS, identificador malformado). `SyncFsm.rejected` não +### tem transição de saída; `networkUp`/`syncStart` não o alcançam. enum: SyncStatus serialized: byName values: @@ -6,3 +12,4 @@ values: - synced - conflict - error + - rejected diff --git a/backend/sinalacs_server/lib/src/runtime/alert_runtime.dart b/backend/sinalacs_server/lib/src/runtime/alert_runtime.dart index 59756d6..42548e8 100644 --- a/backend/sinalacs_server/lib/src/runtime/alert_runtime.dart +++ b/backend/sinalacs_server/lib/src/runtime/alert_runtime.dart @@ -100,7 +100,10 @@ class AlertRuntime { ); /// Trilha de auditoria amarrada à sessão da chamada. - AuditTrail auditTrailFor(Session session) => OrmAuditTrail(session: () => session); + AuditTrail auditTrailFor(Session session) => OrmAuditTrail( + session: () => session, + chainSecret: config.auditChainSecret, + ); /// Drenador do outbox. /// diff --git a/backend/sinalacs_server/migrations/20260914154730381/definition.json b/backend/sinalacs_server/migrations/20260914154730381/definition.json new file mode 100644 index 0000000..54cb3ba --- /dev/null +++ b/backend/sinalacs_server/migrations/20260914154730381/definition.json @@ -0,0 +1,2750 @@ +{ + "__className__": "serverpod.DatabaseDefinition", + "moduleName": "sinalacs", + "tables": [ + { + "__className__": "serverpod.TableDefinition", + "name": "acs", + "dartName": "Acs", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "enrollmentId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "ubsId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "active", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastSyncAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_ubs_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "ubsId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "alert_deliveries", + "dartName": "AlertDeliveryRecord", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "alertId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "acsId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "acknowledgedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "alert_deliveries_fk_0", + "columns": [ + "alertId" + ], + "referenceTable": "alerts", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "alert_deliveries_fk_1", + "columns": [ + "acsId" + ], + "referenceTable": "acs", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alert_deliveries_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alert_deliveries_alert_acs_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "alertId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "acsId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "alert_idempotency_keys", + "dartName": "AlertIdempotencyKey", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "key", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "alertId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "locationHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "alert_idempotency_keys_fk_0", + "columns": [ + "alertId" + ], + "referenceTable": "alerts", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alert_idempotency_keys_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alert_idempotency_keys_key_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "key" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "alert_outbox", + "dartName": "AlertOutboxEntry", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "alertId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "topic", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "payload", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "publishedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "attempts", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "nextAttemptAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastError", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "alert_outbox_fk_0", + "columns": [ + "alertId" + ], + "referenceTable": "alerts", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alert_outbox_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alert_outbox_pending_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "publishedAt" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "nextAttemptAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "alerts", + "dartName": "Alert", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "patientId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "acsId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "microAreaId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "triggeredAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "receivedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "respondedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "acknowledgedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "riskLevel", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:RiskLevel" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "locationHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "status", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:AlertStatus" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "mqttTopic", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "deviceId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "retryCount", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "version", + "columnType": 6, + "isNullable": false, + "dartType": "int" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "alerts_fk_0", + "columns": [ + "patientId" + ], + "referenceTable": "patients", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "alerts_fk_1", + "columns": [ + "acsId" + ], + "referenceTable": "acs", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alerts_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alerts_micro_area_status_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "microAreaId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "status" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "triggeredAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "audit_logs", + "dartName": "AuditLog", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "actionType", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "resourceType", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "resourceId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "timestamp", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "ipHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "result", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sequence", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "previousHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "entryHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "audit_logs_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "audit_logs_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "audit_logs_sequence_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sequence" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "consent_logs", + "dartName": "ConsentLog", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "purpose", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "action", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "version", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "timestamp", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "ipHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userAgent", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "signature", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "consent_logs_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "consent_logs_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "micro_areas", + "dartName": "MicroArea", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "ubsId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "geoJsonBoundary", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "micro_areas_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "micro_areas_ubs_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "ubsId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "patients", + "dartName": "Patient", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "emergencyContact", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "isChronic", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "chronicConditions", + "columnType": 8, + "isNullable": false, + "dartType": "List" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastLocationHash", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastTriageAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "patients_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "triage_sessions", + "dartName": "TriageSession", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "patientId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "answers", + "columnType": 8, + "isNullable": false, + "dartType": "List" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "resultRisk", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:RiskLevel" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "resultDisplay", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "deviceId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "triage_sessions_fk_0", + "columns": [ + "patientId" + ], + "referenceTable": "patients", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "triage_sessions_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "ubs", + "dartName": "Ubs", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "address", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "city", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "state", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "ubs_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "users", + "dartName": "User", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "cpfHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "birthDate", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "role", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:UserRole" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "microAreaId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "users_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "users_cpf_hash_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "cpfHash" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "users_micro_area_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "microAreaId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "visits", + "dartName": "Visit", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "patientId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "acsId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "scheduledAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "startedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "completedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "status", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "riskLevelBefore", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:RiskLevel" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "riskLevelAfter", + "columnType": 0, + "isNullable": true, + "dartType": "protocol:RiskLevel?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "notes", + "columnType": 8, + "isNullable": false, + "dartType": "Map" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "syncStatus", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:SyncStatus" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "localId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "syncAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "version", + "columnType": 6, + "isNullable": false, + "dartType": "int" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "visits_fk_0", + "columns": [ + "patientId" + ], + "referenceTable": "patients", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "visits_fk_1", + "columns": [ + "acsId" + ], + "referenceTable": "acs", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "visits_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "visits_local_id_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "localId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_cloud_storage", + "dartName": "CloudStorageEntry", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "nextval('serverpod_cloud_storage_id_seq'::regclass)", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "storageId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "path", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "addedTime", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "expiration", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "byteData", + "columnType": 5, + "isNullable": false, + "dartType": "dart:typed_data:ByteData" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "verified", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_cloud_storage_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_cloud_storage_path_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "storageId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "path" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_cloud_storage_expiration", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "expiration" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_cloud_storage_direct_upload", + "dartName": "CloudStorageDirectUploadEntry", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "nextval('serverpod_cloud_storage_direct_upload_id_seq'::regclass)", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "storageId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "path", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "expiration", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authKey", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_cloud_storage_direct_upload_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_cloud_storage_direct_upload_storage_path", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "storageId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "path" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_future_call", + "dartName": "FutureCallEntry", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "nextval('serverpod_future_call_id_seq'::regclass)", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "time", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "serializedObject", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "serverId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "identifier", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_future_call_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_future_call_time_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "time" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_future_call_serverId_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "serverId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_future_call_identifier_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "identifier" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_health_connection_info", + "dartName": "ServerHealthConnectionInfo", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "nextval('serverpod_health_connection_info_id_seq'::regclass)", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "serverId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "timestamp", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "active", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "closing", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "idle", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "granularity", + "columnType": 6, + "isNullable": false, + "dartType": "int" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_health_connection_info_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_health_connection_info_timestamp_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "timestamp" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "serverId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "granularity" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_health_metric", + "dartName": "ServerHealthMetric", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "nextval('serverpod_health_metric_id_seq'::regclass)", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "serverId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "timestamp", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "isHealthy", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "value", + "columnType": 3, + "isNullable": false, + "dartType": "double" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "granularity", + "columnType": 6, + "isNullable": false, + "dartType": "int" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_health_metric_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_health_metric_timestamp_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "timestamp" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "serverId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "name" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "granularity" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_log", + "dartName": "LogEntry", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "nextval('serverpod_log_id_seq'::regclass)", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sessionLogId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "messageId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "reference", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "serverId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "time", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "logLevel", + "columnType": 6, + "isNullable": false, + "dartType": "protocol:LogLevel" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "message", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "error", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "stackTrace", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "order", + "columnType": 6, + "isNullable": false, + "dartType": "int" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "serverpod_log_fk_0", + "columns": [ + "sessionLogId" + ], + "referenceTable": "serverpod_session_log", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_log_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_log_sessionLogId_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sessionLogId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_message_log", + "dartName": "MessageLogEntry", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "nextval('serverpod_message_log_id_seq'::regclass)", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sessionLogId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "serverId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "messageId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "endpoint", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "messageName", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "duration", + "columnType": 3, + "isNullable": false, + "dartType": "double" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "error", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "stackTrace", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "slow", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "order", + "columnType": 6, + "isNullable": false, + "dartType": "int" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "serverpod_message_log_fk_0", + "columns": [ + "sessionLogId" + ], + "referenceTable": "serverpod_session_log", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_message_log_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_method", + "dartName": "MethodInfo", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "nextval('serverpod_method_id_seq'::regclass)", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "endpoint", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "method", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_method_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_method_endpoint_method_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "endpoint" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "method" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_migrations", + "dartName": "DatabaseMigrationVersion", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "nextval('serverpod_migrations_id_seq'::regclass)", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "module", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "version", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "timestamp", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_migrations_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_migrations_ids", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "module" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_query_log", + "dartName": "QueryLogEntry", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "nextval('serverpod_query_log_id_seq'::regclass)", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "serverId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sessionLogId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "messageId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "query", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "duration", + "columnType": 3, + "isNullable": false, + "dartType": "double" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "numRows", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "error", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "stackTrace", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "slow", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "order", + "columnType": 6, + "isNullable": false, + "dartType": "int" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "serverpod_query_log_fk_0", + "columns": [ + "sessionLogId" + ], + "referenceTable": "serverpod_session_log", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_query_log_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_query_log_sessionLogId_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sessionLogId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_readwrite_test", + "dartName": "ReadWriteTestEntry", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "nextval('serverpod_readwrite_test_id_seq'::regclass)", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "number", + "columnType": 6, + "isNullable": false, + "dartType": "int" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_readwrite_test_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_runtime_settings", + "dartName": "RuntimeSettings", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "nextval('serverpod_runtime_settings_id_seq'::regclass)", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "logSettings", + "columnType": 8, + "isNullable": false, + "dartType": "protocol:LogSettings" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "logSettingsOverrides", + "columnType": 8, + "isNullable": false, + "dartType": "List" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "logServiceCalls", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "logMalformedCalls", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_runtime_settings_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_session_log", + "dartName": "SessionLogEntry", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "nextval('serverpod_session_log_id_seq'::regclass)", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "serverId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "time", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "module", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "endpoint", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "method", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "duration", + "columnType": 3, + "isNullable": true, + "dartType": "double?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "numQueries", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "slow", + "columnType": 1, + "isNullable": true, + "dartType": "bool?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "error", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "stackTrace", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authenticatedUserId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "isOpen", + "columnType": 1, + "isNullable": true, + "dartType": "bool?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "touched", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_session_log_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_session_log_serverid_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "serverId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_session_log_time_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "time" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_session_log_touched_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "touched" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_session_log_isopen_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "isOpen" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + } + ], + "installedModules": [ + { + "__className__": "serverpod.DatabaseMigrationVersion", + "module": "sinalacs", + "version": "20260914154730381" + }, + { + "__className__": "serverpod.DatabaseMigrationVersion", + "module": "serverpod", + "version": "20260129180959368" + } + ], + "migrationApiVersion": 1 +} \ No newline at end of file diff --git a/backend/sinalacs_server/migrations/20260914154730381/definition.sql b/backend/sinalacs_server/migrations/20260914154730381/definition.sql new file mode 100644 index 0000000..d300463 --- /dev/null +++ b/backend/sinalacs_server/migrations/20260914154730381/definition.sql @@ -0,0 +1,565 @@ +BEGIN; + +-- +-- Class Acs as table acs +-- +CREATE TABLE "acs" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "enrollmentId" text NOT NULL, + "ubsId" uuid NOT NULL, + "active" boolean NOT NULL, + "lastSyncAt" timestamp without time zone +); + +-- Indexes +CREATE INDEX "acs_ubs_idx" ON "acs" USING btree ("ubsId"); + +-- +-- Class AlertDeliveryRecord as table alert_deliveries +-- +CREATE TABLE "alert_deliveries" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "alertId" uuid NOT NULL, + "acsId" uuid NOT NULL, + "acknowledgedAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "alert_deliveries_alert_acs_key" ON "alert_deliveries" USING btree ("alertId", "acsId"); + +-- +-- Class AlertIdempotencyKey as table alert_idempotency_keys +-- +CREATE TABLE "alert_idempotency_keys" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "key" text NOT NULL, + "alertId" uuid NOT NULL, + "locationHash" text NOT NULL, + "createdAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "alert_idempotency_keys_key_key" ON "alert_idempotency_keys" USING btree ("key"); + +-- +-- Class AlertOutboxEntry as table alert_outbox +-- +CREATE TABLE "alert_outbox" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "alertId" uuid NOT NULL, + "topic" text NOT NULL, + "payload" text NOT NULL, + "createdAt" timestamp without time zone NOT NULL, + "publishedAt" timestamp without time zone, + "attempts" bigint NOT NULL, + "nextAttemptAt" timestamp without time zone NOT NULL, + "lastError" text +); + +-- Indexes +CREATE INDEX "alert_outbox_pending_idx" ON "alert_outbox" USING btree ("publishedAt", "nextAttemptAt"); + +-- +-- Class Alert as table alerts +-- +CREATE TABLE "alerts" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "patientId" uuid NOT NULL, + "acsId" uuid, + "microAreaId" uuid, + "triggeredAt" timestamp without time zone NOT NULL, + "receivedAt" timestamp without time zone, + "respondedAt" timestamp without time zone, + "acknowledgedAt" timestamp without time zone, + "riskLevel" text NOT NULL, + "locationHash" text NOT NULL, + "status" text NOT NULL, + "mqttTopic" text NOT NULL, + "deviceId" text NOT NULL, + "retryCount" bigint NOT NULL, + "version" bigint NOT NULL +); + +-- Indexes +CREATE INDEX "alerts_micro_area_status_idx" ON "alerts" USING btree ("microAreaId", "status", "triggeredAt"); + +-- +-- Class AuditLog as table audit_logs +-- +CREATE TABLE "audit_logs" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "userId" uuid NOT NULL, + "actionType" text NOT NULL, + "resourceType" text NOT NULL, + "resourceId" uuid, + "timestamp" timestamp without time zone NOT NULL, + "ipHash" text NOT NULL, + "result" text NOT NULL, + "sequence" bigint NOT NULL, + "previousHash" text NOT NULL, + "entryHash" text NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "audit_logs_sequence_idx" ON "audit_logs" USING btree ("sequence"); + +-- +-- Class ConsentLog as table consent_logs +-- +CREATE TABLE "consent_logs" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "userId" uuid NOT NULL, + "purpose" text NOT NULL, + "action" text NOT NULL, + "version" text NOT NULL, + "timestamp" timestamp without time zone NOT NULL, + "ipHash" text NOT NULL, + "userAgent" text NOT NULL, + "signature" text NOT NULL +); + +-- +-- Class MicroArea as table micro_areas +-- +CREATE TABLE "micro_areas" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "name" text NOT NULL, + "ubsId" uuid NOT NULL, + "geoJsonBoundary" text NOT NULL +); + +-- Indexes +CREATE INDEX "micro_areas_ubs_idx" ON "micro_areas" USING btree ("ubsId"); + +-- +-- Class Patient as table patients +-- +CREATE TABLE "patients" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "emergencyContact" text NOT NULL, + "isChronic" boolean NOT NULL, + "chronicConditions" json NOT NULL, + "lastLocationHash" text, + "lastTriageAt" timestamp without time zone +); + +-- +-- Class TriageSession as table triage_sessions +-- +CREATE TABLE "triage_sessions" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "patientId" uuid NOT NULL, + "answers" json NOT NULL, + "resultRisk" text NOT NULL, + "resultDisplay" text NOT NULL, + "createdAt" timestamp without time zone NOT NULL, + "deviceId" text NOT NULL +); + +-- +-- Class Ubs as table ubs +-- +CREATE TABLE "ubs" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "name" text NOT NULL, + "address" text NOT NULL, + "city" text NOT NULL, + "state" text NOT NULL +); + +-- +-- Class User as table users +-- +CREATE TABLE "users" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "cpfHash" text NOT NULL, + "name" text NOT NULL, + "birthDate" timestamp without time zone NOT NULL, + "role" text NOT NULL, + "microAreaId" uuid, + "createdAt" timestamp without time zone NOT NULL, + "updatedAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE INDEX "users_cpf_hash_idx" ON "users" USING btree ("cpfHash"); +CREATE INDEX "users_micro_area_idx" ON "users" USING btree ("microAreaId"); + +-- +-- Class Visit as table visits +-- +CREATE TABLE "visits" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "patientId" uuid NOT NULL, + "acsId" uuid NOT NULL, + "scheduledAt" timestamp without time zone NOT NULL, + "startedAt" timestamp without time zone, + "completedAt" timestamp without time zone, + "status" text NOT NULL, + "riskLevelBefore" text NOT NULL, + "riskLevelAfter" text, + "notes" json NOT NULL, + "syncStatus" text NOT NULL, + "localId" uuid NOT NULL, + "syncAt" timestamp without time zone, + "version" bigint NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "visits_local_id_key" ON "visits" USING btree ("localId"); + +-- +-- Class CloudStorageEntry as table serverpod_cloud_storage +-- +CREATE TABLE "serverpod_cloud_storage" ( + "id" bigserial PRIMARY KEY, + "storageId" text NOT NULL, + "path" text NOT NULL, + "addedTime" timestamp without time zone NOT NULL, + "expiration" timestamp without time zone, + "byteData" bytea NOT NULL, + "verified" boolean NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "serverpod_cloud_storage_path_idx" ON "serverpod_cloud_storage" USING btree ("storageId", "path"); +CREATE INDEX "serverpod_cloud_storage_expiration" ON "serverpod_cloud_storage" USING btree ("expiration"); + +-- +-- Class CloudStorageDirectUploadEntry as table serverpod_cloud_storage_direct_upload +-- +CREATE TABLE "serverpod_cloud_storage_direct_upload" ( + "id" bigserial PRIMARY KEY, + "storageId" text NOT NULL, + "path" text NOT NULL, + "expiration" timestamp without time zone NOT NULL, + "authKey" text NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "serverpod_cloud_storage_direct_upload_storage_path" ON "serverpod_cloud_storage_direct_upload" USING btree ("storageId", "path"); + +-- +-- Class FutureCallEntry as table serverpod_future_call +-- +CREATE TABLE "serverpod_future_call" ( + "id" bigserial PRIMARY KEY, + "name" text NOT NULL, + "time" timestamp without time zone NOT NULL, + "serializedObject" text, + "serverId" text NOT NULL, + "identifier" text +); + +-- Indexes +CREATE INDEX "serverpod_future_call_time_idx" ON "serverpod_future_call" USING btree ("time"); +CREATE INDEX "serverpod_future_call_serverId_idx" ON "serverpod_future_call" USING btree ("serverId"); +CREATE INDEX "serverpod_future_call_identifier_idx" ON "serverpod_future_call" USING btree ("identifier"); + +-- +-- Class ServerHealthConnectionInfo as table serverpod_health_connection_info +-- +CREATE TABLE "serverpod_health_connection_info" ( + "id" bigserial PRIMARY KEY, + "serverId" text NOT NULL, + "timestamp" timestamp without time zone NOT NULL, + "active" bigint NOT NULL, + "closing" bigint NOT NULL, + "idle" bigint NOT NULL, + "granularity" bigint NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "serverpod_health_connection_info_timestamp_idx" ON "serverpod_health_connection_info" USING btree ("timestamp", "serverId", "granularity"); + +-- +-- Class ServerHealthMetric as table serverpod_health_metric +-- +CREATE TABLE "serverpod_health_metric" ( + "id" bigserial PRIMARY KEY, + "name" text NOT NULL, + "serverId" text NOT NULL, + "timestamp" timestamp without time zone NOT NULL, + "isHealthy" boolean NOT NULL, + "value" double precision NOT NULL, + "granularity" bigint NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "serverpod_health_metric_timestamp_idx" ON "serverpod_health_metric" USING btree ("timestamp", "serverId", "name", "granularity"); + +-- +-- Class LogEntry as table serverpod_log +-- +CREATE TABLE "serverpod_log" ( + "id" bigserial PRIMARY KEY, + "sessionLogId" bigint NOT NULL, + "messageId" bigint, + "reference" text, + "serverId" text NOT NULL, + "time" timestamp without time zone NOT NULL, + "logLevel" bigint NOT NULL, + "message" text NOT NULL, + "error" text, + "stackTrace" text, + "order" bigint NOT NULL +); + +-- Indexes +CREATE INDEX "serverpod_log_sessionLogId_idx" ON "serverpod_log" USING btree ("sessionLogId"); + +-- +-- Class MessageLogEntry as table serverpod_message_log +-- +CREATE TABLE "serverpod_message_log" ( + "id" bigserial PRIMARY KEY, + "sessionLogId" bigint NOT NULL, + "serverId" text NOT NULL, + "messageId" bigint NOT NULL, + "endpoint" text NOT NULL, + "messageName" text NOT NULL, + "duration" double precision NOT NULL, + "error" text, + "stackTrace" text, + "slow" boolean NOT NULL, + "order" bigint NOT NULL +); + +-- +-- Class MethodInfo as table serverpod_method +-- +CREATE TABLE "serverpod_method" ( + "id" bigserial PRIMARY KEY, + "endpoint" text NOT NULL, + "method" text NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "serverpod_method_endpoint_method_idx" ON "serverpod_method" USING btree ("endpoint", "method"); + +-- +-- Class DatabaseMigrationVersion as table serverpod_migrations +-- +CREATE TABLE "serverpod_migrations" ( + "id" bigserial PRIMARY KEY, + "module" text NOT NULL, + "version" text NOT NULL, + "timestamp" timestamp without time zone +); + +-- Indexes +CREATE UNIQUE INDEX "serverpod_migrations_ids" ON "serverpod_migrations" USING btree ("module"); + +-- +-- Class QueryLogEntry as table serverpod_query_log +-- +CREATE TABLE "serverpod_query_log" ( + "id" bigserial PRIMARY KEY, + "serverId" text NOT NULL, + "sessionLogId" bigint NOT NULL, + "messageId" bigint, + "query" text NOT NULL, + "duration" double precision NOT NULL, + "numRows" bigint, + "error" text, + "stackTrace" text, + "slow" boolean NOT NULL, + "order" bigint NOT NULL +); + +-- Indexes +CREATE INDEX "serverpod_query_log_sessionLogId_idx" ON "serverpod_query_log" USING btree ("sessionLogId"); + +-- +-- Class ReadWriteTestEntry as table serverpod_readwrite_test +-- +CREATE TABLE "serverpod_readwrite_test" ( + "id" bigserial PRIMARY KEY, + "number" bigint NOT NULL +); + +-- +-- Class RuntimeSettings as table serverpod_runtime_settings +-- +CREATE TABLE "serverpod_runtime_settings" ( + "id" bigserial PRIMARY KEY, + "logSettings" json NOT NULL, + "logSettingsOverrides" json NOT NULL, + "logServiceCalls" boolean NOT NULL, + "logMalformedCalls" boolean NOT NULL +); + +-- +-- Class SessionLogEntry as table serverpod_session_log +-- +CREATE TABLE "serverpod_session_log" ( + "id" bigserial PRIMARY KEY, + "serverId" text NOT NULL, + "time" timestamp without time zone NOT NULL, + "module" text, + "endpoint" text, + "method" text, + "duration" double precision, + "numQueries" bigint, + "slow" boolean, + "error" text, + "stackTrace" text, + "authenticatedUserId" bigint, + "userId" text, + "isOpen" boolean, + "touched" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE INDEX "serverpod_session_log_serverid_idx" ON "serverpod_session_log" USING btree ("serverId"); +CREATE INDEX "serverpod_session_log_time_idx" ON "serverpod_session_log" USING btree ("time"); +CREATE INDEX "serverpod_session_log_touched_idx" ON "serverpod_session_log" USING btree ("touched"); +CREATE INDEX "serverpod_session_log_isopen_idx" ON "serverpod_session_log" USING btree ("isOpen"); + +-- +-- Foreign relations for "alert_deliveries" table +-- +ALTER TABLE ONLY "alert_deliveries" + ADD CONSTRAINT "alert_deliveries_fk_0" + FOREIGN KEY("alertId") + REFERENCES "alerts"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; +ALTER TABLE ONLY "alert_deliveries" + ADD CONSTRAINT "alert_deliveries_fk_1" + FOREIGN KEY("acsId") + REFERENCES "acs"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "alert_idempotency_keys" table +-- +ALTER TABLE ONLY "alert_idempotency_keys" + ADD CONSTRAINT "alert_idempotency_keys_fk_0" + FOREIGN KEY("alertId") + REFERENCES "alerts"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "alert_outbox" table +-- +ALTER TABLE ONLY "alert_outbox" + ADD CONSTRAINT "alert_outbox_fk_0" + FOREIGN KEY("alertId") + REFERENCES "alerts"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "alerts" table +-- +ALTER TABLE ONLY "alerts" + ADD CONSTRAINT "alerts_fk_0" + FOREIGN KEY("patientId") + REFERENCES "patients"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; +ALTER TABLE ONLY "alerts" + ADD CONSTRAINT "alerts_fk_1" + FOREIGN KEY("acsId") + REFERENCES "acs"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "audit_logs" table +-- +ALTER TABLE ONLY "audit_logs" + ADD CONSTRAINT "audit_logs_fk_0" + FOREIGN KEY("userId") + REFERENCES "users"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "consent_logs" table +-- +ALTER TABLE ONLY "consent_logs" + ADD CONSTRAINT "consent_logs_fk_0" + FOREIGN KEY("userId") + REFERENCES "users"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "triage_sessions" table +-- +ALTER TABLE ONLY "triage_sessions" + ADD CONSTRAINT "triage_sessions_fk_0" + FOREIGN KEY("patientId") + REFERENCES "patients"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "visits" table +-- +ALTER TABLE ONLY "visits" + ADD CONSTRAINT "visits_fk_0" + FOREIGN KEY("patientId") + REFERENCES "patients"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; +ALTER TABLE ONLY "visits" + ADD CONSTRAINT "visits_fk_1" + FOREIGN KEY("acsId") + REFERENCES "acs"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "serverpod_log" table +-- +ALTER TABLE ONLY "serverpod_log" + ADD CONSTRAINT "serverpod_log_fk_0" + FOREIGN KEY("sessionLogId") + REFERENCES "serverpod_session_log"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "serverpod_message_log" table +-- +ALTER TABLE ONLY "serverpod_message_log" + ADD CONSTRAINT "serverpod_message_log_fk_0" + FOREIGN KEY("sessionLogId") + REFERENCES "serverpod_session_log"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "serverpod_query_log" table +-- +ALTER TABLE ONLY "serverpod_query_log" + ADD CONSTRAINT "serverpod_query_log_fk_0" + FOREIGN KEY("sessionLogId") + REFERENCES "serverpod_session_log"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + + +-- +-- MIGRATION VERSION FOR sinalacs +-- +INSERT INTO "serverpod_migrations" ("module", "version", "timestamp") + VALUES ('sinalacs', '20260914154730381', now()) + ON CONFLICT ("module") + DO UPDATE SET "version" = '20260914154730381', "timestamp" = now(); + +-- +-- MIGRATION VERSION FOR serverpod +-- +INSERT INTO "serverpod_migrations" ("module", "version", "timestamp") + VALUES ('serverpod', '20260129180959368', now()) + ON CONFLICT ("module") + DO UPDATE SET "version" = '20260129180959368', "timestamp" = now(); + + +COMMIT; diff --git a/backend/sinalacs_server/migrations/20260914154730381/definition_project.json b/backend/sinalacs_server/migrations/20260914154730381/definition_project.json new file mode 100644 index 0000000..758a9a4 --- /dev/null +++ b/backend/sinalacs_server/migrations/20260914154730381/definition_project.json @@ -0,0 +1,1414 @@ +{ + "__className__": "serverpod.DatabaseDefinition", + "moduleName": "sinalacs", + "tables": [ + { + "__className__": "serverpod.TableDefinition", + "name": "acs", + "dartName": "Acs", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "enrollmentId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "ubsId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "active", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastSyncAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_ubs_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "ubsId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "alert_deliveries", + "dartName": "AlertDeliveryRecord", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "alertId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "acsId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "acknowledgedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "alert_deliveries_fk_0", + "columns": [ + "alertId" + ], + "referenceTable": "alerts", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "alert_deliveries_fk_1", + "columns": [ + "acsId" + ], + "referenceTable": "acs", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alert_deliveries_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alert_deliveries_alert_acs_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "alertId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "acsId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "alert_idempotency_keys", + "dartName": "AlertIdempotencyKey", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "key", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "alertId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "locationHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "alert_idempotency_keys_fk_0", + "columns": [ + "alertId" + ], + "referenceTable": "alerts", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alert_idempotency_keys_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alert_idempotency_keys_key_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "key" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "alert_outbox", + "dartName": "AlertOutboxEntry", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "alertId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "topic", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "payload", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "publishedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "attempts", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "nextAttemptAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastError", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "alert_outbox_fk_0", + "columns": [ + "alertId" + ], + "referenceTable": "alerts", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alert_outbox_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alert_outbox_pending_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "publishedAt" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "nextAttemptAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "alerts", + "dartName": "Alert", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "patientId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "acsId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "microAreaId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "triggeredAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "receivedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "respondedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "acknowledgedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "riskLevel", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:RiskLevel" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "locationHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "status", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:AlertStatus" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "mqttTopic", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "deviceId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "retryCount", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "version", + "columnType": 6, + "isNullable": false, + "dartType": "int" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "alerts_fk_0", + "columns": [ + "patientId" + ], + "referenceTable": "patients", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "alerts_fk_1", + "columns": [ + "acsId" + ], + "referenceTable": "acs", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alerts_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alerts_micro_area_status_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "microAreaId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "status" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "triggeredAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "audit_logs", + "dartName": "AuditLog", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "actionType", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "resourceType", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "resourceId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "timestamp", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "ipHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "result", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sequence", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "previousHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "entryHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "audit_logs_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "audit_logs_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "audit_logs_sequence_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sequence" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "consent_logs", + "dartName": "ConsentLog", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "purpose", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "action", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "version", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "timestamp", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "ipHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userAgent", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "signature", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "consent_logs_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "consent_logs_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "micro_areas", + "dartName": "MicroArea", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "ubsId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "geoJsonBoundary", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "micro_areas_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "micro_areas_ubs_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "ubsId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "patients", + "dartName": "Patient", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "emergencyContact", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "isChronic", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "chronicConditions", + "columnType": 8, + "isNullable": false, + "dartType": "List" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastLocationHash", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastTriageAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "patients_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "triage_sessions", + "dartName": "TriageSession", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "patientId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "answers", + "columnType": 8, + "isNullable": false, + "dartType": "List" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "resultRisk", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:RiskLevel" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "resultDisplay", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "deviceId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "triage_sessions_fk_0", + "columns": [ + "patientId" + ], + "referenceTable": "patients", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "triage_sessions_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "ubs", + "dartName": "Ubs", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "address", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "city", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "state", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "ubs_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "users", + "dartName": "User", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "cpfHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "birthDate", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "role", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:UserRole" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "microAreaId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "users_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "users_cpf_hash_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "cpfHash" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "users_micro_area_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "microAreaId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "visits", + "dartName": "Visit", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "patientId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "acsId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "scheduledAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "startedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "completedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "status", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "riskLevelBefore", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:RiskLevel" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "riskLevelAfter", + "columnType": 0, + "isNullable": true, + "dartType": "protocol:RiskLevel?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "notes", + "columnType": 8, + "isNullable": false, + "dartType": "Map" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "syncStatus", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:SyncStatus" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "localId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "syncAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "version", + "columnType": 6, + "isNullable": false, + "dartType": "int" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "visits_fk_0", + "columns": [ + "patientId" + ], + "referenceTable": "patients", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "visits_fk_1", + "columns": [ + "acsId" + ], + "referenceTable": "acs", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "visits_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "visits_local_id_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "localId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + } + ], + "installedModules": [ + { + "__className__": "serverpod.DatabaseMigrationVersion", + "module": "serverpod", + "version": "20260129180959368" + } + ], + "migrationApiVersion": 1 +} \ No newline at end of file diff --git a/backend/sinalacs_server/migrations/20260914154730381/migration.json b/backend/sinalacs_server/migrations/20260914154730381/migration.json new file mode 100644 index 0000000..153e5a7 --- /dev/null +++ b/backend/sinalacs_server/migrations/20260914154730381/migration.json @@ -0,0 +1,171 @@ +{ + "__className__": "serverpod.DatabaseMigration", + "actions": [ + { + "__className__": "serverpod.DatabaseMigrationAction", + "type": "deleteTable", + "deleteTable": "audit_logs" + }, + { + "__className__": "serverpod.DatabaseMigrationAction", + "type": "createTable", + "createTable": { + "__className__": "serverpod.TableDefinition", + "name": "audit_logs", + "dartName": "AuditLog", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "actionType", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "resourceType", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "resourceId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "timestamp", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "ipHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "result", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sequence", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "previousHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "entryHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "audit_logs_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "audit_logs_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "audit_logs_sequence_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sequence" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + } + } + ], + "warnings": [ + { + "__className__": "serverpod.DatabaseMigrationWarning", + "type": "uniqueIndexCreated", + "message": "Unique index \"audit_logs_sequence_idx\" is added to table \"audit_logs\". If there are existing rows with duplicate values, this migration will fail.", + "table": "audit_logs", + "columns": [ + "sequence" + ], + "destrucive": false + }, + { + "__className__": "serverpod.DatabaseMigrationWarning", + "type": "tableDropped", + "message": "One or more columns are added to table \"audit_logs\" which cannot be added in a table migration. The complete table will be deleted and recreated.", + "table": "audit_logs", + "columns": [ + "sequence" + ], + "destrucive": true + } + ], + "migrationApiVersion": 1 +} \ No newline at end of file diff --git a/backend/sinalacs_server/migrations/20260914154730381/migration.sql b/backend/sinalacs_server/migrations/20260914154730381/migration.sql new file mode 100644 index 0000000..401aa15 --- /dev/null +++ b/backend/sinalacs_server/migrations/20260914154730381/migration.sql @@ -0,0 +1,56 @@ +BEGIN; + +-- +-- ACTION DROP TABLE +-- +DROP TABLE "audit_logs" CASCADE; + +-- +-- ACTION CREATE TABLE +-- +CREATE TABLE "audit_logs" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "userId" uuid NOT NULL, + "actionType" text NOT NULL, + "resourceType" text NOT NULL, + "resourceId" uuid, + "timestamp" timestamp without time zone NOT NULL, + "ipHash" text NOT NULL, + "result" text NOT NULL, + "sequence" bigint NOT NULL, + "previousHash" text NOT NULL, + "entryHash" text NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "audit_logs_sequence_idx" ON "audit_logs" USING btree ("sequence"); + +-- +-- ACTION CREATE FOREIGN KEY +-- +ALTER TABLE ONLY "audit_logs" + ADD CONSTRAINT "audit_logs_fk_0" + FOREIGN KEY("userId") + REFERENCES "users"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + + +-- +-- MIGRATION VERSION FOR sinalacs +-- +INSERT INTO "serverpod_migrations" ("module", "version", "timestamp") + VALUES ('sinalacs', '20260914154730381', now()) + ON CONFLICT ("module") + DO UPDATE SET "version" = '20260914154730381', "timestamp" = now(); + +-- +-- MIGRATION VERSION FOR serverpod +-- +INSERT INTO "serverpod_migrations" ("module", "version", "timestamp") + VALUES ('serverpod', '20260129180959368', now()) + ON CONFLICT ("module") + DO UPDATE SET "version" = '20260129180959368', "timestamp" = now(); + + +COMMIT; diff --git a/backend/sinalacs_server/migrations/migration_registry.txt b/backend/sinalacs_server/migrations/migration_registry.txt index 6b7c816..f3f08a4 100644 --- a/backend/sinalacs_server/migrations/migration_registry.txt +++ b/backend/sinalacs_server/migrations/migration_registry.txt @@ -6,3 +6,4 @@ 20260909011754405 20260909155542184 +20260914154730381 diff --git a/backend/sinalacs_server/test/integration/patient_directory_and_territory_test.dart b/backend/sinalacs_server/test/integration/patient_directory_and_territory_test.dart index 0cc5e52..7a58eeb 100644 --- a/backend/sinalacs_server/test/integration/patient_directory_and_territory_test.dart +++ b/backend/sinalacs_server/test/integration/patient_directory_and_territory_test.dart @@ -1,6 +1,8 @@ import 'package:serverpod/serverpod.dart'; +import 'package:sinalacs_server/src/application/audit/audit_chain_verifier.dart'; import 'package:sinalacs_server/src/config/app_config.dart'; import 'package:sinalacs_server/src/generated/protocol.dart'; +import 'package:sinalacs_server/src/infrastructure/database/orm_audit_chain_reader.dart'; import 'package:sinalacs_server/src/runtime/alert_runtime.dart'; import 'package:test/test.dart'; @@ -22,6 +24,7 @@ const _patientOutsideAreaId = '00000000-0000-4000-8000-000000000009'; AppConfig _config() => AppConfig( mqttBroker: 'localhost:1883', jwtSecret: 'test-secret', + auditChainSecret: 'test-audit-chain-secret', mqttUsername: null, mqttPassword: null, mqttUseTls: false, @@ -155,6 +158,47 @@ void main() { expect(rows.single.ipHash, isNotEmpty); }); + test('duas escritas reais na trilha ficam encadeadas e passam na verificação', + () async { + final session = sessionBuilder.build(); + await _seed(session); + + final login = await endpoints.auth.developmentLogin(sessionBuilder, role: 'acs'); + // Primeira escrita: leitura do diretório (granted). + await endpoints.patients.listMicroArea(sessionBuilder, accessToken: login.accessToken); + // Segunda escrita: recusa territorial no sync. + await endpoints.visits.sync( + sessionBuilder, + accessToken: login.accessToken, + visits: [ + VisitSyncEntry( + localId: '00000000-0000-4000-8000-0000000000b2', + patientId: _patientOutsideAreaId, + scheduledAt: DateTime.utc(2026, 9, 12, 9), + status: 'realizada', + riskLevelBefore: RiskLevel.green, + notes: const {}, + version: 0, + ), + ], + ); + + final rows = await AuditLog.db.find(session, orderBy: (t) => t.sequence); + expect(rows, hasLength(2)); + expect(rows[0].sequence, 1); + expect(rows[1].sequence, 2); + expect(rows[1].previousHash, rows[0].entryHash); + + final verifier = AuditChainVerifier( + reader: OrmAuditChainReader(session: () => session), + secret: 'test-audit-chain-secret', + ); + final result = await verifier.verify(); + + expect(result.ok, isTrue); + expect(result.checked, 2); + }); + test('visits.sync recusa e audita visita para paciente de outra microárea, contra Postgres real', () async { final session = sessionBuilder.build(); @@ -177,7 +221,7 @@ void main() { ], ); - expect(results.single.syncStatus, SyncStatus.error); + expect(results.single.syncStatus, SyncStatus.rejected); final rows = await Visit.db.find(session); expect(rows, isEmpty, reason: 'nada deveria ser gravado para paciente fora do território'); diff --git a/backend/sinalacs_server/test/integration/red_alert_cycle_test.dart b/backend/sinalacs_server/test/integration/red_alert_cycle_test.dart index 0075128..13df521 100644 --- a/backend/sinalacs_server/test/integration/red_alert_cycle_test.dart +++ b/backend/sinalacs_server/test/integration/red_alert_cycle_test.dart @@ -38,6 +38,7 @@ const _ubsId = '00000000-0000-4000-8000-000000000004'; AppConfig _config({required bool enableDevLogin}) => AppConfig( mqttBroker: 'localhost:1883', jwtSecret: 'test-secret', + auditChainSecret: 'test-audit-chain-secret', mqttUsername: null, mqttPassword: null, mqttUseTls: false, diff --git a/backend/sinalacs_server/test/unit/app_config_test.dart b/backend/sinalacs_server/test/unit/app_config_test.dart index 9ca8134..b851ef6 100644 --- a/backend/sinalacs_server/test/unit/app_config_test.dart +++ b/backend/sinalacs_server/test/unit/app_config_test.dart @@ -1,16 +1,23 @@ import 'package:sinalacs_server/src/config/app_config.dart'; import 'package:test/test.dart'; -/// Regras de aceitação do segredo de assinatura. +/// Regras de aceitação dos segredos de assinatura. /// /// A validação anterior cobria apenas `APP_ENV=production` e testava só /// `== null`, então `JWT_SECRET=""` e `APP_ENV=staging` subiam assinando com um /// valor público — e o `docker-compose.yml` sequer passava a variável. +/// `AUDIT_CHAIN_SECRET` segue exatamente a mesma regra, para a cadeia de hash +/// de `audit_logs`. void main() { - AppConfig build({required String appEnv, String? jwtSecret}) => + AppConfig build({ + required String appEnv, + String? jwtSecret, + String? auditChainSecret, + }) => AppConfig.fromMap({ 'APP_ENV': appEnv, 'JWT_SECRET': ?jwtSecret, + 'AUDIT_CHAIN_SECRET': ?auditChainSecret, }); group('JWT_SECRET', () { @@ -75,7 +82,11 @@ void main() { }); test('production com segredo próprio sobe', () { - final config = build(appEnv: 'production', jwtSecret: 'b' * 64); + final config = build( + appEnv: 'production', + jwtSecret: 'b' * 64, + auditChainSecret: 'd' * 64, + ); expect(config.jwtSecret, 'b' * 64); expect(config.isProduction, isTrue); @@ -83,12 +94,85 @@ void main() { test('espaços em volta do segredo são aparados', () { expect( - build(appEnv: 'production', jwtSecret: ' ${'c' * 64} ').jwtSecret, + build( + appEnv: 'production', + jwtSecret: ' ${'c' * 64} ', + auditChainSecret: 'd' * 64, + ).jwtSecret, 'c' * 64, ); }); }); + group('AUDIT_CHAIN_SECRET', () { + // Mesmas regras de JWT_SECRET, testadas de novo porque cada segredo é + // resolvido de forma independente: um poderia estar certo e o outro + // esquecido sem que os testes de JWT_SECRET percebessem. + test('development sem a variável usa o fallback conhecido', () { + expect( + build(appEnv: 'development').auditChainSecret, + AppConfig.developmentAuditChainSecret, + ); + }); + + test('development aceita um segredo próprio', () { + expect( + build(appEnv: 'development', auditChainSecret: 'a' * 64) + .auditChainSecret, + 'a' * 64, + ); + }); + + test('production sem a variável não sobe', () { + expect( + () => build(appEnv: 'production', jwtSecret: 'b' * 64), + throwsA(isA().having( + (error) => error.message, + 'message', + contains('AUDIT_CHAIN_SECRET é obrigatório'), + )), + ); + }); + + test('string vazia conta como ausente', () { + expect( + () => build( + appEnv: 'production', + jwtSecret: 'b' * 64, + auditChainSecret: '', + ), + throwsA(isA()), + ); + }); + + test('o segredo de desenvolvimento não pode ser promovido', () { + expect( + () => build( + appEnv: 'production', + jwtSecret: 'b' * 64, + auditChainSecret: AppConfig.developmentAuditChainSecret, + ), + throwsA(isA().having( + (error) => error.message, + 'message', + contains('valor de desenvolvimento'), + )), + ); + }); + + test('production com segredo próprio sobe, independente do JWT_SECRET', + () { + final config = build( + appEnv: 'production', + jwtSecret: 'b' * 64, + auditChainSecret: 'd' * 64, + ); + + expect(config.auditChainSecret, 'd' * 64); + expect(config.jwtSecret, 'b' * 64); + }); + }); + group('defaults', () { test('um ambiente vazio produz a configuração de desenvolvimento', () { final config = AppConfig.fromMap(const {}); diff --git a/backend/sinalacs_server/test/unit/audit_chain_test.dart b/backend/sinalacs_server/test/unit/audit_chain_test.dart new file mode 100644 index 0000000..6e00aef --- /dev/null +++ b/backend/sinalacs_server/test/unit/audit_chain_test.dart @@ -0,0 +1,292 @@ +import 'package:sinalacs_server/src/application/audit/audit_chain.dart'; +import 'package:sinalacs_server/src/application/audit/audit_chain_verifier.dart'; +import 'package:sinalacs_server/src/application/audit/audit_trail.dart'; +import 'package:test/test.dart'; + +class _ThrowingAuditTrail extends AuditTrail { + @override + Future record(AuditEvent event) async { + throw StateError('trilha de auditoria fora do ar'); + } +} + +/// Simula o que `OrmAuditTrail` faz linha a linha, sem Postgres: monta uma +/// cadeia válida a partir de uma lista de eventos, encadeando cada um ao +/// `entryHash` do anterior. É o que permite montar cenários de adulteração +/// (editar, apagar, reordenar) sobre uma cadeia que nasceu íntegra. +class _ChainBuilder { + _ChainBuilder({required String secret}) : _chain = AuditChain(secret: secret); + + final AuditChain _chain; + final List _entries = []; + + void append({ + String userId = 'user-1', + String actionType = 'read', + String resourceType = 'patient_directory', + String? resourceId, + String result = 'granted', + }) { + final sequence = _entries.length + 1; + final previousHash = + _entries.isEmpty ? AuditChain.genesisHash : _entries.last.entryHash; + final fields = AuditChainFields( + sequence: sequence, + previousHash: previousHash, + userId: userId, + actionType: actionType, + resourceType: resourceType, + resourceId: resourceId, + timestamp: DateTime.utc(2026, 1, sequence), + ipHash: 'hash-de-ip-$sequence', + result: result, + ); + _entries.add(AuditChainEntry( + fields: fields, + entryHash: _chain.computeEntryHash(fields), + )); + } + + List build() => List.of(_entries); +} + +class _FakeReader implements AuditChainReader { + _FakeReader(this.entries); + + final List entries; + + @override + Future> readInOrder() async => entries; +} + +AuditChainFields _withResult(AuditChainFields fields, String result) => + AuditChainFields( + sequence: fields.sequence, + previousHash: fields.previousHash, + userId: fields.userId, + actionType: fields.actionType, + resourceType: fields.resourceType, + resourceId: fields.resourceId, + timestamp: fields.timestamp, + ipHash: fields.ipHash, + result: result, + ); + +AuditChainFields _withSequence(AuditChainFields fields, int sequence) => + AuditChainFields( + sequence: sequence, + previousHash: fields.previousHash, + userId: fields.userId, + actionType: fields.actionType, + resourceType: fields.resourceType, + resourceId: fields.resourceId, + timestamp: fields.timestamp, + ipHash: fields.ipHash, + result: fields.result, + ); + +void main() { + const secret = 'segredo-de-teste'; + + group('AuditChain', () { + test('a mesma entrada produz sempre o mesmo hash', () { + final chain = AuditChain(secret: secret); + final fields = AuditChainFields( + sequence: 1, + previousHash: AuditChain.genesisHash, + userId: 'user-1', + actionType: 'read', + resourceType: 'patient_directory', + resourceId: null, + timestamp: DateTime.utc(2026, 1, 1), + ipHash: 'hash-de-ip', + result: 'granted', + ); + + expect(chain.computeEntryHash(fields), chain.computeEntryHash(fields)); + }); + + test('trocar qualquer campo muda o hash', () { + final chain = AuditChain(secret: secret); + final base = AuditChainFields( + sequence: 1, + previousHash: AuditChain.genesisHash, + userId: 'user-1', + actionType: 'read', + resourceType: 'patient_directory', + resourceId: null, + timestamp: DateTime.utc(2026, 1, 1), + ipHash: 'hash-de-ip', + result: 'granted', + ); + + expect( + chain.computeEntryHash(_withResult(base, 'denied_territory')), + isNot(chain.computeEntryHash(base)), + ); + }); + + test('secrets diferentes produzem hashes diferentes para o mesmo conteúdo', + () { + final fields = AuditChainFields( + sequence: 1, + previousHash: AuditChain.genesisHash, + userId: 'user-1', + actionType: 'read', + resourceType: 'patient_directory', + resourceId: null, + timestamp: DateTime.utc(2026, 1, 1), + ipHash: 'hash-de-ip', + result: 'granted', + ); + + expect( + AuditChain(secret: 'a').computeEntryHash(fields), + isNot(AuditChain(secret: 'b').computeEntryHash(fields)), + ); + }); + }); + + group('AuditChainVerifier', () { + test('uma cadeia vazia é íntegra por vacuidade', () async { + final verifier = + AuditChainVerifier(reader: _FakeReader([]), secret: secret); + + final result = await verifier.verify(); + + expect(result.ok, isTrue); + expect(result.checked, 0); + }); + + test('a gênese usa genesisHash e sequence 1', () async { + final builder = _ChainBuilder(secret: secret)..append(); + final verifier = + AuditChainVerifier(reader: _FakeReader(builder.build()), secret: secret); + + final result = await verifier.verify(); + + expect(result.ok, isTrue); + expect(result.checked, 1); + }); + + test('uma cadeia com várias linhas encadeadas corretamente é íntegra', + () async { + final builder = _ChainBuilder(secret: secret) + ..append(result: 'granted') + ..append(actionType: 'write', resourceType: 'visit', result: 'denied_territory') + ..append(); + final verifier = + AuditChainVerifier(reader: _FakeReader(builder.build()), secret: secret); + + final result = await verifier.verify(); + + expect(result.ok, isTrue); + expect(result.checked, 3); + }); + + test('editar o conteúdo de uma linha é detectado', () async { + final builder = _ChainBuilder(secret: secret) + ..append() + ..append(actionType: 'write', resourceType: 'visit', result: 'denied_territory') + ..append(); + final entries = builder.build(); + + // Adultera a linha do meio SEM recalcular o hash — exatamente o que um + // UPDATE direto no Postgres faria. + final tampered = List.of(entries); + tampered[1] = AuditChainEntry( + fields: _withResult(entries[1].fields, 'granted'), + entryHash: entries[1].entryHash, + ); + + final verifier = + AuditChainVerifier(reader: _FakeReader(tampered), secret: secret); + final result = await verifier.verify(); + + expect(result.ok, isFalse); + expect(result.brokenAtSequence, 2); + expect(result.checked, 1); + expect(result.reason, contains('adulteração')); + }); + + test('apagar uma linha do meio quebra a contiguidade', () async { + final builder = _ChainBuilder(secret: secret) + ..append() + ..append() + ..append(); + final entries = builder.build(); + + final withGap = [entries[0], entries[2]]; // remove a sequence 2 + + final verifier = + AuditChainVerifier(reader: _FakeReader(withGap), secret: secret); + final result = await verifier.verify(); + + expect(result.ok, isFalse); + expect(result.brokenAtSequence, 3); + expect(result.checked, 1); + expect(result.reason, contains('descontínua')); + }); + + test('renumerar uma linha para ocupar o lugar de outra apagada quebra o elo', + () async { + final builder = _ChainBuilder(secret: secret) + ..append() + ..append() + ..append(); + final entries = builder.build(); + + // Apaga a linha 2 e renumera a linha 3 para ocupar o lugar dela, sem + // recalcular previousHash/entryHash — exatamente o que um UPDATE direto + // no Postgres faria sem conhecer o segredo. A sequência fica contígua + // (1, 2), então só o elo denuncia a fraude. + final renumbered = [ + entries[0], + AuditChainEntry( + fields: _withSequence(entries[2].fields, 2), + entryHash: entries[2].entryHash, + ), + ]; + + final verifier = + AuditChainVerifier(reader: _FakeReader(renumbered), secret: secret); + final result = await verifier.verify(); + + expect(result.ok, isFalse); + expect(result.checked, 1); + expect(result.reason, contains('elo quebrado')); + }); + + test('verificar com o segredo errado rejeita uma cadeia legítima', + () async { + final builder = _ChainBuilder(secret: secret)..append()..append(); + final verifier = AuditChainVerifier( + reader: _FakeReader(builder.build()), + secret: 'segredo-errado', + ); + + final result = await verifier.verify(); + + expect(result.ok, isFalse); + expect(result.brokenAtSequence, 1); + expect(result.reason, contains('adulteração')); + }); + }); + + group('AuditTrail.recordSafely', () { + // Regressão: a cadeia de hash não muda o contrato de recordSafely, testado + // a fundo em patient_directory_service_test.dart e + // visit_sync_service_test.dart — uma trilha fora do ar não pode derrubar a + // operação clínica que está tentando auditar. + test('não propaga falha de record', () async { + final trail = _ThrowingAuditTrail(); + + await trail.recordSafely(const AuditEvent( + userId: 'user-1', + actionType: 'read', + resourceType: 'patient_directory', + result: 'granted', + )); + }); + }); +} diff --git a/backend/sinalacs_server/test/unit/sync_fsm_test.dart b/backend/sinalacs_server/test/unit/sync_fsm_test.dart index 167142c..7130dca 100644 --- a/backend/sinalacs_server/test/unit/sync_fsm_test.dart +++ b/backend/sinalacs_server/test/unit/sync_fsm_test.dart @@ -34,5 +34,34 @@ void main() { expect(fsm.state, SyncState.conflict); }); + + test('syncRejected registra recusa definitiva', () { + final fsm = SyncFsm(); + + fsm.trigger(SyncEvent.save); + fsm.trigger(SyncEvent.syncRejected); + + expect(fsm.state, SyncState.rejected); + }); + + test('rejected é terminal: networkUp não tira a visita de lá', () { + final fsm = SyncFsm(); + + fsm.trigger(SyncEvent.save); + fsm.trigger(SyncEvent.syncRejected); + fsm.trigger(SyncEvent.networkUp); + + expect(fsm.state, SyncState.rejected); + }); + + test('rejected é terminal: syncStart não tira a visita de lá', () { + final fsm = SyncFsm(); + + fsm.trigger(SyncEvent.save); + fsm.trigger(SyncEvent.syncRejected); + fsm.trigger(SyncEvent.syncStart); + + expect(fsm.state, SyncState.rejected); + }); }); } diff --git a/backend/sinalacs_server/test/unit/visit_sync_service_test.dart b/backend/sinalacs_server/test/unit/visit_sync_service_test.dart index 2b2cce5..ef1c20b 100644 --- a/backend/sinalacs_server/test/unit/visit_sync_service_test.dart +++ b/backend/sinalacs_server/test/unit/visit_sync_service_test.dart @@ -166,7 +166,8 @@ void main() { ); final results = await service.sync(user: otherAcs, entries: [entry(version: 1)]); - expect(results.single.syncStatus, SyncStatus.error); + // Terminal: o dono do registro não muda com uma próxima tentativa. + expect(results.single.syncStatus, SyncStatus.rejected); expect(store.rows[_localId]?.acsId, UuidValue.fromString(_acsId)); }); @@ -188,7 +189,7 @@ void main() { ); }); - test('identificador inválido vira error, não derruba o lote', () async { + test('identificador inválido vira rejected, não derruba o lote', () async { final results = await service.sync(user: _acs, entries: [ VisitSyncEntry( localId: 'nao-e-uuid', @@ -202,7 +203,8 @@ void main() { entry(), ]); - expect(results.first.syncStatus, SyncStatus.error); + // Terminal: um UUID malformado na origem não vira válido reenviando. + expect(results.first.syncStatus, SyncStatus.rejected); // A visita válida do mesmo lote segue adiante. expect(results.last.syncStatus, SyncStatus.synced); }); @@ -229,7 +231,8 @@ void main() { entries: [entry(patientId: _outroTerritorioPatientId)], ); - expect(results.single.syncStatus, SyncStatus.error); + // Terminal: o território não muda com uma próxima tentativa. + expect(results.single.syncStatus, SyncStatus.rejected); expect(store.rows, isEmpty); // A mensagem não pode citar a microárea alheia nem o nome do paciente. expect(results.single.message, isNot(contains(_otherMicroAreaId))); @@ -252,7 +255,7 @@ void main() { entry(), ]); - expect(results.first.syncStatus, SyncStatus.error); + expect(results.first.syncStatus, SyncStatus.rejected); expect(results.last.syncStatus, SyncStatus.synced); }); @@ -278,7 +281,7 @@ void main() { ); // A operação clínica (recusar a visita) não pode depender da auditoria. - expect(results.single.syncStatus, SyncStatus.error); + expect(results.single.syncStatus, SyncStatus.rejected); }); }); } diff --git a/docker-compose.yml b/docker-compose.yml index accdba2..82e9d63 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -113,6 +113,9 @@ services: # Sem esta variável o servidor caía num segredo de desenvolvimento fixo e # público, com o qual qualquer pessoa forjaria um token de ACS. JWT_SECRET: ${JWT_SECRET:?defina em .env — rode ./scripts/dev/bootstrap_env.sh} + # Chave da cadeia de hash de audit_logs — segredo próprio, nunca derivado + # do JWT_SECRET (rotacionar um não pode invalidar o outro em silêncio). + AUDIT_CHAIN_SECRET: ${AUDIT_CHAIN_SECRET:?defina em .env — rode ./scripts/dev/bootstrap_env.sh} APP_ENV: ${APP_ENV:-development} ENABLE_DEV_LOGIN: "${ENABLE_DEV_LOGIN:-true}" ports: diff --git a/scripts/dev/bootstrap_env.sh b/scripts/dev/bootstrap_env.sh index 3e17242..0c82f1a 100755 --- a/scripts/dev/bootstrap_env.sh +++ b/scripts/dev/bootstrap_env.sh @@ -65,6 +65,7 @@ else MQTT_BACKEND_PASSWORD="$(secret)" \ MQTT_ACS_PASSWORD="$(secret)" \ JWT_SECRET="$(secret)" \ + AUDIT_CHAIN_SECRET="$(secret)" \ awk ' { split($0, kv, "=") @@ -80,7 +81,8 @@ else chmod 600 "$env_file" echo " .env gerado (POSTGRES_PASSWORD, TEST_DATABASE_PASSWORD," - echo " MQTT_BACKEND_PASSWORD, MQTT_ACS_PASSWORD, JWT_SECRET)" + echo " MQTT_BACKEND_PASSWORD, MQTT_ACS_PASSWORD, JWT_SECRET," + echo " AUDIT_CHAIN_SECRET)" fi # --- config/passwords.yaml ------------------------------------------------- diff --git a/spec/PRD_system.md b/spec/PRD_system.md index b8ffd11..017d335 100644 --- a/spec/PRD_system.md +++ b/spec/PRD_system.md @@ -381,7 +381,10 @@ erDiagram uuid resource_id timestamp timestamp string ip_hash - string result "SUCCESS | FAILURE | DENIED" + string result "granted | denied_territory, etc." + bigint sequence "posição na cadeia de hash, única" + string previous_hash "entryHash da linha anterior" + string entry_hash "HMAC-SHA256 do conteúdo da linha" } USER ||--o{ PATIENT : is @@ -593,7 +596,7 @@ Future canAccessPatient(Session session, String patientId) async { | **Comunicação App ↔ Traefik** | TLS 1.3 | Certificado Let's Encrypt (auto-renovável) | Proteção contra MITM | | **Comunicação Traefik ↔ Backend** | TLS 1.3 | Certificado interno (mTLS) | Segurança na rede interna | | **PostgreSQL (SSOT)** | pgcrypto (AES-256) | Chave gerenciada por Vault/HashiCorp | Proteção contra acesso ao banco | -| **Logs de Auditoria** | Assinatura Hash Chain | - | Integridade e não-repúdio | +| **Logs de Auditoria** | Assinatura Hash Chain (HMAC-SHA256) | `AUDIT_CHAIN_SECRET`, próprio, fora do Postgres | Integridade e não-repúdio | #### 4.2.4 Conformidade LGPD (Resumo) diff --git a/spec/lgpd_design.md b/spec/lgpd_design.md index 2441f55..60523b5 100644 --- a/spec/lgpd_design.md +++ b/spec/lgpd_design.md @@ -589,8 +589,8 @@ A estratégia de conformidade adota os princípios de **Privacy by Design** e ** | Propriedade | Descrição | |-------------|-----------| | **Descrição** | Todos os acessos e operações devem ser registrados em logs estruturados (JSON), imutáveis, e auditáveis. | -| **Implementação** | Tabela `audit_logs` no PostgreSQL; assinatura criptográfica dos logs (hash chain); armazenamento em tabela imutável (append-only). A tabela existia desde a migração-base sem escritor nenhum; os dois primeiros — `patients.listMicroArea` e a recusa por território em `visits.sync` (`AuditTrail`, `backend/sinalacs_server/lib/src/application/audit/`) — foram ligados, sempre com `ipHash` (nunca IP em claro) e best-effort (uma falha na trilha não derruba a operação clínica). **A assinatura em hash chain ainda não está implementada** — os demais endpoints que tocam dado sensível (alertas, ack, triagem) também não escrevem na trilha ainda. | -| **Critério de Aceite** | ✓ Todos os endpoints geram logs (parcial: só `patients.listMicroArea` e a recusa territorial de `visits.sync`)
      ✓ Logs são imutáveis (append-only)
      ✓ Logs incluem data, usuário, ação, IP (anonimizado) | +| **Implementação** | Tabela `audit_logs` no PostgreSQL; assinatura criptográfica dos logs (hash chain); armazenamento em tabela imutável (append-only). A tabela existia desde a migração-base sem escritor nenhum; os dois primeiros — `patients.listMicroArea` e a recusa por território em `visits.sync` (`AuditTrail`, `backend/sinalacs_server/lib/src/application/audit/`) — foram ligados, sempre com `ipHash` (nunca IP em claro) e best-effort (uma falha na trilha não derruba a operação clínica). **A assinatura em hash chain está implementada** (`AuditChain`, no mesmo diretório): cada linha carrega `sequence`, `previousHash` e `entryHash` — um HMAC-SHA256 sobre o conteúdo da linha, com um segredo próprio (`AUDIT_CHAIN_SECRET`) que nunca deriva do `JWT_SECRET`, para que rotacionar um não afete o outro. A escrita é serializada por `pg_advisory_xact_lock` dentro da mesma transação do apêndice, e um índice único em `sequence` é o segundo cinto contra bifurcação por concorrência. `AuditChainVerifier` (mesmo pacote) e `bin/audit_chain_check.dart` verificam a cadeia inteira sob demanda, detectando edição, remoção ou reordenação de qualquer linha — inclusive por quem tem acesso de escrita direto ao Postgres, já que o segredo fica fora do banco. **O que ainda falta**: o append-only em si não é imposto pelo banco (nenhum trigger/`REVOKE` bloqueia `UPDATE`/`DELETE` em `audit_logs` — a cadeia só *detecta* a violação, não a impede), e os demais endpoints que tocam dado sensível (alertas, ack, triagem) ainda não escrevem na trilha. | +| **Critério de Aceite** | ✓ Todos os endpoints geram logs (parcial: só `patients.listMicroArea` e a recusa territorial de `visits.sync`)
      ✓ Logs têm assinatura em hash chain verificável (`AuditChainVerifier`)
      ✗ Logs são imutáveis por construção do banco (append-only ainda é só convenção, sem trigger/`REVOKE`)
      ✓ Logs incluem data, usuário, ação, IP (anonimizado) | ### LGPD-RT04 - Consentimento Versionado From a7c56752a07e795efd22eff75f132faa84835791 Mon Sep 17 00:00:00 2001 From: GuilhermeAlmeidadaLuz Date: Mon, 14 Sep 2026 18:02:11 -0400 Subject: [PATCH 32/43] docs: add lgpd_data_audit.md --- spec/lgpd_data_audit.md | 264 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 264 insertions(+) create mode 100644 spec/lgpd_data_audit.md diff --git a/spec/lgpd_data_audit.md b/spec/lgpd_data_audit.md new file mode 100644 index 0000000..52e49f7 --- /dev/null +++ b/spec/lgpd_data_audit.md @@ -0,0 +1,264 @@ +# Auditoria de Dados e Conformidade LGPD - SinalACS + +## 1. Inventário de Dados e Classificação de Sensibilidade + +A tabela abaixo consolida o mapeamento exaustivo de dados persistidos pelo backend do SinalACS, contemplando as 11 tabelas de domínio do sistema, os modelos de transporte/persistência intermediária e as tabelas operacionais geradas pelo framework Serverpod. A classificação adota os parâmetros da LGPD (Lei nº 13.709/2018, Art. 5º, I e II), confrontando-os com as invariantes de negócio (INV-01 a INV-05) e os requisitos funcionais de privacidade (LGPD-RF01 a RF21). + +| Tabela / Entidade | Coluna / Atributo | Tipo de Dado | Classificação LGPD | Tratamento Atual | Risco / Avaliação de Conformidade | +| :--- | :--- | :--- | :--- | :--- | :--- | +| **users** | `id` | `uuid` | Pseudonimizado | UUID v4 (`gen_random_uuid()`) | Identificador técnico interno; sem vazamento direto isoladamente. | +| | `cpfHash` | `text` | Identificável / Pseudonimizado | Hash SHA-256 em texto plano | **Crítico:** O CPF possui espaço de busca restrito ($10^9$ combinações válidas); vulnerável a ataques de força bruta e rainbow tables se não utilizar salt/pepper secreto no backend. | +| | `name` | `text` | Identificável (PII Direto) | Texto claro | Necessário para identificação presencial pelo ACS no território; requer controle rigoroso de acesso e segregação estrita por microárea (INV-01). | +| | `birthDate` | `timestamp without time zone` | Identificável (PII Direto) | Timestamp exato | Permite reidentificação por cruzamento com bases cadastrais externas; avaliar truncamento para apenas data (`date`) ou idade calculada. | +| | `role` | `text` | Metadado Técnico | String (`PATIENT`, `ACS`, `ADMIN`) | Controle de perfil e permissões de acesso da aplicação (RBAC). | +| | `microAreaId` | `uuid` | Pseudonimizado / Territorial | Chave estrangeira (`micro_areas.id`) | Essencial para a aplicação da invariante de privacidade INV-01 (delimitação estrita de acesso por microárea). | +| | `createdAt` | `timestamp without time zone` | Metadado Técnico | Timestamp de criação | Registro temporal técnico de auditoria. | +| | `updatedAt` | `timestamp without time zone` | Metadado Técnico | Timestamp de modificação | Rastreabilidade do ciclo de vida cadastral. | +| **patients** | `id` | `uuid` | Pseudonimizado | UUID (vínculo 1:1 com `users.id`) | Identificador do paciente no domínio clínico; sem risco direto de identificação sem junção com a tabela `users`. | +| | `emergencyContact` | `text` | Identificável (PII de Terceiro) | Texto claro (telefone/nome) | **Alto:** Armazena dados de contato de pessoa externa sem gestão documentada de consentimento desse terceiro titular. | +| | `isChronic` | `boolean` | Sensível (Saúde - Art. 5º, II) | Flag booleana | Sinaliza formalmente a existência de condição médica crônica. | +| | `chronicConditions` | `json` | Sensível (Saúde - Art. 5º, II) | JSON estruturado em texto claro | **Crítico:** Lista de comorbidades (ex: diabetes, hipertensão) exposta sem proteção criptográfica; viola a invariante INV-04 e LGPD-RF09 (ausência de `pgcrypto`/AES-256). | +| | `lastLocationHash` | `text` | Pseudonimizado | Geohash (ex: 7 caracteres base32) | Não é irreversível: codifica latitude e longitude com resolução de ~150m; pode revelar o endereço residencial exato do paciente. | +| | `lastTriageAt` | `timestamp without time zone` | Sensível (Metadado Clínico) | Timestamp | Indica data/hora de eventos de triagem clínica recente. | +| **triage_sessions** | `id` | `uuid` | Pseudonimizado | UUID v4 (`gen_random_uuid()`) | Identificador sintético da sessão de triagem. | +| | `patientId` | `uuid` | Pseudonimizado | Chave estrangeira (`patients.id`) | Liga o histórico clínico diretamente ao perfil do paciente no território. | +| | `answers` | `json` | Sensível (Saúde - Art. 5º, II) | JSON contendo lista de `TriageAnswer` | **Crítico:** Contém sintomas declarados e relatos de dor; viola a invariante INV-04 enquanto persistido sem criptografia de repouso. | +| | `resultRisk` | `text` | Sensível (Saúde - Art. 5º, II) | Enum textual (`VERMELHO`, `AMARELO`, etc.) | Classificação determinística calculada no frontend/backend; uso restrito à priorização da fila de atendimento do ACS. | +| | `resultDisplay` | `text` | Sensível (Saúde - Art. 5º, II) | Texto descritivo | Descrição textual com diretivas clínicas associadas aos sintomas avaliados. | +| | `createdAt` | `timestamp without time zone` | Metadado Técnico | Timestamp | Marco temporal para aplicação da política de retenção legal de 5 anos (LGPD-RF07). | +| | `deviceId` | `text` | Identificador de Dispositivo | String de identificador de hardware | Se atrelado a identificadores fixos do sistema (IMEI, MAC, Android ID), caracteriza dado pessoal rastreável; requer pseudonimização com salt efêmero. | +| **triage_answer** *(DTO)* | `questionId`, `value`, `details` | `String` / Estrutura serializada | Sensível (Saúde - Art. 5º, II) | Payload unitário de resposta | Estrutura unitária persistida dentro do campo `triage_sessions.answers`. | +| **visits** | `id` | `uuid` | Pseudonimizado | UUID v4 (`gen_random_uuid()`) | Identificador do registro de visita domiciliar. | +| | `patientId` | `uuid` | Pseudonimizado | Chave estrangeira (`patients.id`) | Identifica o titular do atendimento de saúde. | +| | `acsId` | `uuid` | Pseudonimizado | Chave estrangeira (`acs.id`) | Identifica o profissional responsável pelo atendimento presencial. | +| | `scheduledAt` | `timestamp without time zone` | Metadado Operacional | Timestamp agendado | Data e horário previstos para o atendimento. | +| | `startedAt` | `timestamp without time zone` | Metadado Operacional | Timestamp de início | Registro do início da intervenção no domicílio. | +| | `completedAt` | `timestamp without time zone` | Metadado Operacional | Timestamp de conclusão | Registro da finalização da visita pelo ACS. | +| | `status` | `text` | Metadado Operacional | Enum textual (`SCHEDULED`, `IN_PROGRESS`, etc.) | Estado operacional do ciclo de vida da visita. | +| | `riskLevelBefore` | `text` | Sensível (Saúde - Art. 5º, II) | Enum textual | Avaliação clínica de gravidade anterior à intervenção. | +| | `riskLevelAfter` | `text` | Sensível (Saúde - Art. 5º, II) | Enum textual | Reavaliação clínica executada pelo ACS após o atendimento presencial. | +| | `notes` | `json` | Sensível (Saúde - Art. 5º, II) | JSON em texto claro | **Crítico:** Campo de anotações não estruturadas do ACS; elevado risco de conter diagnósticos, prescrições e relatos sobre terceiros sem anonimização. | +| | `syncStatus` | `text` | Metadado Técnico | Enum textual (`PENDING`, `SYNCED`, `CONFLICT`) | Controle da máquina de estados (FSM) de sincronização offline-first. | +| | `localId` | `uuid` | Pseudonimizado | UUID gerado no SQLite do cliente | Chave de controle de concorrência e idempotência offline. | +| | `syncAt` | `timestamp without time zone` | Metadado Técnico | Timestamp de sincronização | Momento da persistência no banco central. | +| | `version` | `bigint` | Metadado Técnico | Inteiro incremental | Controle de concorrência otimista (OCC) para resolução de conflitos. | +| **alerts** | `id` | `uuid` | Pseudonimizado | UUID v4 (`gen_random_uuid()`) | Identificador do alerta de emergência disparado. | +| | `patientId` | `uuid` | Pseudonimizado | Chave estrangeira (`patients.id`) | Identifica o paciente em risco clínico. | +| | `acsId` | `uuid` | Pseudonimizado | Chave estrangeira (`acs.id`) | Agente que confirmou ou assumiu o atendimento (nulo até o ACK). | +| | `microAreaId` | `uuid` | Pseudonimizado / Territorial | Chave estrangeira (`micro_areas.id`) | Delimita o escopo geográfico da rota de notificação MQTT. | +| | `triggeredAt` | `timestamp without time zone` | Metadado Operacional | Timestamp de acionamento | Base para medição da métrica TMRAV (< 90 segundos). | +| | `receivedAt` | `timestamp without time zone` | Metadado Operacional | Timestamp de entrega | Telemetria de entrega no dispositivo do agente comunitário. | +| | `respondedAt` | `timestamp without time zone` | Metadado Operacional | Timestamp de ação | Telemetria de início de resposta do ACS. | +| | `acknowledgedAt` | `timestamp without time zone` | Metadado Operacional | Timestamp de confirmação | Registro formal de visualização do alerta vermelho (garantia da INV-03). | +| | `riskLevel` | `text` | Sensível (Saúde - Art. 5º, II) | Enum textual (`VERMELHO`, `AMARELO`) | Grau de urgência do acionamento. | +| | `locationHash` | `text` | Pseudonimizado | Geohash (ex: `'6gyf4bf'`) | Delimitação territorial codificada; requer validação de precisão para não fixar o endereço domiciliar exato. | +| | `status` | `text` | Metadado Operacional | Enum textual (`PENDING`, `ACKNOWLEDGED`, etc.) | Estado da máquina de alerta de urgência. | +| | `mqttTopic` | `text` | Metadado Técnico / Territorial | String (`/alerts/{micro_area_id}`) | Canal de mensageria; não deve conter identificadores diretos do paciente no caminho do tópico. | +| | `deviceId` | `text` | Identificador de Dispositivo | String de identificador do hardware | Rastreamento técnico da origem do botão de pânico. | +| | `retryCount` | `bigint` | Metadado Técnico | Inteiro incremental | Controle de resiliência e retentativas na entrega da mensagem. | +| | `version` | `bigint` | Metadado Técnico | Inteiro incremental | Controle de concorrência e idempotência. | +| **alert_deliveries** | `id` | `uuid` | Pseudonimizado | UUID v4 (`gen_random_uuid()`) | Identificador de entrega. | +| | `alertId` | `uuid` | Pseudonimizado | Chave estrangeira (`alerts.id`) | Referência ao alerta entregue. | +| | `acsId` | `uuid` | Pseudonimizado | Chave estrangeira (`acs.id`) | Agente que confirmou a leitura do alerta. | +| | `acknowledgedAt` | `timestamp without time zone` | Metadado Técnico / Legal | Timestamp de confirmação | Prova técnica de recebimento em cumprimento à invariante INV-03. | +| **alert_delivery_record** *(DTO)* | Payload de entrega | `class` Dart | Metadado Operacional / Trânsito | Estrutura de dados em memória | Objeto serializado para publicação MQTT; não deve ser despejado em logs de infraestrutura. | +| **alert_outbox_entry** *(Model/DB)* | `alertId`, `payload`, `status` | Vários | Sensível / Metadado Técnico | Registro transacional da outbox | Garante atomicidade transacional com o broker; o payload retém dados clínicos e geográficos do alerta até o despacho. | +| **alert_idempotency_keys** | `id` | `uuid` | Pseudonimizado | UUID v4 (`gen_random_uuid()`) | Identificador do registro de controle de duplicação. | +| | `key` | `text` | Metadado Técnico | String única de idempotência | Previne múltiplos envios de alertas em rede instável; não deve concatenar PII. | +| | `alertId` | `uuid` | Pseudonimizado | Chave estrangeira (`alerts.id`) | Referência ao alerta vinculado. | +| | `locationHash` | `text` | Pseudonimizado | Geohash | Geohash associado à tentativa original. | +| | `createdAt` | `timestamp without time zone` | Metadado Técnico | Timestamp de expiração | Controle temporal da janela de deduplicação. | +| **consent_logs** | `id` | `uuid` | Pseudonimizado | UUID v4 (`gen_random_uuid()`) | Identificador do log de consentimento (LGPD-RF04). | +| | `userId` | `uuid` | Pseudonimizado | Chave estrangeira (`users.id`) | Identifica o titular do consentimento. | +| | `purpose` | `text` | Metadado Legal | String declarativa de finalidade | Registro da finalidade específica outorgada (LGPD-RF02). | +| | `action` | `text` | Metadado Legal | Enum textual (`GRANT`, `REVOKE`, etc.) | Ação exercida sobre o consentimento pelo titular. | +| | `version` | `text` | Metadado Legal | String de versão semântica | Versão dos termos aceita no momento da ação (LGPD-RT04). | +| | `timestamp` | `timestamp without time zone` | Metadado Legal | Timestamp de registro | Comprovação temporal imutável da manifestação de vontade. | +| | `ipHash` | `text` | Pseudonimizado | Hash SHA-256 do IP | Se gerado para IPv4 sem salt ($2^{32}$ combinações), é reversível por força bruta imediata; requer salt rotativo. | +| | `userAgent` | `text` | Metadado Técnico / Fingerprint | String de cabeçalho User-Agent | Auxilia na caracterização do dispositivo utilizado. | +| | `signature` | `text` | Metadado Legal / Prova Criptográfica | Assinatura digital/hash | Garantia de não repúdio e integridade do consentimento (LGPD-RT05). | +| **audit_logs** | `id` | `uuid` | Pseudonimizado | UUID v4 (`gen_random_uuid()`) | Identificador do registro de auditoria (LGPD-RF11). | +| | `userId` | `uuid` | Pseudonimizado | Chave estrangeira (`users.id`) | Identifica o operador que executou a ação auditada. | +| | `actionType` | `text` | Metadado Técnico | Enum textual (`READ`, `WRITE`, `DELETE`, etc.) | Operação registrada. | +| | `resourceType` | `text` | Metadado Técnico | String de recurso | Entidade ou endpoint manipulado. | +| | `resourceId` | `uuid` | Pseudonimizado | Identificador do recurso | ID do registro visualizado ou modificado. | +| | `timestamp` | `timestamp without time zone` | Metadado Legal / Técnico | Timestamp do evento | Linha temporal imutável de acesso. | +| | `ipHash` | `text` | Pseudonimizado | Hash SHA-256 do IP | Mesma vulnerabilidade de reversão caso não haja salt/HMAC com chave rotativa. | +| | `result` | `text` | Metadado Técnico | Enum textual (`SUCCESS`, `FAILURE`, `DENIED`) | Desfecho da tentativa de acesso. | +| **acs** | `id` | `uuid` | Pseudonimizado | UUID (vínculo com `users.id`) | Identificador de cadastro funcional do agente de saúde. | +| | `enrollmentId` | `text` | Identificável (Funcional) | Matrícula funcional em texto claro | Identificador de agente público; passível de correlação direta com portais municipais de transparência. | +| | `ubsId` | `uuid` | Pseudonimizado / Organizacional | Chave estrangeira (`ubs.id`) | Lotação institucional do profissional de saúde. | +| | `active` | `boolean` | Metadado Operacional | Booleano | Status funcional de permissão de acesso ao sistema. | +| | `lastSyncAt` | `timestamp without time zone` | Metadado Técnico | Timestamp | Última sincronização do app do ACS com o backend. | +| **micro_areas** | `id` | `uuid` | Pseudonimizado / Territorial | UUID v4 (`gen_random_uuid()`) | Identificador do território sanitário de cobertura. | +| | `name` | `text` | Dado Institucional / Organizacional | String | Nome ou código descritivo da microárea na UBS. | +| | `ubsId` | `uuid` | Pseudonimizado / Organizacional | Chave estrangeira (`ubs.id`) | Vinculação com a Unidade Básica de Saúde gestora. | +| | `geoJsonBoundary` | `text` | Dado Territorial / Cartográfico | String GeoJSON | Delimitação dos polígonos geográficos da microárea sanitária. | +| **ubs** | `id` | `uuid` | Pseudonimizado / Organizacional | UUID v4 (`gen_random_uuid()`) | Identificador da unidade de saúde. | +| | `name` | `text` | Dado Institucional Público | String | Razão social ou denominação do posto de atendimento. | +| | `address` | `text` | Dado Institucional Público | String de endereço | Endereço físico do equipamento de saúde pública. | +| | `city` | `text` | Dado Territorial Público | String de município | Município de lotação da UBS. | +| | `state` | `text` | Dado Territorial Público | String UF | Estado da federação de lotação da UBS. | +| **serverpod_query_log** | `query` | `text` | **Risco Crítico de Fuga Indireta** | Texto SQL completo de queries lentas/falhas | **Crítico:** Se comandos `INSERT`/`UPDATE` com `name`, `emergencyContact`, `chronicConditions` ou `notes` falharem ou forem lentos, o comando SQL completo com dados sensíveis em texto claro será gravado nesta tabela técnica. | +| | Demais colunas | Vários | Metadados de Sistema | Timestamps, durações e IDs numéricos | Métricas de telemetria e depuração de queries do banco de dados. | +| **serverpod_message_log** | `error` / `stackTrace` | `text` | Risco Moderado de Vazamento | Dump de exceções não tratadas | Risco de exposição de payloads RPC contendo dados clínicos sensíveis ou identificadores em stacktraces não sanitizados. | +| | Demais colunas | Vários | Metadados Operacionais | Nomes de métodos RPC, durações e flags | Rastreabilidade de chamadas da API interna. | +| **serverpod_session_log** | `authenticatedUserId` / `userId` | `bigint` / `text` | Pseudonimizado / Técnico | Identificadores de sessão Serverpod | Rastreamento técnico da sessão HTTP/RPC. | +| | Demais colunas | Vários | Metadados de Diagnóstico | Timestamps, contadores de queries e status de erro | Métricas técnicas de desempenho do servidor. | +| **serverpod_cloud_storage*** | Todas as colunas | `bytea`, `text`, etc. | Infraestrutura Interna | Tabelas de blobs e uploads do Serverpod | Devem ser mantidas com permissões estritas para evitar persistência indevida de dados não estruturados de pacientes. | +| **serverpod_health_*** / **future_call** / **migrations** | Todas as colunas | Vários | Metadados de Infraestrutura | Métricas de CPU/memória, controle de jobs e migrações | Neutro sob a ótica de privacidade de titulares de dados pessoais. | + +## 2. Avaliação de Mecanismos Criptográficos e Pseudonimização + +A análise técnica do schema e da base de código do backend (`backend/sinalacs_server`) aponta discrepâncias críticas entre os requisitos de segurança formalizados na especificação do sistema (`spec/PRD_system.md` e `spec/lgpd_design.md`) e a implementação concreta persistida nas migrações do PostgreSQL. Em diversos pontos, mecanismos de mascaramento tratam dados identificáveis e de saúde sob pseudonimização frágil ou texto claro. + +--- + +### 2.1. Análise de Entropia e Fragilidades de Hashing + +A inspeção estática demonstrou que o backend não calcula hashes criptográficos; ele se limita a persistir strings pré-calculadas fornecidas pelos clientes via RPC ou scripts de seed. Isso transfere o risco para os nós de borda e expõe campos críticos a ataques de força bruta e tabelas de busca pré-computadas (*rainbow tables*). + +#### Fragilidade Estrutural em `users.cpfHash` +* **Mapeamento:** Coluna `cpfHash text NOT NULL` indexada via B-tree na tabela `users`. +* **Implementação de Referência:** O helper de seed no PRD documenta o uso de `sha256('123.456.789-00')`. +* **Vulnerabilidade de Entropia:** O CPF é composto por 11 dígitos decimais, dos quais os 2 últimos são dígitos verificadores calculados diretamente sobre os 9 primeiros. Consequentemente, existem apenas $10^9$ combinações possíveis no espaço amostral do documento. +* **Risco de Segurança:** Uma GPU moderna executa bilhões de operações SHA-256 por segundo. Sem a utilização de *salt* individual ou chave secreta global (*pepper*), todo o espaço de CPFs pode ser pré-computado em *rainbow tables* em questão de segundos, anulando o efeito da pseudonimização e convertendo o hash em dado nominal reversível. + +#### Fragilidade em `consent_logs.ipHash` e `audit_logs.ipHash` +* **Mapeamento:** Colunas `ipHash text NOT NULL` registradas para atendimento aos requisitos LGPD-RF04 e LGPD-RF11. +* **Vulnerabilidade de Entropia:** O espaço de endereçamento público do IPv4 é limitado a $2^{32} \approx 4,29 \times 10^9$ combinações possíveis. +* **Risco de Segurança:** A ausência de segredo computacional torna a reversão de IPs por força bruta quase instantânea, expondo o histórico de navegação e a localização geográfica de rede do titular. + +--- + +### 2.2. Risco de Reidentificação Espacial via Geohash (`locationHash`) + +Os campos `patients.lastLocationHash`, `alerts.locationHash` e `alert_idempotency_keys.locationHash` são descritos formalmente como "hash de localização", porém a análise dos testes de unidade (`red_alert_service_test.dart`) revela que o sistema utiliza a codificação alfanumérica **Geohash base32** (ex: `'6gyf4bf'`). + +* **Natureza Algorítmica:** Geohash não é uma função criptográfica unidirecional (irreversível); trata-se de uma partição hierárquica do espaço espacial que decodifica diretamente para intervalos exatos de latitude e longitude. +* **Resolução Geométrica:** Uma cadeia Geohash com precisão de 7 caracteres especifica uma área aproximada de $\approx 153\text{ m} \times 153\text{ m}$. Em áreas urbanas de alta densidade, esse raio isola um quarteirão; em zonas rurais, aponta com frequência para uma única propriedade rural ou domicílio isolado. +* **Consequência LGPD:** A precisão atual de 7 caracteres permite identificar indiretamente a residência do titular quando combinada com a delimitação de microárea sanitária (`microAreaId`), contrariando a premissa de desidentificação do dado em repouso. + +--- + +### 2.3. Armazenamento em Texto Claro de Dados Sensíveis de Saúde (Violação INV-04) + +A invariante de negócio **INV-04** estabelece categoricamente: *"Dados de saúde sensíveis nunca podem ser persistidos em texto plano. Criptografia AES-256 em sqflite e PostgreSQL"*. Adicionalmente, os requisitos **LGPD-RF09** e **LGPD-RT02** determinam o uso de criptografia em repouso para mitigar o risco de vazamento em caso de comprometimento do banco. + +A inspeção do schema físico (`definition.sql` e modelos `.spy.yaml`) revelou que três colunas contêm dados sensíveis (Art. 5º, II da LGPD) persistidos como tipos `json` ou `text` nativos, sem qualquer camada de cifra criptográfica: + +1. **`patients.chronicConditions` (`json`):** Persiste diretamente diagnósticos clínicos estruturados (ex: `['diabetes', 'hipertensao']`). +2. **`triage_sessions.answers` (`json`):** Armazena a listagem de respostas de triagem com a descrição de sintomas, localização corporal e intensidade de dores. +3. **`visits.notes` (`json`):** Armazena anotações livres e não estruturadas realizadas pelo ACS durante o atendimento domiciliar, com alto risco de retenção de dados clínicos circunstanciais e menções a terceiros. + +A exposição desses campos em repouso gera vulnerabilidade crítica: qualquer leitura indevida decorrente de dump de banco, backup comprometido ou injeção de logs expõe de imediato o histórico clínico completo do paciente. + +--- + +### 2.4. Identificadores de Dispositivo (`deviceId`) + +As tabelas `alerts` e `triage_sessions` retêm a coluna `deviceId text NOT NULL`. +* Caso os aplicativos enviem identificadores imutáveis do sistema operacional (como `android_id`, IMEI ou endereço MAC), esse valor atua como PII indireto perene, permitindo correlacionar o histórico de atendimentos a um aparelho físico mesmo após a troca de usuário. +* Para conformidade com os princípios da necessidade e minimização (Art. 6º, I e III da LGPD), o identificador de dispositivo deve ser restrito a uma chave de instalação pseudoaleatória gerada localmente no app móvel ou mascarada antes da persistência. + +--- + +### 2.5. Dados Cadastrais e Credenciais de Autenticação (`users` e `patients`) + +* **`users.name`:** Mantido em texto claro no banco por necessidade estrita da operação assistencial domiciliar do ACS (Art. 6º, I e Art. 7º, V da LGPD). Requer controle de acesso rígido por microárea (INV-01) para que outros agentes não visualizem a listagem nominal. +* **`users.birthDate` como Fator de Autenticação (RF01):** Conforme definido no requisito RF01 do PRD, a data de nascimento atua conjuntamente com o CPF como credencial no fluxo de *Login Passwordless*. + * **Minimização de Tipo:** Como a autenticação exige a data exata, o truncamento para ano/idade é inviável sem quebrar o login. No entanto, o tipo de dado atual no PostgreSQL (`timestamp without time zone`) deve ser alterado para o tipo `date`, descartando horas, minutos e segundos desnecessários. + * **Risco de Correlação:** Armazenar a data de nascimento em texto claro ao lado de um `cpfHash` frágil amplia exponencialmente o risco de reidentificação por cruzamento com bases públicas vazadas. A proteção de `users.cpfHash` via HMAC-SHA-256 com segredo (*pepper*) do backend torna-se mandatória para impedir que a credencial de login do paciente seja quebrada em caso de vazamento do banco. +* **`patients.emergencyContact`:** Armazena dados de contato de terceiros (telefone/nome) em texto claro sem termo de consentimento específico. Deve permanecer sob acesso restrito (RBAC), sendo descriptografado ou revelado ao ACS exclusivamente durante o tratamento de alertas de emergência confirmados (alerta vermelho). + +--- + +### 2.6. Recomendações Técnicas de Remediação Criptográfica + +| Campo Auditado | Vulnerabilidade Identificada | Remediação Técnica Recomendada | +| :--- | :--- | :--- | +| `users.cpfHash` | Espaço amostral $10^9$; reversível por força bruta em segundos. | Migrar para **HMAC-SHA-256** utilizando segredo corporativo (*pepper*) injetado via variável de ambiente restrita ao backend (`CPF_HASH_PEPPER`). O cálculo deve ser feito exclusivamente pelo servidor. | +| `users.birthDate` | Tipo com precisão excessiva de horário (`timestamp`) em credencial sensível. | Alterar a coluna para o tipo SQL `date`, preservando o valor exato para o login passwordless (RF01) e eliminando horas/minutos/segundos. | +| `patients.emergencyContact` | PII de terceiro exposto em texto claro sem consentimento formal direto. | Criptografar a coluna com chave simétrica da aplicação ou proteger o acesso via endpoint dedicado liberado apenas durante o ciclo de vida de um alerta ativo. | +| `consent_logs.ipHash` e `audit_logs.ipHash` | Espaço amostral de IPv4 ($2^{32}$) facilmente mapeável via rainbow table. | Utilizar **HMAC-SHA-256 com rotação periódica de salt** (chave de rotação diária/semanal), preservando a correlação de incidentes daquela janela sem permitir persistência do rastro de rede do titular. | +| `patients.chronicConditions` | Comorbidades e diagnósticos de saúde persistidos em texto plano (violação INV-04). | Criptografia em repouso no nível de aplicação (AES-256-GCM) antes do insert, ou adoção da extensão `pgcrypto` (`pgp_sym_encrypt`) no PostgreSQL, conforme previsto em LGPD-RT02. | +| `triage_sessions.answers` | Respostas clínicas e relatos de sintomas persistidos em JSON aberto. | Serializar e cifrar o payload de respostas com chave simétrica derivada ou chave mestra mantida fora do banco de dados (Envelope Encryption). | +| `visits.notes` | Texto livre do ACS sem higienização, contendo dados clínicos sensíveis. | Criptografia simétrica compulsória em repouso e implementação de máscara ou sanitização preventiva na sincronização. | +| `alerts.locationHash` | Geohash de 7 caracteres especifica área de ~150 metros, viabilizando reidentificação domiciliar. | Truncar o Geohash para 5 caracteres ($\approx 4,9\text{ km} \times 4,9\text{ km}$) ou 6 caracteres ($\approx 1,2\text{ km} \times 0,6\text{ km}$) para roteamento de microárea, isolando as coordenadas exatas apenas no canal de despacho imediato. | +| `alerts.deviceId` e `triage_sessions.deviceId` | Rastreamento persistente de hardware do titular. | Substituir por identificador de instalação efêmero (UUID gerado no onboarding do app e descartado na limpeza de dados). | + +## 3. Matriz de Pontos de Vazamento e Riscos Identificados + +A auditoria estática do fluxo de dados, pontos de entrada RPC, rotinas em segundo plano e infraestrutura de deploy do `sinalacs_server` mapeou os vetores de exposição involuntária de dados pessoais, registros clínicos e credenciais. + +--- + +### 3.1. Matriz de Riscos de Vazamento + +| ID | Componente / Arquivo | Severidade | Descrição do Risco | Mitigação Recomendada | +| :--- | :--- | :--- | :--- | :--- | +| **VAZ-01** | `lib/src/infrastructure/mqtt/mqtt_alert_dispatcher.dart` (linhas 43, 103, 132), `lib/src/application/alerts/alert_outbox_dispatcher.dart` (linha 69) e `lib/server.dart` (linha 96) | **Média** | Despejo de exceções brutas via `stderr.writeln` em falhas de mensageria MQTT e varredura de outbox. Caso o `$error` interceptado contenha instâncias serializadas de `AlertOutboxEntry` ou `AlertDeliveryRecord`, dados sensíveis como `locationHash` e identificadores clínicos são despejados nos logs do console da hospedagem. | Sanitizar a mensagem antes de gravar em `stderr`, registrando apenas o tipo do erro e identificadores sintéticos (ex.: `alertId`), sem imprimir o objeto de domínio bruto ou payloads MQTT. | +| **VAZ-02** | `serverpod_query_log` (Tabela interna do Serverpod) | **Alta** | Persistência indireta de PII e dados sensíveis de saúde em texto claro. Caso transações com o ORM (como em `AlertsEndpoint.createRedAlert` ou sincronização de visitas) falhem ou sofram lentidão, o framework grava a instrução SQL completa na coluna `query`, expondo valores literais de inserções em `users`, `patients`, `triage_sessions` e `visits`. | Configurar `logSettings` em `serverpod_runtime_settings` para desativar o registro textual de queries SQL completas em ambientes com dados reais, assegurando o uso exclusivo de prepared statements parametrizados. | +| **VAZ-03** | `serverpod_message_log` e `serverpod_log` | **Média** | Vazamento de argumentos RPC e sessões em stacktraces. Exceções não tratadas durante chamadas de endpoint podem despejar argumentos serializados de requisições nas colunas `error` e `stackTrace` das tabelas de telemetria do Serverpod. | Implementar filtro global de exceções para expurgar cargas úteis e PII antes da gravação de stacktraces no banco de dados. | +| **VAZ-04** | Broker MQTT Gerenciado (HiveMQ Cloud / Piloto Free-Tier) | **Alta** | Ausência de ACLs dinâmicas por microárea e chaveamento compartilhado (`backend/DEPLOY.md`). O piloto adota credenciais globais estáticas (`MQTT_USERNAME`/`MQTT_PASSWORD`) sem segregação estrita por tópico, permitindo que qualquer nó autenticado assine `/alerts/#` e intercepte alertas de terceiros. | O ambiente free-tier deve permanecer restrito a dados sintéticos. Para produção, adotar broker corporativo (ex.: Mosquitto/EMQX) com autenticação mTLS e arquivos de ACL dinâmicos vinculados à microárea (garantia da INV-01). | +| **VAZ-05** | Topologia Neon / Render (Armazenamento e Computação em Nuvem Pública) | **Média** | Custódia e processamento de dados por operadores terceirizados sem salvaguardas contratuais formais (DPA - *Data Processing Agreement*). Viola o princípio da responsabilização e LGPD-RF17 caso dados reais trafeguem antes da celebração dos instrumentos legais. | Manter a infraestrutura gratuita estritamente restrita a seeds e testes sintéticos. Em ambiente assistencial, formalizar instâncias isoladas (VPC/on-premise institucional) ou contratos corporativos com DPA ativo. | +| **VAZ-06** | `config/passwords.yaml` (Higiene de Repositório) | **Baixa** | Risco de vazamento de segredos mestres do banco de dados em repositórios remotos. A checagem via `git check-ignore` confirmou que o arquivo está devidamente ignorado (.gitignore:15), mas exige monitoramento contínuo contra desvios de branch. | Manter bloqueio no pipeline de CI com ferramentas de *Secret Scanning* (ex: Gitleaks/TruffleHog) para impedir commits acidentais de senhas locais. | + +--- + +### 3.2. Detalhamento dos Vetores de Vazamento + +#### 3.2.1. Auditoria de Boot e Emissões em Console +A varredura estática por `print(` confirmou que o projeto não possui chamadas residuais de depuração direta em stdout. O vazamento originalmente citado na issue (`print('Database URL: ${config.databaseUrl}')`) pertencia à implementação legada `dart:io` e já foi sanado na migração para Serverpod. + +Contudo, a inspeção de fluxos de erro revelou 5 ocorrências de `stderr.writeln` associadas à resiliência do MQTT e ao outbox (`mqtt_alert_dispatcher.dart`, `alert_outbox_dispatcher.dart` e `server.dart`). Em plataformas como Render ou containers Docker, mensagens descarregadas em `stderr` são arquivadas sem tratamento em aggregators de log, criando o risco de expor atributos internos de alertas durante falhas transitórias de conexão. + +#### 3.2.2. Efeito Colateral do Logging Técnico do Serverpod +O Serverpod gera nativamente tabelas de diagnóstico operacional (`serverpod_query_log`, `serverpod_session_log`, `serverpod_message_log`, `serverpod_log`). +* Embora métodos como `AlertsEndpoint.createRedAlert` realizem validações seguras e encapsulem transações via `session.db.transaction`, uma falha de banco aciona a persistência do comando SQL textual em `serverpod_query_log.query`. +* Se dados sensíveis de saúde (`chronicConditions`, `answers`, `notes`) estiverem em texto claro no momento da inserção, a tabela de log passa a atuar como vetor secundário de retenção desprotegida de PII e dados clínicos. + +#### 3.2.3. Superfície de Mensageria no Piloto Gratuito (HiveMQ Cloud) +Conforme detalhado no documento `backend/DEPLOY.md`, o piloto gratuito utiliza o HiveMQ Cloud em plano compartilhado, onde não há segregação de permissões de leitura por tópico baseada em território sanitário. Clientes que utilizarem as credenciais do piloto podem monitorar livremente as publicações da raiz `/alerts/*`, permitindo que um agente escute alertas emitidos em microáreas para as quais não possui atribuição assistencial, quebrando a garantia da invariante INV-01. + +## 4. Retenção, Ciclo de Vida e Ambientes Não Produtivos (LGPD-RF07) + +A gestão do ciclo de vida dos dados é um pilar da LGPD (Art. 15 e 16). O sistema precisa garantir que os dados pessoais não sejam mantidos indefinidamente sem justificativa e que ambientes de engenharia não sejam contaminados com dados reais de titulares. + +### 4.1. Avaliação de Retenção e Exclusão (Ausência de Expurgo) +* **Requisito Legal:** O requisito **LGPD-RF07** estabelece que dados de saúde (alertas, triagens, visitas) devem ser retidos pelo prazo mínimo legal (5 anos) e, após esse período, anonimizados ou eliminados de forma segura por meio de um mecanismo automático de expurgo[cite: 4]. +* **Cenário Atual:** A inspeção nas tabelas `alerts`, `visits` e `triage_sessions` (conforme as migrações geradas pelo Serverpod) confirma a ausência de qualquer *cron job*, *trigger* de banco de dados ou rotina em segundo plano (como *FutureCalls* do Serverpod) programada para limpar registros antigos[cite: 6]. +* **Avaliação:** **Inconforme**. O acúmulo contínuo sem rotina de anonimização ou exclusão viola o princípio da necessidade da LGPD, aumentando a superfície de risco em caso de vazamento futuro. + +### 4.2. Dados em Ambientes Não Produtivos e CI/CD +* **Diretriz:** A invariante de segurança do projeto proíbe estritamente o uso de dados reais de pacientes em testes, logs, capturas de tela ou configurações de desenvolvimento[cite: 5]. +* **Cenário Atual:** A análise do seed em `backend/sinalacs_server/lib/src/infrastructure/database/seeds/development.sql` confirmou a aderência total à regra. Os dados são 100% fictícios: + * Identificadores nominais declarativos (`'Paciente de desenvolvimento'`). + * Hashes sintéticos (`'development-patient'`, `'development-acs'`) no lugar de CPFs formatados. + * O `auth.developmentLogin` utiliza chaves e UUIDs fixos que não correspondem a nenhuma base real, impedindo colisão com titulares[cite: 5]. +* **Avaliação:** **Conforme**. A higiene de dados sintéticos mitiga o risco de exposição de PII em ambientes de Integração Contínua (CI) e testes locais. + +### 4.3. Restrições do Piloto Free-Tier +* **Cenário Atual:** O documento de deploy descreve a hospedagem do protótipo em serviços gratuitos gerenciados por terceiros (Render, Neon, HiveMQ Cloud)[cite: 1]. +* **Avaliação:** Embora arquiteturalmente válido para validação (devido ao suporte a contêineres e hibernação transparente), **é inaceitável o uso de dados reais neste ambiente**[cite: 1]. A falta de Data Processing Agreements (DPAs) firmados com esses fornecedores e a ausência de controles rígidos de retenção tornam o piloto free-tier uma zona de alto risco regulatório se exposta à operação assistencial verdadeira. + +--- + +## 5. Plano de Ação Recomendado + +Com base nas vulnerabilidades e desvios de conformidade mapeados neste relatório, recomenda-se a seguinte ordem de priorização para adequação à LGPD e às invariantes do SinalACS: + +### Prioridade Alta (Curto Prazo - Prevenção de Vazamento Imediato) +1. **Sanitizar Logs de Exceção:** Ajustar os blocos `try/catch` da varredura de outbox e do dispatcher MQTT no backend para não imprimir as entidades de domínio brutas no `stderr`. +2. **Fortalecer Hashes Autenticadores:** Migrar a geração de `users.cpfHash` (usado no login) para **HMAC-SHA-256** utilizando um segredo de servidor (*pepper*). O cálculo deve ocorrer exclusivamente no backend. +3. **Ajustar Tipagem da Data de Nascimento:** Converter a coluna `users.birthDate` para `date` no schema, eliminando a precisão desnecessária de horas/minutos exigida pelo `timestamp` atual. +4. **Isolar o Ambiente Free-Tier:** Reforçar bloqueios ou avisos na UI do piloto garantindo que ele seja alimentado estritamente pelo seed sintético de desenvolvimento. + +### Prioridade Média (Médio Prazo - Criptografia em Repouso e Telemetria) +5. **Criptografar Campos de Saúde (Violação INV-04):** Implementar criptografia de coluna (via `pgcrypto` ou criptografia simétrica na aplicação) para proteger `patients.chronicConditions`, `triage_sessions.answers` e `visits.notes` contra leituras indevidas no banco. +6. **Mascarar Logs do Serverpod:** Desativar ou configurar máscaras para o `serverpod_query_log` em produção, impedindo que transações SQL lentas deixem rastros em texto claro de diagnósticos ou dados de anamnese. +7. **Anonimizar Origens de Rede:** Implementar salt rotativo no cálculo de `ipHash` para os registros de `consent_logs` e `audit_logs`. + +### Prioridade Baixa (Longo Prazo - Ciclo de Vida e Infraestrutura) +8. **Rotina de Expurgo (LGPD-RF07):** Desenvolver um Serverpod *FutureCall* ou rotina periódica no banco para efetuar o *soft-delete* com anonimização de registros de visitas e alertas com mais de 5 anos[cite: 4]. +9. **Reduzir Precisão do Geohash:** Avaliar com a área de produto o truncamento de `locationHash` de 7 para 5 ou 6 caracteres em áreas rurais, impedindo a reidentificação determinística do domicílio do paciente na base de dados. \ No newline at end of file From d9293ab09703f2332b57afba1bebd59f06a9e17a Mon Sep 17 00:00:00 2001 From: vinimartinsufrr Date: Tue, 15 Sep 2026 08:58:29 -0400 Subject: [PATCH 33/43] =?UTF-8?q?fix(admin):=20responder=20aos=20achados?= =?UTF-8?q?=20da=20revis=C3=A3o=20autom=C3=A1tica=20do=20PR=20(Copilot)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01WyMMKXSksyUUNXPieFpSfX --- apps/admin/lib/app/app.dart | 151 +++++++++++++++++++++++++++++------ apps/admin/web/index.html | 6 +- apps/admin/web/manifest.json | 8 +- 3 files changed, 132 insertions(+), 33 deletions(-) diff --git a/apps/admin/lib/app/app.dart b/apps/admin/lib/app/app.dart index 8b7b025..e93465e 100644 --- a/apps/admin/lib/app/app.dart +++ b/apps/admin/lib/app/app.dart @@ -34,8 +34,8 @@ class LoginScreen extends StatefulWidget { } class _LoginScreenState extends State { - final _matricula = TextEditingController(text: 'admin.dev'); - final _senha = TextEditingController(text: '123456'); + final _matricula = TextEditingController(); + final _senha = TextEditingController(); @override void dispose() { @@ -232,15 +232,59 @@ String statusLabel(AlertStatus status) => switch (status) { AlertStatus.escalated => 'Escalonado', }; -class IndicatorsScreen extends StatelessWidget { +/// Estado de erro compartilhado pelas telas assíncronas, com retry. +/// +/// Sem isso, um `FutureBuilder` que falha fica com `hasData == false` para +/// sempre (spinner infinito) ou, pior, cai no mesmo ramo de "vazio" que os +/// dados realmente vazios — escondendo uma falha de rede/backend como se +/// não houvesse nada para mostrar (achado da revisão do Copilot no PR). +class _AsyncError extends StatelessWidget { + const _AsyncError({required this.message, required this.onRetry}); + + final String message; + final VoidCallback onRetry; + + @override + Widget build(BuildContext context) => Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.cloud_off_outlined, size: 32, color: Colors.white70), + const SizedBox(height: 12), + Text(message, textAlign: TextAlign.center), + const SizedBox(height: 16), + OutlinedButton.icon(onPressed: onRetry, icon: const Icon(Icons.refresh), label: const Text('Tentar novamente')), + ], + ), + ), + ); +} + +class IndicatorsScreen extends StatefulWidget { const IndicatorsScreen({required this.dataSource, super.key}); final AdminDataSource dataSource; + @override + State createState() => _IndicatorsScreenState(); +} + +class _IndicatorsScreenState extends State { + late Future _future = widget.dataSource.fetchDashboardIndicators(); + + void _retry() => setState(() { + _future = widget.dataSource.fetchDashboardIndicators(); + }); + @override Widget build(BuildContext context) => FutureBuilder( - future: dataSource.fetchDashboardIndicators(), + future: _future, builder: (context, snapshot) { + if (snapshot.hasError) { + return _AsyncError(message: 'Não foi possível carregar os indicadores.', onRetry: _retry); + } if (!snapshot.hasData) return const Center(child: CircularProgressIndicator()); final data = snapshot.data!; return _page([ @@ -311,18 +355,29 @@ class MicroAreasScreen extends StatefulWidget { } class _MicroAreasScreenState extends State { - @override - void initState() { - super.initState(); - widget.dataSource.recordAccess(actionType: 'view', resourceType: 'micro_areas'); + late Future> _future = _load(); + + /// Registra o acesso *antes* de expor os dados — se o registro falhar, a + /// tela cai no estado de erro em vez de mostrar dado sensível sem auditoria + /// (PRD §4.2.2: acesso do Administrador precisa ser auditado). + Future> _load() async { + await widget.dataSource.recordAccess(actionType: 'view', resourceType: 'micro_areas'); + return widget.dataSource.fetchMicroAreas(); } + void _retry() => setState(() { + _future = _load(); + }); + @override Widget build(BuildContext context) => FutureBuilder>( - future: widget.dataSource.fetchMicroAreas(), + future: _future, builder: (context, snapshot) { - final areas = snapshot.data ?? const []; + if (snapshot.hasError) { + return _AsyncError(message: 'Não foi possível carregar as microáreas.', onRetry: _retry); + } if (!snapshot.hasData) return const Center(child: CircularProgressIndicator()); + final areas = snapshot.data!; return ListView( padding: const EdgeInsets.all(20), children: [ @@ -362,17 +417,51 @@ class AlertsScreen extends StatefulWidget { class _AlertsScreenState extends State { String? _microAreaFilter; AlertStatus? _statusFilter; + late Future _accessRecorded = widget.dataSource.recordAccess(actionType: 'view', resourceType: 'alerts'); @override - void initState() { - super.initState(); - widget.dataSource.recordAccess(actionType: 'view', resourceType: 'alerts'); - } + Widget build(BuildContext context) => FutureBuilder( + future: _accessRecorded, + builder: (context, accessSnapshot) { + if (accessSnapshot.hasError) { + return _AsyncError( + message: 'Não foi possível registrar o acesso a esta tela.', + onRetry: () => setState(() { + _accessRecorded = widget.dataSource.recordAccess(actionType: 'view', resourceType: 'alerts'); + }), + ); + } + if (accessSnapshot.connectionState != ConnectionState.done) { + return const Center(child: CircularProgressIndicator()); + } + return _AlertsList(dataSource: widget.dataSource, microAreaFilter: _microAreaFilter, statusFilter: _statusFilter, onFilterChanged: (microArea, status) => setState(() { + _microAreaFilter = microArea; + _statusFilter = status; + })); + }, + ); +} + +class _AlertsList extends StatelessWidget { + const _AlertsList({ + required this.dataSource, + required this.microAreaFilter, + required this.statusFilter, + required this.onFilterChanged, + }); + + final AdminDataSource dataSource; + final String? microAreaFilter; + final AlertStatus? statusFilter; + final void Function(String? microArea, AlertStatus? status) onFilterChanged; @override Widget build(BuildContext context) => FutureBuilder>( - future: widget.dataSource.fetchAlerts(microAreaName: _microAreaFilter, status: _statusFilter), + future: dataSource.fetchAlerts(microAreaName: microAreaFilter, status: statusFilter), builder: (context, snapshot) { + if (snapshot.hasError) { + return _AsyncError(message: 'Não foi possível carregar os alertas.', onRetry: () => onFilterChanged(microAreaFilter, statusFilter)); + } final alerts = snapshot.data ?? const []; return ListView( padding: const EdgeInsets.all(20), @@ -386,8 +475,12 @@ class _AlertsScreenState extends State { Expanded( child: DropdownButtonFormField( key: const Key('alerts_micro_area_filter'), - initialValue: _microAreaFilter, + initialValue: microAreaFilter, isExpanded: true, + // DropdownButton exibe o hint (não o child do item) quando o + // valor selecionado é null — sem isso, "Todas" some do campo + // fechado assim que selecionado (achado da revisão do PR). + hint: const Text('Todas'), decoration: const InputDecoration(labelText: 'Microárea'), items: const [ DropdownMenuItem(value: null, child: Text('Todas')), @@ -404,22 +497,23 @@ class _AlertsScreenState extends State { child: Text('Microárea 03 — Vila Esperança', overflow: TextOverflow.ellipsis), ), ], - onChanged: (value) => setState(() => _microAreaFilter = value), + onChanged: (value) => onFilterChanged(value, statusFilter), ), ), const SizedBox(width: 12), Expanded( child: DropdownButtonFormField( key: const Key('alerts_status_filter'), - initialValue: _statusFilter, + initialValue: statusFilter, isExpanded: true, + hint: const Text('Todos'), decoration: const InputDecoration(labelText: 'Status'), items: [ const DropdownMenuItem(value: null, child: Text('Todos')), for (final status in AlertStatus.values) DropdownMenuItem(value: status, child: Text(statusLabel(status), overflow: TextOverflow.ellipsis)), ], - onChanged: (value) => setState(() => _statusFilter = value), + onChanged: (value) => onFilterChanged(microAreaFilter, value), ), ), ], @@ -466,24 +560,29 @@ class AuditLogScreen extends StatefulWidget { } class _AuditLogScreenState extends State { - late final Future _selfAuditRecorded; - - @override - void initState() { - super.initState(); - _selfAuditRecorded = widget.dataSource.recordAccess(actionType: 'view', resourceType: 'audit_logs'); - } + late Future _selfAuditRecorded = widget.dataSource.recordAccess(actionType: 'view', resourceType: 'audit_logs'); @override Widget build(BuildContext context) => FutureBuilder( future: _selfAuditRecorded, builder: (context, recordSnapshot) { + if (recordSnapshot.hasError) { + return _AsyncError( + message: 'Não foi possível registrar o acesso a esta tela.', + onRetry: () => setState(() { + _selfAuditRecorded = widget.dataSource.recordAccess(actionType: 'view', resourceType: 'audit_logs'); + }), + ); + } if (recordSnapshot.connectionState != ConnectionState.done) { return const Center(child: CircularProgressIndicator()); } return FutureBuilder>( future: widget.dataSource.fetchAuditLogs(), builder: (context, snapshot) { + if (snapshot.hasError) { + return _AsyncError(message: 'Não foi possível carregar os logs de auditoria.', onRetry: () => setState(() {})); + } final entries = snapshot.data ?? const []; return ListView( padding: const EdgeInsets.all(20), diff --git a/apps/admin/web/index.html b/apps/admin/web/index.html index 490417f..6e9115e 100644 --- a/apps/admin/web/index.html +++ b/apps/admin/web/index.html @@ -18,18 +18,18 @@ - + - + - sinalacs_admin + SinalACS Admin diff --git a/apps/admin/web/manifest.json b/apps/admin/web/manifest.json index 660d789..f3b3bdd 100644 --- a/apps/admin/web/manifest.json +++ b/apps/admin/web/manifest.json @@ -3,10 +3,10 @@ "short_name": "sinalacs_admin", "start_url": ".", "display": "standalone", - "background_color": "#0175C2", - "theme_color": "#0175C2", - "description": "A new Flutter project.", - "orientation": "portrait-primary", + "background_color": "#030712", + "theme_color": "#4F46E5", + "description": "Backoffice administrativo do SinalACS.", + "orientation": "any", "prefer_related_applications": false, "icons": [ { From 5c00d77cbbea86d79148d9fdcea7490f997dd4d7 Mon Sep 17 00:00:00 2001 From: vinimartinsufrr Date: Tue, 15 Sep 2026 08:58:41 -0400 Subject: [PATCH 34/43] =?UTF-8?q?test(admin):=20adicionar=20testes=20de=20?= =?UTF-8?q?erro/retry=20e=20regress=C3=A3o=20dos=20filtros?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01WyMMKXSksyUUNXPieFpSfX --- apps/admin/test/error_handling_test.dart | 81 +++++++++++++++++++ .../support/failing_admin_data_source.dart | 67 +++++++++++++++ 2 files changed, 148 insertions(+) create mode 100644 apps/admin/test/error_handling_test.dart create mode 100644 apps/admin/test/support/failing_admin_data_source.dart diff --git a/apps/admin/test/error_handling_test.dart b/apps/admin/test/error_handling_test.dart new file mode 100644 index 0000000..bc83fc3 --- /dev/null +++ b/apps/admin/test/error_handling_test.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:sinalacs_admin/app/app.dart'; +import 'package:sinalacs_admin/core/data/mock_admin_data_source.dart'; + +import 'support/failing_admin_data_source.dart'; + +Future _loginTo(WidgetTester tester, FailingAdminDataSource dataSource, String destination) async { + await tester.pumpWidget(SinalAdminApp(dataSource: dataSource)); + await tester.tap(find.byKey(const Key('login_button'))); + await tester.pumpAndSettle(); + if (destination != 'Indicadores') { + await tester.tap(find.text(destination).last); + await tester.pumpAndSettle(); + } +} + +void main() { + group('erro e retry, por tela (achado da revisão do PR: erro não pode parecer vazio ou travar em spinner)', () { + testWidgets('Indicadores: mostra erro (não spinner infinito) e retry recupera', (tester) async { + final dataSource = FailingAdminDataSource(inner: MockAdminDataSource())..failNextIndicators = true; + await _loginTo(tester, dataSource, 'Indicadores'); + + expect(find.text('Não foi possível carregar os indicadores.'), findsOneWidget); + expect(find.text('Painel de Indicadores'), findsNothing); + + await tester.tap(find.text('Tentar novamente')); + await tester.pumpAndSettle(); + + expect(find.text('Painel de Indicadores'), findsOneWidget); + expect(find.text('Não foi possível carregar os indicadores.'), findsNothing); + }); + + testWidgets('Microáreas: recordAccess falhando não deve expor a lista sem auditar o acesso', (tester) async { + final dataSource = FailingAdminDataSource(inner: MockAdminDataSource())..failNextRecordAccess = true; + await _loginTo(tester, dataSource, 'Microáreas'); + + expect(find.text('Não foi possível carregar as microáreas.'), findsOneWidget); + expect(find.textContaining('Microárea 12'), findsNothing); + + await tester.tap(find.text('Tentar novamente')); + await tester.pumpAndSettle(); + + expect(find.textContaining('Microárea 12'), findsOneWidget); + }); + + testWidgets('Alertas: erro no carregamento não deve aparecer como "nenhum alerta"', (tester) async { + final dataSource = FailingAdminDataSource(inner: MockAdminDataSource())..failNextAlerts = true; + await _loginTo(tester, dataSource, 'Alertas'); + + expect(find.text('Não foi possível carregar os alertas.'), findsOneWidget); + expect(find.text('Nenhum alerta para o filtro selecionado.'), findsNothing); + + await tester.tap(find.text('Tentar novamente')); + await tester.pumpAndSettle(); + + expect(find.byKey(const Key('alert_alert-1')), findsOneWidget); + }); + + testWidgets('Auditoria: erro no carregamento não deve aparecer como "nenhum acesso registrado"', (tester) async { + final dataSource = FailingAdminDataSource(inner: MockAdminDataSource())..failNextAuditLogs = true; + await _loginTo(tester, dataSource, 'Auditoria'); + + expect(find.text('Não foi possível carregar os logs de auditoria.'), findsOneWidget); + expect(find.text('Nenhum acesso registrado ainda.'), findsNothing); + + await tester.tap(find.text('Tentar novamente')); + await tester.pumpAndSettle(); + + expect(find.textContaining('view • audit_logs'), findsOneWidget); + }); + }); + + testWidgets('filtros de Alertas mostram "Todas"/"Todos" fechados, não em branco (achado da revisão do PR)', (tester) async { + final dataSource = FailingAdminDataSource(inner: MockAdminDataSource()); + await _loginTo(tester, dataSource, 'Alertas'); + + expect(find.text('Todas'), findsOneWidget); + expect(find.text('Todos'), findsOneWidget); + }); +} diff --git a/apps/admin/test/support/failing_admin_data_source.dart b/apps/admin/test/support/failing_admin_data_source.dart new file mode 100644 index 0000000..2f84f91 --- /dev/null +++ b/apps/admin/test/support/failing_admin_data_source.dart @@ -0,0 +1,67 @@ +import 'package:sinalacs_admin/core/data/admin_data_source.dart'; + +/// Duplo de teste que permite forçar falha em qualquer método, uma vez. +/// +/// Existe para provar o comportamento de erro/retry das telas (achado da +/// revisão do PR: `FutureBuilder` sem `hasError` deixava erro parecer "vazio" +/// ou spinner infinito). Cada campo `failNext*` é consumido uma única vez, o +/// que permite testar "falha, depois usuário tenta de novo e funciona". +class FailingAdminDataSource implements AdminDataSource { + FailingAdminDataSource({required this.inner}); + + final AdminDataSource inner; + + bool failNextIndicators = false; + bool failNextMicroAreas = false; + bool failNextAlerts = false; + bool failNextAuditLogs = false; + bool failNextRecordAccess = false; + + int recordAccessCalls = 0; + + @override + Future fetchDashboardIndicators() async { + if (failNextIndicators) { + failNextIndicators = false; + throw StateError('falha simulada: indicadores'); + } + return inner.fetchDashboardIndicators(); + } + + @override + Future> fetchMicroAreas() async { + if (failNextMicroAreas) { + failNextMicroAreas = false; + throw StateError('falha simulada: microáreas'); + } + return inner.fetchMicroAreas(); + } + + @override + Future> fetchAlerts({String? microAreaName, AlertStatus? status}) async { + if (failNextAlerts) { + failNextAlerts = false; + throw StateError('falha simulada: alertas'); + } + return inner.fetchAlerts(microAreaName: microAreaName, status: status); + } + + @override + Future> fetchAuditLogs() async { + if (failNextAuditLogs) { + failNextAuditLogs = false; + throw StateError('falha simulada: auditoria'); + } + return inner.fetchAuditLogs(); + } + + @override + Future recordAccess({required String actionType, required String resourceType}) async { + recordAccessCalls++; + if (failNextRecordAccess) { + failNextRecordAccess = false; + throw StateError('falha simulada: recordAccess'); + } + return inner.recordAccess(actionType: actionType, resourceType: resourceType); + } +} From cecb9cc5feec84b6e28b8d037c513a661eab2650 Mon Sep 17 00:00:00 2001 From: vinimartinsufrr Date: Tue, 15 Sep 2026 09:06:06 -0400 Subject: [PATCH 35/43] =?UTF-8?q?fix(admin):=20tratar=20os=202=20coment?= =?UTF-8?q?=C3=A1rios=20inline=20restantes=20da=20revis=C3=A3o=20do=20PR?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01WyMMKXSksyUUNXPieFpSfX --- apps/admin/lib/app/app.dart | 62 ++++++++++++++++++++++++------------- 1 file changed, 41 insertions(+), 21 deletions(-) diff --git a/apps/admin/lib/app/app.dart b/apps/admin/lib/app/app.dart index e93465e..456f06c 100644 --- a/apps/admin/lib/app/app.dart +++ b/apps/admin/lib/app/app.dart @@ -1,19 +1,23 @@ +import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/material.dart'; import 'package:sinalacs_admin/app/admin_theme.dart'; import 'package:sinalacs_admin/core/data/admin_data_source.dart'; import 'package:sinalacs_admin/core/data/mock_admin_data_source.dart'; class SinalAdminApp extends StatelessWidget { - SinalAdminApp({super.key, AdminDataSource? dataSource}) : dataSource = dataSource ?? MockAdminDataSource(); + SinalAdminApp({super.key, AdminDataSource? dataSource, this.devLoginEnabled}) : dataSource = dataSource ?? MockAdminDataSource(); final AdminDataSource dataSource; + /// Repassado para [LoginScreen]; `null` mantém o default (`kDebugMode`). + final bool? devLoginEnabled; + @override Widget build(BuildContext context) => MaterialApp( title: 'SinalACS Admin', debugShowCheckedModeBanner: false, theme: buildAdminTheme(), - home: LoginScreen(dataSource: dataSource), + home: LoginScreen(dataSource: dataSource, devLoginEnabled: devLoginEnabled), ); } @@ -25,9 +29,15 @@ class SinalAdminApp extends StatelessWidget { /// AlertValidationException. Ligar isso de verdade exige uma mudança no /// backend (fora do escopo desta issue); ver descrição do PR. class LoginScreen extends StatefulWidget { - const LoginScreen({required this.dataSource, super.key}); + /// O banner "ambiente de desenvolvimento" não é um controle de acesso — só + /// avisa. Sem isso, `_login` deixaria qualquer pessoa entrar em produção + /// sem senha (achado da revisão do PR). O default (`kDebugMode`, `false` + /// em builds profile/release) desativa de verdade o bypass fora de dev; + /// o parâmetro existe para os testes poderem exercitar os dois estados. + const LoginScreen({required this.dataSource, super.key, bool? devLoginEnabled}) : devLoginEnabled = devLoginEnabled ?? kDebugMode; final AdminDataSource dataSource; + final bool devLoginEnabled; @override State createState() => _LoginScreenState(); @@ -114,11 +124,20 @@ class _LoginScreenState extends State { child: FilledButton( key: const Key('login_button'), style: FilledButton.styleFrom(minimumSize: const Size(48, 52)), - onPressed: _login, + onPressed: widget.devLoginEnabled ? _login : null, child: const Text('Entrar'), ), ), ), + if (!widget.devLoginEnabled) ...[ + const SizedBox(height: 12), + const Text( + 'Login de desenvolvimento desativado nesta build (fora do modo debug).', + key: Key('dev_login_disabled_notice'), + textAlign: TextAlign.center, + style: TextStyle(color: Colors.white70, fontSize: 12), + ), + ], ], ), ), @@ -455,14 +474,25 @@ class _AlertsList extends StatelessWidget { final AlertStatus? statusFilter; final void Function(String? microArea, AlertStatus? status) onFilterChanged; + /// Busca alertas e microáreas juntos. As opções do filtro vêm daqui, não de + /// uma lista fixa no código — senão uma microárea nova (ou uma fonte real, + /// no lugar do mock) devolveria alertas para um valor que não existe mais + /// como opção selecionável (achado da revisão do PR). + Future<({List alerts, List microAreas})> _load() async { + final alerts = await dataSource.fetchAlerts(microAreaName: microAreaFilter, status: statusFilter); + final microAreas = await dataSource.fetchMicroAreas(); + return (alerts: alerts, microAreas: microAreas); + } + @override - Widget build(BuildContext context) => FutureBuilder>( - future: dataSource.fetchAlerts(microAreaName: microAreaFilter, status: statusFilter), + Widget build(BuildContext context) => FutureBuilder( + future: _load(), builder: (context, snapshot) { if (snapshot.hasError) { return _AsyncError(message: 'Não foi possível carregar os alertas.', onRetry: () => onFilterChanged(microAreaFilter, statusFilter)); } - final alerts = snapshot.data ?? const []; + final alerts = snapshot.data?.alerts ?? const []; + final microAreas = snapshot.data?.microAreas ?? const []; return ListView( padding: const EdgeInsets.all(20), children: [ @@ -482,20 +512,10 @@ class _AlertsList extends StatelessWidget { // fechado assim que selecionado (achado da revisão do PR). hint: const Text('Todas'), decoration: const InputDecoration(labelText: 'Microárea'), - items: const [ - DropdownMenuItem(value: null, child: Text('Todas')), - DropdownMenuItem( - value: 'Microárea 12 — Zona Rural', - child: Text('Microárea 12 — Zona Rural', overflow: TextOverflow.ellipsis), - ), - DropdownMenuItem( - value: 'Microárea 07 — Centro', - child: Text('Microárea 07 — Centro', overflow: TextOverflow.ellipsis), - ), - DropdownMenuItem( - value: 'Microárea 03 — Vila Esperança', - child: Text('Microárea 03 — Vila Esperança', overflow: TextOverflow.ellipsis), - ), + items: [ + const DropdownMenuItem(value: null, child: Text('Todas')), + for (final area in microAreas) + DropdownMenuItem(value: area.name, child: Text(area.name, overflow: TextOverflow.ellipsis)), ], onChanged: (value) => onFilterChanged(value, statusFilter), ), From 549cb2102713d7ccd5b3567e188998488f9d14e1 Mon Sep 17 00:00:00 2001 From: vinimartinsufrr Date: Tue, 15 Sep 2026 09:06:14 -0400 Subject: [PATCH 36/43] =?UTF-8?q?test(admin):=20regress=C3=A3o=20dos=202?= =?UTF-8?q?=20achados=20finais=20(gate=20de=20login,=20filtro=20din=C3=A2m?= =?UTF-8?q?ico)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01WyMMKXSksyUUNXPieFpSfX --- .../test/alerts_filter_dynamic_test.dart | 59 +++++++++++++++++++ apps/admin/test/login_dev_gate_test.dart | 31 ++++++++++ 2 files changed, 90 insertions(+) create mode 100644 apps/admin/test/alerts_filter_dynamic_test.dart create mode 100644 apps/admin/test/login_dev_gate_test.dart diff --git a/apps/admin/test/alerts_filter_dynamic_test.dart b/apps/admin/test/alerts_filter_dynamic_test.dart new file mode 100644 index 0000000..aff75be --- /dev/null +++ b/apps/admin/test/alerts_filter_dynamic_test.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:sinalacs_admin/app/app.dart'; +import 'package:sinalacs_admin/core/data/admin_data_source.dart'; + +/// Duplo com uma microárea que não existe na antiga lista fixa do dropdown de +/// filtro (achado da revisão do PR: as opções eram três strings fixas no +/// código, então uma microárea nova nunca apareceria como filtro possível, +/// mesmo já tendo alertas). +class _CustomAreaDataSource implements AdminDataSource { + static const newArea = 'Microárea 99 — Nova Área'; + + @override + Future fetchDashboardIndicators() async => const DashboardIndicators( + countsByRisk: {}, + openRedAlerts: 0, + acknowledgedRedAlerts: 0, + tmravSeconds: 0, + ); + + @override + Future> fetchMicroAreas() async => const [ + MicroAreaSummary(id: 'ma-99', name: newArea, acsName: 'Fulana', acsEnrollmentId: 'ACS-099', acsActive: true), + ]; + + @override + Future> fetchAlerts({String? microAreaName, AlertStatus? status}) async => [ + AlertSummary( + id: 'alert-99', + patientLabel: 'Paciente #999', + microAreaName: newArea, + riskLevel: RiskLevel.green, + status: AlertStatus.resolved, + triggeredAt: DateTime(2026, 9, 15), + ), + ].where((a) => microAreaName == null || a.microAreaName == microAreaName).toList(); + + @override + Future> fetchAuditLogs() async => const []; + + @override + Future recordAccess({required String actionType, required String resourceType}) async {} +} + +void main() { + testWidgets('opções do filtro de microárea vêm de fetchMicroAreas(), não de uma lista fixa', (tester) async { + await tester.pumpWidget(SinalAdminApp(dataSource: _CustomAreaDataSource())); + await tester.tap(find.byKey(const Key('login_button'))); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Alertas').last); + await tester.pumpAndSettle(); + + await tester.tap(find.byKey(const Key('alerts_micro_area_filter'))); + await tester.pumpAndSettle(); + + expect(find.text(_CustomAreaDataSource.newArea), findsWidgets); + }); +} diff --git a/apps/admin/test/login_dev_gate_test.dart b/apps/admin/test/login_dev_gate_test.dart new file mode 100644 index 0000000..ed624ed --- /dev/null +++ b/apps/admin/test/login_dev_gate_test.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:sinalacs_admin/app/app.dart'; + +void main() { + group('login de desenvolvimento não pode funcionar fora de dev (achado da revisão do PR)', () { + testWidgets('com devLoginEnabled: false, o botão fica desabilitado e não navega', (tester) async { + await tester.pumpWidget(SinalAdminApp(devLoginEnabled: false)); + + final loginButton = tester.widget(find.byKey(const Key('login_button'))); + expect(loginButton.onPressed, isNull); + expect(find.byKey(const Key('dev_login_disabled_notice')), findsOneWidget); + + await tester.tap(find.byKey(const Key('login_button')), warnIfMissed: false); + await tester.pumpAndSettle(); + + expect(find.text('Painel de Indicadores'), findsNothing); + }); + + testWidgets('com devLoginEnabled: true, o botão funciona normalmente', (tester) async { + await tester.pumpWidget(SinalAdminApp(devLoginEnabled: true)); + + expect(find.byKey(const Key('dev_login_disabled_notice')), findsNothing); + + await tester.tap(find.byKey(const Key('login_button'))); + await tester.pumpAndSettle(); + + expect(find.text('Painel de Indicadores'), findsOneWidget); + }); + }); +} From 64e55433c71892653d5a01e710a78c1e293725cf Mon Sep 17 00:00:00 2001 From: Herbert Rocha Date: Tue, 15 Sep 2026 11:25:32 -0400 Subject: [PATCH 37/43] fix(a11y): reauditar e corrigir contraste, alvo de toque e status WCAG nos apps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- PROGRESS.md | 36 +++- apps/acs/lib/app/acs_theme.dart | 24 +++ apps/acs/lib/app/app.dart | 188 ++++++++++++------ apps/acs/test/contrast_tokens_test.dart | 70 +++++++ apps/acs/test/login_flow_test.dart | 55 +++++ apps/acs/test/support/contrast.dart | 40 ++++ apps/patient/lib/app/app.dart | 55 +++-- apps/patient/lib/app/patient_theme.dart | 10 + apps/patient/test/contrast_tokens_test.dart | 55 +++++ apps/patient/test/patient_app_mvp_test.dart | 39 ++++ apps/patient/test/support/contrast.dart | 40 ++++ spec/ux_accessibility_assessment.md | 210 +++++++++++++++----- 12 files changed, 690 insertions(+), 132 deletions(-) create mode 100644 apps/acs/test/contrast_tokens_test.dart create mode 100644 apps/acs/test/support/contrast.dart create mode 100644 apps/patient/test/contrast_tokens_test.dart create mode 100644 apps/patient/test/support/contrast.dart diff --git a/PROGRESS.md b/PROGRESS.md index 8780417..15167f1 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -124,7 +124,7 @@ dados. | M2.3 | MQTT com TLS | Parcialmente implementado | [apps/acs/lib/core/services/mqtt_secure_client.dart](apps/acs/lib/core/services/mqtt_secure_client.dart) adiciona configuração segura e payload de alerta com TLS/WSS e teste em [apps/acs/test/mqtt_secure_client_test.dart](apps/acs/test/mqtt_secure_client_test.dart) | | M2.4 | Sincronização Offline-First | Implementado | [apps/acs/lib/core/services/offline_visit_queue.dart](apps/acs/lib/core/services/offline_visit_queue.dart) com lote, retry e conflito, validado em [apps/acs/test/login_flow_test.dart](apps/acs/test/login_flow_test.dart) | | M2.5 | Testes de Caos (Toxiproxy) | Implementado | [apps/acs/lib/core/services/network_chaos_simulator.dart](apps/acs/lib/core/services/network_chaos_simulator.dart) e [apps/acs/test/network_chaos_test.dart](apps/acs/test/network_chaos_test.dart) simulam latência, jitter e retry em cenários de falha | -| M2.6 | Testes de Usabilidade | Implementado | [apps/acs/test/login_flow_test.dart](apps/acs/test/login_flow_test.dart) e [apps/patient/test/patient_app_mvp_test.dart](apps/patient/test/patient_app_mvp_test.dart) validam labels semânticas e área mínima de toque para os principais botões | +| M2.6 | Testes de Usabilidade e Acessibilidade | Implementado | [spec/ux_accessibility_assessment.md](spec/ux_accessibility_assessment.md) — matriz de contraste WCAG 1.4.3 determinística (`contrast_tokens_test.dart`), `meetsGuideline` de contraste/alvo de toque e `liveRegion` (SC 4.1.3) em [apps/acs/test/login_flow_test.dart](apps/acs/test/login_flow_test.dart) e [apps/patient/test/patient_app_mvp_test.dart](apps/patient/test/patient_app_mvp_test.dart), validado ponta a ponta no emulador contra o backend e o broker reais | ## O que já está pronto - Fase 2 @@ -443,13 +443,33 @@ Os cenários de falha foram validados em [apps/acs/test/network_chaos_test.dart] ### M2.6 - Testes de Usabilidade e Acessibilidade -Foi implementada a validação de UX básica em [apps/acs/test/login_flow_test.dart](apps/acs/test/login_flow_test.dart) e [apps/patient/test/patient_app_mvp_test.dart](apps/patient/test/patient_app_mvp_test.dart): - -- rótulos semânticos para leitores de tela -- mínimo de 48x48 dp nos principais botões de ação -- manutenção do fluxo principal logo após a validação de acessibilidade - -O app também foi ajustado para expor essas metas corretamente em [apps/acs/lib/app/app.dart](apps/acs/lib/app/app.dart) e [apps/patient/lib/app/app.dart](apps/patient/lib/app/app.dart). +[spec/ux_accessibility_assessment.md](spec/ux_accessibility_assessment.md) documenta a auditoria +completa contra a baseline WCAG 2.1 AA do PRD (§4.3). A primeira versão do relatório media +contraste manualmente contra o fundo do `Scaffold`, mas texto de risco/status é renderizado +dentro de `Card` — produziu um falso positivo e deixou passar duas falhas piores (vermelho e azul +de preenchimento usados como cor de texto, abaixo de 4.5:1 sobre o card). A revisão trocou a +medição manual por uma matriz determinística +(`apps/{acs,patient}/test/contrast_tokens_test.dart`), corrigiu os tokens separando cor de +PREENCHIMENTO de cor de TEXTO (`redOnSurface`/`accentOnSurface` no ACS, +`dangerOnSurface`/`accentOnSurface` no paciente, em +[apps/acs/lib/app/acs_theme.dart](apps/acs/lib/app/acs_theme.dart) e +[apps/patient/lib/app/patient_theme.dart](apps/patient/lib/app/patient_theme.dart)), e adicionou: + +- `meetsGuideline(textContrastGuideline/androidTapTargetGuideline/labeledTapTargetGuideline)` em + [apps/acs/test/login_flow_test.dart](apps/acs/test/login_flow_test.dart) e + [apps/patient/test/patient_app_mvp_test.dart](apps/patient/test/patient_app_mvp_test.dart) +- alvo de toque de 60x60 dp no botão "Ligar para o SAMU (192)", que não tinha `minimumSize` + (default de 40dp de altura visual — a medição anterior de "48x52 dp" estava incorreta) +- `Semantics(liveRegion: true)` em sete pontos de status dinâmico (WCAG 4.1.3, critério ausente + da avaliação original), incluindo a confirmação do alerta de emergência do paciente +- o cartão de alerta da fila do ACS passou a ser lido como uma frase única pelo leitor de tela, + em vez de nós soltos + +Validado ponta a ponta no emulador Android (`emulator-5554`): o app Paciente disparou um alerta de +emergência real contra o backend em Docker Compose, e o app ACS recebeu pelo broker MQTT/TLS real, +exibindo o novo contraste, o botão do SAMU no novo tamanho e o risco traduzido corretamente. Os 14 +testes de integração em dispositivo de `apps/acs/integration_test/` (inclusive o que lê o arquivo +do banco criptografado) passam sobre o código revisado. ## Preparação de deploy — piloto em serviços free-tier (backend) diff --git a/apps/acs/lib/app/acs_theme.dart b/apps/acs/lib/app/acs_theme.dart index 8e86790..b9f5e01 100644 --- a/apps/acs/lib/app/acs_theme.dart +++ b/apps/acs/lib/app/acs_theme.dart @@ -9,8 +9,32 @@ abstract final class AcsColors { static const red = Color(0xFFDC2626); static const yellow = Color(0xFFF59E0B); static const green = Color(0xFF10B981); + + /// Variantes de `red`/`accent` para uso como TEXTO/ÍCONE sobre superfície + /// escura (`surface`/`surfaceRaised`), não como preenchimento. + /// + /// `red` e `accent` só atingem ≥4.5:1 (WCAG 1.4.3) sobre o fundo do + /// Scaffold — sobre `surfaceRaised` caem para 3.04:1 e 2.84:1. Como + /// preenchimento de botão eles continuam corretos (branco sobre `red` dá + /// 4.83:1), por isso não são substituídos: só a leitura como texto muda. + /// `yellow` e `green` não precisam de variante — já passam (6.83:1 e + /// 5.79:1) sobre `surfaceRaised`. + static const redOnSurface = Color(0xFFF87171); + static const accentOnSurface = Color(0xFF60A5FA); } +/// Cor de texto/ícone equivalente a uma cor clínica de preenchimento. +/// +/// Ponto único de conversão: qualquer lugar que hoje usa uma cor de +/// `switch (riskLevel) { ... AcsColors.red/accent ... }` como cor de +/// preenchimento (botão, badge, legenda) passa por aqui antes de aplicá-la a +/// um `TextStyle`/`Icon`. `yellow`/`green` retornam inalterados. +Color acsOnSurface(Color fill) => switch (fill) { + AcsColors.red => AcsColors.redOnSurface, + AcsColors.accent => AcsColors.accentOnSurface, + _ => fill, + }; + ThemeData buildAcsTheme() => ThemeData( useMaterial3: true, brightness: Brightness.dark, diff --git a/apps/acs/lib/app/app.dart b/apps/acs/lib/app/app.dart index 586f2a9..7a089bf 100644 --- a/apps/acs/lib/app/app.dart +++ b/apps/acs/lib/app/app.dart @@ -167,7 +167,10 @@ class _LoginScreenState extends State { ))), if (_error != null) Padding( padding: const EdgeInsets.only(top: 16), - child: Text(key: const Key('login_error'), _error!, textAlign: TextAlign.center, style: const TextStyle(color: AcsColors.red, fontWeight: FontWeight.bold)), + child: Semantics( + liveRegion: true, + child: Text(key: const Key('login_error'), _error!, textAlign: TextAlign.center, style: const TextStyle(color: AcsColors.redOnSurface, fontWeight: FontWeight.bold)), + ), ), ]))), ]), @@ -598,16 +601,23 @@ class _InfraBanner extends StatelessWidget { @override Widget build(BuildContext context) => Padding( padding: const EdgeInsets.only(top: 12), - child: Card( - color: AcsColors.accent.withValues(alpha: 0.15), - child: ListTile( - leading: Icon(icon, color: AcsColors.accent), - title: Text(notice.title, style: const TextStyle(fontWeight: FontWeight.bold)), - subtitle: Text(notice.detail), - trailing: onRetry == null ? null : TextButton( - key: const Key('retry_feed'), - onPressed: onRetry, - child: const Text('Tentar agora'), + // SC 4.1.3 (Status Messages): o banner aparece sem tirar o foco de + // onde a pessoa estava — sem `liveRegion`, um leitor de tela nunca + // saberia que o broker ou o armazenamento caíram, a não ser que + // varresse a tela de novo por conta própria. + child: Semantics( + liveRegion: true, + child: Card( + color: AcsColors.accent.withValues(alpha: 0.15), + child: ListTile( + leading: Icon(icon, color: AcsColors.accentOnSurface), + title: Text(notice.title, style: const TextStyle(fontWeight: FontWeight.bold)), + subtitle: Text(notice.detail), + trailing: onRetry == null ? null : TextButton( + key: const Key('retry_feed'), + onPressed: onRetry, + child: const Text('Tentar agora'), + ), ), ), ), @@ -624,16 +634,21 @@ class _AlertCard extends StatelessWidget { /// Cor é sinal clínico, nunca decoração: mapeia estritamente o risco vindo do /// servidor. - (Color, String) get _risk => switch (alert.riskLevel.toLowerCase()) { - 'red' || 'vermelho' => (AcsColors.red, 'Risco: Vermelho'), - 'yellow' || 'amarelo' => (AcsColors.yellow, 'Risco: Amarelo'), - 'green' || 'verde' => (AcsColors.green, 'Risco: Verde'), - _ => (AcsColors.accent, 'Risco: não classificado'), + /// + /// `color` é o preenchimento (borda esquerda, fundo do botão de + /// confirmação); `textColor` é a variante ajustada para ≥4.5:1 como texto + /// sobre o card (`AcsColors.surfaceRaised`) — `color` sozinho falha nisso + /// para vermelho (3.04:1) e para o fallback azul (2.84:1). + (Color, Color, String) get _risk => switch (alert.riskLevel.toLowerCase()) { + 'red' || 'vermelho' => (AcsColors.red, AcsColors.redOnSurface, 'Risco: Vermelho'), + 'yellow' || 'amarelo' => (AcsColors.yellow, AcsColors.yellow, 'Risco: Amarelo'), + 'green' || 'verde' => (AcsColors.green, AcsColors.green, 'Risco: Verde'), + _ => (AcsColors.accent, AcsColors.accentOnSurface, 'Risco: não classificado'), }; @override Widget build(BuildContext context) { - final (color, label) = _risk; + final (color, textColor, label) = _risk; final isRed = alert.riskLevel.toLowerCase() == 'red' || alert.riskLevel.toLowerCase() == 'vermelho'; return Card( @@ -642,19 +657,33 @@ class _AlertCard extends StatelessWidget { padding: const EdgeInsets.all(16), decoration: BoxDecoration(border: Border(left: BorderSide(color: color, width: 4))), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Identificação direta do paciente não trafega no envelope MQTT - // (LGPD): o alerta carrega identificadores, não nome nem endereço. - Text('Paciente ${alert.patientId.substring(0, 8)}', style: const TextStyle(fontWeight: FontWeight.bold)), - const SizedBox(height: 6), - Text(label, style: TextStyle(color: color, fontWeight: FontWeight.bold)), - const SizedBox(height: 6), - Text('Recebido às ${_time(alert.triggeredAt)} • local ${alert.locationHash}'), - if (alert.acknowledged) const Padding( - padding: EdgeInsets.only(top: 8), - child: Row(children: [ - Icon(Icons.check_circle, size: 16, color: AcsColors.green), - SizedBox(width: 6), - Text('Recebimento confirmado', style: TextStyle(color: AcsColors.green)), + // Um leitor de tela lia isto como quatro nós soltos ("Paciente + // 3f2a..." / "Risco: Vermelho" / "Recebido às..." / botão), sem + // ligar a informação entre si. `excludeSemantics` some com a + // leitura nó a nó dos `Text` abaixo em favor da frase única do + // `label`; os botões continuam fora deste bloco, como nós próprios. + Semantics( + container: true, + label: 'Paciente ${alert.patientId.substring(0, 8)}, $label' + '${alert.acknowledged ? ', recebimento confirmado' : ''}, ' + 'recebido às ${_time(alert.triggeredAt)}', + excludeSemantics: true, + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + // Identificação direta do paciente não trafega no envelope MQTT + // (LGPD): o alerta carrega identificadores, não nome nem endereço. + Text('Paciente ${alert.patientId.substring(0, 8)}', style: const TextStyle(fontWeight: FontWeight.bold)), + const SizedBox(height: 6), + Text(label, style: TextStyle(color: textColor, fontWeight: FontWeight.bold)), + const SizedBox(height: 6), + Text('Recebido às ${_time(alert.triggeredAt)} • local ${alert.locationHash}'), + if (alert.acknowledged) const Padding( + padding: EdgeInsets.only(top: 8), + child: Row(children: [ + Icon(Icons.check_circle, size: 16, color: AcsColors.green), + SizedBox(width: 6), + Text('Recebimento confirmado', style: TextStyle(color: AcsColors.green)), + ]), + ), ]), ), const SizedBox(height: 12), @@ -831,7 +860,7 @@ class _MapScreenState extends State { RouteStatus.complete => 'Rota concluída • ${_routePlan!.distanceKm.toStringAsFixed(1)} km • ETA ${_routePlan!.etaMinutes} min', _ => 'Rota ativa • ${_routePlan!.distanceKm.toStringAsFixed(1)} km • ETA ${_routePlan!.etaMinutes} min', }, - style: const TextStyle(color: AcsColors.accent, fontWeight: FontWeight.bold), + style: const TextStyle(color: AcsColors.accentOnSurface, fontWeight: FontWeight.bold), ), if (_routePlan!.status == RouteStatus.complete && _selectedAlert != null) ...[ const SizedBox(height: 8), @@ -988,7 +1017,7 @@ class _AlertMapSummary extends StatelessWidget { ), child: Row( children: [ - Icon(Icons.location_on, color: color), + Icon(Icons.location_on, color: acsOnSurface(color)), const SizedBox(width: 8), Expanded( child: Text( @@ -1230,10 +1259,13 @@ class _VisitRegistrationScreenState extends State { return [ Padding( padding: const EdgeInsets.only(top: 8), - child: Text( - key: const Key('patient_directory_error'), - error.message, - style: const TextStyle(color: AcsColors.accent, fontWeight: FontWeight.bold), + child: Semantics( + liveRegion: true, + child: Text( + key: const Key('patient_directory_error'), + error.message, + style: const TextStyle(color: AcsColors.accentOnSurface, fontWeight: FontWeight.bold), + ), ), ), const SizedBox(height: 8), @@ -1250,7 +1282,7 @@ class _VisitRegistrationScreenState extends State { key: Key('visit_needs_alert'), 'Nenhum paciente cadastrado na sua microárea ainda. Selecione um ' 'alerta na fila para registrar a visita.', - style: TextStyle(color: AcsColors.accent), + style: TextStyle(color: AcsColors.accentOnSurface), ), ), ]; @@ -1317,13 +1349,16 @@ class _VisitRegistrationScreenState extends State { // Aqui, e não só no painel: é nesta tela que a pessoa acabou de gravar. // Obrigá-la a voltar para a Fila para descobrir que não salvou seria o // mesmo defeito de outra forma. - if (queue.persistenceFailed) const Padding( - padding: EdgeInsets.only(top: 8), - child: Text( - key: Key('visit_storage_error'), - 'As visitas não estão sendo salvas neste aparelho — elas só existem ' - 'na memória até sincronizar.', - style: TextStyle(color: AcsColors.accent, fontWeight: FontWeight.bold), + if (queue.persistenceFailed) Padding( + padding: const EdgeInsets.only(top: 8), + child: Semantics( + liveRegion: true, + child: const Text( + key: Key('visit_storage_error'), + 'As visitas não estão sendo salvas neste aparelho — elas só existem ' + 'na memória até sincronizar.', + style: TextStyle(color: AcsColors.accentOnSurface, fontWeight: FontWeight.bold), + ), ), ), const SizedBox(height: 16), @@ -1347,7 +1382,13 @@ class _VisitRegistrationScreenState extends State { ), ), const SizedBox(height: 20), - FilledButton.icon(key: const Key('save_visit'), onPressed: !hasPatient || !_arrivalConfirmed ? null : _save, icon: const Icon(Icons.save_outlined), label: const Text('Salvar e enfileirar sincronização')), + FilledButton.icon( + key: const Key('save_visit'), + onPressed: !hasPatient || !_arrivalConfirmed ? null : _save, + style: FilledButton.styleFrom(minimumSize: const Size(48, 52)), + icon: const Icon(Icons.save_outlined), + label: const Text('Salvar e enfileirar sincronização'), + ), if (hasPatient && _arrivalConfirmed) ...[ const SizedBox(height: 12), const Text( @@ -1367,25 +1408,29 @@ class _VisitRegistrationScreenState extends State { key: const Key('conflict_visits_count'), 'Em conflito: ${queue.conflictCount}', // Conflito de sincronização é operacional, não gravidade clínica. - style: const TextStyle(color: AcsColors.accent, fontWeight: FontWeight.bold), + style: const TextStyle(color: AcsColors.accentOnSurface, fontWeight: FontWeight.bold), ), ), if (queue.rejectedCount > 0) ...[ Padding( padding: const EdgeInsets.only(top: 4), - child: Text( - key: const Key('rejected_visits_count'), - 'Recusada(s) pelo servidor, não serão reenviadas: ${queue.rejectedCount}\n' - '${queue.rejectedVisits.map((v) => v.rejectionReason).whereType().toSet().join('; ')}', - // Recusa de sync é operacional, não gravidade clínica — mesma cor - // do contador de conflito. - style: const TextStyle(color: AcsColors.accent, fontWeight: FontWeight.bold), + child: Semantics( + liveRegion: true, + child: Text( + key: const Key('rejected_visits_count'), + 'Recusada(s) pelo servidor, não serão reenviadas: ${queue.rejectedCount}\n' + '${queue.rejectedVisits.map((v) => v.rejectionReason).whereType().toSet().join('; ')}', + // Recusa de sync é operacional, não gravidade clínica — mesma cor + // do contador de conflito. + style: const TextStyle(color: AcsColors.accentOnSurface, fontWeight: FontWeight.bold), + ), ), ), const SizedBox(height: 8), OutlinedButton.icon( key: const Key('discard_rejected'), onPressed: _discardRejected, + style: OutlinedButton.styleFrom(minimumSize: const Size(48, 52)), icon: const Icon(Icons.delete_outline), label: const Text('Descartar recusada(s)'), ), @@ -1417,25 +1462,38 @@ class EscalationScreen extends StatelessWidget { const Text('Escalonamento rápido', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), const SizedBox(height: 16), _InfoRow('Paciente', current == null ? '—' : current.patientId.substring(0, 8)), - _InfoRow('Risco', current?.riskLevel ?? '—'), + _InfoRow('Risco', current == null ? '—' : _riskLabelPt(current.riskLevel)), // O envelope MQTT não carrega endereço: só o hash da localização (LGPD). _InfoRow('Local (hash)', current?.locationHash ?? '—'), const SizedBox(height: 20), - FilledButton.icon(onPressed: () => _message(context, 'Discagem não está integrada neste protótipo.'), style: FilledButton.styleFrom(backgroundColor: AcsColors.red), icon: const Icon(Icons.call), label: const Text('Ligar para o SAMU (192)')), + FilledButton.icon( + onPressed: () => _message(context, 'Discagem não está integrada neste protótipo.'), + // Alvo de toque de 60x60 (padrão de emergência do PRD): o default do + // Material 3 para `FilledButton.icon` fica em 40dp de altura visual, + // abaixo do exigido para uma ação de acionar o SAMU. + style: FilledButton.styleFrom(backgroundColor: AcsColors.red, minimumSize: const Size(64, 60)), + icon: const Icon(Icons.call), + label: const Text('Ligar para o SAMU (192)'), + ), const SizedBox(height: 12), - OutlinedButton(onPressed: () => _message(context, 'Encaminhamento será integrado à UBS.'), child: const Text('Encaminhar para UBS Central')), + OutlinedButton( + onPressed: () => _message(context, 'Encaminhamento será integrado à UBS.'), + style: OutlinedButton.styleFrom(minimumSize: const Size(48, 52)), + child: const Text('Encaminhar para UBS Central'), + ), if (current != null) ...[ const SizedBox(height: 12), OutlinedButton.icon( key: const Key('escalation_visit'), onPressed: onVisit == null ? null : () => onVisit!(current), + style: OutlinedButton.styleFrom(minimumSize: const Size(48, 52)), icon: const Icon(Icons.alt_route_outlined), label: const Text('Iniciar rota de visita'), ), const SizedBox(height: 8), const Text( 'A visita é acompanhamento do caso e não substitui o acionamento do SAMU.', - style: TextStyle(color: AcsColors.accent), + style: TextStyle(color: AcsColors.accentOnSurface), ), ], ]); @@ -1481,7 +1539,7 @@ class GeofencingScreen extends StatelessWidget { const Text('Check-in passivo', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), const SizedBox(height: 12), _InfoRow('Paciente', 'Paciente ${alert.patientId.substring(0, 8)}'), - _InfoRow('Risco', alert.riskLevel), + _InfoRow('Risco', _riskLabelPt(alert.riskLevel)), _InfoRow('Raio de chegada', '${(routeService.arrivalThresholdKm * 1000).round()} m'), const SizedBox(height: 12), Text( @@ -1492,7 +1550,7 @@ class GeofencingScreen extends StatelessWidget { }, key: const Key('geofence_status'), style: TextStyle( - color: status == ArrivalStatus.arrived ? AcsColors.green : AcsColors.accent, + color: status == ArrivalStatus.arrived ? AcsColors.green : AcsColors.accentOnSurface, fontWeight: FontWeight.bold, ), ), @@ -1511,6 +1569,16 @@ class NoticesScreen extends StatefulWidget { const NoticesScreen({super.key}); @ class _NoticesScreenState extends State { final notice = TextEditingController(); @override void dispose() { notice.dispose(); super.dispose(); } @override Widget build(BuildContext context) => _page([const Text('Aviso comunitário', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), const SizedBox(height: 16), const TextField(decoration: InputDecoration(labelText: 'Público-alvo', hintText: 'Pacientes com condições crônicas')), const SizedBox(height: 16), TextField(controller: notice, maxLines: 4, decoration: const InputDecoration(labelText: 'Mensagem')), const SizedBox(height: 20), FilledButton(onPressed: () => _message(context, 'Envio depende da integração de notificações push.'), child: const Text('Preparar aviso'))]); } Widget _page(List children) => ListView(padding: const EdgeInsets.all(20), children: [Card(child: Padding(padding: const EdgeInsets.all(20), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: children)))]); + +/// Traduz o `riskLevel` cru do servidor (`"red"`) para o rótulo já usado no +/// resto da tela (`"Vermelho"`) — [GeofencingScreen] exibia a string crua do +/// backend, único ponto do app que não passava pelo mapeamento. +String _riskLabelPt(String riskLevel) => switch (riskLevel.toLowerCase()) { + 'red' || 'vermelho' => 'Vermelho', + 'yellow' || 'amarelo' => 'Amarelo', + 'green' || 'verde' => 'Verde', + _ => 'Não classificado', + }; class _InfoRow extends StatelessWidget { const _InfoRow(this.label, this.value); final String label; final String value; @override Widget build(BuildContext context) => Padding(padding: const EdgeInsets.symmetric(vertical: 5), child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text(label), Flexible(child: Text(value, textAlign: TextAlign.end, style: const TextStyle(fontWeight: FontWeight.bold)))])); } class _Header extends StatelessWidget implements PreferredSizeWidget { const _Header(this.eyebrow, this.title, {this.connected}); @@ -1530,7 +1598,7 @@ class _Header extends StatelessWidget implements PreferredSizeWidget { @override Widget build(BuildContext context) => AppBar( title: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(eyebrow.toUpperCase(), style: const TextStyle(fontSize: 10, color: AcsColors.accent, fontWeight: FontWeight.bold)), + Text(eyebrow.toUpperCase(), style: const TextStyle(fontSize: 10, color: AcsColors.accentOnSurface, fontWeight: FontWeight.bold)), Text(title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), ]), actions: [ diff --git a/apps/acs/test/contrast_tokens_test.dart b/apps/acs/test/contrast_tokens_test.dart new file mode 100644 index 0000000..66ff513 --- /dev/null +++ b/apps/acs/test/contrast_tokens_test.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:sinalacs_acs/app/acs_theme.dart'; + +import 'support/contrast.dart'; + +/// Matriz determinística de contraste (WCAG 2.1 §1.4.3), no lugar da medição +/// manual no WebAIM que produziu `spec/ux_accessibility_assessment.md`. +/// +/// A causa do relatório anterior estar errado era medir todo texto contra o +/// fundo do `Scaffold` (`AcsColors.background`), quando texto de risco e de +/// status é renderizado dentro de `Card` (`AcsColors.surfaceRaised`) ou de +/// `AppBar` (`AcsColors.surface`) — por isso cada linha aqui declara a +/// SUPERFÍCIE real, não só o par de cores. +void main() { + const normalText = 4.5; + const largeTextOrUi = 3.0; + + /// (rótulo, texto/ícone, superfície, limiar) — um por par realmente + /// renderizado no app. `branco` sobre qualquer superfície e `yellow`/ + /// `green` sobre card já eram os casos "fáceis"; ficam aqui como guarda + /// contra regressão, não porque fossem os achados do relatório. + const cases = <(String, Color, Color, double)>[ + ('branco sobre scaffold', Colors.white, AcsColors.background, normalText), + ('branco sobre card', Colors.white, AcsColors.surfaceRaised, normalText), + ('yellow sobre card (badge de risco amarelo)', AcsColors.yellow, AcsColors.surfaceRaised, normalText), + ('green sobre card (badge de risco verde)', AcsColors.green, AcsColors.surfaceRaised, normalText), + // Variantes de texto que o app usa em vez do fill puro (ver os testes de + // documentação abaixo para a prova de que o fill sozinho falha). + ('redOnSurface sobre card', AcsColors.redOnSurface, AcsColors.surfaceRaised, normalText), + ('accentOnSurface sobre card', AcsColors.accentOnSurface, AcsColors.surfaceRaised, normalText), + ('redOnSurface sobre scaffold', AcsColors.redOnSurface, AcsColors.background, normalText), + ('accentOnSurface sobre scaffold', AcsColors.accentOnSurface, AcsColors.background, normalText), + ('accentOnSurface sobre appbar', AcsColors.accentOnSurface, AcsColors.surface, normalText), + // Preenchimento de botão: texto branco sobre a cor de fundo — o teste que + // prova que não precisamos trocar `red`/`accent` como fill. + ('branco sobre botão vermelho (SAMU/pânico)', Colors.white, AcsColors.red, largeTextOrUi), + ('branco sobre botão accent (login)', Colors.white, AcsColors.accent, largeTextOrUi), + ]; + + for (final (label, fg, bg, threshold) in cases) { + test(label, () { + final ratio = contrastOn(fg, bg); + expect( + ratio, + greaterThanOrEqualTo(threshold), + reason: '$label: $ratio:1 abaixo do limiar $threshold:1 exigido pela WCAG 1.4.3', + ); + }); + } + + // Documenta os dois achados do relatório reescrito (B e C): os TOKENS DE + // PREENCHIMENTO, usados como texto direto sobre o card, falham — é por + // isso que `redOnSurface`/`accentOnSurface` existem, em vez de reaproveitar + // `red`/`accent` também para texto. + test('red (fill) usado como texto sobre card fica abaixo de 4.5:1', () { + expect(contrastOn(AcsColors.red, AcsColors.surfaceRaised), lessThan(normalText)); + }); + + test('accent (fill) usado como texto sobre card fica abaixo de 4.5:1', () { + expect(contrastOn(AcsColors.accent, AcsColors.surfaceRaised), lessThan(normalText)); + }); + + test('#EF4444 (correção sugerida pelo relatório antigo) continua abaixo de 4.5:1 sobre card', () { + // Documenta por que a Fase 2 não adotou a sugestão original do relatório: + // clarear o token único não bastava. + final ratio = contrastOn(const Color(0xFFEF4444), AcsColors.surfaceRaised); + expect(ratio, lessThan(normalText)); + }); +} diff --git a/apps/acs/test/login_flow_test.dart b/apps/acs/test/login_flow_test.dart index 352435a..1c5bad4 100644 --- a/apps/acs/test/login_flow_test.dart +++ b/apps/acs/test/login_flow_test.dart @@ -1,3 +1,4 @@ + import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; @@ -94,9 +95,42 @@ void main() { expect(find.text('Recebimento confirmado'), findsOneWidget); }); + testWidgets('o cartão de alerta é lido como uma frase única pelo leitor de tela', (tester) async { + // Antes, um leitor de tela lia o cartão como quatro nós soltos + // ("Paciente 3f2a..." / "Risco: Vermelho" / "Recebido às..." / botão), + // sem ligar a informação entre si. + final handle = tester.ensureSemantics(); + final backend = FakeAcsBackend(); + late FakeAlertFeed feed; + + await tester.pumpWidget(SinalAcsApp( + backend: backend, + feedBuilder: (queue) => feed = FakeAlertFeed(queue), + )); + await tester.tap(find.byKey(const Key('login_button'))); + await tester.pumpAndSettle(); + + feed.deliver(testAlert(alertId: 'alerta-frase')); + await tester.pumpAndSettle(); + + // Sem hora fixa no rótulo esperado: `_time` converte para o fuso local + // da máquina que roda o teste, e o que importa aqui é que as quatro + // informações — paciente, risco, confirmação e horário — cheguem como + // UMA frase, não a hora exata. + expect( + find.bySemanticsLabel(RegExp(r'Paciente .*, Risco: Vermelho, recebido às \d{2}:\d{2}')), + findsOneWidget, + ); + // Os botões continuam como nós próprios, não somem dentro do bloco + // mesclado. + expect(find.bySemanticsLabel('Confirmar recebimento'), findsOneWidget); + handle.dispose(); + }); + testWidgets('deve avisar quando a central de alertas está inacessível', (tester) async { // Um ACS que não sabe que parou de receber alertas é o pior modo de falha // do produto: a falha precisa ser visível, não silenciosa. + final handle = tester.ensureSemantics(); await tester.pumpWidget(SinalAcsApp( backend: FakeAcsBackend(), feedBuilder: (queue) => FakeAlertFeed(queue, failOnStart: true), @@ -106,6 +140,12 @@ void main() { await tester.pumpAndSettle(); expect(find.byKey(const Key('feed_error')), findsOneWidget); + // SC 4.1.3: o banner troca de estado sem tirar o foco de onde a + // pessoa estava — só é percebido por um leitor de tela se a + // `SemanticsNode` estiver marcada como região viva. + final semantics = tester.getSemantics(find.byKey(const Key('feed_error'))); + expect(semantics.flagsCollection.isLiveRegion, isTrue); + handle.dispose(); }); testWidgets('deve expor rótulo semântico e alvo de toque acessível no login do ACS', (tester) async { @@ -122,6 +162,21 @@ void main() { expect(minimumSize.width, greaterThanOrEqualTo(48)); }); + testWidgets('a tela de login atende às diretrizes de contraste e alvo de toque do Flutter', (tester) async { + // Substitui a auditoria manual no WebAIM/TalkBack do relatório anterior + // por uma verificação determinística que o CI roda sozinho. + final handle = tester.ensureSemantics(); + await tester.pumpWidget(SinalAcsApp( + backend: FakeAcsBackend(), + feedBuilder: (queue) => FakeAlertFeed(queue), + )); + + await expectLater(tester, meetsGuideline(textContrastGuideline)); + await expectLater(tester, meetsGuideline(androidTapTargetGuideline)); + await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); + handle.dispose(); + }); + testWidgets('não deve pré-preencher credenciais no formulário', (tester) async { await tester.pumpWidget(SinalAcsApp( backend: FakeAcsBackend(), diff --git a/apps/acs/test/support/contrast.dart b/apps/acs/test/support/contrast.dart new file mode 100644 index 0000000..ea3c8ed --- /dev/null +++ b/apps/acs/test/support/contrast.dart @@ -0,0 +1,40 @@ +import 'dart:math' as math; + +import 'package:flutter/material.dart'; + +/// Luminância relativa WCAG (definição de 1.4.3), 0.0 (preto) a 1.0 (branco). +double relativeLuminance(Color color) { + double channel(double srgb) => + srgb <= 0.04045 ? srgb / 12.92 : math.pow((srgb + 0.055) / 1.055, 2.4).toDouble(); + // `Color.r/g/b` (0.0-1.0) substitui `.red/.green/.blue` (0-255), + // descontinuados nesta versão do Flutter. + return 0.2126 * channel(color.r) + 0.7152 * channel(color.g) + 0.0722 * channel(color.b); +} + +/// Razão de contraste WCAG entre duas cores OPACAS (sem canal alfa a +/// resolver). Para uma cor com alfa, componha antes com [compositeOver]. +double contrastRatio(Color a, Color b) { + final la = relativeLuminance(a); + final lb = relativeLuminance(b); + final hi = la > lb ? la : lb; + final lo = la > lb ? lb : la; + return (hi + 0.05) / (lo + 0.05); +} + +/// Resolve uma cor com transparência (`Colors.white54`, +/// `color.withValues(alpha: ...)`) contra o fundo em que ela realmente é +/// pintada — sem isto, a luminância de uma cor semitransparente não +/// corresponde ao que a tela mostra. +Color compositeOver(Color fg, Color bg) { + final a = fg.a; // 0.0-1.0 + double mix(double f, double b) => a * f + (1 - a) * b; + return Color.from( + alpha: 1.0, + red: mix(fg.r, bg.r), + green: mix(fg.g, bg.g), + blue: mix(fg.b, bg.b), + ); +} + +/// Razão de contraste já resolvendo o alfa de [fg] contra [bg]. +double contrastOn(Color fg, Color bg) => contrastRatio(compositeOver(fg, bg), bg); diff --git a/apps/patient/lib/app/app.dart b/apps/patient/lib/app/app.dart index 7b178ea..fdfcc8c 100644 --- a/apps/patient/lib/app/app.dart +++ b/apps/patient/lib/app/app.dart @@ -147,13 +147,19 @@ class _PatientLoginScreenState extends State { if (_error != null) Padding( padding: const EdgeInsets.only(top: 16), - child: Text( - key: const Key('login_error'), - _error!, - textAlign: TextAlign.center, - style: const TextStyle( - color: PatientColors.danger, - fontWeight: FontWeight.bold, + // SC 4.1.3: o erro aparece sem mover o foco — sem + // `liveRegion` o leitor de tela nunca saberia que + // o login falhou. + child: Semantics( + liveRegion: true, + child: Text( + key: const Key('login_error'), + _error!, + textAlign: TextAlign.center, + style: const TextStyle( + color: PatientColors.dangerOnSurface, + fontWeight: FontWeight.bold, + ), ), ), ), @@ -343,7 +349,19 @@ class _EmergencyScreenState extends State { ), ), const SizedBox(height: 28), - Card(child: Padding(padding: const EdgeInsets.all(16), child: Column(children: [const Icon(Icons.location_on_outlined), const SizedBox(height: 8), const Text('A localização disponível será anexada ao alerta.', textAlign: TextAlign.center), const SizedBox(height: 8), Text(_state, style: const TextStyle(color: PatientColors.accent, fontWeight: FontWeight.bold))]))), + Card(child: Padding(padding: const EdgeInsets.all(16), child: Column(children: [ + const Icon(Icons.location_on_outlined), + const SizedBox(height: 8), + const Text('A localização disponível será anexada ao alerta.', textAlign: TextAlign.center), + const SizedBox(height: 8), + // SC 4.1.3: é a confirmação de que o alerta de emergência chegou à + // equipe — o ponto mais crítico do app para um leitor de tela + // anunciar sem depender de a pessoa varrer a tela de novo. + Semantics( + liveRegion: true, + child: Text(_state, style: const TextStyle(color: PatientColors.accentOnSurface, fontWeight: FontWeight.bold)), + ), + ]))), ], ); } @@ -484,11 +502,14 @@ class _TriageScreenState extends State { if (_error != null) Padding( padding: const EdgeInsets.only(top: 16), - child: Text( - key: const Key('triage_error'), - _error!, - textAlign: TextAlign.center, - style: const TextStyle(color: PatientColors.danger, fontWeight: FontWeight.bold), + child: Semantics( + liveRegion: true, + child: Text( + key: const Key('triage_error'), + _error!, + textAlign: TextAlign.center, + style: const TextStyle(color: PatientColors.dangerOnSurface, fontWeight: FontWeight.bold), + ), ), ), ], @@ -508,10 +529,14 @@ class _TriageResult extends StatelessWidget { @override Widget build(BuildContext context) { + // `color` só é usada como texto/ícone sobre o card (nenhum botão herda + // este tom), por isso a variante `OnSurface` entra direto na tupla — + // `PatientColors.danger` e `PatientColors.accent` caem para 3.03:1 e + // 3.91:1 sobre `surfaceRaised`, abaixo de 4.5:1 (WCAG 1.4.3). final (label, color, guidance) = switch (risk) { RiskLevel.red => ( 'Risco: Vermelho', - PatientColors.danger, + PatientColors.dangerOnSurface, 'Sua equipe de saúde foi avisada com prioridade máxima. ' 'Se piorar, ligue para o SAMU (192).', ), @@ -522,7 +547,7 @@ class _TriageResult extends StatelessWidget { ), RiskLevel.green => ( 'Risco: Verde', - PatientColors.accent, + PatientColors.accentOnSurface, 'Sem sinais de urgência. Sua solicitação entrou na fila de rotina.', ), }; diff --git a/apps/patient/lib/app/patient_theme.dart b/apps/patient/lib/app/patient_theme.dart index 792a0dc..ede0130 100644 --- a/apps/patient/lib/app/patient_theme.dart +++ b/apps/patient/lib/app/patient_theme.dart @@ -8,6 +8,16 @@ abstract final class PatientColors { static const accent = Color(0xFF0D9488); static const accentDark = Color(0xFF0F766E); static const danger = Color(0xFFDC2626); + + /// Variantes de `accent`/`danger` para uso como TEXTO/ÍCONE sobre + /// superfície escura (`surface`/`surfaceRaised`), não como preenchimento. + /// + /// `accent` só atinge 5.37:1 sobre o fundo do Scaffold — sobre + /// `surfaceRaised` cai para 3.91:1, abaixo de 4.5:1 (WCAG 1.4.3). `danger` + /// como preenchimento do botão de pânico continua correto (branco sobre + /// ele dá 4.83:1); como texto sobre card cairia para 3.03:1. + static const accentOnSurface = Color(0xFF2DD4BF); + static const dangerOnSurface = Color(0xFFF87171); } ThemeData buildPatientTheme() { diff --git a/apps/patient/test/contrast_tokens_test.dart b/apps/patient/test/contrast_tokens_test.dart new file mode 100644 index 0000000..a3f4d22 --- /dev/null +++ b/apps/patient/test/contrast_tokens_test.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:sinalacs_patient/app/patient_theme.dart'; + +import 'support/contrast.dart'; + +/// Matriz determinística de contraste (WCAG 2.1 §1.4.3) — ver o irmão em +/// `apps/acs/test/contrast_tokens_test.dart` para o porquê deste teste +/// existir no lugar da medição manual no WebAIM. +void main() { + const normalText = 4.5; + const largeTextOrUi = 3.0; + + const cases = <(String, Color, Color, double)>[ + ('branco sobre scaffold', Colors.white, PatientColors.background, normalText), + ('branco sobre card', Colors.white, PatientColors.surfaceRaised, normalText), + // Achado A do relatório antigo era falso positivo: passa com folga. + ('white54 sobre card (rodapé da triagem)', Colors.white54, PatientColors.surfaceRaised, normalText), + ('yellow #E0A800 sobre card (risco amarelo)', Color(0xFFE0A800), PatientColors.surfaceRaised, normalText), + // Variantes de texto que o app usa em vez do fill puro (ver os testes de + // documentação abaixo para a prova de que o fill sozinho falha). + ('dangerOnSurface sobre card', PatientColors.dangerOnSurface, PatientColors.surfaceRaised, normalText), + ('dangerOnSurface sobre scaffold', PatientColors.dangerOnSurface, PatientColors.background, normalText), + ('accentOnSurface sobre card', PatientColors.accentOnSurface, PatientColors.surfaceRaised, normalText), + // Preenchimento de botão: continua correto sem token novo. + ('branco sobre botão de pânico (danger fill)', Colors.white, PatientColors.danger, largeTextOrUi), + ]; + + for (final (label, fg, bg, threshold) in cases) { + test(label, () { + final ratio = contrastOn(fg, bg); + expect( + ratio, + greaterThanOrEqualTo(threshold), + reason: '$label: $ratio:1 abaixo do limiar $threshold:1 exigido pela WCAG 1.4.3', + ); + }); + } + + // Achado A (refutado) e os achados de texto do relatório reescrito: os + // TOKENS DE PREENCHIMENTO, usados como texto direto sobre superfície, + // falham — por isso `dangerOnSurface`/`accentOnSurface` existem em vez de + // reaproveitar `danger`/`accent` também para texto. + test('danger (fill) usado como texto sobre card fica abaixo de 4.5:1', () { + expect(contrastOn(PatientColors.danger, PatientColors.surfaceRaised), lessThan(normalText)); + }); + + test('danger (fill) usado como texto sobre scaffold fica abaixo de 4.5:1', () { + expect(contrastOn(PatientColors.danger, PatientColors.background), lessThan(normalText)); + }); + + test('accent (fill) usado como texto sobre card fica abaixo de 4.5:1', () { + expect(contrastOn(PatientColors.accent, PatientColors.surfaceRaised), lessThan(normalText)); + }); +} diff --git a/apps/patient/test/patient_app_mvp_test.dart b/apps/patient/test/patient_app_mvp_test.dart index 397cd68..cde3713 100644 --- a/apps/patient/test/patient_app_mvp_test.dart +++ b/apps/patient/test/patient_app_mvp_test.dart @@ -1,3 +1,4 @@ + import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:sinalacs_client/sinalacs_client.dart' show RiskLevel; @@ -40,6 +41,7 @@ void main() { }); testWidgets('não deve avançar quando a autenticação falha', (tester) async { + final handle = tester.ensureSemantics(); final backend = FakePatientBackend( loginFailure: const BackendFailure('Sem conexão com o servidor.'), ); @@ -52,6 +54,11 @@ void main() { expect(find.text('Triagem rápida'), findsNothing); expect(find.byKey(const Key('login_error')), findsOneWidget); expect(find.text('Sem conexão com o servidor.'), findsOneWidget); + // SC 4.1.3: o erro aparece sem mover o foco — sem `liveRegion` um leitor + // de tela nunca saberia que o login falhou. + final semantics = tester.getSemantics(find.byKey(const Key('login_error'))); + expect(semantics.flagsCollection.isLiveRegion, isTrue); + handle.dispose(); }); testWidgets('deve enviar os sintomas ao servidor e exibir o risco recebido', (tester) async { @@ -112,6 +119,26 @@ void main() { expect(backend.idempotencyKeys.first, backend.idempotencyKeys.last); }); + testWidgets('o estado do alerta de emergência é anunciado ao leitor de tela', (tester) async { + // É a confirmação de que o alerta chegou à equipe — o ponto mais crítico + // do app para um leitor de tela anunciar sem depender de a pessoa + // varrer a tela de novo. + final handle = tester.ensureSemantics(); + await tester.pumpWidget(SinalAcsApp(backend: FakePatientBackend())); + + await login(tester); + await tester.tap(find.text('Urgência')); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('panic_button'))); + await tester.pumpAndSettle(); + await tester.tap(find.text('Confirmar alerta')); + await tester.pumpAndSettle(); + + final semantics = tester.getSemantics(find.text('Alerta recebido pela equipe')); + expect(semantics.flagsCollection.isLiveRegion, isTrue); + handle.dispose(); + }); + testWidgets('deve expor rótulo semântico e alvo de toque acessível no fluxo do paciente', (tester) async { await tester.pumpWidget(SinalAcsApp(backend: FakePatientBackend())); @@ -122,4 +149,16 @@ void main() { expect(minimumSize.height, greaterThanOrEqualTo(48)); expect(minimumSize.width, greaterThanOrEqualTo(48)); }); + + testWidgets('a tela de login atende às diretrizes de contraste e alvo de toque do Flutter', (tester) async { + // Substitui a auditoria manual no WebAIM/TalkBack do relatório anterior + // por uma verificação determinística que o CI roda sozinho. + final handle = tester.ensureSemantics(); + await tester.pumpWidget(SinalAcsApp(backend: FakePatientBackend())); + + await expectLater(tester, meetsGuideline(textContrastGuideline)); + await expectLater(tester, meetsGuideline(androidTapTargetGuideline)); + await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); + handle.dispose(); + }); } diff --git a/apps/patient/test/support/contrast.dart b/apps/patient/test/support/contrast.dart new file mode 100644 index 0000000..ea3c8ed --- /dev/null +++ b/apps/patient/test/support/contrast.dart @@ -0,0 +1,40 @@ +import 'dart:math' as math; + +import 'package:flutter/material.dart'; + +/// Luminância relativa WCAG (definição de 1.4.3), 0.0 (preto) a 1.0 (branco). +double relativeLuminance(Color color) { + double channel(double srgb) => + srgb <= 0.04045 ? srgb / 12.92 : math.pow((srgb + 0.055) / 1.055, 2.4).toDouble(); + // `Color.r/g/b` (0.0-1.0) substitui `.red/.green/.blue` (0-255), + // descontinuados nesta versão do Flutter. + return 0.2126 * channel(color.r) + 0.7152 * channel(color.g) + 0.0722 * channel(color.b); +} + +/// Razão de contraste WCAG entre duas cores OPACAS (sem canal alfa a +/// resolver). Para uma cor com alfa, componha antes com [compositeOver]. +double contrastRatio(Color a, Color b) { + final la = relativeLuminance(a); + final lb = relativeLuminance(b); + final hi = la > lb ? la : lb; + final lo = la > lb ? lb : la; + return (hi + 0.05) / (lo + 0.05); +} + +/// Resolve uma cor com transparência (`Colors.white54`, +/// `color.withValues(alpha: ...)`) contra o fundo em que ela realmente é +/// pintada — sem isto, a luminância de uma cor semitransparente não +/// corresponde ao que a tela mostra. +Color compositeOver(Color fg, Color bg) { + final a = fg.a; // 0.0-1.0 + double mix(double f, double b) => a * f + (1 - a) * b; + return Color.from( + alpha: 1.0, + red: mix(fg.r, bg.r), + green: mix(fg.g, bg.g), + blue: mix(fg.b, bg.b), + ); +} + +/// Razão de contraste já resolvendo o alfa de [fg] contra [bg]. +double contrastOn(Color fg, Color bg) => contrastRatio(compositeOver(fg, bg), bg); diff --git a/spec/ux_accessibility_assessment.md b/spec/ux_accessibility_assessment.md index 9bfaf93..92807a7 100644 --- a/spec/ux_accessibility_assessment.md +++ b/spec/ux_accessibility_assessment.md @@ -1,8 +1,23 @@ # Relatório de Avaliação de UI/UX e Acessibilidade -**Aplicativos Avaliados:** `apps/patient` e `apps/acs` -**Referência Baseline:** PRD de Sistemas (Seção 4.3 — WCAG 2.1 Nível AA), `spec/ui_design.md` -**Metodologia e Ferramentas:** WebAIM Contrast Checker, Auditoria Estática de Código (Dart AST/grep), Validação Prática via Android TalkBack em Dispositivo Físico USB e Especificação de Testes Remotos no UXtweak. +**Aplicativos Avaliados:** `apps/patient` e `apps/acs` +**Referência Baseline:** PRD de Sistemas (Seção 4.3 — WCAG 2.1 Nível AA), `spec/ui_design.md` +**Metodologia e Ferramentas:** Matriz de contraste determinística (`apps/*/test/contrast_tokens_test.dart`, +implementando a fórmula de luminância relativa WCAG 1.4.3 contra a superfície REAL de +renderização de cada token — ver §2.1), matchers nativos do Flutter no CI +(`meetsGuideline(textContrastGuideline/androidTapTargetGuideline/labeledTapTargetGuideline)` +em `login_flow_test.dart` e `patient_app_mvp_test.dart`), Auditoria Estática de Código (Dart +AST/grep), Validação em dispositivo real (`apps/acs/integration_test/`, incluindo o teste que lê o +arquivo do banco criptografado) e Verificação Prática no emulador Android (`emulator-5554`) contra +o backend e o broker reais, cobrindo os dois apps em conjunto (paciente envia alerta → ACS recebe +pelo MQTT/TLS real). + +> **Nota de revisão:** a versão anterior deste relatório mediu contraste manualmente no WebAIM +> Contrast Checker contra o fundo do `Scaffold`, mas texto de risco e de status é renderizado +> dentro de `Card`/`AppBar`, com uma cor de superfície diferente. Isso produziu um falso positivo +> (§3, achado A) e deixou passar duas falhas piores (§3, achados B e C). Esta revisão substitui a +> medição manual por um teste determinístico que roda no CI — ver `apps/acs/test/contrast_tokens_test.dart` +> e `apps/patient/test/contrast_tokens_test.dart`. --- @@ -10,11 +25,12 @@ | Critério WCAG 2.1 | Descrição do Critério | Requisito do PRD / Issue | Status | Resumo do Diagnóstico | | :--- | :--- | :--- | :---: | :--- | -| **1.4.1 Color Use** | A cor não deve ser o único indicador visual de estado/risco | Duplo canal (Texto/Ícone + Cor) em sinais clínicos de risco | **Conforme** | Aplicado no paciente: rótulos explícitos `'Risco: Vermelho'`, `'Risco: Amarelo'` e `'Risco: Verde'` acompanhados de ícones. | -| **1.4.3 Contrast (Minimum)** | Razão de contraste min. de 4.5:1 (texto normal) e 3:1 (texto grande/UI) | ≥ 4.5:1 para texto sobre fundo escuro nos temas | **Não Conforme (Parcial)** | O vermelho (`#DC2626`) em texto atinge 4.16:1 sobre fundo `#030712`. O texto `Colors.white54` no Card da triagem atinge apenas ~3.2:1 (falha). | -| **2.4.7 Focus Visible** | Indicador claro de foco visual ao navegar por campos interativos | Foco visível em todos os elementos selecionáveis | **Conforme** | Testado via TalkBack em dispositivo Android: moldura de seleção envolve 100% dos elementos sem cortes visuais. | -| **2.5.5 Target Size** | Alvo de toque adequado para interatividade | ≥ 48x48 dp (padrão), ≥ 60x60 dp (botão de emergência/pânico) | **Conforme** | Botão de pânico mede `208x208 dp`. Botões de formulário e login medem `48x52 dp`. | -| **4.1.2 Name, Role, Value** | Árvore semântica exposta para leitores de tela nativos | Rótulos e papeis em 100% dos fluxos críticos | **Conforme** | Árvore semântica nativa do Flutter expõe abas (**Área, Fila, Mapa, Visita, Mais**) e formulários com clareza. | +| **1.4.1 Color Use** | A cor não deve ser o único indicador visual de estado/risco | Duplo canal (Texto/Ícone + Cor) em sinais clínicos de risco | **Conforme** | Rótulos explícitos `'Risco: Vermelho'`, `'Risco: Amarelo'`, `'Risco: Verde'` acompanhados de ícone, nos dois apps. | +| **1.4.3 Contrast (Minimum)** | Razão de contraste min. de 4.5:1 (texto normal) e 3:1 (texto grande/UI) | ≥ 4.5:1 para texto sobre fundo escuro nos temas | **Conforme (corrigido)** | Cinco pares de token/superfície falhavam quando medidos corretamente (§2.1); corrigidos separando token de PREENCHIMENTO de token de TEXTO (`redOnSurface`/`accentOnSurface` no ACS, `dangerOnSurface`/`accentOnSurface` no paciente). Guardado por teste determinístico. | +| **2.4.7 Focus Visible** | Indicador claro de foco visual ao navegar por campos interativos | Foco visível em todos os elementos selecionáveis | **Conforme** | Indicador de foco nativo do Android acompanha todos os alvos tocáveis, sem truncamento. | +| **2.5.5 Target Size** | Alvo de toque adequado para interatividade | ≥ 48x48 dp (padrão), ≥ 60x60 dp (botão de emergência/pânico) | **Conforme (corrigido)** | Botão de pânico do paciente: `208x208 dp`. "Ligar para o SAMU (192)" no ACS não tinha `minimumSize` (default M3 de 40dp de altura visual) — corrigido para `64x60 dp`. Quatro outros botões de ação primária no ACS também não tinham `minimumSize` explícito; padronizados em `48x52 dp`. | +| **4.1.2 Name, Role, Value** | Árvore semântica exposta para leitores de tela nativos | Rótulos e papeis em 100% dos fluxos críticos | **Conforme** | Árvore semântica nativa do Flutter expõe abas (**Área, Fila, Mapa, Visita, Mais**) e formulários com clareza; o cartão de alerta da fila passou a ser lido como uma frase única (§3, achado antigo de prioridade Baixa). | +| **4.1.3 Status Messages** | Uma mudança de status deve ser anunciada por tecnologia assistiva sem exigir foco | (ausente da avaliação anterior) | **Conforme (corrigido)** | Critério não coberto pelo relatório original. Sete pontos de status dinâmico (erro de login nos dois apps, confirmação de alerta de emergência, banners de falha de broker/armazenamento, erro do diretório de pacientes, contador de visitas recusadas) não eram anunciados; corrigidos com `Semantics(liveRegion: true)`. | --- @@ -22,32 +38,62 @@ ### 2.1 Contraste e Uso de Cor (WCAG 1.4.3 e 1.4.1) -Auditoria realizada comparando as cores declaradas em `acs_theme.dart` e `patient_theme.dart` com as superfícies reais de renderização utilizando o WebAIM Contrast Checker: +Cada app declara cores de PREENCHIMENTO (fundo de botão, badge) que também eram reaproveitadas +como cor de TEXTO/ícone sobre `Card`. Um preenchimento e um texto têm requisitos diferentes — um +botão vermelho com texto branco por cima só precisa de 3:1 (texto grande/UI), mas o mesmo +vermelho usado como cor de um `Text` precisa de 4.5:1. A tabela abaixo mede cada par contra a +superfície onde ele é **de fato** renderizado, com composição de alfa quando aplicável +(`Colors.white54` etc.): #### Tabela de Razão de Contraste Renderizado -| Aplicativo | Par de Cores (Texto vs Fundo) | Contexto de Uso | Razão Real (WebAIM) | Exigência WCAG | Status | -| :--- | :--- | :--- | :---: | :---: | :---: | -| **Ambos** | Branco (`#FFFFFF`) / Background (`#030712`) | Texto principal / AppBar | **20.13:1** | 4.5:1 | Conforme | -| **ACS** | Amarelo (`#F59E0B`) / Background (`#030712`) | Risco Médio / Status | **9.37:1** | 4.5:1 | Conforme | -| **ACS** | Verde (`#10B981`) / Background (`#030712`) | Risco Baixo / Status | **7.93:1** | 4.5:1 | Conforme | -| **Paciente** | Teal Accent (`#0D9488`) / Background (`#030712`) | Elementos primários | **5.37:1** | 4.5:1 | Conforme | -| **Ambos** | Vermelho (`#DC2626`) / Background (`#030712`) | Alertas e textos críticos | **4.16:1** | 4.5:1 | Falha em Texto Normal | -| **Paciente** | Branco 54% opaco (`Colors.white54`) / Card (`#1E293B`) | Rodapé explicativo da triagem | **~3.2:1** | 4.5:1 | Falha (Texto pequeno) | - -#### Evidências Visuais de Validação no WebAIM - -![Validação de Contraste da Cor Vermelha no WebAIM](assets/contrast_red_fail.png) -*Figura 2.1: Teste de contraste entre a cor vermelha (#DC2626) e o fundo escuro (#030712) no WebAIM, indicando razão de 4.16:1 (Falha para Normal Text no WCAG AA).* - -![Validação de Contraste da Cor Teal no WebAIM](assets/contrast_teal_pass.png) -*Figura 2.2: Teste de contraste entre o tom Teal (#0D9488) e o fundo escuro (#030712) no WebAIM, aprovado com razão de 5.37:1.* - -![Validação de Contraste da Cor Branca no WebAIM](assets/contrast_white_pass.png) -*Figura 2.3: Teste de contraste entre a cor branca (#FFFFFF) e o fundo escuro (#030712) no WebAIM, aprovado com razão de 20.13:1.* +| App | Token | Papel | Superfície real | Razão (WCAG) | Exigência | Status | +| :--- | :--- | :--- | :--- | :---: | :---: | :---: | +| Ambos | Branco `#FFFFFF` | texto | Scaffold `#030712` | 20.13:1 | 4.5:1 | Conforme | +| Ambos | Branco `#FFFFFF` | texto | Card (`#1F2937`/`#1E293B`) | 14.68:1 / 14.63:1 | 4.5:1 | Conforme | +| ACS | `AcsColors.yellow #F59E0B` | texto | Card `#1F2937` | 6.83:1 | 4.5:1 | Conforme | +| ACS | `AcsColors.green #10B981` | texto | Card `#1F2937` | 5.79:1 | 4.5:1 | Conforme | +| ACS | `AcsColors.red #DC2626` | **texto** | Card `#1F2937` | **3.04:1** | 4.5:1 | **Falha** (achado B) | +| ACS | `AcsColors.accent #2563EB` | **texto** | Card `#1F2937` | **2.84:1** | 4.5:1 | **Falha** (achado C, não detectado antes) | +| ACS | branco | texto do botão | fill `AcsColors.red` | 4.83:1 | 3:1 (UI) | Conforme — fill não muda | +| Paciente | `Colors.white54` | texto | Card `#1E293B` | **5.36:1** | 4.5:1 | **Conforme** — achado A do relatório anterior era falso positivo (media 3.2:1 contra o Scaffold) | +| Paciente | amarelo `#E0A800` | texto | Card `#1E293B` | 6.81:1 | 4.5:1 | Conforme | +| Paciente | `PatientColors.accent #0D9488` | **texto** | Card `#1E293B` | **3.91:1** | 4.5:1 | **Falha** (achado D, relatado antes como conforme por medir no Scaffold) | +| Paciente | `PatientColors.danger #DC2626` | **texto** | Card `#1E293B` | **3.03:1** | 4.5:1 | **Falha** | +| Paciente | `PatientColors.danger #DC2626` | **texto** | Scaffold `#030712` | **4.17:1** | 4.5:1 | **Falha** — este é o par que o relatório original mediu (4.16:1) e classificou "Média"; a correção sugerida (`#EF4444`) foi avaliada e descartada (linha abaixo) | +| Paciente | branco | texto do botão | fill `PatientColors.danger` | 4.83:1 | 3:1 (UI) | Conforme — fill não muda | +| — | `#EF4444` (correção sugerida pelo relatório anterior) | texto | Card `#1F2937` | **3.90:1** | 4.5:1 | **Ainda falha** — não adotada | + +Reprodução: `flutter test test/contrast_tokens_test.dart` em cada app. + +#### Correção aplicada: separar token de preenchimento de token de texto + +Clarear o token único (a sugestão original, `#EF4444`) resolvia a leitura sobre o `Scaffold` mas +continuava falhando sobre `Card` (3.90:1) — não bastava. A correção adotada foi acrescentar uma +variante de TEXTO a cada token de preenchimento que falhava como texto, mantendo o preenchimento +original intacto (ele já cumpre 3:1 com texto branco por cima): + +| App | Novo token | Valor | Sobre `surfaceRaised` | Substitui, como texto | +| :--- | :--- | :--- | :---: | :--- | +| ACS | `AcsColors.redOnSurface` | `#F87171` | 5.31:1 | `AcsColors.red` | +| ACS | `AcsColors.accentOnSurface` | `#60A5FA` | 5.77:1 | `AcsColors.accent` | +| Paciente | `PatientColors.dangerOnSurface` | `#F87171` | 5.29:1 | `PatientColors.danger` | +| Paciente | `PatientColors.accentOnSurface` | `#2DD4BF` | 7.86:1 | `PatientColors.accent` | + +`AcsColors.red`/`AcsColors.accent` e `PatientColors.danger`/`PatientColors.accent` continuam +sendo a cor de PREENCHIMENTO do botão de pânico, do botão "Ligar para o SAMU" e do botão +"Confirmar recebimento" — nenhum desses mudou de cor. Um helper único (`acsOnSurface()` no ACS) +converte a cor de preenchimento na variante de texto no ponto em que um `switch` de risco +alimenta tanto um `backgroundColor` quanto um `TextStyle`, para nunca haver dois pontos de +verdade sobre qual vermelho usar onde. #### Validação de Duplo Canal (WCAG 1.4.1) -No arquivo `apps/patient/lib/app/app.dart` (linhas 503-560), o componente `_TriageResult` utiliza rótulos de texto explícitos concatenados ao sinal visual: `'Risco: Vermelho'`, `'Risco: Amarelo'` e `'Risco: Verde'`. + +`_TriageResult` (paciente) e `_AlertCard`/`_riskLabelPt` (ACS) usam rótulos de texto explícitos +concatenados ao sinal visual: `'Risco: Vermelho'`, `'Risco: Amarelo'`, `'Risco: Verde'`. Um +defeito adjacente foi corrigido nesta revisão: `GeofencingScreen` e `EscalationScreen` exibiam a +string crua vinda do servidor (`"red"`) em vez do rótulo traduzido — único ponto do app que não +passava pelo mapeamento, confirmado e corrigido junto com a auditoria de contraste. --- @@ -55,46 +101,96 @@ No arquivo `apps/patient/lib/app/app.dart` (linhas 503-560), o componente `_Tria | App | Componente / Fluxo | Dimensão Renderizada | Mínimo Requerido | Status | | :--- | :--- | :---: | :---: | :---: | -| **Paciente** | Botão de Emergência/Pânico (`panic_button`) | `208 x 208 dp` | 60 x 60 dp | Conforme (Amplo) | -| **Paciente** | Botões de Login e Envio de Triagem | `48 x 52 dp` | 48 x 48 dp | Conforme | -| **ACS** | Ações de Login, Confirmar Recebimento e Sincronizar | `48 x 52 dp` | 48 x 48 dp | Conforme | -| **ACS** | Ações de Emergência do SAMU ("Acionar/Ligar SAMU") | `48 x 52 dp` | 60 x 60 dp | Sugestão de Destaque | +| Paciente | Botão de Emergência/Pânico (`panic_button`) | `208 x 208 dp` | 60 x 60 dp | Conforme (Amplo) | +| Paciente | Botões de Login e Envio de Triagem | `48 x 52 dp` | 48 x 48 dp | Conforme | +| ACS | Ações de Login, Confirmar Recebimento e Sincronizar | `48 x 52 dp` | 48 x 48 dp | Conforme | +| ACS | "Ligar para o SAMU (192)" | ~~`48x52 dp`~~ **sem `minimumSize` → 40dp de altura visual** | 60 x 60 dp | **Corrigido para `64x60 dp`** — a medição anterior ("48x52 dp") estava incorreta; o botão não declarava `minimumSize` e caía no default do Material 3. | +| ACS | "Salvar e enfileirar sincronização", "Descartar recusada(s)", "Encaminhar para UBS Central", "Iniciar rota de visita" (escalonamento) | sem `minimumSize` (40dp) | 48 x 48 dp | **Corrigido para `48x52 dp`**, não detectados no relatório anterior | + +Reprodução: `meetsGuideline(androidTapTargetGuideline)` em ambos os apps (`login_flow_test.dart`, +`patient_app_mvp_test.dart`). --- ### 2.3 Rótulos Semânticos e Navegação por Leitor de Tela (WCAG 4.1.2) #### Inspeção Estática de Código -* **Instâncias de `Semantics()`:** Mapeadas nos fluxos de Login ACS (`app.dart:159`), Login Paciente (`app.dart:127`) e Botão de Pânico (`app.dart:327`). -* **Instâncias de `tooltip:`:** Mapeados em `apps/patient/lib/app/app.dart` nas linhas 606 (`'Enviar mensagem'`) e 696 (`'Novo alarme'`). - -#### Teste Prático com Android TalkBack (Dispositivo Físico USB - ACS) -* **Navegação por Varredura nas Abas:** O leitor anunciou perfeitamente o nome e o papel semântico de todas as guias da barra inferior: **"Área"**, **"Fila"**, **"Mapa"**, **"Visita"** e **"Mais"** (ex: *"Mapa, guia 3 de 5, selecionado"*). -* **Fluxo de Visita e Formulários:** O motor semântico nativo do Flutter converteu os widgets `Text`, `Icon` e `IconButton` em elementos acessíveis sem interrupções. +* **Instâncias de `Semantics()`:** Login ACS (`app.dart`), Login Paciente, Botão de Pânico, + banners de infraestrutura, e — nesta revisão — o cartão de alerta da fila. +* **Instâncias de `tooltip:`:** `'Enviar mensagem'`, `'Novo alarme'` no app paciente. + +#### Correção: cartão de alerta lido como frase única + +Achado de prioridade "Baixa" do relatório anterior, agora corrigido: o `_AlertCard` do ACS era +lido pelo TalkBack como quatro nós soltos ("Paciente 3f2a1b8c" / "Risco: Vermelho" / "Recebido +às..." / botão), sem ligação entre as informações. `Semantics(label: ..., excludeSemantics: true)` +envolve o bloco de identificação (paciente, risco, confirmação, horário) numa frase única; os +botões de ação continuam como nós próprios, fora do bloco. Coberto por teste +(`login_flow_test.dart`, "o cartão de alerta é lido como uma frase única pelo leitor de tela") e +confirmado em dispositivo real no emulador. + +#### Teste Prático em Dispositivo (emulador Android + integração real) + +* **Fluxo completo ponta a ponta:** app Paciente disparou um alerta de emergência real contra o + backend rodando em Docker Compose; o app ACS recebeu o alerta pelo broker MQTT/TLS real, + exibiu-o na fila com o novo `redOnSurface`, e o fluxo de escalonamento mostrou o botão do SAMU + no novo tamanho e o risco traduzido (`_riskLabelPt`). +* **14 testes de integração em dispositivo** (`apps/acs/integration_test/`, incluindo + `encrypted_storage_test.dart`, que lê o arquivo do banco e confirma que não é SQLite em texto + plano) passam sobre o código revisado — a única forma de provar criptografia real, e a mesma + suíte que exercita as telas de mapa e escalonamento tocadas por esta revisão. --- ### 2.4 Indicador de Foco Visível (WCAG 2.4.7) -* **Validação Prática:** Realizada via varredura com TalkBack habilitado em dispositivo Android físico conectado via USB no aplicativo ACS. -* **Resultado:** O indicador de foco nativo do sistema (moldura verde de destaque) acompanhou com precisão todos os alvos tocáveis, sem truncamento ou ocultação visual. +Sem alteração nesta revisão: o indicador de foco nativo do sistema acompanha os alvos tocáveis +sem truncamento, confirmado durante a navegação manual no emulador. --- -## 3. Matriz de Priorização de Achados e Sugestões de Ajustes +### 2.5 Status Messages (WCAG 4.1.3) — critério novo + +Ausente da avaliação anterior. Um leitor de tela só percebe uma mudança de conteúdo que não move +o foco se o nó semântico correspondente estiver marcado como região viva +(`Semantics(liveRegion: true)`). Sete pontos de status dinâmico foram auditados e não tinham essa +marcação: + +| App | Local | O que muda | +| :--- | :--- | :--- | +| Paciente | `login_error` | Falha de autenticação | +| Paciente | `_state` do alerta de emergência | Confirmação de que o alerta chegou à equipe — o ponto mais crítico do app | +| Paciente | `triage_error` | Falha ao enviar a triagem | +| ACS | `login_error` | Falha de autenticação | +| ACS | `feed_error` / `storage_error` (`_InfraBanner`) | Broker ou armazenamento local caindo | +| ACS | `visit_storage_error` | Persistência falhando durante o registro da visita | +| ACS | `patient_directory_error` | Falha ao carregar pacientes da microárea | +| ACS | `rejected_visits_count` | Servidor recusou uma visita em definitivo | + +Todos corrigidos com `Semantics(liveRegion: true)`. Cobertos por teste +(`SemanticsFlags.isLiveRegion`, em `login_flow_test.dart` e `patient_app_mvp_test.dart`) e +confirmados em dispositivo: o texto "Alerta recebido pela equipe" do fluxo de emergência real +chegou marcado como região viva. -| Prioridade | Critério | Localização | Problema Identificado | Sugestão Concreta de UI / Código | -| :---: | :---: | :--- | :--- | :--- | -| **Alta** | 1.4.3 | `patient_app.dart` (linha 542) | `Colors.white54` sobre Card resulta em contraste de **~3.2:1** (falha WCAG AA para 12pt). | Substituir `color: Colors.white54` por `color: Colors.white70` ou `color: Color(0xFFE2E8F0)` (~5.2:1). | -| **Média** | 1.4.3 | `acs_theme.dart` e `patient_theme.dart` | Vermelho `danger` (`#DC2626`) como cor de texto sobre fundo escuro resulta em **4.16:1**. | Ajustar a constante da cor de perigo para `Color(0xFFEF4444)` (eleva a razão de contraste para **~5.7:1**). | -| **Média** | 2.5.5 | `apps/acs/lib/app/app.dart` | Botões de acionamento crítico do SAMU usam tamanho padrão (`48x52 dp`). | Aplicar `minSize` estendido de `60x60 dp` nas ações do SAMU para corresponder ao padrão tátil de emergência. | -| **Baixa** | 4.1.2 | `apps/acs/lib/app/app.dart` | Cartões de pacientes e badges de risco dependem da árvore automática. | Envolver os cartões do dashboard em widgets `Semantics(label: "Paciente X, Risco Vermelho Alto", ...)` explícitos. | +--- + +## 3. Matriz de Achados — Situação Após Esta Revisão + +| Prioridade original | Critério | Localização | Situação nesta revisão | +| :---: | :---: | :--- | :--- | +| Alta | 1.4.3 | `patient/app.dart` (`Colors.white54`) | **Refutado.** 5.36:1 sobre `Card` — a medição original comparava contra o Scaffold. Nenhuma ação necessária. | +| Média | 1.4.3 | `acs_theme.dart`/`patient_theme.dart` (vermelho como texto) | **Corrigido**, mas o remédio proposto (`#EF4444`) foi descartado por insuficiência (3.90:1 sobre card); adotada a separação fill/texto (§2.1). | +| (não detectado) | 1.4.3 | `AcsColors.accent`/`PatientColors.accent` como texto sobre card | **Corrigido** (achados C e D) — pior que o item "Média" do relatório anterior e não constava nele. | +| Média | 2.5.5 | Botão do SAMU | **Corrigido** para `64x60 dp`; a medição original ("48x52 dp") estava incorreta — o botão não tinha `minimumSize`. | +| Baixa | 4.1.2 | Cartões de alerta do ACS | **Corrigido** — cartão agora é uma frase semântica única. | +| (não avaliado) | 4.1.3 | Sete pontos de status dinâmico | **Critério ausente da baseline anterior, incorporado e corrigido nesta revisão.** | --- ## 4. Planejamento de Testes de Usabilidade Remota (UXtweak) -Para complementar a auditoria técnica de acessibilidade com testes de fricção de UX com usuários reais, foram estruturados os seguintes cenários no **UXtweak**: +Sem alteração — planejamento de teste com usuários reais, independente dos achados técnicos +corrigidos acima. ### Cenário 1: Disparo de Alerta de Urgência (App Paciente) * **Objetivo:** Avaliar o tempo de reação e a taxa de sucesso no acionamento do botão de pânico. @@ -105,3 +201,19 @@ Para complementar a auditoria técnica de acessibilidade com testes de fricção * **Objetivo:** Avaliar a clareza da visualização dos sinais de risco em ambiente com baixo ruído visual. * **Métrica:** Taxa de cliques corretos no paciente de maior risco da lista. * **Instrução dada ao usuário:** *"No seu painel de atendimento, identifique o paciente que exige visita prioritária imediata e acesse os detalhes dele."* + +--- + +## 5. Cobertura Automatizada (substitui a auditoria manual) + +| Verificação | Onde roda | O que impede de regredir | +| :--- | :--- | :--- | +| Matriz de contraste WCAG 1.4.3 | `apps/{acs,patient}/test/contrast_tokens_test.dart` | Qualquer token de tema cair abaixo de 4.5:1/3:1 na superfície real | +| `meetsGuideline` (contraste + alvo de toque) | `login_flow_test.dart`, `patient_app_mvp_test.dart` | Regressão de contraste ou alvo de toque nas telas de login | +| `SemanticsFlags.isLiveRegion` | idem | Status dinâmico deixar de ser anunciado | +| Rótulo semântico do cartão de alerta | `login_flow_test.dart` | Cartão voltar a ser lido como nós soltos | +| 14 testes de integração em dispositivo | `apps/acs/integration_test/` | Regressão de fluxo real (criptografia, MQTT/TLS, mapa, escalonamento) | + +Este conjunto roda no CI (`serverpod-backend`, `patient-app`, `acs-app` — ver `.github/workflows/ci.yml` +para os dois primeiros grupos; o de integração em dispositivo é manual via `scripts/qa/e2e.sh --emulator`) +e é a evidência que substitui as capturas de tela do WebAIM desta revisão em diante. From 73447d6c781484812fcde3665f42781568a4e63c Mon Sep 17 00:00:00 2001 From: Herbert Rocha Date: Tue, 15 Sep 2026 11:41:54 -0400 Subject: [PATCH 38/43] docs(ux): adicionar plano de testes de UX/UI baseado em spec/ui_design.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 https://github.com/hbgit/SinalACS/issues/8, atribuída a sarahw3b. Signed-off-by: Herbert Rocha --- spec/ux_ui_test_plan.md | 162 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 spec/ux_ui_test_plan.md diff --git a/spec/ux_ui_test_plan.md b/spec/ux_ui_test_plan.md new file mode 100644 index 0000000..eee363e --- /dev/null +++ b/spec/ux_ui_test_plan.md @@ -0,0 +1,162 @@ +# Plano de Testes de UX/UI — apps/patient e apps/acs + +**Referência normativa:** `spec/ui_design.md` (linguagem visual e comportamento de UX). +**Escopo:** comportamento visual e de interação — responsividade, hierarquia, microinterações, +modo escuro, fricção de fluxo. Não duplica `spec/ux_accessibility_assessment.md` (WCAG 2.1 AA: +contraste, alvo de toque, semântica, status messages) — os dois documentos são complementares. + +`spec/ui_design.md` faz quatro afirmações transversais e três específicas por app. Cada uma vira +uma frente de teste abaixo, já cruzada com o estado atual do código (`apps/acs/lib/app/app.dart`, +`apps/patient/lib/app/app.dart`) para que a atividade comece por uma hipótese concreta, não por +uma exploração às cegas. + +--- + +## 1. Testes Transversais (Ambos os Apps) + +### 1.1 Container responsivo mobile-first, centralizado em telas largas + +> *"container responsivo que simula a tela do celular e adapta-se centralizado em computadores desktop"* + +| | | +|---|---| +| **Método** | `flutter run -d emulator-5554` redimensionando a janela (Android em modo desktop/tablet, ou `flutter run -d chrome` se aplicável) e `flutter test` com `tester.view.physicalSize` variando de 360dp a 1280dp de largura. | +| **Achado a confirmar** | Hoje só a tela de login aplica `ConstrainedBox(maxWidth: ...)` + `Center` — [patient/app.dart:91-92](apps/patient/lib/app/app.dart#L91) (420dp) e [acs/app.dart:146-147](apps/acs/lib/app/app.dart#L146) (600dp). Todas as telas pós-login do ACS passam pelo helper `_page()` ([acs/app.dart:1571](apps/acs/lib/app/app.dart#L1571)), que é só `ListView(Card(...))` sem `ConstrainedBox` nem `Center` — em janela larga, o conteúdo estica borda a borda. O mesmo vale para as telas pós-login do paciente (Triagem, Status, Perfil, Lembretes). | +| **Critério de aceite** | Decisão de produto: ou (a) confirmar que a regra vale só para o login (então corrigir `ui_design.md`, que fala em "as telas" no plural) ou (b) estender `ConstrainedBox`/`Center` para os `_page()` e para as telas pós-login do paciente, replicando a intenção descrita. | +| **Prioridade** | Alta — é a primeira frase do documento de design e hoje só ⅓ das telas a cumprem. | + +### 1.2 Dark Mode nativo (economia de bateria / fadiga visual) + +> *"Dark Mode Nativo focado em economia de bateria e redução de fadiga visual"* + +| | | +|---|---| +| **Método** | Alternar o modo claro/escuro do sistema Android (`adb shell "cmd uimode night yes/no"`) com o app aberto e confirmar visualmente e via `flutter test` que a UI **não muda**. | +| **Achado a confirmar** | Nenhum dos dois `MaterialApp` declara `themeMode`/`darkTheme` ([acs/app.dart:66](apps/acs/lib/app/app.dart#L66), [patient/app.dart:33](apps/patient/lib/app/app.dart#L33)) — só `theme: buildAcsTheme()/buildPatientTheme()`, cada um com `brightness: Brightness.dark` fixo dentro do `ThemeData`. Isso é "dark mode nativo" no sentido de único e permanente, não adaptativo ao SO. | +| **Critério de aceite** | Confirmar que essa é a intenção do produto (provável, dado o contexto clínico/campo) e documentar explicitamente em `ui_design.md` que não há modo claro — hoje o texto pode ser lido como se o app seguisse o tema do sistema, o que não acontece. | +| **Prioridade** | Baixa (comportamento correto, só falta registrar a decisão). | + +### 1.3 Estados de foco suave + +> *"estados de foco suave"* + +Já coberto tecnicamente por WCAG 2.4.7 em `spec/ux_accessibility_assessment.md` §2.4. Adicionar +aqui apenas a checagem **visual** (não semântica) com teclado físico/Bluetooth ligado ao +emulador: confirmar que o anel de foco do Material 3 (herdado, nenhum `focusColor`/`overlayColor` +customizado em `acs_theme.dart`/`patient_theme.dart`) é legível sobre `AcsColors.surfaceRaised` e +`PatientColors.surfaceRaised` — não só presente, mas com contraste suficiente contra o card escuro. +**Prioridade:** Baixa. + +### 1.4 Microinterações de clique (`active:scale`) + +> *"microinterações de clique (active:scale)"* + +| | | +|---|---| +| **Achado a confirmar** | O termo vem literalmente dos protótipos HTML — `transition-transform active:scale-95`/`active:scale-[0.98]` em `spec/ui_acs/acs_1_login.html`, `acs_3_dashboard_priorizacao.html`, `acs_4_mapa_interativo.html` etc. Os widgets Flutter reais (`FilledButton`/`OutlinedButton`) usam o **ripple** M3 nativo, não um scale-down — não há `AnimatedScale`/`GestureDetector` customizado em nenhum dos dois apps. | +| **Método** | Tocar os botões primários dos dois apps no emulador (login, pânico, confirmar recebimento, SAMU, sincronizar) e julgar se o feedback tátil percebido (ripple M3) cumpre a mesma função do `active:scale` do protótipo — resposta imediata e visível ao toque — mesmo sendo um mecanismo diferente. | +| **Critério de aceite** | Isto não é uma divergência a corrigir por padrão — ripple é a linguagem idiomática do Material/Flutter e o protótipo HTML é referência visual, não código-fonte (`CLAUDE.md`: "não são código vivo"). Registrar a equivalência intencional em `ui_design.md` para não ser lido como pendência de implementação. | +| **Prioridade** | Baixa — é uma verificação de intenção, não um teste de regressão. | + +--- + +## 2. Testes Específicos — App Paciente + +### 2.1 Baixíssima fricção no fluxo crítico + +> *"Focado em baixíssima fricção... na urgência"* + +| | | +|---|---| +| **Método** | Medir o número de toques e o tempo decorrido do app aberto até o alerta confirmado: `login (1 toque) → aba Urgência (1 toque) → botão de pânico (1 toque) → confirmar no diálogo (1 toque)`. Repetir cronometrando com `tool/live_check.dart` como baseline de rede e comparando com um cronômetro manual no emulador. | +| **Critério de aceite** | ≤ 4 toques e ≤ 10s do app aberto ao alerta confirmado em rede saudável (hoje bate: 4 toques, medido nesta sessão no emulador-5554). Definir esse número explicitamente em `spec/ui_design.md`/PRD como métrica, já que hoje é qualitativo ("baixíssima fricção") sem limiar. | +| **Prioridade** | Média — vale como guarda de regressão de fluxo, não achado de defeito. | + +### 2.2 Contraste elevado + +Já coberto por `spec/ux_accessibility_assessment.md` (WCAG 1.4.3, matriz determinística em +`apps/patient/test/contrast_tokens_test.dart`). Sem atividade nova aqui — apontar para lá. + +### 2.3 Hierarquia visual: o Botão de Emergência domina a tela + +> *"O 'Botão de Emergência' domina a hierarquia visual"* + +| | | +|---|---| +| **Método** | Captura de tela da aba Urgência no emulador e checagem por área ocupada: o círculo do `panic_button` ([patient/app.dart:327-343](apps/patient/lib/app/app.dart#L327)) mede `208x208dp`, o maior elemento tocável do app — comparar com os demais botões (`48x52dp`) e confirmar que nenhum elemento da tela (texto, ícone, card informativo) compete visualmente em tamanho ou saturação de cor. | +| **Critério de aceite** | O botão deve seguir sendo o elemento de maior área e maior saturação cromática (vermelho puro `PatientColors.danger`, sem tingir) em toda tela onde aparece — hoje conforme, confirmado nesta sessão via captura real no emulador. Vale como teste de regressão visual (golden test ou revisão manual) a cada mudança na `TriageScreen`/`UrgencyScreen`. | +| **Prioridade** | Média — proteger contra um elemento futuro (banner, notificação) que dispute a hierarquia sem intenção. | + +--- + +## 3. Testes Específicos — App ACS + +### 3.1 Cor restrita ao ranqueamento clínico determinístico (nunca decorativa) + +> *"O design restringe completamente as cores de emergência (Vermelho, Amarelo, Verde) ao ranqueamento clínico determinístico"* + +| | | +|---|---| +| **Método** | Auditoria estática: `grep` por `AcsColors.red`/`.yellow`/`.green` em `apps/acs/lib/app/app.dart` e classificar cada ocorrência como (a) ligada a `RiskLevel`/`riskLevel` vindo do servidor ou (b) decorativa/UI genérica. Repetir a cada PR que toque o arquivo. | +| **Achado a confirmar** | Todas as ocorrências atuais de `red`/`yellow`/`green` estão ligadas a `alert.riskLevel` (fila, mapa, escalonamento) — nenhuma decorativa. O único ponto de atenção é `AcsColors.green` no botão "Local alcançado" do fluxo de geofencing ([acs/app.dart:1495](apps/acs/lib/app/app.dart#L1495)) e no aviso "Local alcançado, pode registrar a visita" ([acs/app.dart:840](apps/acs/lib/app/app.dart#L840)) — não é `RiskLevel`, é status de chegada geográfica. Decidir se isso é uma violação da regra (cor clínica usada para outro sinal) ou uma exceção aceitável (verde = "ok/liberado" é convenção universal, não compete com risco na mesma tela). | +| **Critério de aceite** | Zero uso de `red`/`yellow`/`green` fora do mapeamento de `RiskLevel`, ou uma exceção documentada explicitamente em `ui_design.md` para o "verde de status operacional". | +| **Prioridade** | Média — risco de erosão silenciosa da regra a cada nova tela. | + +### 3.2 Fila dinâmica com reordenação determinística + +> *"Fila Dinâmica"* + +| | | +|---|---| +| **Método** | No emulador, com o app ACS aberto na aba Fila, disparar do app Paciente (ou via `tool/_manual_probe_create_alert.dart`) alertas de risco crescente e decrescente em sequência e observar a reordenação em tempo real sem precisar puxar para atualizar. | +| **Critério de aceite** | Já coberto por teste automatizado (`AlertQueue`, `login_flow_test.dart` — "deve ordenar por risco e, no mesmo risco, pelo mais antigo"). Esta atividade é a confirmação **visual** de que a transição de posição no `ListView` é perceptível (não um "pulo" brusco que confunda o ACS em campo) — hoje não há `AnimatedList`/transição de reordenação, é um rebuild direto. Avaliar se vale a pena uma animação de reordenação para reduzir a chance de o ACS perder de vista qual card é o novo alerta de maior risco. | +| **Prioridade** | Média — impacto direto em erro de priorização humana, não só estética. | + +### 3.3 UX do offline-first (não só a mecânica de sincronização) + +> *"Offline-first"* + +Complementa (não duplica) o teste funcional de `offline_visit_queue.dart` já coberto em +`login_flow_test.dart`. Aqui o foco é o que o ACS **vê e entende** em campo, sem sinal: + +| | | +|---|---| +| **Método** | Usar `network_chaos_simulator.dart` (ou desligar o Wi-Fi do emulador) durante o registro de uma visita e observar, sem consultar o código, se fica claro: (1) que a visita foi salva localmente, (2) que não foi enviada ainda, (3) o que fazer para enviá-la depois. | +| **Achado a confirmar** | Os elementos existem (`pending_visits_count`, banner `storage_error`, botão `sync_visits`) mas estão espalhados em pontos diferentes da tela de Visita — avaliar se a leitura em sequência (de cima para baixo) conta essa história na ordem certa para alguém sem contexto técnico. | +| **Critério de aceite** | Um ACS em campo, sem explicação prévia, consegue dizer corretamente "isso foi salvo mas não enviado, preciso sincronizar depois" só olhando a tela — validar como um cenário do UXtweak (`spec/ux_accessibility_assessment.md` §4) em vez de auditoria técnica. | +| **Prioridade** | Alta — é o risco arquitetural nº1 citado em `AGENTS.md`. | + +### 3.4 Container mais largo para acomodar dados de triagem + +> *"O container é um pouco mais largo (max-w-2xl) para acomodar dados de triagem mantendo a estrutura UI de aplicativo"* + +| | | +|---|---| +| **Método** | Mesmo método do item 1.1, mas comparando os dois valores: protótipo HTML usa `max-w-2xl` (672px, ver `spec/ui_acs/*.html`) contra os `600dp` hoje hardcoded só na tela de login do ACS ([acs/app.dart:147](apps/acs/lib/app/app.dart#L147)). | +| **Critério de aceite** | Definir se `600dp` é a tradução intencional de `max-w-2xl` (razoável, mas nunca documentada) e — como no item 1.1 — se esse limite deveria valer também para `VisitRegistrationScreen`, onde "dados de triagem" (seletor de paciente, condições crônicas, observações) de fato vivem, hoje sem nenhum limite de largura. | +| **Prioridade** | Alta — mesma causa raiz do item 1.1, mas este é o caso que `ui_design.md` cita explicitamente por nome (dados de triagem), tornando a lacuna mais visível. | + +--- + +## 4. Matriz de dispositivos/viewports recomendada + +| Classe | Exemplo | Por quê | +|---|---|---| +| Telefone compacto | 360x800dp (`emulator-5554` no perfil padrão) | Baseline mobile-first — já validado nesta sessão. | +| Telefone grande / phablet | 412x915dp | Ponto onde o card de `_page()`/telas pós-login começa a esticar sem limite (item 1.1/3.4). | +| Tablet 7-10" | 800x1280dp, 1280x800dp (landscape) | Cenário onde a ausência de `ConstrainedBox` fora do login fica mais evidente — provável uso real de um ACS com tablet institucional. | +| Desktop (debug/dev) | `flutter run -d linux`/Chrome, janela redimensionável | Só para os itens 1.1/3.4; não é alvo de produção segundo `spec/stack.md`, mas é o ambiente mais rápido para iterar o teste de container. | + +## 5. Ferramentas e ambiente + +- **Emulador real** (`emulator-5554`, já configurado): itens 1.2, 2.1, 2.3, 3.2, 3.3 — qualquer + teste que dependa de percepção visual, tempo ou feedback tátil precisa do dispositivo real, não + de `flutter test`. +- **`flutter test` + `tester.view.physicalSize`**: itens 1.1 e 3.4 — regressão de container é + determinística e não precisa de emulador uma vez definido o critério de aceite. +- **`scripts/qa/e2e.sh --emulator`**: baseline de que nenhuma mudança de UX quebra o fluxo real + contra backend/broker — rodar antes e depois de qualquer alteração motivada por este plano. +- **`network_chaos_simulator.dart`**: item 3.3. +- **UXtweak** (já planejado em `spec/ux_accessibility_assessment.md` §4): estender o Cenário 2 + (priorização na fila) para cobrir também o item 3.3 (entendimento do estado offline), em vez de + abrir uma frente de pesquisa nova. From 22709071b0afab0c69620f35b8388ffffd5be90b Mon Sep 17 00:00:00 2001 From: Herbert Rocha Date: Tue, 15 Sep 2026 12:05:07 -0400 Subject: [PATCH 39/43] docs: atualizar CLAUDE.md e AGENTS.md com o estado real do projeto MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- AGENTS.md | 8 +++++++- CLAUDE.md | 13 +++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index b15eb2d..bf8ffc8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,6 +23,9 @@ Antes de mexer em produto, arquitetura ou comportamento, consulte primeiro: - [spec/stack.md](spec/stack.md) — decisões de stack e infraestrutura; - [spec/ui_design.md](spec/ui_design.md) — linguagem visual e UX; - [spec/lgpd_design.md](spec/lgpd_design.md) — privacidade e LGPD; +- [spec/lgpd_data_audit.md](spec/lgpd_data_audit.md) — classificação de sensibilidade LGPD, campo a campo, de todas as tabelas persistidas; +- [spec/ux_accessibility_assessment.md](spec/ux_accessibility_assessment.md) — auditoria WCAG 2.2 AA (contraste, alvo de toque, semântica) dos apps ACS e paciente; +- [spec/ux_ui_test_plan.md](spec/ux_ui_test_plan.md) — plano de testes de UX/UI derivado de `spec/ui_design.md`; - [CLAUDE.md](CLAUDE.md) — guia técnico e comandos para IA; é a referência mais atualizada do repositório; - [PROGRESS.md](PROGRESS.md) — status dos milestones e histórico de migração; - [backend/](backend) — workspace Dart com `sinalacs_server` e `sinalacs_client`; @@ -58,6 +61,7 @@ Não violar estes pontos sob qualquer hipótese: - O app ACS é o mais crítico em termos de offline-first e sincronização: guarda visitas locais, tenta sincronizar em fila e trata conflitos sem perder registros. - O tema visual é dark mode, com foco em legibilidade e uso de cor restrito a sinal clínico (vermelho, amarelo, verde). - O app do paciente não deve reintroduzir regras de risco no cliente; a classificação deve vir do backend via `triage.evaluate`. +- [apps/admin](apps/admin) (`sinalacs_admin`) é o backoffice administrativo — deixou de ser um esqueleto de pubspec e hoje é um app navegável real, com 4 telas somente leitura (Indicadores, Microáreas, Alertas, Auditoria) atrás de `AdminHomeShell`. Ainda não consome `sinalacs_client`: usa a interface `AdminDataSource`, hoje implementada só por `MockAdminDataSource`, seguindo o mesmo padrão de DI de `PatientBackend`/`AcsBackend`. O login é local e não chama `auth.developmentLogin` (o backend só aceita `role: 'patient'`/`role: 'acs'` hoje). Toda tela que exibe dado sensível registra o próprio acesso via `recordAccess()` antes de renderizar, por exigência de auditoria do PRD §4.2.2. É o único dos três apps com suporte a Flutter Web. ### Infraestrutura local - O ambiente de desenvolvimento usa Docker Compose com PostgreSQL, Mosquitto, backend e Traefik. @@ -92,6 +96,7 @@ Não violar estes pontos sob qualquer hipótese: - Manter dark mode, alta legibilidade e baixo ruído visual. - Usar cores apenas para sinal clínico; não decorar interfaces com vermelho/amarelo/verde sem relação com risco. - Manter foco em mobile-first e acessibilidade. +- Ao reaproveitar uma cor clínica de preenchimento (`red`/`accent`/`danger`) como cor de texto/ícone, usar a variante `*OnSurface` (`acs_theme.dart`/`patient_theme.dart`) e medir contraste contra a superfície real (`Card`/`surfaceRaised`), não contra o fundo do Scaffold — ver `spec/ux_accessibility_assessment.md` e os testes em `test/contrast_tokens_test.dart` de cada app. ### 6) Quando o trabalho for de backend ou dados - Considerar uso de SQLite/SQLCipher e filas locais para operação offline. @@ -122,12 +127,13 @@ Rodar análise do Flutter nos apps: ```bash cd apps/patient && flutter pub get && flutter analyze && flutter test cd apps/acs && flutter pub get && flutter analyze && flutter test +cd apps/admin && flutter pub get && flutter analyze && flutter test ``` Importante: - `flutter test` é hermético e não substitui validações com stack local real; - os testes de integração e validação de conexão vivem fora do `flutter test` e utilizam a stack Docker/VM; -- o CI do projeto valida jobs separados para backend e apps. +- o CI do projeto valida cinco jobs separados: `serverpod-backend`, `backend-docker-build`, `patient-app`, `acs-app` e `admin-app`. ## Observações finais diff --git a/CLAUDE.md b/CLAUDE.md index d48353b..480d8d2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,6 +17,9 @@ Read these before making product/architecture decisions — when project docs co - [spec/stack.md](spec/stack.md) — stack/infra architecture decisions. - [spec/ui_design.md](spec/ui_design.md) — visual language and UX behavior. - [spec/lgpd_design.md](spec/lgpd_design.md) — privacy/LGPD design. +- [spec/lgpd_data_audit.md](spec/lgpd_data_audit.md) — field-by-field LGPD sensitivity classification for every persisted table (11 domain tables plus Serverpod's own). +- [spec/ux_accessibility_assessment.md](spec/ux_accessibility_assessment.md) — WCAG 2.2 AA audit (contrast, touch targets, semantics) for the ACS/patient apps; read before touching any color used as text/icon, not just fill. +- [spec/ux_ui_test_plan.md](spec/ux_ui_test_plan.md) — UX/UI test plan derived from `spec/ui_design.md` (visual/interaction behavior, complementary to the accessibility assessment). - [AGENTS.md](AGENTS.md) — full agent working rules (Portuguese), summarized below. ## Business/security invariants (do not violate) @@ -112,7 +115,7 @@ flutter build apk --debug # debug APK, validated with compileSdk/targetSdk 36 ``` For the ACS app, run `./scripts/dev/run_acs.sh` (or `--build` for the APK) instead of bare `flutter run`: `SINALACS_MQTT_PASSWORD` is a compile-time constant with **no default**, and the broker's password is generated per machine by `bootstrap_env.sh`. The script reads `.env`, runs `sync_dev_ca.sh` (the CA is a gitignored asset the build requires) and passes all four defines via `--dart-define-from-file` (a temp file it creates and deletes, so the password never sits in `flutter`'s argv). `apps/acs/android/app/build.gradle.kts` makes a bare `flutter build apk` **fail** with the right command instead of silently producing an APK that never connects; the escape hatch for a deliberately-passwordless build (e.g. to see the "compiled without the password" banner) is `-Psinalacs.allowMissingMqttPassword=true`. -`apps/admin` exists only as a pubspec skeleton (backoffice), no implementation yet. +`apps/admin` (package `sinalacs_admin`) is a real backoffice app now, not a skeleton — see Architecture below. Same commands apply (`cd apps/admin && flutter pub get && flutter analyze && flutter test`); it's also the only one of the three with Flutter Web enabled (`flutter build web` works). ### Validating the real connection to the backend @@ -130,7 +133,7 @@ The script brings up Docker Compose, waits for the healthcheck, applies the seed Debug builds of both apps carry `android/app/src/debug/res/xml/network_security_config.xml`, which permits cleartext only to `10.0.2.2` and loopback; release manifests are untouched. -CI (`.github/workflows/ci.yml`) runs four parallel jobs on push/PR to main: `serverpod-backend` (spins up the Postgres the test harness expects on port 9090, then `dart analyze` and the full 25-test suite), `backend-docker-build` (builds `backend/sinalacs_server/Dockerfile` to catch build breakage before deploy), `patient-app`, `acs-app` (each `flutter analyze && flutter test`, Flutter 3.44.8). Mirror this locally before pushing. +CI (`.github/workflows/ci.yml`) runs five parallel jobs on push/PR to main: `serverpod-backend` (spins up the Postgres the test harness expects on port 9090, then `dart analyze` and the full 25-test suite), `backend-docker-build` (builds `backend/sinalacs_server/Dockerfile` to catch build breakage before deploy), `patient-app`, `acs-app`, `admin-app` (each `flutter analyze && flutter test`, Flutter 3.44.8). Mirror this locally before pushing. ## Architecture @@ -165,6 +168,11 @@ Risk classification now comes **only** from `triage.evaluate`: the patient app's Color is a clinical signal only in these apps: red/yellow/green map strictly to `RiskLevel`, never used decoratively. +**WCAG contrast tokens**: `app/acs_theme.dart` and `app/patient_theme.dart` each carry a text-safe variant of every clinical fill color — `redOnSurface`/`accentOnSurface` (ACS) and `dangerOnSurface`/`accentOnSurface` (patient). `red`/`accent`/`danger` only clear WCAG 1.4.3's 4.5:1 as a *fill* (e.g. white text on a red button); reused as *text* color on `surfaceRaised`/`Card` — where risk/status text is actually rendered — they drop to ~3:1. `acsOnSurface()` (and its patient-app equivalent) converts fill → text color at the single point where a `switch (riskLevel)` used to feed both, instead of at every call site. `test/contrast_tokens_test.dart` in each app checks the full token×surface matrix by computing relative luminance directly (`test/support/contrast.dart`) rather than by eyeballing a contrast checker against the wrong surface — which is exactly the false positive/negative `spec/ux_accessibility_assessment.md` documents from the original manual audit. The same pass added `minimumSize` (48×52dp, 64×60dp for the SAMU emergency call) per WCAG 2.5.5 and `Semantics(liveRegion: true)` on dynamic status messages (login errors, feed/storage errors, rejected-visit counts) per WCAG 4.1.3. + +### Admin app (`apps/admin/`) +Package `sinalacs_admin`, read-only backoffice. Same skeleton as ACS/patient: `lib/main.dart` → `lib/app/app.dart` (+ `admin_theme.dart`) → `lib/core/data/`. Unlike ACS/patient it does not consume `sinalacs_client` yet: `AdminDataSource` (`lib/core/data/admin_data_source.dart`) is an abstract interface whose models (`RiskLevel`, `AlertStatus`, `DashboardIndicators`, `MicroAreaSummary`, `AlertSummary`, `AuditLogEntry`) mirror the backend's `.spy.yaml` models field-for-field, backed today only by `MockAdminDataSource` — swapping in a real implementation over `sinalacs_client` shouldn't require reshaping the screens, the same DI pattern as `PatientBackend`/`AcsBackend`. `AdminHomeShell` exposes four read-only sections: **Indicadores** (risk counters + TMRAV, the PRD's North Star metric), **Microáreas** (ACS↔microarea binding), **Alertas** (filterable by micro-area/status, with filter options sourced from the fetched data itself rather than a hardcoded list — a hardcoded list would silently drop a real micro-area as a selectable option) and **Auditoria** (an `audit_logs` viewer). Every screen touching sensitive data calls `dataSource.recordAccess(actionType: 'view', resourceType: ...)` *before* rendering it, per `spec/PRD_system.md` §4.2.2 (admin access must itself be audited) — the audit log screen audits its own access too, and a failed `recordAccess` blocks the data from rendering rather than failing open. Login is intentionally local-only, **not** wired to `auth.developmentLogin`: the backend's `auth_endpoint.dart` only accepts `role: 'patient'`/`role: 'acs'`, there is no fixed dev user for `admin`, so real auth is out of scope until the backend supports it (see the `LoginScreen` doc comment in `app.dart`). The "ambiente de desenvolvimento" banner is informational, not access control — the actual gate is `devLoginEnabled`, which defaults to `kDebugMode` and disables the login bypass outside debug builds. Layout is desktop-first per `spec/PRD_system.md` §2.1: `NavigationRail` at ≥640px width, `NavigationBar` below, same `ThemeData` either way. `apps/admin` is the only one of the three apps with Flutter Web enabled and the only one versioning `pubspec.lock` from the start (the other two adopted that convention later). `docs/telas-admin.md` documents its five screens with real screenshots, mirroring `docs/telas-acs.md`. CI covers it in the `admin-app` job, identical in shape to `acs-app`/`patient-app`. + ### `spec/` Product/architecture source of truth (PRD, UX flows, LGPD design, stack decisions) plus static HTML prototypes under `spec/ui_acs/` and `spec/ui_paciente/` — these are the visual reference for building out the real Flutter screens, not live code. @@ -173,6 +181,7 @@ Product/architecture source of truth (PRD, UX flows, LGPD design, stack decision - Prefer simple, predictable solutions aligned with the stack already chosen (Flutter + Dart backend + Postgres + MQTT) over introducing new frameworks/services not in `spec/stack.md`. - Keep triage/prioritization logic deterministic and consistent with the Manchester Protocol model referenced in the PRD — do not make risk classification probabilistic or user-overridable. - When touching sync behavior (backend `SyncFsm` or the ACS `offline_visit_queue.dart`), preserve retry/queue/conflict semantics — offline-first correctness is the primary architectural risk called out in `AGENTS.md`. +- When reusing a clinical fill color (`red`/`accent`/`danger`/`yellow`/`green`) as text or icon color in the Flutter apps, use the `*OnSurface` token and measure contrast against the surface it actually renders on (commonly `Card`/`surfaceRaised`), not the Scaffold background — see the WCAG contrast tokens note above and `spec/ux_accessibility_assessment.md`. - Never commit real patient data, credentials, or the dev Docker Compose secrets into anything beyond local development. ## graphify From c0ee03d7a2aa6e3957fcf3b84eaf6eb8437a3e0d Mon Sep 17 00:00:00 2001 From: Rosialdo <79479462+Rosialdo@users.noreply.github.com> Date: Tue, 15 Sep 2026 17:29:50 -0400 Subject: [PATCH 40/43] create and documentation of security assessment --- spec/security_assessment.md | 330 ++++++++++++++++++++++++++++++++++++ 1 file changed, 330 insertions(+) create mode 100644 spec/security_assessment.md diff --git a/spec/security_assessment.md b/spec/security_assessment.md new file mode 100644 index 0000000..67779aa --- /dev/null +++ b/spec/security_assessment.md @@ -0,0 +1,330 @@ +# Análise de Cibersegurança — SinalACS + +> Referência: NIST Cybersecurity Framework (CSF) 2.0 como estrutura principal, +> complementado por controles do NIST SP 800-53 Rev. 5 onde fez sentido +> detalhar. Ver [issue #1](../../../issues/1). + +## 1. Contexto e metodologia + +Esta análise é uma revisão estática (leitura de código, configuração e +documentação — sem testes de penetração ativos) do estado do repositório na +revisão avaliada. Cobre: + +- `backend/sinalacs_server` (Serverpod/Dart): endpoints, autenticação, + autorização por microárea, MQTT, banco, auditoria, configuração. +- `apps/acs`, `apps/patient`, `apps/admin` (Flutter): apenas superficialmente, + focando em como consomem a autenticação do backend. +- `infra/docker`, `backend/DEPLOY.md`: topologia do piloto free-tier. +- `spec/lgpd_design.md`, `spec/PRD_system.md`, `README.md`, + `.github/SECURITY.md`: requisitos já assumidos pelo projeto, para checar + aderência técnica. + +**Nota importante sobre desatualização da issue**: a issue #1 descreve um +servidor `dart:io` cru (`backend/bin/server.dart`, `postgres_alert_store.dart`) +que já não existe nesta revisão — o backend migrou para o framework +**Serverpod**. Vários pontos levantados na issue já foram endereçados nessa +migração (autenticação JWT HMAC com verificação por endpoint, autorização por +microárea reforçada no banco, MQTT com ACK e outbox transacional, segredos com +fail-closed fora de `development`). Isso é registrado explicitamente abaixo em +cada achado correspondente, para não reabrir como novo o que já foi corrigido +— e para focar o esforço no que **de fato** continua em aberto. + +Severidade: **Crítica / Alta / Média / Baixa / Informativa**, considerando que +o projeto é um piloto acadêmico sem dados reais de pacientes (conforme +`README.md` e `.github/SECURITY.md`), não um sistema em produção com dados +sensíveis reais. + +## 2. Sumário executivo + +| ID | Achado | Função CSF | Severidade | Status | +|----|--------|-----------|------------|--------| +| F1 | Dev-login como único mecanismo de autenticação | PROTECT (PR.AA) | Alta | Conhecido/documentado, sem mitigação | +| F2 | ACL do broker MQTT no piloto não segrega por microárea | PROTECT (PR.DS/PR.AA) | Alta | Regressão dev→piloto, não documentada como risco | +| F3 | TLS do MQTT não é obrigatório por ambiente | PROTECT (PR.DS) | Média | Gap de "fail closed" | +| F4 | Autorização por endpoint é manual, não há enforcement central | PROTECT (PR.AA) | Média/Alta | Débito de arquitetura | +| F5 | Ausência de MFA para ACS | PROTECT (PR.AA) | Média | Gap frente a LGPD-RF11 | +| F6 | Sem rate limiting / anti-automação no `developmentLogin` | PROTECT (PR.AA) | Média | Gap | +| F7 | Gestão de segredos correta em código, mas sem cofre/rotação | PROTECT (PR.DS) | Baixa/Média | Parcialmente mitigado | +| F8 | Sem correlação/alerta de eventos de segurança (SIEM) | DETECT (DE.CM) | Média | Gap | +| F9 | Sem plano de resposta a incidentes / SLA ANPD | RESPOND (RS.MA/RS.CO) | Alta (para LGPD) | Gap | +| F10 | Sem scanning automatizado de dependências | IDENTIFY (ID.RA) | Média | Gap | +| F11 | Drift entre `README.md` e o estado real de segurança | GOVERN (GV.OC) | Informativa | Documentação desatualizada | +| F12 | Sem plano de recuperação/backup testado para o piloto | RECOVER (RC.RP) | Baixa | Gap (aceitável em piloto) | + +## 3. Achados detalhados por função do NIST CSF 2.0 + +### GOVERN (GV) + +**F11 — Documentação de postura de segurança desatualizada (Informativa)** +*Categoria:* GV.OC (Contexto organizacional) · *SP 800-53:* PL-2 + +`README.md`, seção "Segurança e escopo", ainda afirma que "as garantias de +autenticação, autorização por microárea e entrega MQTT com ACK permanecem +pendentes de integração real". Isso não reflete mais o código: `auth_endpoint.dart` +emite JWT HMAC verificado em cada chamada, `orm_alert_store.dart` reforça o +filtro de microárea no predicado SQL (`t.microAreaId.equals(microAreaUuid)`, +linha 109), e `mqtt_alert_dispatcher.dart` + `alert_outbox_dispatcher.dart` +implementam entrega com ACK e retry. + +Documentação desatualizada nos dois sentidos é um risco: subestimar a +maturidade atrasa a priorização correta (revisores podem redescobrir o que já +existe); superestimar levaria a assumir garantias que não existem — o que não +é o caso aqui, mas vale manter o README como fonte de verdade viva. + +*Recomendação:* atualizar a seção para descrever com precisão o que está +implementado (dev-login JWT + autorização por microárea reforçada no banco) +versus o que continua pendente (autenticação institucional real, MFA, ACL de +microárea no broker gerenciado — ver F1, F2, F5). + +**Governança geral (sem achado numerado)**: não há política de segurança +formal além de `.github/SECURITY.md` (que é boa, mas é uma política de +*divulgação de vulnerabilidades*, não de governança de risco). Para o escopo +de um TCC/piloto, isso é proporcional; não recomendamos um programa de GRC +completo, apenas registrar como item de "aceite de risco" formal antes de +qualquer uso com dados reais, dado que `README.md` e `.github/SECURITY.md` já +proíbem dados reais no piloto. + +### IDENTIFY (ID) + +**F10 — Ausência de scanning automatizado de dependências (Média)** +*Categoria:* ID.RA-01 (vulnerabilidades identificadas e registradas) · *SP 800-53:* RA-5, SA-11 + +`.github/workflows/ci.yml` roda `dart analyze` e os testes, mas não há +`dependabot.yml`, nem passo de `dart pub outdated`/OSV-Scanner/`govulncheck` +equivalente para o ecossistema Dart, nem verificação de dependências dos apps +Flutter (`apps/acs`, `apps/patient`, `apps/admin`) e dos módulos Node +(`video/remotion`). `backend/sinalacs_server/pubspec.lock` fixa `serverpod: +3.4.13`, `mqtt_client: ^10.0.0`, `crypto: ^3.0.3`, `uuid: ^4.5.0` — não há como +avaliar CVEs conhecidos sem acesso à base do pub.dev/OSV no momento desta +análise; o ponto não é que exista uma vulnerabilidade confirmada, é a +ausência de qualquer processo contínuo para detectá-la. + +*Recomendação:* habilitar Dependabot (ou Renovate) para `pubspec.yaml` dos +quatro pacotes Dart e para `video/remotion/package.json`; adicionar um job de +CI que rode `dart pub outdated --mode=null-safety` (ou equivalente) e falhe em +vulnerabilidades conhecidas de severidade alta/crítica. + +### PROTECT (PR) + +**F1 — Dev-login como único mecanismo de autenticação (Alta)** +*Categoria:* PR.AA-01/03 (identidades e credenciais geridas) · *SP 800-53:* IA-2, IA-5 + +`auth_endpoint.dart` expõe `developmentLogin(role)`, que emite um token válido +sem senha real para dois UUIDs fixos de seed. É corretamente gateado por +`ENABLE_DEV_LOGIN` (retorna `EndpointDisabledException` como 404, não 403, +para não revelar a rota — boa prática de "fail closed sem vazar existência"). +Mas, conforme `backend/DEPLOY.md` ("Limitações conhecidas deste piloto"), esse +é **o único mecanismo de autenticação existente**: "qualquer pessoa com a URL +pode se autenticar como paciente ou ACS". Isso já é conhecido e documentado +pelo projeto — o achado aqui é formalizar a severidade e um caminho de saída. + +*Recomendação:* definir e priorizar a integração com identidade institucional +real (ex.: e-SUS APS/CNS para profissionais de saúde, ou um IdP simples com +OAuth2/OIDC para o escopo do TCC) antes de qualquer uso fora do ambiente de +demonstração controlada. Enquanto isso persistir, manter `ENABLE_DEV_LOGIN` +desligado por padrão em qualquer ambiente exposto publicamente (verificar +`backend/DEPLOY.md` para confirmar que a variável não está setada como +`true` no Render). + +**F2 — ACL do broker MQTT no piloto não segrega por microárea (Alta)** +*Categoria:* PR.AA-05 / PR.DS-02 (confidencialidade em trânsito, menor +privilégio) · *SP 800-53:* AC-3, AC-6, SC-8 + +Comparando `infra/docker/mosquitto/aclfile` (ambiente local) com +`backend/DEPLOY.md`: localmente, o Mosquitto aplica ACL por tópico — o usuário +`acs-area-12` só pode ler +`sinalacs/v1/microareas/00000000-.../alerts` da própria microárea. Já no +caminho de deploy documentado (`backend/DEPLOY.md`, seção "Limitações +conhecidas deste piloto"), o broker gerenciado free-tier (HiveMQ Cloud) usa +"usuário/senha único" — sem ACL dinâmica por microárea. Isso é uma +**regressão de confidencialidade entre desenvolvimento e o piloto real**: +qualquer cliente com a credencial única do broker pode assinar +`sinalacs/v1/#` e ver `patientId`, `microAreaId` e `locationHash` de alertas +de **todas** as microáreas, não só a própria — mesmo que o backend e o app do +ACS façam a coisa certa. A invariante de territorialização (INV-01), reforçada +corretamente no banco (`orm_alert_store.dart:106-112`), não se estende ao +transporte MQTT em produção. + +*Recomendação:* no curto prazo, documentar explicitamente esse risco residual +em `backend/DEPLOY.md` (hoje ele lista a limitação técnica, mas não o impacto +de confidencialidade que decorre dela). No médio prazo, avaliar um broker +gerenciado com ACL por tópico no tier gratuito (ex.: EMQX Cloud, já citado como +alternativa na mesma tabela do `DEPLOY.md`) ou aplicar criptografia de +payload por microárea antes de publicar, para que a credencial única do +broker não seja suficiente para ler o conteúdo. + +**F3 — TLS do MQTT não é obrigatório por ambiente (Média)** +*Categoria:* PR.DS-02 (dados em trânsito) · *SP 800-53:* SC-8, SC-13 + +`AppConfig.fromMap` (`app_config.dart:84`) lê `mqttUseTls` de +`MQTT_USE_TLS == 'true'`, com default `false`, e não há validação equivalente +à de `JWT_SECRET`/`AUDIT_CHAIN_SECRET` que recuse subir em `APP_ENV=production` +sem essa flag ligada. Ou seja: o padrão de "falhar no boot em vez de na +primeira requisição" — que o próprio time já aplicou corretamente aos +segredos (`_resolveSecret`, `app_config.dart:101-126`) — não foi estendido ao +transporte MQTT. Um operador que esqueça `MQTT_USE_TLS=true` em produção não +recebe erro nenhum; o tráfego (incluindo `locationHash` e IDs de pacientes) +simplesmente vai em texto claro. + +*Recomendação:* aplicar a mesma política de fail-closed: `AppConfig` deve +recusar subir com `appEnv != 'development'` e `mqttUseTls == false`, no mesmo +padrão de `_resolveSecret`. + +**F4 — Autorização por endpoint é manual, sem enforcement central (Média/Alta)** +*Categoria:* PR.AA-05 (least privilege / segurança por padrão) · *SP 800-53:* AC-3, AC-6 + +Todos os endpoints (`alerts_endpoint.dart`, `patients_endpoint.dart`, +`visits_endpoint.dart`, `triage_endpoint.dart`, `health_endpoint.dart`) +declaram `requireLogin => false` — abrindo mão do mecanismo de sessão nativo +do Serverpod — e cada método chama manualmente +`AlertRuntime.instance.auth.verifyToken(accessToken)`. Isso funciona hoje +porque todo endpoint que toca dado sensível lembra de chamar `_authenticate`. +Mas é um padrão "seguro por convenção", não "seguro por padrão": nada no +framework impede que um endpoint futuro que manipule dados de saúde seja +adicionado sem essa chamada — ele simplesmente ficaria aberto, e nenhum teste +de tipo pegaria isso automaticamente (só revisão humana ou um teste dedicado). +`triage_endpoint.dart` já é hoje um exemplo de endpoint sem autenticação — +aparentemente intencional (a classificação é determinística e sem dado do +paciente), mas isso reforça que a decisão de "quem precisa de token" está +espalhada, não centralizada. + +*Recomendação:* extrair a verificação de token para um método de classe-base +ou um `Endpoint` intermediário (`AuthenticatedEndpoint extends Endpoint`) que +todo endpoint sensível estenda, de forma que esquecer a chamada vire erro de +compilação/design óbvio, não uma omissão silenciosa. Adicionar um teste que +enumere os endpoints e falhe se um novo endpoint com acesso a +`Session.db`/dados de paciente não herdar dessa base. + +**F5 — Ausência de MFA para ACS (Média)** +*Categoria:* PR.AA-01 · *SP 800-53:* IA-2(1) + +`spec/lgpd_design.md` (LGPD-RF11) já define como critério de aceite "MFA para +ACS", mas a única autenticação existente (F1) não implementa nenhum segundo +fator. Enquanto o dev-login for o mecanismo de autenticação, MFA não é +aplicável tecnicamente; o achado é para não perder o requisito de vista +quando a autenticação institucional (F1) for desenhada. + +*Recomendação:* incluir MFA no desenho da autenticação institucional futura +desde já, em vez de tratá-lo como incremento posterior. + +**F6 — Sem rate limiting no `developmentLogin` (Média)** +*Categoria:* PR.AA-01 · *SP 800-53:* AC-7, SC-5 + +`AuthEndpoint.developmentLogin` não tem limite de tentativas nem +throttling — hoje isso importa pouco porque não há senha para forçar (o +`role` só aceita `patient`/`acs`), mas o padrão importa: quando a +autenticação real (F1) substituir isso, o mesmo endpoint (ou seu sucessor) +precisa nascer com rate limiting, para não repetir a lacuna. + +*Recomendação:* tratar como pré-requisito de design da autenticação +institucional, não como item avulso. + +**F7 — Gestão de segredos correta em código, sem cofre/rotação (Baixa/Média)** +*Categoria:* PR.DS-01 · *SP 800-53:* SC-12, SC-28 + +Ponto positivo a registrar: `AppConfig._resolveSecret` +(`app_config.dart:101-126`) já faz o que a maioria dos projetos deste porte +não faz — recusa subir em `staging`/`production` sem `JWT_SECRET`/ +`AUDIT_CHAIN_SECRET` próprios, e recusa mesmo que alguém copie o valor de +desenvolvimento para outro ambiente por engano. O `.gitignore` protege +`config/passwords.yaml`, e o CI corrigiu recentemente +(`ci.yml`, comentário nas primeiras linhas do job) uma senha de teste que +estava compartilhada com o arquivo local gitignorado. O que falta é só +maturidade operacional: os segredos hoje vivem como variáveis de ambiente +simples no Render, sem cofre dedicado (Vault, AWS/GCP Secret Manager) nem +rotação programada. + +*Recomendação:* aceitável para o estágio de piloto; registrar como item de +hardening antes de produção real, não como bloqueador agora. + +### DETECT (DE) + +**F8 — Sem correlação/alerta de eventos de segurança (Média)** +*Categoria:* DE.CM-01/03 (monitoramento contínuo) · *SP 800-53:* AU-6, AU-12, SI-4 + +A issue original afirmava que "o backend só usa `print`" — isso está +desatualizado: a migração para Serverpod trouxe `sessionLogs` estruturados +(`config/*.yaml`, `consoleLogFormat: json` em produção) e um serviço de +auditoria dedicado, `AuditTrail` (`audit_trail.dart`), que grava eventos +`granted`/`denied_territory` de acesso a dado sensível numa trilha +append-only com cadeia de hash (`auditChainSecret`) — desenhada +especificamente para atender "logs de acesso com quem, quando e quais dados" +(LGPD-RF11) e o alerta de tentativa de acesso fora da microárea. Isso é uma +base sólida que a issue não previa. + +O que continua faltando é a camada seguinte: não há nada que **leia** essa +trilha e gere alerta ativo (ex.: N tentativas de `denied_territory` do mesmo +usuário em M minutos), nem centralização fora do host do Render (que +hiberna e não retém logs entre reinícios de forma confiável, conforme +`backend/DEPLOY.md`). Falhas de auditoria também só vão para +`stderr.writeln` (`audit_trail.dart:58`), sem alerta. + +*Recomendação:* para o escopo do piloto, o mínimo viável é exportar +`sessionLogs` e a tabela `audit_logs` para um destino persistente fora do +Render (mesmo que seja um job periódico simples), e alertar manualmente sobre +`denied_territory` como parte da rotina do responsável técnico enquanto não +houver orçamento para uma ferramenta de SIEM. + +### RESPOND (RS) + +**F9 — Sem plano de resposta a incidentes / SLA de notificação (Alta para conformidade LGPD)** +*Categoria:* RS.MA-01, RS.CO-02 · *SP 800-53:* IR-1, IR-4, IR-6 + +`.github/SECURITY.md` cobre bem a **divulgação coordenada de +vulnerabilidades** (relato privado, prazo de confirmação em 5 dias úteis, +divulgação em até 90 dias) — mas o próprio documento é explícito sobre seu +limite: "não substitui... resposta a incidentes. Um evento real que envolva +dados pessoais ou de saúde pode exigir medidas adicionais". `spec/lgpd_design.md` +(LGPD-RF12) já formaliza a obrigação legal: notificação à ANPD e aos +titulares em até 72h (legal) / 48h (recomendado), com "capacidade de +identificar titulares afetados em até 24 horas" — nenhum desses processos +está documentado ou testado. + +*Recomendação:* redigir um runbook curto de resposta a incidentes +(quem aciona, como isolar o `ENABLE_DEV_LOGIN`/rotacionar segredos em +minutos, como consultar `audit_logs` para escopo do incidente, template de +comunicação à ANPD/titulares) — não precisa ser extenso dado o estágio do +projeto, mas precisa existir antes de qualquer piloto com usuários reais, +mesmo que sintéticos por enquanto. + +### RECOVER (RC) + +**F12 — Sem plano de recuperação/backup testado (Baixa)** +*Categoria:* RC.RP-01 · *SP 800-53:* CP-9, CP-10 + +`backend/DEPLOY.md` documenta bem as limitações de disponibilidade do piloto +free-tier (hibernação do Render, autosuspend do Neon), mas não há menção a +backup do Postgres (Neon free tier) nem a um teste de restauração. Para um +piloto sem dados reais, a severidade é baixa; registrar como item a resolver +antes de qualquer dado real trafegar pelo sistema. + +*Recomendação:* documentar a política de retenção/backup do Neon (mesmo que +seja "o free tier não garante backup, logo nenhum dado real deve trafegar +aqui" — o que já é consistente com o restante da política do projeto). + +## 4. Priorização recomendada + +1. **Antes de qualquer dado real (bloqueadores)**: F1 (autenticação + institucional), F2 (ACL do broker MQTT no piloto), F9 (runbook de + incidentes com SLA de notificação). +2. **Curto prazo, baixo custo de implementação**: F3 (fail-closed de TLS no + MQTT), F4 (endpoint base com autenticação obrigatória), F11 (atualizar + README). +3. **Médio prazo**: F5 (MFA), F6 (rate limiting), F10 (scanning de + dependências no CI). +4. **Hardening contínuo, sem bloquear o piloto**: F7 (cofre de segredos), F8 + (alerta ativo sobre a trilha de auditoria), F12 (backup/restore). + +## 5. Referências + +- [NIST Cybersecurity Framework 2.0](https://www.nist.gov/cyberframework) +- [NIST SP 800-53 Rev. 5](https://csrc.nist.gov/pubs/sp/800/53/r5/upd1/final) +- [`CLAUDE.md`](../CLAUDE.md) — invariantes de negócio/segurança (INV-01, + INV-02, INV-03) +- [`spec/lgpd_design.md`](lgpd_design.md) — LGPD-RF09, RF11, RF12, RF13 +- [`spec/PRD_system.md`](PRD_system.md) — seção 6.2 (M3.2 observabilidade, + M3.4 LGPD Compliance) +- [`backend/DEPLOY.md`](../backend/DEPLOY.md) — limitações conhecidas do + piloto free-tier +- [`.github/SECURITY.md`](../.github/SECURITY.md) — política de divulgação + de vulnerabilidades From cb9d5bf27412b152ab78cd6c0e9061d319b14950 Mon Sep 17 00:00:00 2001 From: Rosialdo <79479462+Rosialdo@users.noreply.github.com> Date: Tue, 15 Sep 2026 18:17:36 -0400 Subject: [PATCH 41/43] Update assessment --- spec/security_assessment.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/security_assessment.md b/spec/security_assessment.md index 67779aa..3bd13b5 100644 --- a/spec/security_assessment.md +++ b/spec/security_assessment.md @@ -4,6 +4,8 @@ > complementado por controles do NIST SP 800-53 Rev. 5 onde fez sentido > detalhar. Ver [issue #1](../../../issues/1). +**Relatório Baseado no código disponível no dia 15/09/2026** + ## 1. Contexto e metodologia Esta análise é uma revisão estática (leitura de código, configuração e From 351542675439d2b9ddea45109c035b04ed640581 Mon Sep 17 00:00:00 2001 From: Herbert Rocha Date: Tue, 15 Sep 2026 21:42:23 -0400 Subject: [PATCH 42/43] feat(admin): adicionar suporte Android e layout compacto ao backoffice MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit apps/admin era web-only: a raiz do app tinha apenas lib/, test/ e web/, e .metadata registrava só as plataformas root e web. Não havia android/ — ou seja, era impossível rodar o backoffice em qualquer dispositivo, e `flutter run -d emulator-5554` falhava por ausência de alvo. A responsividade existia pela metade: um único LayoutBuilder trocava NavigationRail por NavigationBar no literal 640, mas nenhuma tela de conteúdo tinha sido pensada para 360dp. O backoffice continua desktop-first (spec/PRD_system.md §2.1). Mobile é complemento, nunca substituição: o layout com rail segue sendo o padrão, e a prova de que ele não regrediu é que os dez testes de widget que já existiam passam sem uma linha de edição. Plataforma Android - Criada com `flutter create --platforms=android,web --empty .`. As duas flags são obrigatórias e o motivo está registrado aqui porque não é óbvio: create_base.dart reescreve o .metadata do zero com um MigrateConfig() vazio, então `--platforms=android` puro apaga a entrada `- platform: web`; e sem `--empty` o template gera um test/widget_test.dart chamando MyApp(), classe que não existe neste pacote, quebrando analyze, test e o job admin-app do CI. - applicationId e namespace br.com.prismrr.sinalacs.admin, seguindo o padrão já adotado pelo app ACS (o paciente ainda está em com.example). - compileSdk/minSdk/targetSdk como literais 36/24/36, iguais aos irmãos, em vez de flutter.*Version: os valores coincidem hoje, mas assim um upgrade do Flutter não move o targetSdk — mudando permissões e edge-to-edge em release — sem aparecer em diff nenhum. - network_security_config só em debug, copiado do ACS. Está marcado no comentário como preparação, não requisito: o admin ainda roda sobre MockAdminDataSource e não faz requisição alguma. - Deliberadamente NÃO copiados do ACS/paciente: a guarda de SINALACS_MQTT_PASSWORD (faria `flutter build apk` falhar sempre aqui), o manifestPlaceholders do Google Maps (quebraria o merge do manifest sem o plugin) e o coreLibraryDesugaring (o admin não tem plugin algum). - iOS ficou fora: nenhum app irmão tem ios/, o CI roda em ubuntu-latest e ninguém compilaria o diretório. Layout compacto (lib/app/admin_layout.dart, novo) - AdminBreakpoints extrai o literal 640 para constantes nomeadas (rail 640, stacked 480, counterCardMin 160), agora consultadas em pontos distantes do app.dart. - adminHeaderHeight() existe porque PreferredSizeWidget.preferredSize é um getter sem BuildContext: sem ele, o cabeçalho de duas linhas corta o título sob fonte grande do sistema em vez de crescer. Ajustes em lib/app/app.dart - Filtros de Alertas empilham abaixo de stacked. Lado a lado num celular cada campo recebia ~170dp; isExpanded e a elipse evitavam o estouro, mas rótulo e valor disputavam a mesma linha e "Microárea 12 — Zona Rural" virava reticências. Não era bug de layout: era campo ilegível. - _LinhaComSelo, novo widget compartilhado por Microáreas e Auditoria: abaixo de stacked o trailing do ListTile desce para o subtítulo. Como trailing, "Sem ACS ativo" comia ~140dp dos ~320dp úteis. - _CounterCard troca width:160 fixo por grade calculada e fontSize:28 literal por textTheme.headlineMedium, passando a respeitar textScaler e tema. No desktop os três cartões passam a preencher a linha. - _InfoRow empilha rótulo sobre valor abaixo de stacked; "TMRAV (tempo médio de resposta)" quebrava em três linhas ao lado do valor. - _Header recebe a altura calculada, ganha maxLines/ellipsis e troca o chip "Acesso auditado" por um ícone em tela estreita, preservando o rótulo via Semantics — o selo é informação, não controle. - NavigationRail com scrollable: true. Em paisagem de celular a largura passa de 640 e o rail entra com ~288dp de altura disponível; ao contrário do que se supunha, com os quatro destinos atuais ele cabe por poucos pixels em fonte padrão e só estoura a partir de ~150% — os dois casos estão cobertos por teste. - SafeArea passa a envolver a linha inteira no ramo largo: era o rail, e não o conteúdo, que encostava no recorte da câmera em paisagem. - formatAuditTimestamp substitui DateTime.toString() cru (2026-09-15 08:32:11.000Z) por dd/MM/aaaa HH:mm. Sem adicionar intl: o app não tem nenhuma dependência externa e i18n não está no escopo. Testes (19 → 40 herméticos, mais 4 no dispositivo) - test/support/layout_harness.dart documenta, no cabeçalho, as três formas de um teste de overflow ficar cego — checar antes da pintura, nunca rolar o ListView além da primeira dobra, e takeException() consumir uma exceção por chamada. Um harness cego deixaria a suíte responsiva inteira verde sem verificar nada, que é a falha mais cara possível aqui porque não parece uma falha. - test/layout_harness_sanity_test.dart prova que o detector detecta. - Novos: responsive_layout_test, text_scale_test (130% e 200%, WCAG 1.4.4), alerts_filters_layout_test, admin_header_test, touch_targets_test, audit_timestamp_test. - Os dez arquivos de teste anteriores não foram tocados. Rodam em 800x600 e são o gabarito do comportamento desktop. - integration_test/admin_mobile_smoke_test.dart é HERMÉTICO, ao contrário dos integration_test/ do ACS e do paciente: roda sobre MockAdminDataSource e dispensa docker compose, seed e --dart-define. O cabeçalho do arquivo diz isso para ninguém subir a stack à toa. Inclui o caso de recordAccess falhando bloquear a renderização (PRD §4.2.2), agora provado também no runtime real do Android. CI - Novo job admin-android-build com `flutter build apk --debug` e cache do Gradle: é o único job do repositório que executa Gradle. analyze e test são cegos para namespace inconsistente, AGP/Kotlin incompatíveis, MainActivity em pacote errado e merge de manifest quebrado — exatamente a classe de erro que este commit arrisca introduzir. Cobre o admin e não o ACS porque o build.gradle.kts do ACS falha de propósito sem SINALACS_MQTT_PASSWORD. O job admin-app segue intocado como gate rápido. Validação em emulador - emulator-5554 (API 36) e emulator-5556 (Medium_Tablet, 2560x1600): integration_test 4/4 em ambos, `flutter build apk --debug` verde, retrato, paisagem, rotação e fonte do sistema a 200% sem nenhum estouro de layout. Rotação preserva destino e filtros, graças ao configChanges do manifest. Capturas em docs/screenshots/admin/06..08. Documentação - CLAUDE.md, AGENTS.md, README.md, apps/admin/README.md e docs/telas-admin.md atualizados: Android como plataforma, os pontos de quebra nomeados, seis jobs de CI, e o alerta de que o integration_test deste app é hermético. Fora de escopo, registrado para issue própria: o admin repete um defeito de contraste que o ACS já corrigiu — AdminColors.red como texto sobre surfaceRaised dá 3,02:1 e accent dá 2,82:1, contra o mínimo de 4,5:1 de WCAG 1.4.3 (visível no "Risco: Vermelho" das capturas). A solução é o mesmo par de tokens *OnSurface de acs_theme.dart. Ficou de fora por ser mudança de natureza diferente da deste commit. Signed-off-by: Herbert Rocha --- .github/workflows/ci.yml | 33 +++ AGENTS.md | 6 +- CLAUDE.md | 8 +- README.md | 4 +- apps/admin/.metadata | 3 + apps/admin/README.md | 12 +- apps/admin/android/.gitignore | 14 + apps/admin/android/app/build.gradle.kts | 48 +++ .../android/app/src/debug/AndroidManifest.xml | 17 ++ .../debug/res/xml/network_security_config.xml | 19 ++ .../android/app/src/main/AndroidManifest.xml | 45 +++ .../prismrr/sinalacs/admin/MainActivity.kt | 5 + .../res/drawable-v21/launch_background.xml | 12 + .../main/res/drawable/launch_background.xml | 12 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 544 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 442 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 721 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 1031 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 1443 bytes .../app/src/main/res/values-night/styles.xml | 18 ++ .../app/src/main/res/values/styles.xml | 18 ++ .../app/src/profile/AndroidManifest.xml | 7 + apps/admin/android/build.gradle.kts | 24 ++ apps/admin/android/gradle.properties | 6 + .../gradle/wrapper/gradle-wrapper.properties | 5 + apps/admin/android/settings.gradle.kts | 26 ++ .../admin_mobile_smoke_test.dart | 93 ++++++ apps/admin/lib/app/admin_layout.dart | 38 +++ apps/admin/lib/app/app.dart | 274 ++++++++++++++---- apps/admin/pubspec.lock | 65 ++++- apps/admin/pubspec.yaml | 2 + apps/admin/test/admin_header_test.dart | 37 +++ .../test/alerts_filters_layout_test.dart | 37 +++ apps/admin/test/audit_timestamp_test.dart | 26 ++ .../test/layout_harness_sanity_test.dart | 28 ++ apps/admin/test/responsive_layout_test.dart | 57 ++++ apps/admin/test/support/layout_harness.dart | 101 +++++++ apps/admin/test/text_scale_test.dart | 32 ++ apps/admin/test/touch_targets_test.dart | 46 +++ docs/screenshots/admin/06-android-retrato.png | Bin 0 -> 182168 bytes .../screenshots/admin/07-android-paisagem.png | Bin 0 -> 135210 bytes docs/screenshots/admin/08-tablet-rail.png | Bin 0 -> 179581 bytes docs/telas-admin.md | 23 +- 43 files changed, 1129 insertions(+), 72 deletions(-) create mode 100644 apps/admin/android/.gitignore create mode 100644 apps/admin/android/app/build.gradle.kts create mode 100644 apps/admin/android/app/src/debug/AndroidManifest.xml create mode 100644 apps/admin/android/app/src/debug/res/xml/network_security_config.xml create mode 100644 apps/admin/android/app/src/main/AndroidManifest.xml create mode 100644 apps/admin/android/app/src/main/kotlin/br/com/prismrr/sinalacs/admin/MainActivity.kt create mode 100644 apps/admin/android/app/src/main/res/drawable-v21/launch_background.xml create mode 100644 apps/admin/android/app/src/main/res/drawable/launch_background.xml create mode 100644 apps/admin/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 apps/admin/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 apps/admin/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 apps/admin/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 apps/admin/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 apps/admin/android/app/src/main/res/values-night/styles.xml create mode 100644 apps/admin/android/app/src/main/res/values/styles.xml create mode 100644 apps/admin/android/app/src/profile/AndroidManifest.xml create mode 100644 apps/admin/android/build.gradle.kts create mode 100644 apps/admin/android/gradle.properties create mode 100644 apps/admin/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 apps/admin/android/settings.gradle.kts create mode 100644 apps/admin/integration_test/admin_mobile_smoke_test.dart create mode 100644 apps/admin/lib/app/admin_layout.dart create mode 100644 apps/admin/test/admin_header_test.dart create mode 100644 apps/admin/test/alerts_filters_layout_test.dart create mode 100644 apps/admin/test/audit_timestamp_test.dart create mode 100644 apps/admin/test/layout_harness_sanity_test.dart create mode 100644 apps/admin/test/responsive_layout_test.dart create mode 100644 apps/admin/test/support/layout_harness.dart create mode 100644 apps/admin/test/text_scale_test.dart create mode 100644 apps/admin/test/touch_targets_test.dart create mode 100644 docs/screenshots/admin/06-android-retrato.png create mode 100644 docs/screenshots/admin/07-android-paisagem.png create mode 100644 docs/screenshots/admin/08-tablet-rail.png diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c84be5d..5622077 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -120,3 +120,36 @@ jobs: - run: flutter pub get - run: flutter analyze - run: flutter test + + # Único job do repositório que executa o Gradle. `flutter analyze` e + # `flutter test` não enxergam namespace inconsistente, AGP/Kotlin + # incompatíveis, MainActivity em pacote errado nem merge de manifest + # quebrado — erros que só apareceriam quando alguém tentasse compilar. + # + # É o app admin e não o ACS porque `apps/acs/android/app/build.gradle.kts` + # tem a guarda que faz o build falhar sem SINALACS_MQTT_PASSWORD: cobri-lo + # aqui exigiria compilar um APK que a própria guarda declara inútil. + admin-android-build: + runs-on: ubuntu-latest + defaults: + run: + working-directory: apps/admin + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '17' + - uses: subosito/flutter-action@v2 + with: + flutter-version: '3.44.8' + # Sem cache, cada execução rebaixa ~200MB de Gradle e AGP. + - uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-admin-${{ hashFiles('apps/admin/android/**/*.gradle.kts', 'apps/admin/android/gradle/wrapper/gradle-wrapper.properties') }} + restore-keys: gradle-admin- + - run: flutter pub get + - run: flutter build apk --debug diff --git a/AGENTS.md b/AGENTS.md index bf8ffc8..c020bdd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -61,7 +61,7 @@ Não violar estes pontos sob qualquer hipótese: - O app ACS é o mais crítico em termos de offline-first e sincronização: guarda visitas locais, tenta sincronizar em fila e trata conflitos sem perder registros. - O tema visual é dark mode, com foco em legibilidade e uso de cor restrito a sinal clínico (vermelho, amarelo, verde). - O app do paciente não deve reintroduzir regras de risco no cliente; a classificação deve vir do backend via `triage.evaluate`. -- [apps/admin](apps/admin) (`sinalacs_admin`) é o backoffice administrativo — deixou de ser um esqueleto de pubspec e hoje é um app navegável real, com 4 telas somente leitura (Indicadores, Microáreas, Alertas, Auditoria) atrás de `AdminHomeShell`. Ainda não consome `sinalacs_client`: usa a interface `AdminDataSource`, hoje implementada só por `MockAdminDataSource`, seguindo o mesmo padrão de DI de `PatientBackend`/`AcsBackend`. O login é local e não chama `auth.developmentLogin` (o backend só aceita `role: 'patient'`/`role: 'acs'` hoje). Toda tela que exibe dado sensível registra o próprio acesso via `recordAccess()` antes de renderizar, por exigência de auditoria do PRD §4.2.2. É o único dos três apps com suporte a Flutter Web. +- [apps/admin](apps/admin) (`sinalacs_admin`) é o backoffice administrativo — deixou de ser um esqueleto de pubspec e hoje é um app navegável real, com 4 telas somente leitura (Indicadores, Microáreas, Alertas, Auditoria) atrás de `AdminHomeShell`. Ainda não consome `sinalacs_client`: usa a interface `AdminDataSource`, hoje implementada só por `MockAdminDataSource`, seguindo o mesmo padrão de DI de `PatientBackend`/`AcsBackend`. O login é local e não chama `auth.developmentLogin` (o backend só aceita `role: 'patient'`/`role: 'acs'` hoje). Toda tela que exibe dado sensível registra o próprio acesso via `recordAccess()` antes de renderizar, por exigência de auditoria do PRD §4.2.2. É o único dos três apps com suporte a Flutter Web, e desde a adição da plataforma Android também roda em celular e tablet (`flutter run -d emulator-5554`). O layout segue desktop-first: os pontos de quebra ficam em `lib/app/admin_layout.dart` e o layout compacto é complemento, nunca substituição — os dez testes de widget originais continuam passando sem edição, o que é o que prova isso. ### Infraestrutura local - O ambiente de desenvolvimento usa Docker Compose com PostgreSQL, Mosquitto, backend e Traefik. @@ -128,12 +128,14 @@ Rodar análise do Flutter nos apps: cd apps/patient && flutter pub get && flutter analyze && flutter test cd apps/acs && flutter pub get && flutter analyze && flutter test cd apps/admin && flutter pub get && flutter analyze && flutter test +cd apps/admin && flutter run -d emulator-5554 # no emulador Android +cd apps/admin && flutter test integration_test -d emulator-5554 # hermético: não precisa da stack ``` Importante: - `flutter test` é hermético e não substitui validações com stack local real; - os testes de integração e validação de conexão vivem fora do `flutter test` e utilizam a stack Docker/VM; -- o CI do projeto valida cinco jobs separados: `serverpod-backend`, `backend-docker-build`, `patient-app`, `acs-app` e `admin-app`. +- o CI do projeto valida seis jobs separados: `serverpod-backend`, `backend-docker-build`, `patient-app`, `acs-app`, `admin-app` e `admin-android-build` (único que executa Gradle, compilando o APK do admin). ## Observações finais diff --git a/CLAUDE.md b/CLAUDE.md index 480d8d2..fcf94cd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -115,7 +115,7 @@ flutter build apk --debug # debug APK, validated with compileSdk/targetSdk 36 ``` For the ACS app, run `./scripts/dev/run_acs.sh` (or `--build` for the APK) instead of bare `flutter run`: `SINALACS_MQTT_PASSWORD` is a compile-time constant with **no default**, and the broker's password is generated per machine by `bootstrap_env.sh`. The script reads `.env`, runs `sync_dev_ca.sh` (the CA is a gitignored asset the build requires) and passes all four defines via `--dart-define-from-file` (a temp file it creates and deletes, so the password never sits in `flutter`'s argv). `apps/acs/android/app/build.gradle.kts` makes a bare `flutter build apk` **fail** with the right command instead of silently producing an APK that never connects; the escape hatch for a deliberately-passwordless build (e.g. to see the "compiled without the password" banner) is `-Psinalacs.allowMissingMqttPassword=true`. -`apps/admin` (package `sinalacs_admin`) is a real backoffice app now, not a skeleton — see Architecture below. Same commands apply (`cd apps/admin && flutter pub get && flutter analyze && flutter test`); it's also the only one of the three with Flutter Web enabled (`flutter build web` works). +`apps/admin` (package `sinalacs_admin`) is a real backoffice app now, not a skeleton — see Architecture below. Same commands apply (`cd apps/admin && flutter pub get && flutter analyze && flutter test`); it's the only one of the three with Flutter Web enabled (`flutter build web` works), and since the Android platform was added it also runs on a device: `flutter run -d emulator-5554`, `flutter build apk --debug`, and `flutter test integration_test -d emulator-5554`. Unlike the ACS app there is no wrapper script and no `--dart-define` to remember — the admin has no secrets. ### Validating the real connection to the backend @@ -131,9 +131,9 @@ The script brings up Docker Compose, waits for the healthcheck, applies the seed `integration_test/` in each app holds the on-device version, excluded from `flutter test` by construction. Run it against an already-running emulator with `--dart-define=SINALACS_HOST=http://10.0.2.2:8080/` (and, for the ACS, `SINALACS_MQTT_HOST=10.0.2.2` plus `SINALACS_MQTT_PASSWORD="$MQTT_ACS_PASSWORD"` — without the password the suite fails in `setUpAll` with the command you need). `scripts/qa/e2e.sh --emulator` already passes all three. CI's four jobs are unchanged and never need a backend. -Debug builds of both apps carry `android/app/src/debug/res/xml/network_security_config.xml`, which permits cleartext only to `10.0.2.2` and loopback; release manifests are untouched. +Debug builds of all three apps carry `android/app/src/debug/res/xml/network_security_config.xml`, which permits cleartext only to `10.0.2.2` and loopback; release manifests are untouched. -CI (`.github/workflows/ci.yml`) runs five parallel jobs on push/PR to main: `serverpod-backend` (spins up the Postgres the test harness expects on port 9090, then `dart analyze` and the full 25-test suite), `backend-docker-build` (builds `backend/sinalacs_server/Dockerfile` to catch build breakage before deploy), `patient-app`, `acs-app`, `admin-app` (each `flutter analyze && flutter test`, Flutter 3.44.8). Mirror this locally before pushing. +CI (`.github/workflows/ci.yml`) runs six parallel jobs on push/PR to main: `serverpod-backend` (spins up the Postgres the test harness expects on port 9090, then `dart analyze` and the full 25-test suite), `backend-docker-build` (builds `backend/sinalacs_server/Dockerfile` to catch build breakage before deploy), `patient-app`, `acs-app`, `admin-app` (each `flutter analyze && flutter test`, Flutter 3.44.8), and `admin-android-build` (`flutter build apk --debug` with a Gradle cache). That last one is the only job in the repository that executes Gradle at all: `analyze` and `test` are blind to an inconsistent `namespace`, incompatible AGP/Kotlin, a `MainActivity` in the wrong package or a broken manifest merge. It covers the admin rather than the ACS because `apps/acs/android/app/build.gradle.kts` deliberately fails the build without `SINALACS_MQTT_PASSWORD`. Mirror this locally before pushing. ## Architecture @@ -171,7 +171,7 @@ Color is a clinical signal only in these apps: red/yellow/green map strictly to **WCAG contrast tokens**: `app/acs_theme.dart` and `app/patient_theme.dart` each carry a text-safe variant of every clinical fill color — `redOnSurface`/`accentOnSurface` (ACS) and `dangerOnSurface`/`accentOnSurface` (patient). `red`/`accent`/`danger` only clear WCAG 1.4.3's 4.5:1 as a *fill* (e.g. white text on a red button); reused as *text* color on `surfaceRaised`/`Card` — where risk/status text is actually rendered — they drop to ~3:1. `acsOnSurface()` (and its patient-app equivalent) converts fill → text color at the single point where a `switch (riskLevel)` used to feed both, instead of at every call site. `test/contrast_tokens_test.dart` in each app checks the full token×surface matrix by computing relative luminance directly (`test/support/contrast.dart`) rather than by eyeballing a contrast checker against the wrong surface — which is exactly the false positive/negative `spec/ux_accessibility_assessment.md` documents from the original manual audit. The same pass added `minimumSize` (48×52dp, 64×60dp for the SAMU emergency call) per WCAG 2.5.5 and `Semantics(liveRegion: true)` on dynamic status messages (login errors, feed/storage errors, rejected-visit counts) per WCAG 4.1.3. ### Admin app (`apps/admin/`) -Package `sinalacs_admin`, read-only backoffice. Same skeleton as ACS/patient: `lib/main.dart` → `lib/app/app.dart` (+ `admin_theme.dart`) → `lib/core/data/`. Unlike ACS/patient it does not consume `sinalacs_client` yet: `AdminDataSource` (`lib/core/data/admin_data_source.dart`) is an abstract interface whose models (`RiskLevel`, `AlertStatus`, `DashboardIndicators`, `MicroAreaSummary`, `AlertSummary`, `AuditLogEntry`) mirror the backend's `.spy.yaml` models field-for-field, backed today only by `MockAdminDataSource` — swapping in a real implementation over `sinalacs_client` shouldn't require reshaping the screens, the same DI pattern as `PatientBackend`/`AcsBackend`. `AdminHomeShell` exposes four read-only sections: **Indicadores** (risk counters + TMRAV, the PRD's North Star metric), **Microáreas** (ACS↔microarea binding), **Alertas** (filterable by micro-area/status, with filter options sourced from the fetched data itself rather than a hardcoded list — a hardcoded list would silently drop a real micro-area as a selectable option) and **Auditoria** (an `audit_logs` viewer). Every screen touching sensitive data calls `dataSource.recordAccess(actionType: 'view', resourceType: ...)` *before* rendering it, per `spec/PRD_system.md` §4.2.2 (admin access must itself be audited) — the audit log screen audits its own access too, and a failed `recordAccess` blocks the data from rendering rather than failing open. Login is intentionally local-only, **not** wired to `auth.developmentLogin`: the backend's `auth_endpoint.dart` only accepts `role: 'patient'`/`role: 'acs'`, there is no fixed dev user for `admin`, so real auth is out of scope until the backend supports it (see the `LoginScreen` doc comment in `app.dart`). The "ambiente de desenvolvimento" banner is informational, not access control — the actual gate is `devLoginEnabled`, which defaults to `kDebugMode` and disables the login bypass outside debug builds. Layout is desktop-first per `spec/PRD_system.md` §2.1: `NavigationRail` at ≥640px width, `NavigationBar` below, same `ThemeData` either way. `apps/admin` is the only one of the three apps with Flutter Web enabled and the only one versioning `pubspec.lock` from the start (the other two adopted that convention later). `docs/telas-admin.md` documents its five screens with real screenshots, mirroring `docs/telas-acs.md`. CI covers it in the `admin-app` job, identical in shape to `acs-app`/`patient-app`. +Package `sinalacs_admin`, read-only backoffice. Same skeleton as ACS/patient: `lib/main.dart` → `lib/app/app.dart` (+ `admin_theme.dart`) → `lib/core/data/`. Unlike ACS/patient it does not consume `sinalacs_client` yet: `AdminDataSource` (`lib/core/data/admin_data_source.dart`) is an abstract interface whose models (`RiskLevel`, `AlertStatus`, `DashboardIndicators`, `MicroAreaSummary`, `AlertSummary`, `AuditLogEntry`) mirror the backend's `.spy.yaml` models field-for-field, backed today only by `MockAdminDataSource` — swapping in a real implementation over `sinalacs_client` shouldn't require reshaping the screens, the same DI pattern as `PatientBackend`/`AcsBackend`. `AdminHomeShell` exposes four read-only sections: **Indicadores** (risk counters + TMRAV, the PRD's North Star metric), **Microáreas** (ACS↔microarea binding), **Alertas** (filterable by micro-area/status, with filter options sourced from the fetched data itself rather than a hardcoded list — a hardcoded list would silently drop a real micro-area as a selectable option) and **Auditoria** (an `audit_logs` viewer). Every screen touching sensitive data calls `dataSource.recordAccess(actionType: 'view', resourceType: ...)` *before* rendering it, per `spec/PRD_system.md` §4.2.2 (admin access must itself be audited) — the audit log screen audits its own access too, and a failed `recordAccess` blocks the data from rendering rather than failing open. Login is intentionally local-only, **not** wired to `auth.developmentLogin`: the backend's `auth_endpoint.dart` only accepts `role: 'patient'`/`role: 'acs'`, there is no fixed dev user for `admin`, so real auth is out of scope until the backend supports it (see the `LoginScreen` doc comment in `app.dart`). The "ambiente de desenvolvimento" banner is informational, not access control — the actual gate is `devLoginEnabled`, which defaults to `kDebugMode` and disables the login bypass outside debug builds. Layout is desktop-first per `spec/PRD_system.md` §2.1: `NavigationRail` above `AdminBreakpoints.rail`, `NavigationBar` below, same `ThemeData` either way. The breakpoints live in `lib/app/admin_layout.dart` (`rail` 640, `stacked` 480, `counterCardMin` 160) plus `adminHeaderHeight()`, which exists because `PreferredSizeWidget.preferredSize` is a getter with no `BuildContext` — without it the two-line header clips instead of growing under a large system font. Android was added after the web-only phase, and mobile is a complement, never a replacement: below `stacked` the two alert filters stack instead of sharing a row, `_LinhaComSelo` moves a `ListTile`'s `trailing` badge down into the subtitle (Microáreas and Auditoria had the same squeeze), `_InfoRow` stacks label over value, the header's "Acesso auditado" chip becomes an icon that keeps its `Semantics` label, and the counter cards use a computed grid instead of a fixed `width: 160`. `NavigationRail` is `scrollable: true` because a phone in landscape clears the 640 threshold with only ~288dp of height left — it fits by a few pixels at default font size and overflows at 150%. The guard against regressing any of this is that the ten original widget tests still pass **unedited** at 800×600; the new `test/responsive_layout_test.dart`, `text_scale_test.dart`, `alerts_filters_layout_test.dart`, `admin_header_test.dart` and `touch_targets_test.dart` drive `test/support/layout_harness.dart`, whose header documents the three ways an overflow test silently goes blind (checking before paint, never scrolling a `ListView` past the first fold, and `takeException()` consuming one exception per call) — `test/layout_harness_sanity_test.dart` proves the detector still detects. **`apps/admin/integration_test/` is hermetic**, unlike the ACS and patient ones described above: it runs on `MockAdminDataSource`, so `flutter test integration_test -d emulator-5554` needs no `docker compose`, no seed and no `--dart-define`. `apps/admin` is the only one of the three apps with Flutter Web enabled and the only one versioning `pubspec.lock` from the start (the other two adopted that convention later). `docs/telas-admin.md` documents its five screens with real screenshots, mirroring `docs/telas-acs.md`. CI covers it in the `admin-app` job, identical in shape to `acs-app`/`patient-app`. ### `spec/` Product/architecture source of truth (PRD, UX flows, LGPD design, stack decisions) plus static HTML prototypes under `spec/ui_acs/` and `spec/ui_paciente/` — these are the visual reference for building out the real Flutter screens, not live code. diff --git a/README.md b/README.md index e88bd3c..c48db7c 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ Consulte [PROGRESS.md](PROGRESS.md) para o status detalhado dos milestones e apps/ acs/ Aplicativo Flutter do Agente Comunitário de Saúde patient/ Aplicativo Flutter do paciente - admin/ Base do aplicativo administrativo + admin/ Backoffice administrativo (Flutter Web e Android) backend/ Backend Dart (dart:io, sem framework) e regras de domínio infra/ Configuração local de infraestrutura spec/ PRD, UX, privacidade e fluxos do produto @@ -42,7 +42,7 @@ tests/ Testes compartilhados ## Pré-requisitos - Flutter SDK compatível com Dart `>=3.3.0 <4.0.0`. -- Android SDK com API 36 e JDK 17 para gerar ou executar o app ACS no Android. +- Android SDK com API 36 e JDK 17 para gerar ou executar os apps ACS, paciente e admin no Android. - Docker Engine com Docker Compose v2 para subir a stack local. - Um emulador Android ou dispositivo físico, opcional para execução mobile. diff --git a/apps/admin/.metadata b/apps/admin/.metadata index 2557b48..c8f4bda 100644 --- a/apps/admin/.metadata +++ b/apps/admin/.metadata @@ -15,6 +15,9 @@ migration: - platform: root create_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6 base_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6 + - platform: android + create_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6 + base_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6 - platform: web create_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6 base_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6 diff --git a/apps/admin/README.md b/apps/admin/README.md index 6653bdb..f338d8a 100644 --- a/apps/admin/README.md +++ b/apps/admin/README.md @@ -1,5 +1,15 @@ # SinalACS — Backoffice Admin -Backoffice administrativo (Flutter Web, desktop-first): login de desenvolvimento, painel de indicadores por risco clínico, listagem de microáreas e vínculo ACS, consulta filtrável de alertas e logs de auditoria. +Backoffice administrativo (Flutter Web e Android, desktop-first com layout compacto para celular): login de desenvolvimento, painel de indicadores por risco clínico, listagem de microáreas e vínculo ACS, consulta filtrável de alertas e logs de auditoria. + +## Rodar no Android + +```bash +flutter run -d emulator-5554 # emulador de celular +flutter build apk --debug # valida a configuração Gradle +flutter test integration_test -d emulator-5554 +``` + +Os testes de integração deste app são **herméticos** — rodam sobre `MockAdminDataSource` e não precisam do `docker compose`, do seed nem de `--dart-define`, ao contrário dos apps ACS e do paciente. Ver o [README.md](../../README.md) na raiz do repositório para pré-requisitos, comandos de execução/teste e o estado atual do projeto. Documentação visual das telas em [docs/telas-admin.md](../../docs/telas-admin.md). diff --git a/apps/admin/android/.gitignore b/apps/admin/android/.gitignore new file mode 100644 index 0000000..be3943c --- /dev/null +++ b/apps/admin/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/apps/admin/android/app/build.gradle.kts b/apps/admin/android/app/build.gradle.kts new file mode 100644 index 0000000..f61204b --- /dev/null +++ b/apps/admin/android/app/build.gradle.kts @@ -0,0 +1,48 @@ +plugins { + id("com.android.application") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "br.com.prismrr.sinalacs.admin" + // compileSdk/minSdk/targetSdk são literais, e não `flutter.*Version`, pelos + // mesmos valores de apps/acs e apps/patient: assim um upgrade do Flutter não + // move o targetSdk — o que muda comportamento de permissões e edge-to-edge em + // release — sem aparecer em nenhum diff e sem ninguém ter decidido. + compileSdk = 36 + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + defaultConfig { + applicationId = "br.com.prismrr.sinalacs.admin" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = 24 + targetSdk = 36 + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +kotlin { + compilerOptions { + jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + } +} + +flutter { + source = "../.." +} diff --git a/apps/admin/android/app/src/debug/AndroidManifest.xml b/apps/admin/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..24c4f0a --- /dev/null +++ b/apps/admin/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + + diff --git a/apps/admin/android/app/src/debug/res/xml/network_security_config.xml b/apps/admin/android/app/src/debug/res/xml/network_security_config.xml new file mode 100644 index 0000000..0e78459 --- /dev/null +++ b/apps/admin/android/app/src/debug/res/xml/network_security_config.xml @@ -0,0 +1,19 @@ + + + + + + 10.0.2.2 + + localhost + 127.0.0.1 + + diff --git a/apps/admin/android/app/src/main/AndroidManifest.xml b/apps/admin/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..971e090 --- /dev/null +++ b/apps/admin/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/admin/android/app/src/main/kotlin/br/com/prismrr/sinalacs/admin/MainActivity.kt b/apps/admin/android/app/src/main/kotlin/br/com/prismrr/sinalacs/admin/MainActivity.kt new file mode 100644 index 0000000..794a286 --- /dev/null +++ b/apps/admin/android/app/src/main/kotlin/br/com/prismrr/sinalacs/admin/MainActivity.kt @@ -0,0 +1,5 @@ +package br.com.prismrr.sinalacs.admin + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/apps/admin/android/app/src/main/res/drawable-v21/launch_background.xml b/apps/admin/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/apps/admin/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/apps/admin/android/app/src/main/res/drawable/launch_background.xml b/apps/admin/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/apps/admin/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/apps/admin/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/apps/admin/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..db77bb4b7b0906d62b1847e87f15cdcacf6a4f29 GIT binary patch literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ literal 0 HcmV?d00001 diff --git a/apps/admin/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/apps/admin/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..17987b79bb8a35cc66c3c1fd44f5a5526c1b78be GIT binary patch literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xx&nMcT!A!W`0S9QKQy;}1Cl^CgaH=;G9cpY;r$Q>i*pfB zP2drbID<_#qf;rPZx^FqH)F_D#*k@@q03KywUtLX8Ua?`H+NMzkczFPK3lFz@i_kW%1NOn0|D2I9n9wzH8m|-tHjsw|9>@K=iMBhxvkv6m8Y-l zytQ?X=U+MF$@3 zt`~i=@j|6y)RWMK--}M|=T`o&^Ni>IoWKHEbBXz7?A@mgWoL>!*SXo`SZH-*HSdS+ yn*9;$7;m`l>wYBC5bq;=U}IMqLzqbYCidGC!)_gkIk_C@Uy!y&wkt5C($~2D>~)O*cj@FGjOCM)M>_ixfudOh)?xMu#Fs z#}Y=@YDTwOM)x{K_j*Q;dPdJ?Mz0n|pLRx{4n|)f>SXlmV)XB04CrSJn#dS5nK2lM zrZ9#~WelCp7&e13Y$jvaEXHskn$2V!!DN-nWS__6T*l;H&Fopn?A6HZ-6WRLFP=R` zqG+CE#d4|IbyAI+rJJ`&x9*T`+a=p|0O(+s{UBcyZdkhj=yS1>AirP+0R;mf2uMgM zC}@~JfByORAh4SyRgi&!(cja>F(l*O+nd+@4m$|6K6KDn_&uvCpV23&>G9HJp{xgg zoq1^2_p9@|WEo z*X_Uko@K)qYYv~>43eQGMdbiGbo>E~Q& zrYBH{QP^@Sti!`2)uG{irBBq@y*$B zi#&(U-*=fp74j)RyIw49+0MRPMRU)+a2r*PJ$L5roHt2$UjExCTZSbq%V!HeS7J$N zdG@vOZB4v_lF7Plrx+hxo7(fCV&}fHq)$ literal 0 HcmV?d00001 diff --git a/apps/admin/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/apps/admin/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..d5f1c8d34e7a88e3f88bea192c3a370d44689c3c GIT binary patch literal 1031 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof literal 0 HcmV?d00001 diff --git a/apps/admin/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/apps/admin/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..4d6372eebdb28e45604e46eeda8dd24651419bc0 GIT binary patch literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` literal 0 HcmV?d00001 diff --git a/apps/admin/android/app/src/main/res/values-night/styles.xml b/apps/admin/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/apps/admin/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/apps/admin/android/app/src/main/res/values/styles.xml b/apps/admin/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/apps/admin/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/apps/admin/android/app/src/profile/AndroidManifest.xml b/apps/admin/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/apps/admin/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/apps/admin/android/build.gradle.kts b/apps/admin/android/build.gradle.kts new file mode 100644 index 0000000..dbee657 --- /dev/null +++ b/apps/admin/android/build.gradle.kts @@ -0,0 +1,24 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = + rootProject.layout.buildDirectory + .dir("../../build") + .get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/apps/admin/android/gradle.properties b/apps/admin/android/gradle.properties new file mode 100644 index 0000000..e96108c --- /dev/null +++ b/apps/admin/android/gradle.properties @@ -0,0 +1,6 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +# This newDsl flag was added by the Flutter template +android.newDsl=false +# This builtInKotlin flag was added by the Flutter template +android.builtInKotlin=false diff --git a/apps/admin/android/gradle/wrapper/gradle-wrapper.properties b/apps/admin/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..2d428bf --- /dev/null +++ b/apps/admin/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-all.zip diff --git a/apps/admin/android/settings.gradle.kts b/apps/admin/android/settings.gradle.kts new file mode 100644 index 0000000..c21f0c5 --- /dev/null +++ b/apps/admin/android/settings.gradle.kts @@ -0,0 +1,26 @@ +pluginManagement { + val flutterSdkPath = + run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "9.0.1" apply false + id("org.jetbrains.kotlin.android") version "2.3.20" apply false +} + +include(":app") diff --git a/apps/admin/integration_test/admin_mobile_smoke_test.dart b/apps/admin/integration_test/admin_mobile_smoke_test.dart new file mode 100644 index 0000000..7bbc72c --- /dev/null +++ b/apps/admin/integration_test/admin_mobile_smoke_test.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:sinalacs_admin/app/app.dart'; +import 'package:sinalacs_admin/core/data/mock_admin_data_source.dart'; + +import '../test/support/failing_admin_data_source.dart'; + +/// Validação do backoffice num dispositivo Android real (ou emulador). +/// +/// ATENÇÃO — este arquivo é **hermético**, ao contrário dos `integration_test/` +/// do app ACS e do app do paciente. O backoffice ainda roda sobre +/// `MockAdminDataSource`, então aqui **não** é preciso `docker compose up`, +/// nem seed, nem `--dart-define`. Basta: +/// +/// flutter test integration_test -d emulator-5554 +/// +/// O que isto cobre e `flutter test` não consegue cobrir: o runtime real do +/// Android — densidade de tela, insets do sistema, rotação de verdade e a +/// escala de fonte do aparelho — em vez da janela sintética de 800x600 do +/// `flutter_test`. +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + Future entrar(WidgetTester tester, {dynamic dataSource}) async { + await tester.pumpWidget(SinalAdminApp(dataSource: dataSource, devLoginEnabled: true)); + await tester.pumpAndSettle(); + final entrar = find.byKey(const Key('login_button')); + await tester.ensureVisible(entrar); + await tester.pumpAndSettle(); + await tester.tap(entrar); + await tester.pumpAndSettle(); + } + + Future irPara(WidgetTester tester, String destino) async { + final alvo = find.text(destino).last; + await tester.ensureVisible(alvo); + await tester.pumpAndSettle(); + await tester.tap(alvo); + await tester.pumpAndSettle(); + } + + testWidgets('abre o backoffice no dispositivo e navega pelos quatro destinos', (tester) async { + await entrar(tester); + + expect(find.text('Painel de Indicadores'), findsOneWidget); + await irPara(tester, 'Microáreas'); + expect(find.text('Microáreas e vínculo ACS'), findsOneWidget); + await irPara(tester, 'Alertas'); + expect(find.text('Alertas da UBS'), findsOneWidget); + await irPara(tester, 'Auditoria'); + expect(find.text('Logs de auditoria'), findsOneWidget); + }); + + testWidgets('escolhe a navegação conforme a largura real do aparelho', (tester) async { + await entrar(tester); + + // Em celular sai a barra inferior; em tablet, o rail lateral. Qual dos dois + // depende do aparelho — o que se afirma aqui é que existe exatamente um. + final rail = find.byKey(const Key('admin_navigation_rail')).evaluate().length; + final barra = find.byKey(const Key('admin_navigation_bar')).evaluate().length; + expect(rail + barra, 1, reason: 'deve haver exatamente uma navegação, nunca as duas nem nenhuma'); + }); + + testWidgets('não estoura o layout ao girar o aparelho', (tester) async { + await entrar(tester); + + final tamanhoOriginal = tester.view.physicalSize; + addTearDown(tester.view.reset); + + tester.view.physicalSize = Size(tamanhoOriginal.height, tamanhoOriginal.width); + await tester.pumpAndSettle(); + expect(tester.takeException(), isNull, reason: 'estouro de layout após girar para paisagem'); + + tester.view.physicalSize = tamanhoOriginal; + await tester.pumpAndSettle(); + expect(tester.takeException(), isNull, reason: 'estouro de layout ao voltar para retrato'); + }); + + testWidgets('bloqueia a exibição de dado sensível quando a auditoria falha', (tester) async { + // PRD §4.2.2: acesso do administrador precisa ser auditado. Se o registro + // falha, a tela mostra erro — nunca o dado sem auditoria (nada de + // fail-open). Já coberto em `flutter test`; repetido aqui para valer também + // no runtime real do Android. + final dataSource = FailingAdminDataSource(inner: MockAdminDataSource())..failNextRecordAccess = true; + await entrar(tester, dataSource: dataSource); + + await irPara(tester, 'Microáreas'); + + expect(find.text('Microáreas e vínculo ACS'), findsNothing); + expect(find.textContaining('Não foi possível'), findsOneWidget); + }); +} diff --git a/apps/admin/lib/app/admin_layout.dart b/apps/admin/lib/app/admin_layout.dart new file mode 100644 index 0000000..4fcd2fb --- /dev/null +++ b/apps/admin/lib/app/admin_layout.dart @@ -0,0 +1,38 @@ +import 'package:flutter/widgets.dart'; + +/// Pontos de quebra do backoffice. +/// +/// O backoffice é desktop-first (`spec/PRD_system.md` §2.1): o layout com +/// NavigationRail continua sendo o padrão e o layout compacto é complemento +/// para celular, nunca substituição. +/// +/// Existem como constantes nomeadas porque o mesmo número é consultado em +/// pontos distantes do `app.dart` (shell, cabeçalho, filtros, cartões, +/// listas). Enquanto era o literal `640` solto em um único `LayoutBuilder`, +/// qualquer segundo uso teria sido uma cópia sem relação declarada com a +/// primeira. +abstract final class AdminBreakpoints { + /// Acima disto, NavigationRail lateral; abaixo, NavigationBar inferior. + static const double rail = 640; + + /// Abaixo disto, pares de controles lado a lado passam a empilhar: os dois + /// filtros de Alertas, o rótulo/valor de `_InfoRow` e o `trailing` das + /// listas. 480 e não 600 porque só afeta pares — a 480dp dois campos ainda + /// têm ~230dp cada, que é onde o rótulo do dropdown ainda cabe. + static const double stacked = 480; + + /// Largura-alvo mínima de um cartão de contador antes de reduzir a grade. + static const double counterCardMin = 160; +} + +/// Altura do cabeçalho, acompanhando a escala de fonte do sistema. +/// +/// `PreferredSizeWidget.preferredSize` é um getter sem `BuildContext`, então a +/// altura precisa ser calculada por quem monta o `Scaffold` e passada adiante. +/// Sem isso, com fonte grande no Android as duas linhas do título estouram os +/// 72dp fixos — e `AppBar` corta em vez de crescer. +/// +/// O teto de 132 existe para que fonte a 200% não coma metade da tela de um +/// celular; o título já usa elipse, então o corte é o do texto, não do layout. +double adminHeaderHeight(BuildContext context) => + MediaQuery.textScalerOf(context).scale(72).clamp(72.0, 132.0); diff --git a/apps/admin/lib/app/app.dart b/apps/admin/lib/app/app.dart index 456f06c..16d4cfe 100644 --- a/apps/admin/lib/app/app.dart +++ b/apps/admin/lib/app/app.dart @@ -1,5 +1,6 @@ import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/material.dart'; +import 'package:sinalacs_admin/app/admin_layout.dart'; import 'package:sinalacs_admin/app/admin_theme.dart'; import 'package:sinalacs_admin/core/data/admin_data_source.dart'; import 'package:sinalacs_admin/core/data/mock_admin_data_source.dart'; @@ -62,7 +63,7 @@ class _LoginScreenState extends State { @override Widget build(BuildContext context) => Scaffold( - appBar: const _Header('Backoffice SinalACS', 'Acesso administrativo'), + appBar: _Header('Backoffice SinalACS', 'Acesso administrativo', height: adminHeaderHeight(context)), body: Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 600), @@ -191,19 +192,31 @@ class _AdminHomeShellState extends State { @override Widget build(BuildContext context) => LayoutBuilder( builder: (context, constraints) { - final content = SafeArea(child: _content()); + final content = _content(); + final header = _Header('Backoffice • admin.dev', 'Painel administrativo', height: adminHeaderHeight(context)); // Backoffice é desktop-first (spec/PRD_system.md §2.1): NavigationRail - // acima de 640px, NavigationBar abaixo — mesmo ThemeData nos dois. - if (constraints.maxWidth >= 640) { + // acima de AdminBreakpoints.rail, NavigationBar abaixo — mesmo + // ThemeData nos dois. + if (constraints.maxWidth >= AdminBreakpoints.rail) { return Scaffold( - appBar: const _Header('Backoffice • admin.dev', 'Painel administrativo'), - body: Row( + appBar: header, + // SafeArea envolve a linha inteira, e não só o conteúdo: num + // celular em paisagem é o rail que encosta no recorte da câmera. + // `top: false` porque a AppBar já trata o inset de cima. + body: SafeArea( + top: false, + child: Row( children: [ NavigationRail( key: const Key('admin_navigation_rail'), selectedIndex: destination.index, onDestinationSelected: _select, labelType: NavigationRailLabelType.all, + // Em paisagem de celular sobram ~288dp de altura para + // quatro destinos rotulados: cabe por poucos pixels com + // fonte padrão e estoura com fonte ampliada. `scrollable` + // troca o estouro por rolagem em vez de cortar destino. + scrollable: true, destinations: [ for (final value in AdminDestination.values) NavigationRailDestination(icon: Icon(value.icon), label: Text(value.label)), @@ -212,12 +225,15 @@ class _AdminHomeShellState extends State { const VerticalDivider(width: 1), Expanded(child: content), ], + ), ), ); } return Scaffold( - appBar: const _Header('Backoffice • admin.dev', 'Painel administrativo'), - body: content, + appBar: header, + // `bottom: false`: o NavigationBar do Scaffold já trata o inset + // inferior; duplicar aqui abriria uma faixa morta acima dele. + body: SafeArea(top: false, bottom: false, child: content), bottomNavigationBar: NavigationBar( key: const Key('admin_navigation_bar'), selectedIndex: destination.index, @@ -244,6 +260,19 @@ Color riskColor(RiskLevel level) => switch (level) { RiskLevel.green => AdminColors.green, }; +/// Timestamp de auditoria em `dd/MM/aaaa HH:mm`, hora local. +/// +/// A tela imprimia `DateTime.toString()` cru — `2026-09-15 08:32:11.000Z` — +/// que é largo e cheio de ruído (milissegundos, sufixo Z) sem valor nenhum +/// para quem audita. Sem `intl` de propósito: o backoffice não tem nenhuma +/// dependência externa hoje e um formato pt-BR fixo basta; internacionalização +/// não está no escopo. +String formatAuditTimestamp(DateTime timestamp) { + final local = timestamp.toLocal(); + String dois(int valor) => valor.toString().padLeft(2, '0'); + return '${dois(local.day)}/${dois(local.month)}/${local.year} ${dois(local.hour)}:${dois(local.minute)}'; +} + String statusLabel(AlertStatus status) => switch (status) { AlertStatus.pending => 'Pendente', AlertStatus.acknowledged => 'Reconhecido', @@ -311,18 +340,32 @@ class _IndicatorsScreenState extends State { const SizedBox(height: 4), const Text('Contadores por risco clínico da UBS'), const SizedBox(height: 16), - Wrap( - spacing: 12, - runSpacing: 12, - children: [ - for (final level in RiskLevel.values) - _CounterCard( - key: Key('risk_counter_${level.name}'), - label: riskLabel(level), - value: '${data.countsByRisk[level] ?? 0}', - color: riskColor(level), - ), - ], + LayoutBuilder( + builder: (context, constraints) { + // Os cartões tinham 160dp fixos dentro de um Wrap: cabiam, mas + // deixavam um vão à direita no desktop e não se adaptavam a + // nada. A grade calculada distribui a largura disponível e cai + // para uma coluna quando não há espaço para duas. + const espaco = 12.0; + final colunas = ((constraints.maxWidth + espaco) / (AdminBreakpoints.counterCardMin + espaco)) + .floor() + .clamp(1, RiskLevel.values.length); + final largura = (constraints.maxWidth - espaco * (colunas - 1)) / colunas; + return Wrap( + spacing: espaco, + runSpacing: espaco, + children: [ + for (final level in RiskLevel.values) + _CounterCard( + key: Key('risk_counter_${level.name}'), + label: riskLabel(level), + value: '${data.countsByRisk[level] ?? 0}', + color: riskColor(level), + width: largura, + ), + ], + ); + }, ), const SizedBox(height: 20), const Divider(), @@ -338,15 +381,16 @@ class _IndicatorsScreenState extends State { } class _CounterCard extends StatelessWidget { - const _CounterCard({required this.label, required this.value, required this.color, super.key}); + const _CounterCard({required this.label, required this.value, required this.color, required this.width, super.key}); final String label; final String value; final Color color; + final double width; @override Widget build(BuildContext context) => Container( - width: 160, + width: width, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AdminColors.surfaceRaised, @@ -356,7 +400,7 @@ class _CounterCard extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(value, style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold)), + Text(value, style: Theme.of(context).textTheme.headlineMedium?.copyWith(fontWeight: FontWeight.bold)), const SizedBox(height: 4), Text(label, style: TextStyle(color: color, fontWeight: FontWeight.bold)), ], @@ -408,10 +452,10 @@ class _MicroAreasScreenState extends State { Card( key: Key('micro_area_${area.id}'), margin: const EdgeInsets.only(bottom: 12), - child: ListTile( - title: Text(area.name), - subtitle: Text('ACS: ${area.acsName} (${area.acsEnrollmentId})'), - trailing: Chip( + child: _LinhaComSelo( + titulo: Text(area.name), + descricao: Text('ACS: ${area.acsName} (${area.acsEnrollmentId})'), + selo: Chip( avatar: Icon(area.acsActive ? Icons.check_circle_outline : Icons.remove_circle_outline, size: 18), label: Text(area.acsActive ? 'Ativo' : 'Sem ACS ativo'), backgroundColor: AdminColors.surface, @@ -500,10 +544,8 @@ class _AlertsList extends StatelessWidget { const SizedBox(height: 4), const Text('Consulta somente leitura — nenhuma reclassificação de risco é permitida aqui.'), const SizedBox(height: 16), - Row( - children: [ - Expanded( - child: DropdownButtonFormField( + _FiltrosDeAlertas( + microArea: DropdownButtonFormField( key: const Key('alerts_micro_area_filter'), initialValue: microAreaFilter, isExpanded: true, @@ -519,10 +561,7 @@ class _AlertsList extends StatelessWidget { ], onChanged: (value) => onFilterChanged(value, statusFilter), ), - ), - const SizedBox(width: 12), - Expanded( - child: DropdownButtonFormField( + status: DropdownButtonFormField( key: const Key('alerts_status_filter'), initialValue: statusFilter, isExpanded: true, @@ -535,8 +574,6 @@ class _AlertsList extends StatelessWidget { ], onChanged: (value) => onFilterChanged(microAreaFilter, value), ), - ), - ], ), const SizedBox(height: 16), if (snapshot.connectionState == ConnectionState.waiting) @@ -570,6 +607,35 @@ class _AlertsList extends StatelessWidget { ); } +/// Os dois filtros de Alertas, lado a lado ou empilhados. +/// +/// Lado a lado num celular cada campo recebia ~170dp: `isExpanded` e a elipse +/// dos itens evitavam o estouro, mas o rótulo do campo e o valor selecionado +/// passavam a disputar a mesma linha e "Microárea 12 — Zona Rural" virava +/// reticências. Não era um bug de layout — era um campo ilegível. +class _FiltrosDeAlertas extends StatelessWidget { + const _FiltrosDeAlertas({required this.microArea, required this.status}); + + final Widget microArea; + final Widget status; + + @override + Widget build(BuildContext context) => LayoutBuilder( + builder: (context, constraints) { + if (constraints.maxWidth < AdminBreakpoints.stacked) { + return Column(children: [microArea, const SizedBox(height: 12), status]); + } + return Row( + children: [ + Expanded(child: microArea), + const SizedBox(width: 12), + Expanded(child: status), + ], + ); + }, + ); +} + class AuditLogScreen extends StatefulWidget { const AuditLogScreen({required this.dataSource, super.key}); @@ -618,11 +684,11 @@ class _AuditLogScreenState extends State { Card( key: Key('audit_${entry.id}'), margin: const EdgeInsets.only(bottom: 8), - child: ListTile( - leading: const Icon(Icons.verified_user_outlined), - title: Text('${entry.actionType} • ${entry.resourceType}'), - subtitle: Text('${entry.userLabel} — ${entry.timestamp}'), - trailing: Text(entry.result), + child: _LinhaComSelo( + icone: const Icon(Icons.verified_user_outlined), + titulo: Text('${entry.actionType} • ${entry.resourceType}'), + descricao: Text('${entry.userLabel} — ${formatAuditTimestamp(entry.timestamp)}'), + selo: Text(entry.result), ), ), ], @@ -645,6 +711,46 @@ Widget _page(List children) => ListView( ], ); +/// `ListTile` cujo `trailing` desce para baixo da descrição em tela estreita. +/// +/// Um `trailing` largo ("Sem ACS ativo") comia ~140dp dos ~320dp úteis de um +/// celular e espremia o título contra a borda. Abaixo do ponto de quebra o selo +/// vira mais uma linha do conteúdo, em vez de competir por largura. +/// +/// Usado pelas telas de Microáreas e de Auditoria, que tinham exatamente o +/// mesmo formato e o mesmo problema. +class _LinhaComSelo extends StatelessWidget { + const _LinhaComSelo({required this.titulo, required this.descricao, required this.selo, this.icone}); + + final Widget titulo; + final Widget descricao; + final Widget selo; + final Widget? icone; + + @override + Widget build(BuildContext context) => LayoutBuilder( + builder: (context, constraints) { + final compacto = constraints.maxWidth < AdminBreakpoints.stacked; + return ListTile( + leading: icone, + title: titulo, + isThreeLine: compacto, + subtitle: compacto + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + descricao, + const SizedBox(height: 8), + Align(alignment: Alignment.centerLeft, child: selo), + ], + ) + : descricao, + trailing: compacto ? null : selo, + ); + }, + ); +} + class _InfoRow extends StatelessWidget { const _InfoRow(this.label, this.value); @@ -654,35 +760,83 @@ class _InfoRow extends StatelessWidget { @override Widget build(BuildContext context) => Padding( padding: const EdgeInsets.symmetric(vertical: 5), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded(child: Text(label)), - const SizedBox(width: 12), - Text(value, textAlign: TextAlign.end, style: const TextStyle(fontWeight: FontWeight.bold)), - ], + child: LayoutBuilder( + builder: (context, constraints) { + final valor = Text(value, style: const TextStyle(fontWeight: FontWeight.bold)); + // Rótulos como "TMRAV (tempo médio de resposta)" quebram em três + // linhas ao lado do valor num celular. Empilhados, o rótulo usa a + // largura toda e lê como uma frase. + if (constraints.maxWidth < AdminBreakpoints.stacked) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [Text(label), valor], + ); + } + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(child: Text(label)), + const SizedBox(width: 12), + Text(value, textAlign: TextAlign.end, style: const TextStyle(fontWeight: FontWeight.bold)), + ], + ); + }, ), ); } class _Header extends StatelessWidget implements PreferredSizeWidget { - const _Header(this.eyebrow, this.title); + const _Header(this.eyebrow, this.title, {required this.height}); final String eyebrow; final String title; + /// Calculada por quem monta o Scaffold, via [adminHeaderHeight] — ver o + /// porquê lá: `preferredSize` não tem acesso ao `BuildContext`. + final double height; + @override - Size get preferredSize => const Size.fromHeight(72); + Size get preferredSize => Size.fromHeight(height); @override - Widget build(BuildContext context) => AppBar( - title: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(eyebrow.toUpperCase(), style: const TextStyle(fontSize: 10, color: AdminColors.accent, fontWeight: FontWeight.bold)), - Text(title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), - ], + Widget build(BuildContext context) { + // O selo é informação, não controle. Em tela estreita ele disputa espaço + // com duas linhas de título num AppBar, então vira ícone — mantendo o + // rótulo para leitores de tela, que é o que de fato carrega o significado. + final estreito = MediaQuery.sizeOf(context).width < AdminBreakpoints.stacked; + return AppBar( + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + eyebrow.toUpperCase(), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 10, color: AdminColors.accent, fontWeight: FontWeight.bold), + ), + Text( + title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + ], + ), + actions: [ + Padding( + padding: const EdgeInsets.only(right: 12), + child: estreito + ? Tooltip( + message: 'Acesso auditado', + child: Semantics( + label: 'Acesso auditado', + child: const Icon(key: Key('admin_audit_badge'), Icons.verified_user_outlined), + ), + ) + : const Chip(key: Key('admin_audit_badge'), label: Text('Acesso auditado')), ), - actions: const [Padding(padding: EdgeInsets.only(right: 12), child: Chip(label: Text('Acesso auditado')))], - ); + ], + ); + } } diff --git a/apps/admin/pubspec.lock b/apps/admin/pubspec.lock index 4fd6608..6c22124 100644 --- a/apps/admin/pubspec.lock +++ b/apps/admin/pubspec.lock @@ -49,11 +49,24 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.3" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_driver: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" flutter_lints: dependency: "direct dev" description: @@ -67,6 +80,16 @@ packages: description: flutter source: sdk version: "0.0.0" + fuchsia_remote_debug_protocol: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + integration_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" leak_tracker: dependency: transitive description: @@ -131,6 +154,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + platform: + dependency: transitive + description: + name: platform + sha256: a36d119c13416516a7b5913fbe8af8531e11633d784c550b2125f76c758524ec + url: "https://pub.dev" + source: hosted + version: "3.2.0" + process: + dependency: transitive + description: + name: process + sha256: "4242ba3508d37e01808bdf71ad1d5bb93a8d671bf2e7450e6b1b353fb0808891" + url: "https://pub.dev" + source: hosted + version: "5.0.6" sky_engine: dependency: transitive description: flutter @@ -168,6 +207,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + sync_http: + dependency: transitive + description: + name: sync_http + sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" + url: "https://pub.dev" + source: hosted + version: "0.3.1" term_glyph: dependency: transitive description: @@ -200,6 +247,22 @@ packages: url: "https://pub.dev" source: hosted version: "15.3.0" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + webdriver: + dependency: transitive + description: + name: webdriver + sha256: "28b82ec894fed45dd71c23ba62d1af973ed97dd59a4f5790a4d38b0b13e5657e" + url: "https://pub.dev" + source: hosted + version: "3.2.0" sdks: - dart: ">=3.10.0-0 <4.0.0" + dart: ">=3.10.0 <4.0.0" flutter: ">=3.18.0-18.0.pre.54" diff --git a/apps/admin/pubspec.yaml b/apps/admin/pubspec.yaml index 350b27d..281286c 100644 --- a/apps/admin/pubspec.yaml +++ b/apps/admin/pubspec.yaml @@ -15,6 +15,8 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter + integration_test: + sdk: flutter flutter_lints: ^3.0.0 flutter: diff --git a/apps/admin/test/admin_header_test.dart b/apps/admin/test/admin_header_test.dart new file mode 100644 index 0000000..0758a00 --- /dev/null +++ b/apps/admin/test/admin_header_test.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'support/layout_harness.dart'; + +/// O cabeçalho carrega duas linhas de texto e o selo "Acesso auditado". +/// +/// Em 72dp fixos de altura e menos de 400dp de largura os três competem pelo +/// mesmo espaço. O selo é informativo, não um controle, então em tela estreita +/// ele pode virar ícone — desde que continue anunciado a leitores de tela, que +/// é o que `Semantics(label:)` garante. +void main() { + testWidgets('mostra o selo de acesso auditado como ícone em tela estreita', (tester) async { + await abrirBackoffice(tester, tamanho: const Size(360, 800)); + + expect(find.byKey(const Key('admin_audit_badge')), findsOneWidget); + expect(tester.widget(find.byKey(const Key('admin_audit_badge'))), isA()); + expect(find.byType(Chip), findsNothing); + }); + + testWidgets('o selo continua anunciado a leitores de tela quando vira ícone', (tester) async { + await abrirBackoffice(tester, tamanho: const Size(360, 800)); + + expect( + find.bySemanticsLabel('Acesso auditado'), + findsOneWidget, + reason: 'trocar o rótulo por um ícone não pode remover a informação de quem usa leitor de tela', + ); + }); + + testWidgets('mostra o selo de acesso auditado como chip em tela larga', (tester) async { + await abrirBackoffice(tester, tamanho: const Size(1024, 768)); + + expect(find.byKey(const Key('admin_audit_badge')), findsOneWidget); + expect(tester.widget(find.byKey(const Key('admin_audit_badge'))), isA()); + }); +} diff --git a/apps/admin/test/alerts_filters_layout_test.dart b/apps/admin/test/alerts_filters_layout_test.dart new file mode 100644 index 0000000..f1bba93 --- /dev/null +++ b/apps/admin/test/alerts_filters_layout_test.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'support/layout_harness.dart'; + +/// Os dois filtros de Alertas ficavam sempre lado a lado. +/// +/// Em 360dp cada `Expanded` recebia ~170dp: o rótulo do campo e o valor +/// selecionado disputavam o mesmo espaço e "Microárea 12 — Zona Rural" virava +/// reticências quase inteiras. `isExpanded` e `TextOverflow.ellipsis` +/// impediam o estouro, então nenhum teste de overflow pegaria isto — é um +/// defeito de legibilidade, e precisa da sua própria asserção. +void main() { + Offset posicaoDe(WidgetTester tester, String chave) => tester.getTopLeft(find.byKey(Key(chave))); + + testWidgets('empilha os filtros de microárea e status abaixo de 480dp', (tester) async { + await abrirBackoffice(tester, tamanho: const Size(360, 800)); + await irPara(tester, 'Alertas'); + + final microArea = posicaoDe(tester, 'alerts_micro_area_filter'); + final status = posicaoDe(tester, 'alerts_status_filter'); + + expect(status.dy, greaterThan(microArea.dy), reason: 'o filtro de status deveria estar abaixo, não ao lado'); + expect(status.dx, equals(microArea.dx), reason: 'empilhados, os dois começam na mesma margem'); + }); + + testWidgets('mantém os filtros lado a lado no layout largo', (tester) async { + await abrirBackoffice(tester, tamanho: const Size(1024, 768)); + await irPara(tester, 'Alertas'); + + final microArea = posicaoDe(tester, 'alerts_micro_area_filter'); + final status = posicaoDe(tester, 'alerts_status_filter'); + + expect(status.dy, equals(microArea.dy)); + expect(status.dx, greaterThan(microArea.dx)); + }); +} diff --git a/apps/admin/test/audit_timestamp_test.dart b/apps/admin/test/audit_timestamp_test.dart new file mode 100644 index 0000000..9cf36f6 --- /dev/null +++ b/apps/admin/test/audit_timestamp_test.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'support/layout_harness.dart'; + +/// A tela de auditoria imprimia `DateTime.toString()` cru. +/// +/// Em arquivo próprio, e não somado a `audit_log_screen_test.dart`, para que os +/// testes que já existiam continuem servindo de gabarito intocado do +/// comportamento desktop. +/// +/// Não usa `intl`: o backoffice não tem nenhuma dependência externa hoje, e um +/// formato pt-BR fixo basta — internacionalização não está no escopo. +void main() { + testWidgets('formata o horário do log como dd/MM/aaaa HH:mm', (tester) async { + await abrirBackoffice(tester, tamanho: const Size(1024, 768)); + await irPara(tester, 'Auditoria'); + + expect( + find.textContaining('.000'), + findsNothing, + reason: 'milissegundos e sufixo Z são ruído de DateTime.toString(), não informação de auditoria', + ); + expect(find.textContaining(RegExp(r'\d{2}/\d{2}/\d{4} \d{2}:\d{2}')), findsWidgets); + }); +} diff --git a/apps/admin/test/layout_harness_sanity_test.dart b/apps/admin/test/layout_harness_sanity_test.dart new file mode 100644 index 0000000..fc02f4f --- /dev/null +++ b/apps/admin/test/layout_harness_sanity_test.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'support/layout_harness.dart'; + +/// Prova que o detector de estouro do harness realmente detecta. +/// +/// Sem este teste, um harness cego — checando antes da pintura, ou esquecendo +/// de chamar `takeException` — faria toda a suíte responsiva passar em verde +/// sem verificar nada, que é a falha mais cara possível aqui: ela não parece +/// uma falha. +void main() { + testWidgets('esperarSemEstouroDeLayout falha diante de um estouro proposital', (tester) async { + addTearDown(tester.view.reset); + tester.view.devicePixelRatio = 1.0; + tester.view.physicalSize = const Size(200, 400); + + await tester.pumpWidget(const MaterialApp( + home: Scaffold(body: Row(children: [SizedBox(width: 500, height: 20)])), + )); + await tester.pumpAndSettle(); + + expect( + () => esperarSemEstouroDeLayout(tester, 'estouro proposital'), + throwsA(isA()), + ); + }); +} diff --git a/apps/admin/test/responsive_layout_test.dart b/apps/admin/test/responsive_layout_test.dart new file mode 100644 index 0000000..2563c54 --- /dev/null +++ b/apps/admin/test/responsive_layout_test.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:sinalacs_admin/app/admin_layout.dart'; + +import 'support/layout_harness.dart'; + +/// Prova que as quatro telas cabem na janela, do celular ao desktop. +/// +/// O backoffice nasceu desktop-first (`spec/PRD_system.md` §2.1) e até agora só +/// rodava em web; o único teste responsivo que existia checava qual barra de +/// navegação aparece, nada sobre o conteúdo. Ver `layout_harness.dart` para o +/// porquê de cada cuidado na detecção de estouro. +void main() { + for (final (largura, altura) in const [(360.0, 800.0), (400.0, 800.0), (768.0, 1024.0), (1024.0, 768.0)]) { + testWidgets('não estoura o layout em nenhuma das quatro telas a ${largura.toInt()}x${altura.toInt()}', (tester) async { + await abrirBackoffice(tester, tamanho: Size(largura, altura)); + await percorrerBackofficeInteiro(tester, '${largura.toInt()}x${altura.toInt()}'); + }); + } + + testWidgets('não estoura o rail de navegação em paisagem de celular (800x360)', (tester) async { + // 800dp de largura passa de AdminBreakpoints.rail, então o NavigationRail + // entra — mas sobram ~288dp de altura depois do cabeçalho. `NavigationRail` + // só rola com `scrollable: true`, que não é o default; com os quatro + // destinos atuais o conteúdo cabe por poucos pixels. É uma folga que some + // ao acrescentar um quinto destino — daí o caso seguinte, com fonte + // ampliada, que é onde a margem acaba de verdade. + await abrirBackoffice(tester, tamanho: const Size(800, 360)); + + expect(find.byKey(const Key('admin_navigation_rail')), findsOneWidget); + await percorrerBackofficeInteiro(tester, 'paisagem de celular'); + }); + + testWidgets('não estoura o rail em paisagem de celular com fonte a 150%', (tester) async { + await abrirBackoffice(tester, tamanho: const Size(800, 360), escalaDeFonte: 1.5); + + expect(find.byKey(const Key('admin_navigation_rail')), findsOneWidget); + await percorrerBackofficeInteiro(tester, 'paisagem de celular com fonte a 150%'); + }); + + testWidgets('mantém o NavigationRail no tablet em 1280x800 (desktop-first preservado)', (tester) async { + await abrirBackoffice(tester, tamanho: const Size(1280, 800)); + + expect(find.byKey(const Key('admin_navigation_rail')), findsOneWidget); + expect(find.byKey(const Key('admin_navigation_bar')), findsNothing); + await percorrerBackofficeInteiro(tester, 'tablet 1280x800'); + }); + + testWidgets('troca para NavigationBar exatamente no ponto de quebra declarado', (tester) async { + await abrirBackoffice(tester, tamanho: const Size(AdminBreakpoints.rail, 800)); + expect(find.byKey(const Key('admin_navigation_rail')), findsOneWidget); + + redimensionar(tester, const Size(AdminBreakpoints.rail - 1, 800)); + await tester.pumpAndSettle(); + expect(find.byKey(const Key('admin_navigation_bar')), findsOneWidget); + }); +} diff --git a/apps/admin/test/support/layout_harness.dart b/apps/admin/test/support/layout_harness.dart new file mode 100644 index 0000000..44f8053 --- /dev/null +++ b/apps/admin/test/support/layout_harness.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:sinalacs_admin/app/app.dart'; +import 'package:sinalacs_admin/core/data/admin_data_source.dart'; + +/// Ferramentas para provar que uma tela cabe na janela em que foi posta. +/// +/// Detectar estouro de layout em teste de widget tem três armadilhas, e errar +/// qualquer uma produz um teste que passa sempre — pior do que não ter teste: +/// +/// 1. `RenderFlex` só denuncia o estouro quando **pinta** o listrado amarelo e +/// preto (`DebugOverflowIndicatorMixin.paintOverflowIndicator` chamando +/// `FlutterError.reportError`). Checar antes de `pumpAndSettle()` não vê +/// nada. +/// 2. Item de `ListView` fora da viewport não é construído, logo não pinta e +/// nunca reclama. Todas as telas do backoffice são `ListView` na raiz, então +/// checar só o primeiro frame cobre a primeira dobra e mais nada — daí +/// [percorrerTelaInteira]. +/// 3. `takeException()` consome **uma** exceção por chamada. Duas telas +/// estourando com uma única checagem no fim reportariam uma só. + +/// Nomes dos quatro destinos, na ordem de `AdminDestination`. +const destinosDoBackoffice = ['Indicadores', 'Microáreas', 'Alertas', 'Auditoria']; + +/// Abre o backoffice já logado, numa janela de tamanho e escala de fonte fixos. +/// +/// [tamanho] é em pixels lógicos (dp) porque `devicePixelRatio` é forçado a 1. +Future abrirBackoffice( + WidgetTester tester, { + required Size tamanho, + double escalaDeFonte = 1.0, + AdminDataSource? dataSource, +}) async { + redimensionar(tester, tamanho, escalaDeFonte: escalaDeFonte); + addTearDown(tester.view.reset); + addTearDown(tester.platformDispatcher.clearTextScaleFactorTestValue); + + await tester.pumpWidget(SinalAdminApp(dataSource: dataSource, devLoginEnabled: true)); + + // `ensureVisible` antes do tap não é zelo: numa janela baixa (celular em + // paisagem, 360dp de altura) o botão fica abaixo da dobra e `tap` acerta o + // vazio sem lançar nada — os testes seguiriam medindo a tela de login + // achando que estavam no backoffice. + final entrar = find.byKey(const Key('login_button')); + await tester.ensureVisible(entrar); + await tester.pumpAndSettle(); + await tester.tap(entrar); + await tester.pumpAndSettle(); +} + +/// Troca o tamanho da janela de uma sessão já aberta — usado para girar a tela. +void redimensionar(WidgetTester tester, Size tamanho, {double escalaDeFonte = 1.0}) { + tester.view.devicePixelRatio = 1.0; + tester.view.physicalSize = tamanho; + tester.platformDispatcher.textScaleFactorTestValue = escalaDeFonte; +} + +/// Falha se o frame recém-pintado reportou estouro de layout. +/// +/// Precisa vir depois de `pumpAndSettle()` — ver armadilha 1 no topo. +void esperarSemEstouroDeLayout(WidgetTester tester, String contexto) { + final erro = tester.takeException(); + expect(erro, isNull, reason: 'estouro de layout em $contexto: $erro'); +} + +/// Rola a tela até o fim, checando estouro a cada passo. +/// +/// Sem isso só a primeira dobra seria coberta — ver armadilha 2 no topo. +Future percorrerTelaInteira(WidgetTester tester, String contexto) async { + esperarSemEstouroDeLayout(tester, '$contexto (topo)'); + final lista = find.byType(Scrollable).last; + for (var passo = 1; passo <= 8; passo++) { + await tester.drag(lista, const Offset(0, -320)); + await tester.pumpAndSettle(); + esperarSemEstouroDeLayout(tester, '$contexto (rolagem $passo)'); + } +} + +/// Visita os quatro destinos, percorrendo cada um por inteiro. +Future percorrerBackofficeInteiro(WidgetTester tester, String contexto) async { + for (final destino in destinosDoBackoffice) { + await irPara(tester, destino); + await percorrerTelaInteira(tester, '$contexto / $destino'); + } +} + +Future irPara(WidgetTester tester, String destino) async { + final alvo = find.text(destino).last; + await tester.ensureVisible(alvo); + await tester.pumpAndSettle(); + await tester.tap(alvo); + await tester.pumpAndSettle(); + esperarSemEstouroDeLayout(tester, 'navegação para $destino'); +} + +/// Complemento de [esperarSemEstouroDeLayout] para o caso que ele não pega: +/// um widget mais largo que a janela sem `Flex` intermediário que reclame. +void esperarCaberNaLargura(WidgetTester tester, Finder alvo, String contexto) { + final larguraDaJanela = tester.view.physicalSize.width / tester.view.devicePixelRatio; + expect(tester.getSize(alvo).width, lessThanOrEqualTo(larguraDaJanela), reason: contexto); +} diff --git a/apps/admin/test/text_scale_test.dart b/apps/admin/test/text_scale_test.dart new file mode 100644 index 0000000..4a84f0f --- /dev/null +++ b/apps/admin/test/text_scale_test.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'support/layout_harness.dart'; + +/// WCAG 1.4.4 exige que o conteúdo sobreviva a 200% de escala de texto. +/// +/// O caminho é o layout aguentar, e não `MediaQuery.withClampedTextScaling`: +/// limitar a escala resolve o estouro desobedecendo à preferência de acessi- +/// bilidade de quem precisa dela. +void main() { + for (final escala in const [1.3, 2.0]) { + testWidgets('não estoura o layout com fonte a ${(escala * 100).toInt()}% em 360x800', (tester) async { + await abrirBackoffice(tester, tamanho: const Size(360, 800), escalaDeFonte: escala); + await percorrerBackofficeInteiro(tester, 'fonte a ${(escala * 100).toInt()}%'); + }); + } + + testWidgets('o cabeçalho cresce quando a escala de fonte aumenta em tempo de execução', (tester) async { + // Mudar a escala com o app aberto é o caso real: no Android a preferência + // de tamanho de fonte muda em Configurações, com o app já em segundo plano. + await abrirBackoffice(tester, tamanho: const Size(360, 800)); + final alturaPadrao = tester.getSize(find.byType(AppBar)).height; + + redimensionar(tester, const Size(360, 800), escalaDeFonte: 2.0); + await tester.pumpAndSettle(); + final alturaAmpliada = tester.getSize(find.byType(AppBar)).height; + + expect(alturaAmpliada, greaterThan(alturaPadrao)); + esperarSemEstouroDeLayout(tester, 'cabeçalho com fonte a 200%'); + }); +} diff --git a/apps/admin/test/touch_targets_test.dart b/apps/admin/test/touch_targets_test.dart new file mode 100644 index 0000000..ac87f22 --- /dev/null +++ b/apps/admin/test/touch_targets_test.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:sinalacs_admin/app/app.dart'; +import 'package:sinalacs_admin/core/data/mock_admin_data_source.dart'; + +import 'support/failing_admin_data_source.dart'; +import 'support/layout_harness.dart'; + +/// WCAG 2.5.5, na régua de `spec/ux_accessibility_assessment.md`: 48dp. +/// +/// Os apps ACS e paciente já fixam `minimumSize` nos botões; o backoffice +/// nunca precisou porque era só web com mouse. Em celular o default do +/// Material 3 para `OutlinedButton`/`TextButton` é 40dp de altura — abaixo do +/// mínimo. A correção mora no tema, não no widget, senão o próximo botão +/// adicionado nasce fora da régua de novo. +void main() { + const alturaMinima = 48.0; + + testWidgets('o botão de tentar novamente tem pelo menos 48dp de altura', (tester) async { + final dataSource = FailingAdminDataSource(inner: MockAdminDataSource())..failNextIndicators = true; + + await abrirBackoffice(tester, tamanho: const Size(360, 800), dataSource: dataSource); + + expect(find.text('Tentar novamente'), findsOneWidget); + final botao = tester.getSize(find.widgetWithText(OutlinedButton, 'Tentar novamente')); + expect(botao.height, greaterThanOrEqualTo(alturaMinima)); + }); + + testWidgets('o botão de entrar tem pelo menos 48dp de altura', (tester) async { + redimensionar(tester, const Size(360, 800)); + addTearDown(tester.view.reset); + + await tester.pumpWidget(SinalAdminApp(devLoginEnabled: true)); + await tester.pumpAndSettle(); + + final botao = tester.getSize(find.byKey(const Key('login_button'))); + expect(botao.height, greaterThanOrEqualTo(alturaMinima)); + }); + + testWidgets('os destinos da navegação inferior têm pelo menos 48dp de altura', (tester) async { + await abrirBackoffice(tester, tamanho: const Size(360, 800)); + + final barra = tester.getSize(find.byKey(const Key('admin_navigation_bar'))); + expect(barra.height, greaterThanOrEqualTo(alturaMinima)); + }); +} diff --git a/docs/screenshots/admin/06-android-retrato.png b/docs/screenshots/admin/06-android-retrato.png new file mode 100644 index 0000000000000000000000000000000000000000..e5d5c467c668de3cd0ca3485e4f6a4e8ff47d730 GIT binary patch literal 182168 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
      X{n7;`rxGLbD-0>m&Wy>SeHJUI)oH12QdtP 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�(r^*d?VmzLLysi+COK<@v2 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 literal 0 HcmV?d00001 diff --git a/docs/screenshots/admin/07-android-paisagem.png b/docs/screenshots/admin/07-android-paisagem.png new file mode 100644 index 0000000000000000000000000000000000000000..03587f7850d0486b6bba9cb229e16ebbe107835e GIT binary patch literal 135210 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

      A!L7FZV@b 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 literal 0 HcmV?d00001 diff --git a/docs/telas-admin.md b/docs/telas-admin.md index a490d6b..50127ec 100644 --- a/docs/telas-admin.md +++ b/docs/telas-admin.md @@ -1,10 +1,10 @@ # Telas do Backoffice Admin -Documentação visual do protótipo Flutter do backoffice administrativo (`apps/admin`). Assim como `docs/telas-acs.md` e `docs/telas-paciente.md`, as imagens abaixo foram capturadas rodando o app (`flutter run -d web-server`, já que o alvo é Flutter Web) com dados sintéticos. +Documentação visual do protótipo Flutter do backoffice administrativo (`apps/admin`). Assim como `docs/telas-acs.md` e `docs/telas-paciente.md`, as imagens abaixo foram capturadas rodando o app com dados sintéticos: as cinco primeiras em Flutter Web (`flutter run -d web-server`), as da seção *Layout em celular* num emulador Android (`flutter run -d emulator-5554`). ## Navegação -O backoffice é desktop-first (`spec/PRD_system.md` §2.1): em telas ≥640px de largura a navegação usa `NavigationRail` lateral; abaixo disso, `NavigationBar` inferior — mesmo `ThemeData`, só muda o container de navegação. Os quatro destinos são **Indicadores**, **Microáreas**, **Alertas** e **Auditoria**. +O backoffice é desktop-first (`spec/PRD_system.md` §2.1): acima de `AdminBreakpoints.rail` (640dp) a navegação usa `NavigationRail` lateral; abaixo disso, `NavigationBar` inferior — mesmo `ThemeData`, só muda o container de navegação. Os quatro destinos são **Indicadores**, **Microáreas**, **Alertas** e **Auditoria**. Os pontos de quebra são constantes nomeadas em [`apps/admin/lib/app/admin_layout.dart`](../apps/admin/lib/app/admin_layout.dart). ## Login institucional (ambiente de desenvolvimento) @@ -38,9 +38,28 @@ Lista de alertas filtrável por microárea e status. Não há nenhum controle de Log somente leitura. Toda visita às telas de Microáreas, Alertas e Auditoria registra uma entrada própria via `AdminDataSource.recordAccess`, simulando o requisito do PRD §4.2.2 de que o acesso do Administrador também é auditado. +## Layout em celular (Android) + +O app roda em Android desde que a plataforma foi adicionada. Mobile é **complemento do desktop, não substituição**: o layout com rail continua sendo o padrão, e abaixo de `AdminBreakpoints.stacked` (480dp) o que mudaria de significado ao ser espremido passa a empilhar. + +![Backoffice em celular, retrato](screenshots/admin/06-android-retrato.png) + +Em retrato: `NavigationBar` inferior com os quatro destinos; o selo "Acesso auditado" do cabeçalho vira ícone (mantendo o rótulo para leitores de tela, via `Semantics`); e o `Chip` de vínculo do ACS desce para baixo da descrição em vez de disputar largura com o título — antes, como `trailing` de um `ListTile`, "Sem ACS ativo" comia ~140dp dos ~320dp úteis. + +![Backoffice em celular, paisagem](screenshots/admin/07-android-paisagem.png) + +Em paisagem o aparelho passa dos 640dp e o `NavigationRail` volta, junto com o chip e com os dois filtros de Alertas lado a lado. Sobram ~288dp de altura para quatro destinos rotulados: cabe por poucos pixels com fonte padrão e estoura a partir de ~150%, então o rail usa `scrollable: true`. A rotação preserva o destino e os filtros selecionados, porque o `AndroidManifest.xml` declara `configChanges` para `orientation` e `fontScale`. + +![Backoffice em tablet](screenshots/admin/08-tablet-rail.png) + +Em tablet (AVD `Medium_Tablet`, 2560x1600) o layout é indistinguível do web: rail lateral, chip no cabeçalho, filtros lado a lado. É essa a checagem de que o desktop-first não regrediu ao ganhar o layout compacto. + +Validado no emulador em retrato, paisagem, em tablet e com a fonte do sistema a 200% (WCAG 1.4.4), sem nenhum estouro de layout. A régua automatizada correspondente está em `apps/admin/test/responsive_layout_test.dart` e `text_scale_test.dart`; `apps/admin/integration_test/` repete o percurso no runtime real do Android e é **hermético** — não precisa da stack Docker, ao contrário dos apps ACS e do paciente. + ## Referências - [Implementação Flutter](../apps/admin/lib/app/app.dart) +- [Pontos de quebra e altura do cabeçalho](../apps/admin/lib/app/admin_layout.dart) - [Camada de dados](../apps/admin/lib/core/data/admin_data_source.dart) - [Protótipos de referência](../spec/ui_acs) (linguagem visual — não há protótipo HTML específico do admin ainda) - [Guia visual](../spec/ui_design.md) From a9dcbc58abfcfc8c88f946d609c33010e3420779 Mon Sep 17 00:00:00 2001 From: Herbert Rocha Date: Wed, 16 Sep 2026 10:15:14 -0400 Subject: [PATCH 43/43] fix(admin): corrigir contraste WCAG 1.4.3 de red/accent como texto MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit O backoffice repetia um defeito que o ACS e o app do paciente já haviam corrigido: usar a mesma cor como preenchimento e como texto. As duas funções têm exigências opostas — AdminColors.red (#DC2626) é um ótimo fundo de botão (branco por cima dá 4,83:1) e um péssimo texto sobre card (3,04:1, contra o mínimo de 4,5:1 da WCAG 1.4.3). O admin nunca tinha passado por essa auditoria: spec/ux_accessibility_assessment.md avaliava só apps/patient e apps/acs. Medido com a mesma fórmula de apps/acs/test/support/contrast.dart: scaffold #030712 appbar #111827 card #1F2937 accent 3,20 2,82 falha 2,33 falha red 4,17 3,67 3,04 falha yellow 9,37 8,26 6,83 ok green 7,94 6,99 5,79 ok yellow/green já passavam e não ganharam variante, replicando a decisão do ACS. Correção à premissa original da issue: "o mesmo par de tokens de acs_theme.dart" vale para o vermelho (#DC2626 é idêntico nos dois apps, redOnSurface = #F87171 serve literalmente), mas não para o accent — o admin usa indigo #4F46E5, o ACS usa azul #2563EB. Copiar o backoffice, deixando indigo e azul lado a lado no mesmo banner, onde existentes é usar o passo -400 da mesma cor do fill -600 (#DC2626→#F87171, #2563EB→#60A5FA); aplicada ao admin dá indigo-400 scaffold). lib/app/admin_theme.dart - AdminColors.redOnSurface (#F87171) e accentOnSurface (#818CF8), com o comentário registrando as razões medidas e por que accentOnSurface não é o valor literal do ACS. - adminOnSurface(fill), helper top-level espelhando acsOnSurface(): converte fill em variante de texto no ponto único onde riskColor() alimenta tanto um backgroundColor/borda quanto um TextStyle. lib/app/app.dart — quatro pontos de texto/ícone corrigidos: - ícone do banner "Ambiente de desenvolvimento" (sobre surfaceRaised) - _CounterCard: label do contador de risco (sobre surfaceRaised) - "Risco: ..." nos cards de alerta (sobre surfaceRaised, dentro do Card) — o caso citado na issue original - eyebrow do cabeçalho (sobre a AppBar/surface, não sobre card — é por isso que accentOnSurface precisa passar nas duas superfícies) Preenchimentos e bordas não tocados: fundo/borda do banner, faixa lateral de 4px dos cards de risco, fundo do Chip de vínculo ACS, e o switch de riskColor(), que continua devolvendo o fill puro. test/support/contrast.dart (novo, copiado sem alteração de apps/acs/test/support/) e test/contrast_tokens_test.dart (novo, 17 testes no formato de apps/acs/test/contrast_tokens_test.dart): matriz token×superfície com os pares realmente renderizados, mais testes de documentação que provam que o fill puro falha como texto e que registram por que #60A5FA do ACS foi descartado apesar de passar matematicamente. Os 16 arquivos de teste anteriores não foram tocados — nenhum referenciava AdminColors ou riskColor. Documentação: spec/ux_accessibility_assessment.md passa a cobrir os três apps (escopo, matriz medida, tabela de tokens novos); CLAUDE.md e AGENTS.md citam admin_theme.dart/adminOnSurface() ao lado dos equivalentes do ACS e do paciente. Validação: flutter analyze limpo, 56/56 testes (40 existentes + 16 novos). Confirmado visualmente no emulador Android — "Risco: Vermelho" na tela de Alertas passou de vermelho escuro quase ilegível para coral claro nitidamente legível, com a faixa lateral de 4px mantendo o vermelho saturado original. Signed-off-by: Herbert Rocha --- AGENTS.md | 4 +- CLAUDE.md | 4 +- apps/admin/lib/app/admin_theme.dart | 27 ++++++++ apps/admin/lib/app/app.dart | 8 +-- apps/admin/test/contrast_tokens_test.dart | 80 +++++++++++++++++++++++ apps/admin/test/support/contrast.dart | 40 ++++++++++++ spec/ux_accessibility_assessment.md | 34 +++++++--- 7 files changed, 180 insertions(+), 17 deletions(-) create mode 100644 apps/admin/test/contrast_tokens_test.dart create mode 100644 apps/admin/test/support/contrast.dart diff --git a/AGENTS.md b/AGENTS.md index c020bdd..4ffc33c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,7 +24,7 @@ Antes de mexer em produto, arquitetura ou comportamento, consulte primeiro: - [spec/ui_design.md](spec/ui_design.md) — linguagem visual e UX; - [spec/lgpd_design.md](spec/lgpd_design.md) — privacidade e LGPD; - [spec/lgpd_data_audit.md](spec/lgpd_data_audit.md) — classificação de sensibilidade LGPD, campo a campo, de todas as tabelas persistidas; -- [spec/ux_accessibility_assessment.md](spec/ux_accessibility_assessment.md) — auditoria WCAG 2.2 AA (contraste, alvo de toque, semântica) dos apps ACS e paciente; +- [spec/ux_accessibility_assessment.md](spec/ux_accessibility_assessment.md) — auditoria WCAG 2.2 AA (contraste, alvo de toque, semântica) dos apps ACS, paciente e admin; - [spec/ux_ui_test_plan.md](spec/ux_ui_test_plan.md) — plano de testes de UX/UI derivado de `spec/ui_design.md`; - [CLAUDE.md](CLAUDE.md) — guia técnico e comandos para IA; é a referência mais atualizada do repositório; - [PROGRESS.md](PROGRESS.md) — status dos milestones e histórico de migração; @@ -96,7 +96,7 @@ Não violar estes pontos sob qualquer hipótese: - Manter dark mode, alta legibilidade e baixo ruído visual. - Usar cores apenas para sinal clínico; não decorar interfaces com vermelho/amarelo/verde sem relação com risco. - Manter foco em mobile-first e acessibilidade. -- Ao reaproveitar uma cor clínica de preenchimento (`red`/`accent`/`danger`) como cor de texto/ícone, usar a variante `*OnSurface` (`acs_theme.dart`/`patient_theme.dart`) e medir contraste contra a superfície real (`Card`/`surfaceRaised`), não contra o fundo do Scaffold — ver `spec/ux_accessibility_assessment.md` e os testes em `test/contrast_tokens_test.dart` de cada app. +- Ao reaproveitar uma cor clínica de preenchimento (`red`/`accent`/`danger`) como cor de texto/ícone, usar a variante `*OnSurface` (`acs_theme.dart`/`patient_theme.dart`/`admin_theme.dart`) e medir contraste contra a superfície real (`Card`/`surfaceRaised`), não contra o fundo do Scaffold — ver `spec/ux_accessibility_assessment.md` e os testes em `test/contrast_tokens_test.dart` de cada app. ### 6) Quando o trabalho for de backend ou dados - Considerar uso de SQLite/SQLCipher e filas locais para operação offline. diff --git a/CLAUDE.md b/CLAUDE.md index fcf94cd..7248c60 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -18,7 +18,7 @@ Read these before making product/architecture decisions — when project docs co - [spec/ui_design.md](spec/ui_design.md) — visual language and UX behavior. - [spec/lgpd_design.md](spec/lgpd_design.md) — privacy/LGPD design. - [spec/lgpd_data_audit.md](spec/lgpd_data_audit.md) — field-by-field LGPD sensitivity classification for every persisted table (11 domain tables plus Serverpod's own). -- [spec/ux_accessibility_assessment.md](spec/ux_accessibility_assessment.md) — WCAG 2.2 AA audit (contrast, touch targets, semantics) for the ACS/patient apps; read before touching any color used as text/icon, not just fill. +- [spec/ux_accessibility_assessment.md](spec/ux_accessibility_assessment.md) — WCAG 2.2 AA audit (contrast, touch targets, semantics) for the ACS/patient/admin apps; read before touching any color used as text/icon, not just fill. - [spec/ux_ui_test_plan.md](spec/ux_ui_test_plan.md) — UX/UI test plan derived from `spec/ui_design.md` (visual/interaction behavior, complementary to the accessibility assessment). - [AGENTS.md](AGENTS.md) — full agent working rules (Portuguese), summarized below. @@ -168,7 +168,7 @@ Risk classification now comes **only** from `triage.evaluate`: the patient app's Color is a clinical signal only in these apps: red/yellow/green map strictly to `RiskLevel`, never used decoratively. -**WCAG contrast tokens**: `app/acs_theme.dart` and `app/patient_theme.dart` each carry a text-safe variant of every clinical fill color — `redOnSurface`/`accentOnSurface` (ACS) and `dangerOnSurface`/`accentOnSurface` (patient). `red`/`accent`/`danger` only clear WCAG 1.4.3's 4.5:1 as a *fill* (e.g. white text on a red button); reused as *text* color on `surfaceRaised`/`Card` — where risk/status text is actually rendered — they drop to ~3:1. `acsOnSurface()` (and its patient-app equivalent) converts fill → text color at the single point where a `switch (riskLevel)` used to feed both, instead of at every call site. `test/contrast_tokens_test.dart` in each app checks the full token×surface matrix by computing relative luminance directly (`test/support/contrast.dart`) rather than by eyeballing a contrast checker against the wrong surface — which is exactly the false positive/negative `spec/ux_accessibility_assessment.md` documents from the original manual audit. The same pass added `minimumSize` (48×52dp, 64×60dp for the SAMU emergency call) per WCAG 2.5.5 and `Semantics(liveRegion: true)` on dynamic status messages (login errors, feed/storage errors, rejected-visit counts) per WCAG 4.1.3. +**WCAG contrast tokens**: `app/acs_theme.dart`, `app/patient_theme.dart` and `app/admin_theme.dart` each carry a text-safe variant of every clinical fill color — `redOnSurface`/`accentOnSurface` (ACS), `dangerOnSurface`/`accentOnSurface` (patient), `redOnSurface`/`accentOnSurface` (admin). `red`/`accent`/`danger` only clear WCAG 1.4.3's 4.5:1 as a *fill* (e.g. white text on a red button); reused as *text* color on `surfaceRaised`/`Card` — where risk/status text is actually rendered — they drop to ~3:1. `acsOnSurface()`/`adminOnSurface()` (and the patient-app equivalent) convert fill → text color at the single point where a `switch (riskLevel)` used to feed both, instead of at every call site. The admin's `accentOnSurface` (`#818CF8`, indigo-400) is deliberately *not* the ACS's `#60A5FA`: the admin's fill is indigo `#4F46E5`, not the ACS's blue `#2563EB` — the shared rule across apps is "same color, -400 step," not a literal value, and copying the ACS constant would have passed contrast while clashing with the accent-colored border right next to it. `test/contrast_tokens_test.dart` in each app checks the full token×surface matrix by computing relative luminance directly (`test/support/contrast.dart`) rather than by eyeballing a contrast checker against the wrong surface — which is exactly the false positive/negative `spec/ux_accessibility_assessment.md` documents from the original manual audit. The same pass added `minimumSize` (48×52dp, 64×60dp for the SAMU emergency call) per WCAG 2.5.5 and `Semantics(liveRegion: true)` on dynamic status messages (login errors, feed/storage errors, rejected-visit counts) per WCAG 4.1.3. ### Admin app (`apps/admin/`) Package `sinalacs_admin`, read-only backoffice. Same skeleton as ACS/patient: `lib/main.dart` → `lib/app/app.dart` (+ `admin_theme.dart`) → `lib/core/data/`. Unlike ACS/patient it does not consume `sinalacs_client` yet: `AdminDataSource` (`lib/core/data/admin_data_source.dart`) is an abstract interface whose models (`RiskLevel`, `AlertStatus`, `DashboardIndicators`, `MicroAreaSummary`, `AlertSummary`, `AuditLogEntry`) mirror the backend's `.spy.yaml` models field-for-field, backed today only by `MockAdminDataSource` — swapping in a real implementation over `sinalacs_client` shouldn't require reshaping the screens, the same DI pattern as `PatientBackend`/`AcsBackend`. `AdminHomeShell` exposes four read-only sections: **Indicadores** (risk counters + TMRAV, the PRD's North Star metric), **Microáreas** (ACS↔microarea binding), **Alertas** (filterable by micro-area/status, with filter options sourced from the fetched data itself rather than a hardcoded list — a hardcoded list would silently drop a real micro-area as a selectable option) and **Auditoria** (an `audit_logs` viewer). Every screen touching sensitive data calls `dataSource.recordAccess(actionType: 'view', resourceType: ...)` *before* rendering it, per `spec/PRD_system.md` §4.2.2 (admin access must itself be audited) — the audit log screen audits its own access too, and a failed `recordAccess` blocks the data from rendering rather than failing open. Login is intentionally local-only, **not** wired to `auth.developmentLogin`: the backend's `auth_endpoint.dart` only accepts `role: 'patient'`/`role: 'acs'`, there is no fixed dev user for `admin`, so real auth is out of scope until the backend supports it (see the `LoginScreen` doc comment in `app.dart`). The "ambiente de desenvolvimento" banner is informational, not access control — the actual gate is `devLoginEnabled`, which defaults to `kDebugMode` and disables the login bypass outside debug builds. Layout is desktop-first per `spec/PRD_system.md` §2.1: `NavigationRail` above `AdminBreakpoints.rail`, `NavigationBar` below, same `ThemeData` either way. The breakpoints live in `lib/app/admin_layout.dart` (`rail` 640, `stacked` 480, `counterCardMin` 160) plus `adminHeaderHeight()`, which exists because `PreferredSizeWidget.preferredSize` is a getter with no `BuildContext` — without it the two-line header clips instead of growing under a large system font. Android was added after the web-only phase, and mobile is a complement, never a replacement: below `stacked` the two alert filters stack instead of sharing a row, `_LinhaComSelo` moves a `ListTile`'s `trailing` badge down into the subtitle (Microáreas and Auditoria had the same squeeze), `_InfoRow` stacks label over value, the header's "Acesso auditado" chip becomes an icon that keeps its `Semantics` label, and the counter cards use a computed grid instead of a fixed `width: 160`. `NavigationRail` is `scrollable: true` because a phone in landscape clears the 640 threshold with only ~288dp of height left — it fits by a few pixels at default font size and overflows at 150%. The guard against regressing any of this is that the ten original widget tests still pass **unedited** at 800×600; the new `test/responsive_layout_test.dart`, `text_scale_test.dart`, `alerts_filters_layout_test.dart`, `admin_header_test.dart` and `touch_targets_test.dart` drive `test/support/layout_harness.dart`, whose header documents the three ways an overflow test silently goes blind (checking before paint, never scrolling a `ListView` past the first fold, and `takeException()` consuming one exception per call) — `test/layout_harness_sanity_test.dart` proves the detector still detects. **`apps/admin/integration_test/` is hermetic**, unlike the ACS and patient ones described above: it runs on `MockAdminDataSource`, so `flutter test integration_test -d emulator-5554` needs no `docker compose`, no seed and no `--dart-define`. `apps/admin` is the only one of the three apps with Flutter Web enabled and the only one versioning `pubspec.lock` from the start (the other two adopted that convention later). `docs/telas-admin.md` documents its five screens with real screenshots, mirroring `docs/telas-acs.md`. CI covers it in the `admin-app` job, identical in shape to `acs-app`/`patient-app`. diff --git a/apps/admin/lib/app/admin_theme.dart b/apps/admin/lib/app/admin_theme.dart index c32d3fe..7075197 100644 --- a/apps/admin/lib/app/admin_theme.dart +++ b/apps/admin/lib/app/admin_theme.dart @@ -9,8 +9,35 @@ abstract final class AdminColors { static const red = Color(0xFFDC2626); static const yellow = Color(0xFFF59E0B); static const green = Color(0xFF10B981); + + /// Variantes de `red`/`accent` para uso como TEXTO/ÍCONE sobre superfície + /// escura (`surface`/`surfaceRaised`), não como preenchimento. + /// + /// `red` cai para 3,04:1 sobre `surfaceRaised` e `accent` para 2,33:1, contra + /// os 4,5:1 da WCAG 1.4.3. Como preenchimento os dois continuam corretos + /// (branco sobre `red` dá 4,83:1; sobre `accent`, 6,29:1), por isso não são + /// substituídos — só a leitura como texto muda. `yellow` e `green` não + /// precisam de variante: já passam (6,83:1 e 5,79:1) sobre `surfaceRaised`. + /// + /// `accentOnSurface` é indigo-400, e não o `#60A5FA` do ACS: o accent do + /// backoffice é indigo `#4F46E5`, não o azul `#2563EB` do ACS. A regra + /// compartilhada é o passo -400 da mesma cor, não o valor literal. + static const redOnSurface = Color(0xFFF87171); + static const accentOnSurface = Color(0xFF818CF8); } +/// Cor de texto/ícone equivalente a uma cor clínica de preenchimento. +/// +/// Ponto único de conversão: `riskColor()` em `app.dart` alimenta tanto um +/// `backgroundColor`/borda (fill) quanto, em alguns pontos, um `TextStyle` +/// direto — sem este helper cada call site teria que lembrar sozinho de +/// trocar `red`/`accent` pela variante. `yellow`/`green` retornam inalterados. +Color adminOnSurface(Color fill) => switch (fill) { + AdminColors.red => AdminColors.redOnSurface, + AdminColors.accent => AdminColors.accentOnSurface, + _ => fill, + }; + ThemeData buildAdminTheme() => ThemeData( useMaterial3: true, brightness: Brightness.dark, diff --git a/apps/admin/lib/app/app.dart b/apps/admin/lib/app/app.dart index 16d4cfe..291bb2d 100644 --- a/apps/admin/lib/app/app.dart +++ b/apps/admin/lib/app/app.dart @@ -81,7 +81,7 @@ class _LoginScreenState extends State { ), child: const Row( children: [ - Icon(Icons.science_outlined, color: AdminColors.accent), + Icon(Icons.science_outlined, color: AdminColors.accentOnSurface), SizedBox(width: 12), Expanded( child: Text( @@ -402,7 +402,7 @@ class _CounterCard extends StatelessWidget { children: [ Text(value, style: Theme.of(context).textTheme.headlineMedium?.copyWith(fontWeight: FontWeight.bold)), const SizedBox(height: 4), - Text(label, style: TextStyle(color: color, fontWeight: FontWeight.bold)), + Text(label, style: TextStyle(color: adminOnSurface(color), fontWeight: FontWeight.bold)), ], ), ); @@ -595,7 +595,7 @@ class _AlertsList extends StatelessWidget { const SizedBox(height: 4), Text(alert.microAreaName), const SizedBox(height: 6), - Text('Risco: ${riskLabel(alert.riskLevel)}', style: TextStyle(color: riskColor(alert.riskLevel), fontWeight: FontWeight.bold)), + Text('Risco: ${riskLabel(alert.riskLevel)}', style: TextStyle(color: adminOnSurface(riskColor(alert.riskLevel)), fontWeight: FontWeight.bold)), Text('Status: ${statusLabel(alert.status)}'), ], ), @@ -813,7 +813,7 @@ class _Header extends StatelessWidget implements PreferredSizeWidget { eyebrow.toUpperCase(), maxLines: 1, overflow: TextOverflow.ellipsis, - style: const TextStyle(fontSize: 10, color: AdminColors.accent, fontWeight: FontWeight.bold), + style: const TextStyle(fontSize: 10, color: AdminColors.accentOnSurface, fontWeight: FontWeight.bold), ), Text( title, diff --git a/apps/admin/test/contrast_tokens_test.dart b/apps/admin/test/contrast_tokens_test.dart new file mode 100644 index 0000000..d3c5a08 --- /dev/null +++ b/apps/admin/test/contrast_tokens_test.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:sinalacs_admin/app/admin_theme.dart'; + +import 'support/contrast.dart'; + +/// Matriz determinística de contraste (WCAG 2.1 §1.4.3), no mesmo formato de +/// `apps/acs/test/contrast_tokens_test.dart` e +/// `apps/patient/test/contrast_tokens_test.dart`. +/// +/// O admin repetia o defeito que os outros dois apps já haviam corrigido: +/// `AdminColors.red`/`accent` usados como cor de PREENCHIMENTO (botão, faixa +/// lateral de risco) também eram usados como cor de TEXTO/ÍCONE sobre +/// `Card`/`AppBar` — papéis com exigências opostas. Cada linha abaixo declara +/// a SUPERFÍCIE real onde o texto/ícone é renderizado, não só o par de cores. +void main() { + const normalText = 4.5; + const largeTextOrUi = 3.0; + + /// (rótulo, texto/ícone, superfície, limiar) — um por par realmente + /// renderizado no app. + const cases = <(String, Color, Color, double)>[ + ('branco sobre scaffold', Colors.white, AdminColors.background, normalText), + ('branco sobre card', Colors.white, AdminColors.surfaceRaised, normalText), + ('branco sobre appbar', Colors.white, AdminColors.surface, normalText), + ('yellow sobre card (badge de risco amarelo)', AdminColors.yellow, AdminColors.surfaceRaised, normalText), + ('green sobre card (badge de risco verde)', AdminColors.green, AdminColors.surfaceRaised, normalText), + // Variantes de texto que o app passa a usar em vez do fill puro (ver os + // testes de documentação abaixo para a prova de que o fill sozinho falha). + ('redOnSurface sobre card', AdminColors.redOnSurface, AdminColors.surfaceRaised, normalText), + ('accentOnSurface sobre card', AdminColors.accentOnSurface, AdminColors.surfaceRaised, normalText), + ('redOnSurface sobre scaffold', AdminColors.redOnSurface, AdminColors.background, normalText), + ('accentOnSurface sobre scaffold', AdminColors.accentOnSurface, AdminColors.background, normalText), + // O eyebrow do cabeçalho renderiza sobre a AppBar (AdminColors.surface), + // não sobre um Card — é por isso que accentOnSurface precisa passar nas + // duas superfícies, e não só sobre card. + ('accentOnSurface sobre appbar (eyebrow do cabeçalho)', AdminColors.accentOnSurface, AdminColors.surface, normalText), + // Preenchimento: texto branco sobre a cor de fundo — a prova de que não + // precisamos trocar `red`/`accent` como fill. + ('branco sobre red (faixa/chip de risco)', Colors.white, AdminColors.red, largeTextOrUi), + ('branco sobre accent (botão do colorScheme)', Colors.white, AdminColors.accent, largeTextOrUi), + ]; + + for (final (label, fg, bg, threshold) in cases) { + test(label, () { + final ratio = contrastOn(fg, bg); + expect( + ratio, + greaterThanOrEqualTo(threshold), + reason: '$label: $ratio:1 abaixo do limiar $threshold:1 exigido pela WCAG', + ); + }); + } + + // Documenta os achados originais: os TOKENS DE PREENCHIMENTO, usados como + // texto/ícone direto, falham — é por isso que redOnSurface/accentOnSurface + // existem, em vez de reaproveitar red/accent também para texto. + test('red (fill) usado como texto sobre card fica abaixo de 4,5:1', () { + expect(contrastOn(AdminColors.red, AdminColors.surfaceRaised), lessThan(normalText)); + }); + + test('accent (fill) usado como texto sobre card fica abaixo de 4,5:1', () { + expect(contrastOn(AdminColors.accent, AdminColors.surfaceRaised), lessThan(normalText)); + }); + + test('accent (fill) usado como ícone sobre card fica abaixo de 3:1', () { + // O ícone do banner "Ambiente de desenvolvimento" é WCAG 1.4.11 (limiar + // 3:1 para componente não-textual), não 1.4.3 — mas falha nos dois. + expect(contrastOn(AdminColors.accent, AdminColors.surfaceRaised), lessThan(largeTextOrUi)); + }); + + test('o accentOnSurface do ACS (#60A5FA) passaria no contraste mas troca o matiz', () { + // Documenta por que o valor não foi copiado literalmente de acs_theme.dart: + // o par (fill, texto) do admin é indigo/indigo, não indigo/azul. #60A5FA + // atinge 4,5:1 aqui também — o problema não é matemático, é de identidade + // visual, e não aparece num teste de contraste isolado. + const acsAccentOnSurface = Color(0xFF60A5FA); + expect(contrastOn(acsAccentOnSurface, AdminColors.surfaceRaised), greaterThanOrEqualTo(normalText)); + }); +} diff --git a/apps/admin/test/support/contrast.dart b/apps/admin/test/support/contrast.dart new file mode 100644 index 0000000..ea3c8ed --- /dev/null +++ b/apps/admin/test/support/contrast.dart @@ -0,0 +1,40 @@ +import 'dart:math' as math; + +import 'package:flutter/material.dart'; + +/// Luminância relativa WCAG (definição de 1.4.3), 0.0 (preto) a 1.0 (branco). +double relativeLuminance(Color color) { + double channel(double srgb) => + srgb <= 0.04045 ? srgb / 12.92 : math.pow((srgb + 0.055) / 1.055, 2.4).toDouble(); + // `Color.r/g/b` (0.0-1.0) substitui `.red/.green/.blue` (0-255), + // descontinuados nesta versão do Flutter. + return 0.2126 * channel(color.r) + 0.7152 * channel(color.g) + 0.0722 * channel(color.b); +} + +/// Razão de contraste WCAG entre duas cores OPACAS (sem canal alfa a +/// resolver). Para uma cor com alfa, componha antes com [compositeOver]. +double contrastRatio(Color a, Color b) { + final la = relativeLuminance(a); + final lb = relativeLuminance(b); + final hi = la > lb ? la : lb; + final lo = la > lb ? lb : la; + return (hi + 0.05) / (lo + 0.05); +} + +/// Resolve uma cor com transparência (`Colors.white54`, +/// `color.withValues(alpha: ...)`) contra o fundo em que ela realmente é +/// pintada — sem isto, a luminância de uma cor semitransparente não +/// corresponde ao que a tela mostra. +Color compositeOver(Color fg, Color bg) { + final a = fg.a; // 0.0-1.0 + double mix(double f, double b) => a * f + (1 - a) * b; + return Color.from( + alpha: 1.0, + red: mix(fg.r, bg.r), + green: mix(fg.g, bg.g), + blue: mix(fg.b, bg.b), + ); +} + +/// Razão de contraste já resolvendo o alfa de [fg] contra [bg]. +double contrastOn(Color fg, Color bg) => contrastRatio(compositeOver(fg, bg), bg); diff --git a/spec/ux_accessibility_assessment.md b/spec/ux_accessibility_assessment.md index 92807a7..ebd1c79 100644 --- a/spec/ux_accessibility_assessment.md +++ b/spec/ux_accessibility_assessment.md @@ -1,6 +1,6 @@ # Relatório de Avaliação de UI/UX e Acessibilidade -**Aplicativos Avaliados:** `apps/patient` e `apps/acs` +**Aplicativos Avaliados:** `apps/patient`, `apps/acs` e `apps/admin` **Referência Baseline:** PRD de Sistemas (Seção 4.3 — WCAG 2.1 Nível AA), `spec/ui_design.md` **Metodologia e Ferramentas:** Matriz de contraste determinística (`apps/*/test/contrast_tokens_test.dart`, implementando a fórmula de luminância relativa WCAG 1.4.3 contra a superfície REAL de @@ -26,7 +26,7 @@ pelo MQTT/TLS real). | Critério WCAG 2.1 | Descrição do Critério | Requisito do PRD / Issue | Status | Resumo do Diagnóstico | | :--- | :--- | :--- | :---: | :--- | | **1.4.1 Color Use** | A cor não deve ser o único indicador visual de estado/risco | Duplo canal (Texto/Ícone + Cor) em sinais clínicos de risco | **Conforme** | Rótulos explícitos `'Risco: Vermelho'`, `'Risco: Amarelo'`, `'Risco: Verde'` acompanhados de ícone, nos dois apps. | -| **1.4.3 Contrast (Minimum)** | Razão de contraste min. de 4.5:1 (texto normal) e 3:1 (texto grande/UI) | ≥ 4.5:1 para texto sobre fundo escuro nos temas | **Conforme (corrigido)** | Cinco pares de token/superfície falhavam quando medidos corretamente (§2.1); corrigidos separando token de PREENCHIMENTO de token de TEXTO (`redOnSurface`/`accentOnSurface` no ACS, `dangerOnSurface`/`accentOnSurface` no paciente). Guardado por teste determinístico. | +| **1.4.3 Contrast (Minimum)** | Razão de contraste min. de 4.5:1 (texto normal) e 3:1 (texto grande/UI) | ≥ 4.5:1 para texto sobre fundo escuro nos temas | **Conforme (corrigido)** | Cinco pares de token/superfície falhavam quando medidos corretamente (§2.1); corrigidos separando token de PREENCHIMENTO de token de TEXTO (`redOnSurface`/`accentOnSurface` no ACS, `dangerOnSurface`/`accentOnSurface` no paciente, `redOnSurface`/`accentOnSurface` no admin). Guardado por teste determinístico. | | **2.4.7 Focus Visible** | Indicador claro de foco visual ao navegar por campos interativos | Foco visível em todos os elementos selecionáveis | **Conforme** | Indicador de foco nativo do Android acompanha todos os alvos tocáveis, sem truncamento. | | **2.5.5 Target Size** | Alvo de toque adequado para interatividade | ≥ 48x48 dp (padrão), ≥ 60x60 dp (botão de emergência/pânico) | **Conforme (corrigido)** | Botão de pânico do paciente: `208x208 dp`. "Ligar para o SAMU (192)" no ACS não tinha `minimumSize` (default M3 de 40dp de altura visual) — corrigido para `64x60 dp`. Quatro outros botões de ação primária no ACS também não tinham `minimumSize` explícito; padronizados em `48x52 dp`. | | **4.1.2 Name, Role, Value** | Árvore semântica exposta para leitores de tela nativos | Rótulos e papeis em 100% dos fluxos críticos | **Conforme** | Árvore semântica nativa do Flutter expõe abas (**Área, Fila, Mapa, Visita, Mais**) e formulários com clareza; o cartão de alerta da fila passou a ser lido como uma frase única (§3, achado antigo de prioridade Baixa). | @@ -56,6 +56,13 @@ superfície onde ele é **de fato** renderizado, com composição de alfa quando | ACS | `AcsColors.red #DC2626` | **texto** | Card `#1F2937` | **3.04:1** | 4.5:1 | **Falha** (achado B) | | ACS | `AcsColors.accent #2563EB` | **texto** | Card `#1F2937` | **2.84:1** | 4.5:1 | **Falha** (achado C, não detectado antes) | | ACS | branco | texto do botão | fill `AcsColors.red` | 4.83:1 | 3:1 (UI) | Conforme — fill não muda | +| Admin | `AdminColors.yellow #F59E0B` | texto | Card `#1F2937` | 6.83:1 | 4.5:1 | Conforme | +| Admin | `AdminColors.green #10B981` | texto | Card `#1F2937` | 5.79:1 | 4.5:1 | Conforme | +| Admin | `AdminColors.red #DC2626` | **texto** | Card `#1F2937` | **3.04:1** | 4.5:1 | **Falha** (mesmo achado do ACS — `red` é idêntico nos dois apps) | +| Admin | `AdminColors.accent #4F46E5` | **texto** | Card `#1F2937` | **2.33:1** | 4.5:1 | **Falha** | +| Admin | `AdminColors.accent #4F46E5` | **ícone** | Card `#1F2937` | **2.33:1** | 3:1 (1.4.11) | **Falha** (ícone do banner "Ambiente de desenvolvimento") | +| Admin | `AdminColors.accent #4F46E5` | **texto** | AppBar `#111827` | **2.82:1** | 4.5:1 | **Falha** (eyebrow do cabeçalho) | +| Admin | branco | texto do botão | fill `AdminColors.red` | 4.83:1 | 3:1 (UI) | Conforme — fill não muda | | Paciente | `Colors.white54` | texto | Card `#1E293B` | **5.36:1** | 4.5:1 | **Conforme** — achado A do relatório anterior era falso positivo (media 3.2:1 contra o Scaffold) | | Paciente | amarelo `#E0A800` | texto | Card `#1E293B` | 6.81:1 | 4.5:1 | Conforme | | Paciente | `PatientColors.accent #0D9488` | **texto** | Card `#1E293B` | **3.91:1** | 4.5:1 | **Falha** (achado D, relatado antes como conforme por medir no Scaffold) | @@ -64,7 +71,7 @@ superfície onde ele é **de fato** renderizado, com composição de alfa quando | Paciente | branco | texto do botão | fill `PatientColors.danger` | 4.83:1 | 3:1 (UI) | Conforme — fill não muda | | — | `#EF4444` (correção sugerida pelo relatório anterior) | texto | Card `#1F2937` | **3.90:1** | 4.5:1 | **Ainda falha** — não adotada | -Reprodução: `flutter test test/contrast_tokens_test.dart` em cada app. +Reprodução: `flutter test test/contrast_tokens_test.dart` em cada um dos três apps. #### Correção aplicada: separar token de preenchimento de token de texto @@ -77,15 +84,24 @@ original intacto (ele já cumpre 3:1 com texto branco por cima): | :--- | :--- | :--- | :---: | :--- | | ACS | `AcsColors.redOnSurface` | `#F87171` | 5.31:1 | `AcsColors.red` | | ACS | `AcsColors.accentOnSurface` | `#60A5FA` | 5.77:1 | `AcsColors.accent` | +| Admin | `AdminColors.redOnSurface` | `#F87171` | 5.31:1 | `AdminColors.red` | +| Admin | `AdminColors.accentOnSurface` | `#818CF8` | 4.92:1 | `AdminColors.accent` | | Paciente | `PatientColors.dangerOnSurface` | `#F87171` | 5.29:1 | `PatientColors.danger` | | Paciente | `PatientColors.accentOnSurface` | `#2DD4BF` | 7.86:1 | `PatientColors.accent` | -`AcsColors.red`/`AcsColors.accent` e `PatientColors.danger`/`PatientColors.accent` continuam -sendo a cor de PREENCHIMENTO do botão de pânico, do botão "Ligar para o SAMU" e do botão -"Confirmar recebimento" — nenhum desses mudou de cor. Um helper único (`acsOnSurface()` no ACS) -converte a cor de preenchimento na variante de texto no ponto em que um `switch` de risco -alimenta tanto um `backgroundColor` quanto um `TextStyle`, para nunca haver dois pontos de -verdade sobre qual vermelho usar onde. +O admin não reaproveita o `#60A5FA` do ACS para `accentOnSurface`: o accent do backoffice é +indigo `#4F46E5`, não o azul `#2563EB` do ACS — copiar o valor literal passaria no contraste +(4.5:1+) mas trocaria o matiz, deixando indigo e azul lado a lado no mesmo banner, onde +`AdminColors.accent` continua como borda. `#818CF8` é o passo -400 da mesma cor do fill -600, +a mesma relação usada pelos outros dois apps. + +`AcsColors.red`/`AcsColors.accent`, `PatientColors.danger`/`PatientColors.accent` e +`AdminColors.red`/`AdminColors.accent` continuam sendo a cor de PREENCHIMENTO do botão de +pânico, do botão "Ligar para o SAMU", do botão "Confirmar recebimento" e da faixa lateral de +risco do admin — nenhum desses mudou de cor. Um helper único (`acsOnSurface()` no ACS, +`adminOnSurface()` no admin) converte a cor de preenchimento na variante de texto no ponto em +que um `switch` de risco alimenta tanto um `backgroundColor`/borda quanto um `TextStyle`, para +nunca haver dois pontos de verdade sobre qual vermelho usar onde. #### Validação de Duplo Canal (WCAG 1.4.1)