prompt inicial do projeto
This commit is contained in:
79
prompts/FASE-0-bootstrap.md
Normal file
79
prompts/FASE-0-bootstrap.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# FASE 0 — Bootstrap do monorepo
|
||||
|
||||
## Objetivo
|
||||
|
||||
Estrutura completa do monorepo, ambiente Docker subindo, CI verde. Nenhuma regra de negócio ainda — apenas o esqueleto onde tudo será construído.
|
||||
|
||||
## Pré-requisitos
|
||||
|
||||
Ler `docs/01-ARQUITETURA.md` (§3 estrutura do monorepo) e `docs/05-INFRA-DOCKER.md` (compose completo).
|
||||
|
||||
## Tarefas
|
||||
|
||||
### 1. Raiz do Gradle
|
||||
|
||||
`settings.gradle.kts` incluindo `:shared` e `:backend`. Version catalog em `gradle/libs.versions.toml` com: Kotlin 2.1+, Spring Boot 3.4+, jOOQ, Flyway, PostGIS driver, Testcontainers, ShedLock, Resilience4j, LiveKit server SDK, kotlinx.serialization, kotlinx.datetime.
|
||||
|
||||
JDK 21. Toolchain configurado explicitamente.
|
||||
|
||||
### 2. Módulo `shared/`
|
||||
|
||||
KMP com targets `jvm()`, `androidTarget()`, `iosArm64()`, `iosSimulatorArm64()`. Pacote base `br.com.portaria.shared`. Apenas kotlinx.serialization e kotlinx.datetime como dependências — **este módulo não conhece Spring, Android nem iOS**.
|
||||
|
||||
Pacotes vazios criados: `model/`, `dto/`, `state/`, `validation/`.
|
||||
|
||||
### 3. Módulo `backend/`
|
||||
|
||||
Spring Boot 3 + Kotlin, estrutura hexagonal conforme `01-ARQUITETURA.md` §3. Perfis `local`, `test`, `prod`. Actuator com `health`, `prometheus`, `info` expostos. OpenAPI via springdoc em `/v3/api-docs`.
|
||||
|
||||
Um controller `GET /api/v1/health` devolvendo `{"status":"UP"}`, só para provar a esteira ponta a ponta.
|
||||
|
||||
### 4. `web/` com pnpm workspaces
|
||||
|
||||
`pnpm-workspace.yaml` com `visitor`, `admin`, `shared-ui`. Vite + React 19 + TS em ambos os apps. `visitor` configurado como PWA.
|
||||
|
||||
`shared-ui` exporta `tokens.json` e o `tokens.css` gerado. Script `pnpm tokens:build` que gera `tokens.css` e `Theme.kt` a partir do JSON — ver `docs/04-DESIGN-SYSTEM-UX.md` §2.
|
||||
|
||||
### 5. `apps/` Compose Multiplatform
|
||||
|
||||
`composeApp` (comum), `androidApp`, `iosApp`. Depende de `:shared`. Uma tela "Hello Portaria" em ambas as plataformas, apenas para validar o build.
|
||||
|
||||
### 6. `infra/`
|
||||
|
||||
`docker-compose.yml` exatamente como em `docs/05-INFRA-DOCKER.md` §2, mais `docker-compose.observability.yml` e `.env.example`. Configs em `livekit/livekit.yaml`, `traefik/`, `postgres/init/01-extensions.sql` (`CREATE EXTENSION postgis; CREATE EXTENSION pgcrypto;`).
|
||||
|
||||
### 7. CI — GitHub Actions
|
||||
|
||||
```
|
||||
.github/workflows/ci.yml
|
||||
├─ gradle build (backend + shared, com Testcontainers)
|
||||
├─ pnpm lint && test && build
|
||||
├─ gate de bundle: visitor > 200KB gzip → FALHA
|
||||
├─ docker compose config
|
||||
└─ verificação de contraste dos tokens
|
||||
```
|
||||
|
||||
O gate de bundle não é opcional — a escolha de React em vez de Wasm foi motivada por TTI, e sem gate automático essa vantagem se perde em poucas sprints.
|
||||
|
||||
### 8. Documentação de arranque
|
||||
|
||||
`README.md` na raiz com pré-requisitos, `docker compose up`, como rodar cada app, e o mapa de portas.
|
||||
|
||||
## Critérios de aceite
|
||||
|
||||
- [ ] `./gradlew build` verde
|
||||
- [ ] `pnpm -r build` verde
|
||||
- [ ] `docker compose -f infra/docker-compose.yml config` sem erro
|
||||
- [ ] `docker compose up -d` sobe tudo; todos os healthchecks passam
|
||||
- [ ] `GET /api/v1/health` responde via Traefik com TLS
|
||||
- [ ] `/actuator/prometheus` expõe métricas
|
||||
- [ ] App Android e iOS compilam e abrem a tela de teste
|
||||
- [ ] `pnpm tokens:build` gera `tokens.css` e `Theme.kt`
|
||||
- [ ] CI verde no primeiro push
|
||||
|
||||
## Não faça nesta fase
|
||||
|
||||
- Nenhuma tabela de negócio (é a FASE 1)
|
||||
- Nenhum endpoint além do health
|
||||
- Nenhuma tela real
|
||||
- Nenhuma integração com LiveKit, FCM ou APNs
|
||||
99
prompts/FASE-1-dominio-e-dados.md
Normal file
99
prompts/FASE-1-dominio-e-dados.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# FASE 1 — Domínio compartilhado e camada de dados
|
||||
|
||||
## Objetivo
|
||||
|
||||
Modelo de domínio em `shared/`, schema completo no Postgres, infraestrutura de confiabilidade (outbox, fila, idempotência) funcionando, e autenticação. Ainda sem endpoints de negócio.
|
||||
|
||||
## Pré-requisitos
|
||||
|
||||
FASE 0 concluída. Ler `docs/02-MODELO-DE-DADOS.md` inteiro e `docs/01-ARQUITETURA.md` §4 e §5.
|
||||
|
||||
## Tarefas
|
||||
|
||||
### 1. Domínio em `shared/`
|
||||
|
||||
`state/VisitState.kt` e `VisitStateMachine.kt` conforme `01-ARQUITETURA.md` §4 e o diagrama de `03-FLUXOS-E-CONTRATOS.md` §1. A máquina de estados é a peça mais importante deste módulo — **teste toda transição válida e inválida**.
|
||||
|
||||
`model/` — enums e value objects: `VisitKind`, `DeliveryRule`, `MemberRole`, `MediaKind`, `DegradedMode`, `FeatureKey`, `GeoPoint`.
|
||||
|
||||
`dto/` — DTOs `@Serializable` de todos os contratos de `03-FLUXOS-E-CONTRATOS.md` §6.
|
||||
|
||||
`validation/` — validação de nome de visitante, telefone E.164, formato de unidade, tamanho de mídia. Usada no cliente e no servidor.
|
||||
|
||||
### 2. Migrations Flyway
|
||||
|
||||
As dez migrations de `02-MODELO-DE-DADOS.md` §11, exatamente na ordem indicada. Pontos que não podem ser esquecidos:
|
||||
|
||||
- `tenant_id uuid NOT NULL` em **toda** tabela de negócio
|
||||
- `version integer NOT NULL DEFAULT 0` onde há escrita concorrente
|
||||
- Nenhum `ON DELETE CASCADE` em dado auditável
|
||||
- Índice GIST em `gates.location`
|
||||
- Índice parcial em `visits` para estados ativos
|
||||
- `V8` revoga `UPDATE`/`DELETE` em `audit_log` para o usuário da aplicação
|
||||
- `V9` cria as policies de RLS e as deixa **desativadas**
|
||||
|
||||
### 3. jOOQ
|
||||
|
||||
Geração de código a partir do schema migrado (Testcontainers no build, não banco local). Repositórios para os agregados principais. **Todo repositório recebe `tenantId` como parâmetro obrigatório** — sem default, sem opcional. É o que torna a ativação futura do multi-tenant um interruptor.
|
||||
|
||||
### 4. Outbox transacional
|
||||
|
||||
```kotlin
|
||||
interface OutboxPublisher {
|
||||
fun enfileirar(evento: DomainEvent) // participa da transação corrente
|
||||
}
|
||||
```
|
||||
|
||||
Publisher que lê `outbox_events` pendentes com `FOR UPDATE SKIP LOCKED`, despacha, marca `published_at`, e em falha aplica backoff exponencial (`next_attempt_at`) e grava `last_error`. Após 10 tentativas, alerta.
|
||||
|
||||
**Teste obrigatório:** transação que falha após `enfileirar()` não pode deixar evento na tabela. É exatamente a garantia que o padrão existe para dar.
|
||||
|
||||
### 5. Fila de jobs e ShedLock
|
||||
|
||||
```kotlin
|
||||
interface JobScheduler {
|
||||
fun agendar(tipo: JobType, payload: JsonObject, quando: Instant): Long
|
||||
fun cancelar(jobId: Long)
|
||||
}
|
||||
```
|
||||
|
||||
Poller `@Scheduled(fixedDelay = 1000)` + `@SchedulerLock`, consumindo com `FOR UPDATE SKIP LOCKED`. Handlers registrados por `JobType`.
|
||||
|
||||
**Teste obrigatório com duas instâncias simultâneas:** cada job executa exatamente uma vez.
|
||||
|
||||
### 6. Idempotência
|
||||
|
||||
Filtro/interceptor que lê `Idempotency-Key`, consulta `idempotency_keys`, e:
|
||||
|
||||
- chave nova → executa e grava a resposta
|
||||
- chave repetida com mesmo `request_hash` → devolve a resposta original
|
||||
- chave repetida com corpo diferente → `422 IDEMPOTENCY_KEY_REUSE`
|
||||
|
||||
### 7. Segurança base
|
||||
|
||||
Spring Security com três cadeias de filtro conforme `06-LGPD-E-SEGURANCA.md` §6: `/api/v1/visitor/**` (JWT efêmero), `/api/v1/app/**` (OIDC), `/api/v1/admin/**` (OIDC + MFA).
|
||||
|
||||
Emissão e validação do JWT de visitante: 15 min, escopo de uma visita, atado a `gateId` e IP.
|
||||
|
||||
### 8. Auditoria
|
||||
|
||||
Aspecto que grava em `audit_log` toda mutação de entidade auditável, com `before`/`after` em `jsonb`.
|
||||
|
||||
## Critérios de aceite
|
||||
|
||||
- [ ] `./gradlew :shared:allTests` verde (JVM + iOS)
|
||||
- [ ] Cobertura de 100% das transições da máquina de estados, válidas e inválidas
|
||||
- [ ] `flyway migrate` do zero cria o schema completo
|
||||
- [ ] jOOQ gera código a partir do schema migrado
|
||||
- [ ] Teste: rollback de transação não deixa evento no outbox
|
||||
- [ ] Teste: duas instâncias do poller executam cada job exatamente uma vez
|
||||
- [ ] Teste: `UPDATE` em `audit_log` falha por permissão do banco
|
||||
- [ ] Teste: chave de idempotência repetida devolve a resposta original
|
||||
- [ ] Teste: repositório sem `tenantId` não compila
|
||||
|
||||
## Não faça nesta fase
|
||||
|
||||
- Endpoints de negócio (FASE 2)
|
||||
- Integração com LiveKit, FCM ou APNs
|
||||
- Qualquer tela
|
||||
- Ativar RLS
|
||||
107
prompts/FASE-2-backend-api.md
Normal file
107
prompts/FASE-2-backend-api.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# FASE 2 — API do backend
|
||||
|
||||
## Objetivo
|
||||
|
||||
Todos os endpoints REST, WebSocket de sinalização, armazenamento de mídia e o ciclo de vida da visita funcionando ponta a ponta — verificável por teste de integração, ainda sem interface.
|
||||
|
||||
## Pré-requisitos
|
||||
|
||||
FASES 0 e 1 concluídas. Ler `docs/03-FLUXOS-E-CONTRATOS.md` inteiro e `docs/06-LGPD-E-SEGURANCA.md` §5.
|
||||
|
||||
## Tarefas
|
||||
|
||||
### 1. Validação de QR e sessão de visitante
|
||||
|
||||
`GET /api/v1/visitor/gates/{gateId}/preview?s={sig}` — valida assinatura HMAC com `gates.qr_secret` e confere `qr_version`. Devolve nome do condomínio e o aviso de tratamento vigente. Sem autenticação.
|
||||
|
||||
`POST /api/v1/visitor/sessions` — recebe coordenadas, valida geofence com `ST_DWithin(gates.location, ponto, geofence_meters)`, registra `privacy_notices` (versão do aviso, IP, user agent) e emite o JWT efêmero.
|
||||
|
||||
Localização ausente ou negada → `403 LOCATION_REQUIRED`. Fora do raio → `403 OUTSIDE_GEOFENCE` com `detail` em português pronto para exibir.
|
||||
|
||||
### 2. Criação de visita — busca cega
|
||||
|
||||
`POST /api/v1/visitor/visits`, multipart (JSON + mídia).
|
||||
|
||||
**Esta é a implementação mais sensível do sistema.** Requisitos, todos obrigatórios:
|
||||
|
||||
- Resolve `unit_id` a partir de bloco + unidade; **se não existir, cria a visita com `unit_id = NULL`** e preserva `unit_input`
|
||||
- Resposta **idêntica** — mesmo corpo, mesmo status, mesmos headers — nos dois casos
|
||||
- **Tempo de resposta constante:** meça o caminho mais lento e aplique delay artificial no mais rápido. Sem isso o ataque vira timing attack e a busca cega não serve para nada
|
||||
- Nome de morador nunca aparece na resposta
|
||||
- Rate limit por IP e por dispositivo, com bloqueio progressivo
|
||||
|
||||
Verifica `quiet_hours` do condomínio: dentro da janela, a visita pula o toque ao morador e vai direto para `FILA_OPERADOR` ou `RECADO_EM_VIDEO`.
|
||||
|
||||
Enfileira `VisitaCriada` no outbox e agenda `VISIT_RING_TIMEOUT` (20s) e `VISIT_EXPIRE` (10 min).
|
||||
|
||||
### 3. Ciclo de vida da visita
|
||||
|
||||
Serviço de aplicação com as transições de `03-FLUXOS-E-CONTRATOS.md` §1, sempre validadas por `VisitStateMachine` e sempre com optimistic locking em `visits.version`.
|
||||
|
||||
Handlers de job:
|
||||
|
||||
| Job | Ação |
|
||||
|---|---|
|
||||
| `VISIT_RING_TIMEOUT` | → `ESCALONADA`; toca para os demais `unit_members` por `ring_order`; agenda `VISIT_ESCALATE` |
|
||||
| `VISIT_ESCALATE` | → `FILA_OPERADOR` se `operator_queue` ativo, senão `RECADO_EM_VIDEO` |
|
||||
| `VISIT_QUEUE_TIMEOUT` | → `RECADO_EM_VIDEO` |
|
||||
| `VISIT_EXPIRE` | → `EXPIRADA` |
|
||||
| `DELIVERY_DEFAULT_RULE` | aplica `units.delivery_rule` |
|
||||
|
||||
`POST /api/v1/app/visits/{id}/resolve` exige `Idempotency-Key`. Conflito de versão devolve `409 VISIT_ALREADY_RESOLVED` com `resolvedBy` e `resolvedAt` — o app precisa mostrar "Maria já autorizou", não um erro técnico.
|
||||
|
||||
### 4. Armazenamento de mídia
|
||||
|
||||
Porta `StoragePort` com adaptador MinIO/S3. Ao receber mídia:
|
||||
|
||||
- Valida `Content-Type` por **magic bytes**, nunca por extensão
|
||||
- Limites: 8MB foto, 30MB vídeo
|
||||
- **Remove EXIF** — carrega GPS próprio, que não é o dado que coletamos e não passou pelo aviso
|
||||
- Calcula `sha256` e grava em `media_assets`
|
||||
- Nome de objeto gerado pelo servidor
|
||||
- Define `expires_at` conforme a retenção do tipo (`06-LGPD-E-SEGURANCA.md` §4)
|
||||
|
||||
URL assinada de 5 min para leitura, **sempre** registrando acesso em `audit_log`. Nenhuma URL pública, em nenhuma hipótese.
|
||||
|
||||
### 5. WebSocket
|
||||
|
||||
`/ws/visitor/{visitId}` e `/ws/app`, com o envelope e os tipos de `03-FLUXOS-E-CONTRATOS.md` §7. Heartbeat de 20s.
|
||||
|
||||
**O WebSocket nunca é fonte de verdade.** Toda mudança de estado é confirmada por REST ou push. Se o WS cair, o cliente faz polling — e a portaria continua funcionando.
|
||||
|
||||
Com múltiplas réplicas do backend, a difusão de eventos usa Redis pub/sub (só transporte; a durabilidade fica no outbox).
|
||||
|
||||
### 6. Endpoints de app e admin
|
||||
|
||||
Todos os de `03-FLUXOS-E-CONTRATOS.md` §6, com autorização por método:
|
||||
|
||||
```kotlin
|
||||
@PreAuthorize("@access.canViewVisit(#visitId, authentication)")
|
||||
```
|
||||
|
||||
Morador vê apenas visitas das suas unidades; operador, apenas as da fila; admin, apenas as do seu tenant.
|
||||
|
||||
Importação CSV de unidades com **dry-run obrigatório** — devolve o que seria criado, alterado e rejeitado antes de gravar.
|
||||
|
||||
### 7. Tratamento de erros
|
||||
|
||||
`application/problem+json` (RFC 7807) com os `code` da tabela de `03-FLUXOS-E-CONTRATOS.md` §9. **`detail` sempre em português e pronto para exibir ao visitante** — a tela da portaria não é lugar para mensagem técnica.
|
||||
|
||||
## Critérios de aceite
|
||||
|
||||
- [ ] Testes de integração com Testcontainers cobrindo os fluxos A, B e C ponta a ponta
|
||||
- [ ] **Teste de busca cega:** unidade existente e inexistente produzem resposta idêntica, e a diferença de tempo fica abaixo do ruído de medição
|
||||
- [ ] Teste: geofence rejeita fora do raio e aceita dentro
|
||||
- [ ] Teste: `quiet_hours` desvia do morador
|
||||
- [ ] Teste: dois `resolve` concorrentes — um vence, outro recebe `409` com o autor
|
||||
- [ ] Teste: `resolve` repetido com mesma `Idempotency-Key` não duplica
|
||||
- [ ] Teste: escalonamento dispara após restart do backend (job persistido)
|
||||
- [ ] Teste: EXIF removido da imagem armazenada
|
||||
- [ ] Teste: acesso a mídia gera linha em `audit_log`
|
||||
- [ ] OpenAPI completo em `/v3/api-docs`
|
||||
|
||||
## Não faça nesta fase
|
||||
|
||||
- Integração com LiveKit (FASE 4) — `room_name` fica como placeholder
|
||||
- Push real (FASE 5) — `NotificationChannel` com implementação de log
|
||||
- Qualquer interface
|
||||
90
prompts/FASE-3-web-visitante.md
Normal file
90
prompts/FASE-3-web-visitante.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# FASE 3 — Web do visitante
|
||||
|
||||
## Objetivo
|
||||
|
||||
PWA do visitante completa: QR → aviso → permissões → captura → destino → espera narrada → resultado. Ambos os funis (visita e entrega). Sem videochamada ainda.
|
||||
|
||||
## Pré-requisitos
|
||||
|
||||
FASES 0–2 concluídas. Ler `docs/04-DESIGN-SYSTEM-UX.md` §3 e `docs/03-FLUXOS-E-CONTRATOS.md` §2 e §3.
|
||||
|
||||
## Contexto que determina cada decisão
|
||||
|
||||
O visitante está **em pé na calçada, no sol, com pressa, em 4G, e não vai instalar nada**. Tudo aqui deriva disso: bundle mínimo, uma ação por tela, tipografia grande, contraste alto.
|
||||
|
||||
## Tarefas
|
||||
|
||||
### 1. Base
|
||||
|
||||
Vite + React 19 + TS em `web/visitor`. Roteamento por `/v/{gateId}?s={sig}`. Tokens de `shared-ui`. PWA com manifest, **sem service worker de cache agressivo** — o visitante usa uma vez e o cache velho causaria mais problema que benefício.
|
||||
|
||||
**Orçamento: 200KB gzip**, com gate em CI. O SDK do LiveKit entra por `import()` dinâmico apenas na FASE 4, na tela de chamada — ele sozinho tem o tamanho de todo o resto do app.
|
||||
|
||||
### 2. Telas
|
||||
|
||||
Cinco telas conforme `04-DESIGN-SYSTEM-UX.md` §3, uma ação cada.
|
||||
|
||||
**Tela 1 — Aviso de tratamento.** Não é modal de consentimento com checkbox. A base legal é legítimo interesse; o que se faz é **informar com clareza**. Texto de `06-LGPD-E-SEGURANCA.md` §1, com link "Saiba mais". O botão diz "Continuar", não "Aceito".
|
||||
|
||||
**Tela 2 — Escolha do funil.** `📦 Entrega` primeiro, porque é a maioria do volume. `👤 Visita` abaixo.
|
||||
|
||||
**Tela 3 — Permissões e captura.** Solicita câmera e localização, com explicação **antes** do prompt nativo — pedir permissão sem contexto é a principal causa de negativa. Captura foto do rosto (visita) ou do pacote (entrega). Comprime no cliente para no máximo 1600px no lado maior.
|
||||
|
||||
Permissão negada: localização é bloqueante (`403 LOCATION_REQUIRED`), com instrução de como reabilitar. Câmera negada em entrega degrada para "sem foto"; em visita, é bloqueante.
|
||||
|
||||
**Tela 4 — Destino.** Bloco e unidade, campos livres. **Nenhum autocomplete, nenhuma sugestão, nenhuma validação de existência** — é a busca cega, e um autocomplete a destruiria. Nome do visitante obrigatório; documento e telefone opcionais.
|
||||
|
||||
**Tela 5 — Espera narrada.** A tela onde o produto se ganha ou se perde:
|
||||
|
||||
```
|
||||
TOCANDO "Chamando o morador..." barra 0→20s
|
||||
ESCALONADA "Tentando outros moradores..." barra 0→15s
|
||||
FILA_OPERADOR "Transferindo para a portaria" + posição na fila
|
||||
RECADO "Grave um recado em vídeo" → gravação
|
||||
```
|
||||
|
||||
Nunca um spinner mudo. Um spinner de 35 segundos é indistinguível de um app quebrado.
|
||||
|
||||
**Tela 6 — Resultado.** Autorizado: PIN de 6 dígitos em `display`, QR grande, contagem regressiva de validade. Negado: mensagem clara e sem tom acusatório. Expirado: instrução de contingência (botão físico da portaria).
|
||||
|
||||
### 3. Estado e conexão
|
||||
|
||||
Estado da visita via WebSocket com **fallback automático para polling** de 3s se o WS cair ou não conectar em 5s. O visitante em 4G instável é o caso comum, não a exceção.
|
||||
|
||||
Reconexão com backoff exponencial e teto de 10s. Estado da sessão em `sessionStorage`, para que refresh acidental não perca a visita.
|
||||
|
||||
### 4. Gravação de recado
|
||||
|
||||
`MediaRecorder`, 30s máximo, preview antes de enviar, upload com barra de progresso e retry.
|
||||
|
||||
### 5. Erros
|
||||
|
||||
Todo erro exibido vem do `detail` do `problem+json`, em português e acionável: **"Aproxime-se da entrada e tente novamente"**, nunca `OUTSIDE_GEOFENCE`.
|
||||
|
||||
### 6. Acessibilidade e legibilidade em campo
|
||||
|
||||
- Alvos de toque ≥ 48×48px, sem exceção
|
||||
- Corpo de texto ≥ 16px
|
||||
- Contraste ≥ 4.5:1, verificado em CI
|
||||
- Apenas extremos da escala neutra — sol direto derruba tons médios
|
||||
- `prefers-reduced-motion` respeitado
|
||||
- `lang="pt-BR"`, textos externalizados para i18n futura
|
||||
- Funciona em Safari iOS 15+ e Chrome Android 100+
|
||||
|
||||
## Critérios de aceite
|
||||
|
||||
- [ ] **Bundle ≤ 200KB gzip** (gate de CI)
|
||||
- [ ] TTI < 2s em 4G simulado (Lighthouse throttling)
|
||||
- [ ] Lighthouse: Performance ≥ 90, Acessibilidade = 100
|
||||
- [ ] Fluxo de entrega completo em ≤ 4 toques
|
||||
- [ ] Tela 4 não tem autocomplete nem validação de unidade
|
||||
- [ ] WS derrubado à força → polling assume e o fluxo continua
|
||||
- [ ] Localização negada → mensagem acionável, não erro técnico
|
||||
- [ ] Testado em iPhone e Android reais, sob sol
|
||||
- [ ] Todos os textos de erro em português, sem código técnico
|
||||
|
||||
## Não faça nesta fase
|
||||
|
||||
- Videochamada (FASE 4)
|
||||
- Service worker com cache de assets versionados
|
||||
- Qualquer tela de admin
|
||||
93
prompts/FASE-4-chamada.md
Normal file
93
prompts/FASE-4-chamada.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# FASE 4 — Videochamada
|
||||
|
||||
## Objetivo
|
||||
|
||||
LiveKit integrado ponta a ponta: emissão de tokens, sala por visita, vídeo no navegador do visitante, e degradação graciosa quando o SFU falha.
|
||||
|
||||
## Pré-requisitos
|
||||
|
||||
FASES 0–3 concluídas. Ler `docs/05-INFRA-DOCKER.md` §3 e §4, e `docs/01-ARQUITETURA.md` §5.5.
|
||||
|
||||
> **Faça a POC antes de escrever código de produção.** Suba `livekit-server` + `coturn` e feche uma chamada entre um navegador móvel em 4G real e um Android, medindo o tempo até o primeiro frame. É o maior risco técnico do projeto e o mais barato de derrubar cedo. Se a POC falhar, o problema quase sempre é a faixa UDP fechada no firewall.
|
||||
|
||||
## Tarefas
|
||||
|
||||
### 1. Porta de vídeo
|
||||
|
||||
```kotlin
|
||||
interface VideoCallProvider {
|
||||
suspend fun criarSala(visitId: UUID): RoomInfo
|
||||
suspend fun emitirToken(visitId: UUID, participante: Participant): String
|
||||
suspend fun encerrarSala(visitId: UUID)
|
||||
suspend fun saudavel(): Boolean
|
||||
}
|
||||
```
|
||||
|
||||
Adaptador `LiveKitProvider`. A porta existe para permitir trocar por serviço gerenciado sem tocar nos apps — decisão registrada em `01-ARQUITETURA.md` ADR-003.
|
||||
|
||||
### 2. Emissão de tokens
|
||||
|
||||
Token JWT do LiveKit gerado **pelo backend**, nunca pelo cliente. Escopo mínimo:
|
||||
|
||||
- `roomJoin` apenas na sala daquela visita
|
||||
- `canPublish` para vídeo e áudio
|
||||
- `canPublishData: false`
|
||||
- TTL de 10 minutos, alinhado à expiração da visita
|
||||
|
||||
`room_name` derivado determinísticamente de `visitId`, gravado em `visits.room_name`.
|
||||
|
||||
**Um token nunca dá acesso a outra sala.** Verifique isso com teste explícito.
|
||||
|
||||
### 3. Ciclo de vida da sala
|
||||
|
||||
Sala criada na transição para `EM_CHAMADA`, não antes — criar em `PENDENTE` desperdiça recurso do SFU em visitas que nunca são atendidas.
|
||||
|
||||
Encerrada ao chegar em estado final. `empty_timeout: 120` no LiveKit é a rede de segurança para sala órfã.
|
||||
|
||||
### 4. Vídeo na web do visitante
|
||||
|
||||
SDK `livekit-client` carregado por **`import()` dinâmico**, apenas quando a visita entra em `EM_CHAMADA`. Ele sozinho pesa mais que todo o resto do app — carregá-lo no bundle inicial destruiria o orçamento de 200KB da FASE 3.
|
||||
|
||||
Vídeo remoto em tela cheia, auto-preview pequeno no canto, botão de mudo e de encerrar. Indicador de qualidade de conexão.
|
||||
|
||||
### 5. Degradação graciosa
|
||||
|
||||
Circuit breaker (Resilience4j) sobre o `VideoCallProvider`. Ao abrir:
|
||||
|
||||
```
|
||||
LiveKit fora ─► AUDIO (mesma sala, sem vídeo, muito menos banda)
|
||||
└─► FOTO_TEXTO (visitante manda foto, morador aprova sem chamada)
|
||||
└─► FILA_OPERADOR (resolução por telefone)
|
||||
```
|
||||
|
||||
O nível vigente vai em `visits.degraded_mode`, é emitido como `DEGRADED_MODE` no WebSocket, aparece em `/actuator/health` e no painel admin.
|
||||
|
||||
**Comunique a degradação, nunca a esconda.** Esconder faz o usuário achar que o produto quebrou; nomear faz o produto parecer resiliente — que é o que ele é.
|
||||
|
||||
### 6. Configuração de mídia
|
||||
|
||||
**Limite de resolução imposto no servidor**, não no cliente: 480p a ~600kbps. 480p basta para reconhecer alguém na portaria; 1080p multiplicaria o custo de banda por quatro sem melhorar nenhuma decisão. Ver o dimensionamento em `05-INFRA-DOCKER.md` §4.
|
||||
|
||||
Simulcast desligado (só dois participantes). `adaptiveStream` e `dynacast` ligados. TURN configurado com o segredo do coturn.
|
||||
|
||||
### 7. Métricas
|
||||
|
||||
`portaria.call.time_to_first_frame` · `portaria.call.duration` · `portaria.call.failed_ratio` · `portaria.call.turn_usage_ratio` (uso alto de TURN indica problema de rede na portaria) · `portaria.call.degraded_ratio`.
|
||||
|
||||
## Critérios de aceite
|
||||
|
||||
- [ ] POC validada em rede real antes do código de produção
|
||||
- [ ] Chamada completa entre navegador móvel em 4G e Android
|
||||
- [ ] Tempo até o primeiro frame < 3s em rede boa
|
||||
- [ ] Teste: token de uma sala é rejeitado em outra sala
|
||||
- [ ] Teste: com LiveKit derrubado, o sistema degrada para áudio e depois para foto+texto
|
||||
- [ ] Teste: sala encerrada em todos os estados finais
|
||||
- [ ] SDK do LiveKit **não** aparece no bundle inicial do visitante
|
||||
- [ ] Bundle inicial continua ≤ 200KB gzip
|
||||
- [ ] Chamada funciona atrás de NAT simétrico (valida o coturn)
|
||||
|
||||
## Não faça nesta fase
|
||||
|
||||
- Gravação (módulo `video_recording`, FASE 8)
|
||||
- App do morador (FASE 5)
|
||||
- Mais de 2 participantes por sala
|
||||
91
prompts/FASE-5-app-morador.md
Normal file
91
prompts/FASE-5-app-morador.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# FASE 5 — App do morador
|
||||
|
||||
## Objetivo
|
||||
|
||||
App Compose Multiplatform (Android + iOS) que **toca de verdade** com o app fechado, permite resolver entrega em um toque na notificação, e atende videochamada.
|
||||
|
||||
## Pré-requisitos
|
||||
|
||||
FASES 0–4 concluídas. Ler `docs/04-DESIGN-SYSTEM-UX.md` §4.
|
||||
|
||||
## O ponto que define esta fase
|
||||
|
||||
**No iOS, notificação comum não faz o telefone tocar como chamada.** Só PushKit + CallKit fazem. Sem isso o app não funciona como portaria — o morador vê a chamada quando abrir o celular, dez minutos depois, e o visitante já foi embora.
|
||||
|
||||
E a Apple **obriga**: ao receber um push do PushKit, o app precisa reportar a chamada ao CallKit imediatamente, na mesma execução. Não reportar faz o sistema matar o app e, com reincidência, revogar o direito de receber VoIP push. Não há meio-termo aqui.
|
||||
|
||||
## Tarefas
|
||||
|
||||
### 1. Estrutura
|
||||
|
||||
`apps/composeApp` com Compose Multiplatform, dependendo de `:shared`. Arquitetura MVVM com `ViewModel` compartilhado. Ktor Client para HTTP e WebSocket. Material 3 Expressive com o `Theme.kt` gerado dos tokens.
|
||||
|
||||
Um único app com dois perfis (morador e operador), decididos por `GET /me`.
|
||||
|
||||
### 2. Notificação de chamada — Android
|
||||
|
||||
FCM com prioridade `high`. Notificação `CallStyle` com full-screen intent para visita; notificação com ações para entrega.
|
||||
|
||||
Permissões necessárias: `POST_NOTIFICATIONS` (13+), `USE_FULL_SCREEN_INTENT` (14+), `FOREGROUND_SERVICE_MICROPHONE` e `FOREGROUND_SERVICE_CAMERA`.
|
||||
|
||||
**Otimização de bateria é o inimigo silencioso.** Xiaomi, Samsung e Huawei matam apps agressivamente e o push nunca chega. O app precisa detectar e orientar o usuário a isentá-lo — e o painel admin precisa mostrar quem está com push falhando (`05-INFRA-DOCKER.md` §6).
|
||||
|
||||
### 3. Notificação de chamada — iOS
|
||||
|
||||
**Visita** → PushKit VoIP push → `CXProvider.reportNewIncomingCall()` **imediatamente**, antes de qualquer chamada de rede. Só depois busque os detalhes da visita.
|
||||
|
||||
**Entrega** → notificação comum com `UNNotificationCategory` e ações. CallKit para uma entrega seria intrusivo e é risco de reprovação na App Review.
|
||||
|
||||
Entitlement de VoIP configurado. Chave APNs `.p8` no backend. Áudio configurado via `AVAudioSession` com categoria `playAndRecord`.
|
||||
|
||||
**Nota de App Review:** documente na submissão que o app é VoIP legítimo de controle de acesso. Descreva o fluxo. Apps que usam CallKit para notificação genérica são reprovados — este não é o caso, mas o revisor precisa entender por quê.
|
||||
|
||||
### 4. Ações de um toque
|
||||
|
||||
O morador resolve **sem abrir o app**:
|
||||
|
||||
- Android: `Notification.Action` com `PendingIntent` para um `BroadcastReceiver`
|
||||
- iOS: `UNNotificationAction` tratada na extensão
|
||||
|
||||
Ambas chamam `POST /visits/{id}/resolve` com `Idempotency-Key` gerada no dispositivo. **Trate a falha de rede com retry**: a ação disparada em rede ruim que falha em silêncio é a pior falha possível deste app — o morador acha que autorizou e o entregador continua parado.
|
||||
|
||||
### 5. Telas
|
||||
|
||||
**Home** — pendências (recados não resolvidos) no topo, histórico abaixo, estado de conexão visível.
|
||||
|
||||
**Chamada** — vídeo do visitante em tela cheia, auto-preview no canto. Sobreposto: nome informado, unidade de destino, e o **selo de geofence** (`✓ Na entrada` verde / `⚠ A 340m da portaria` âmbar). Esse selo é a informação de segurança mais útil da tela — alguém acionando de longe é sinal claro. Ações: `Autorizar` (verde, primária), `Negar`, mudo, encerrar.
|
||||
|
||||
**Detalhe da visita** — foto, mapa do ponto, timeline de `visit_attempts`, player do recado.
|
||||
|
||||
**Configurações** — regra de entrega por unidade, ordem de toque, dispositivos ativos, notificações.
|
||||
|
||||
### 6. Registro de dispositivo
|
||||
|
||||
`POST /devices` no login e a cada renovação de token, enviando `push_token` e — no iOS — também `voip_token`. Renove sempre que o sistema emitir novo token; token expirado é a causa número um de "não recebi a chamada".
|
||||
|
||||
### 7. Concorrência entre moradores
|
||||
|
||||
Ao receber `409 VISIT_ALREADY_RESOLVED`, mostre **"Maria já autorizou"** com horário. Nunca um erro técnico — dois moradores respondendo juntos é situação normal, não falha.
|
||||
|
||||
### 8. Tempo real
|
||||
|
||||
WebSocket enquanto o app está em foreground, com fallback para push. Ao voltar do background, reconcilia estado via REST antes de confiar no WS.
|
||||
|
||||
## Critérios de aceite
|
||||
|
||||
- [ ] **iOS: chamada toca em tela cheia com o app fechado e o telefone bloqueado**
|
||||
- [ ] **Android: full-screen intent aparece com o app fechado e a tela apagada**
|
||||
- [ ] Entrega resolvida pela notificação, sem abrir o app, em ambas as plataformas
|
||||
- [ ] Teste com o app morto por otimização de bateria (Xiaomi/Samsung)
|
||||
- [ ] Videochamada funciona com o app vindo do background
|
||||
- [ ] `409` mostra quem já resolveu
|
||||
- [ ] Ação em rede ruim tem retry e não falha em silêncio
|
||||
- [ ] Registro de dispositivo renova o token corretamente
|
||||
- [ ] Testado em dispositivos físicos — simulador não recebe push
|
||||
- [ ] Acessibilidade: TalkBack e VoiceOver navegam a tela de chamada
|
||||
|
||||
## Não faça nesta fase
|
||||
|
||||
- App do operador completo (FASE 7) — perfil de operador fica oculto
|
||||
- WhatsApp (FASE 8)
|
||||
- Autorizações recorrentes (v2)
|
||||
106
prompts/FASE-6-painel-admin.md
Normal file
106
prompts/FASE-6-painel-admin.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# FASE 6 — Painel administrativo
|
||||
|
||||
## Objetivo
|
||||
|
||||
Painel web completo para síndico e administradora: gestão do condomínio, auditoria de visitas com mídia e mapa, importação de moradores, reconciliação e feature flags.
|
||||
|
||||
## Pré-requisitos
|
||||
|
||||
FASES 0–5 concluídas. Ler `docs/04-DESIGN-SYSTEM-UX.md` §6 e `docs/03-FLUXOS-E-CONTRATOS.md` §6.
|
||||
|
||||
## Contexto
|
||||
|
||||
Ao contrário da tela do visitante, aqui **densidade é virtude**: usuário recorrente, em desktop, que precisa de tabela grande, filtro fino e comparação. O orçamento de bundle não se aplica.
|
||||
|
||||
## Tarefas
|
||||
|
||||
### 1. Base
|
||||
|
||||
Vite + React 19 + TS em `web/admin`. TanStack Router e TanStack Query. TanStack Table para grades virtualizadas. Leaflet para mapa (tiles OSM). Tokens de `shared-ui`.
|
||||
|
||||
Login OIDC com **MFA obrigatório** — este perfil vê fotos de todos os visitantes e a localização de todos os acessos; é o alvo mais valioso do sistema.
|
||||
|
||||
### 2. Auditoria de visitas — a tela mais importante
|
||||
|
||||
Tabela virtualizada com filtros persistidos na URL: período, condomínio, bloco, unidade, tipo, estado, **dentro/fora do geofence**, e quem resolveu.
|
||||
|
||||
Colunas: data/hora, tipo, visitante, unidade, estado, quem resolveu, duração, selo de geofence.
|
||||
|
||||
**Detalhe da visita:**
|
||||
|
||||
- Foto do visitante ou do pacote (URL assinada de 5 min)
|
||||
- **Mapa** com o ponto do visitante e o raio do portão desenhado — é aqui que uma tentativa remota fica óbvia
|
||||
- Timeline de `visit_attempts`: quem foi notificado, por qual canal, entregue ou não, quem atendeu. **É o que responde "por que ninguém atendeu?"** — a pergunta mais comum do síndico
|
||||
- Player do recado, se houver
|
||||
- Registro de exibição do aviso (`privacy_notices`) com a versão do texto
|
||||
- Hash `sha256` da mídia, para valor probatório
|
||||
|
||||
Exportação CSV e PDF do resultado filtrado, com marca d'água de quem exportou e quando.
|
||||
|
||||
**Todo acesso a mídia gera linha em `audit_log`** — inclusive o do admin. Isso não é opcional.
|
||||
|
||||
### 3. Gestão do condomínio
|
||||
|
||||
CRUD de condomínios, blocos, unidades, pessoas, vínculos, portarias e responsáveis pela abertura.
|
||||
|
||||
**Importação CSV de unidades e moradores com dry-run obrigatório**: mostra o que será criado, alterado e rejeitado, e só grava após confirmação. Importar uma base errada em cima de outra correta é dano difícil de reverter.
|
||||
|
||||
Convite de morador por link, com acompanhamento (enviado / aberto / aceito).
|
||||
|
||||
Configuração da portaria: posição no mapa, `geofence_meters` com o raio desenhado para calibragem visual, `qr_version` com botão de invalidar-e-reimprimir, e geração do PDF do QR pronto para impressão (fosco, 15×15cm, conforme `07-REQUISITOS-DE-CAMPO.md`).
|
||||
|
||||
### 4. Reconciliação
|
||||
|
||||
Tela dedicada ao relatório de `GET /admin/reconciliation`:
|
||||
|
||||
- Moradores com `valid_until` vencido
|
||||
- **Unidades sem nenhum dispositivo ativo** — estas nunca atendem
|
||||
- Dispositivos com push falhando há mais de 7 dias (token morto = desinstalou)
|
||||
- Unidades com taxa de atendimento abaixo de 30% no mês
|
||||
- Convites nunca aceitos
|
||||
|
||||
Cada item com ação direta: reenviar convite, marcar como saído, contatar.
|
||||
|
||||
Esta tela é o que mantém o produto vivo depois do primeiro mês — base desatualizada quebra tudo silenciosamente.
|
||||
|
||||
### 5. Configuração do app do morador
|
||||
|
||||
O que o síndico controla no app dos moradores: regra padrão de entrega por unidade, `ring_order`, `quiet_hours` do condomínio, e quais funcionalidades aparecem.
|
||||
|
||||
### 6. Planos e módulos
|
||||
|
||||
Visualização do plano do tenant e toggles de `tenant_features`: `whatsapp_notifications`, `operator_queue`, `video_recording`, `access_control_hardware`, `recurring_authorizations`.
|
||||
|
||||
Cada toggle mostra o efeito prático em uma frase — "Sem operador, visitas não atendidas viram recado em vídeo" — e registra a mudança em `audit_log` com autor.
|
||||
|
||||
### 7. SLOs e saúde
|
||||
|
||||
Dashboard de `GET /admin/slo` com as métricas de `00-VISAO-E-PRODUTO.md` §6: latência QR→toque, taxa de atendimento, latência de entrega, abandono.
|
||||
|
||||
**Modo degradado vigente exibido com destaque.** O síndico precisa saber que o sistema está reduzido antes de receber a reclamação.
|
||||
|
||||
### 8. LGPD
|
||||
|
||||
Atendimento a titulares: busca por CPF ou telefone, exportação JSON, e eliminação — que apaga mídia e anonimiza nome **preservando a linha da visita** (`06-LGPD-E-SEGURANCA.md` §3).
|
||||
|
||||
Cadastro do encarregado (DPO), exibido no aviso de privacidade. Editor do texto do aviso com **versionamento** — mudar o texto cria nova versão, e as visitas antigas continuam referenciando a versão que foi realmente exibida.
|
||||
|
||||
## Critérios de aceite
|
||||
|
||||
- [ ] Login exige MFA
|
||||
- [ ] Tabela de auditoria fluida com 100 mil visitas (virtualização)
|
||||
- [ ] Mapa mostra ponto do visitante e raio do portão
|
||||
- [ ] Timeline de tentativas explica por que ninguém atendeu
|
||||
- [ ] Acesso a mídia usa URL assinada e gera auditoria
|
||||
- [ ] Importação CSV com dry-run funcional
|
||||
- [ ] Relatório de reconciliação identifica unidades sem dispositivo ativo
|
||||
- [ ] PDF do QR sai no tamanho e formato de `07-REQUISITOS-DE-CAMPO.md`
|
||||
- [ ] Toggle de módulo registra autor em `audit_log`
|
||||
- [ ] Eliminação LGPD preserva a linha anonimizada
|
||||
- [ ] Acessibilidade: navegação completa por teclado, contraste ≥ 4.5:1
|
||||
|
||||
## Não faça nesta fase
|
||||
|
||||
- Billing automatizado (v2)
|
||||
- Gestão de tenants (multi-tenancy segue desativada)
|
||||
- App do operador (FASE 7)
|
||||
91
prompts/FASE-7-escalonamento.md
Normal file
91
prompts/FASE-7-escalonamento.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# FASE 7 — Escalonamento, fila de operador e recado
|
||||
|
||||
## Objetivo
|
||||
|
||||
Completar a retaguarda: escalonamento entre moradores, fila de operador humano com contexto, app do operador, e recado em vídeo. É o que transforma "chamada não atendida" em "visita resolvida".
|
||||
|
||||
## Pré-requisitos
|
||||
|
||||
FASES 0–6 concluídas. Ler `docs/03-FLUXOS-E-CONTRATOS.md` §4 e `docs/00-VISAO-E-PRODUTO.md` §1.
|
||||
|
||||
## Por que esta fase existe
|
||||
|
||||
A tese do produto é que o morador atende. A realidade é que ele nem sempre atende — gente em reunião, dirigindo, dormindo. É por isso que o mercado consolidado usa operador humano 24h.
|
||||
|
||||
Esta fase é o que torna a aposta vendável: quando o morador não atende, o visitante **não fica sem saída**. E é o que sustenta comercialmente o plano Assistido.
|
||||
|
||||
## Tarefas
|
||||
|
||||
### 1. Escalonamento entre moradores
|
||||
|
||||
`VISIT_RING_TIMEOUT` (20s) → `ESCALONADA`. Toca para os demais `unit_members` da unidade, **em paralelo**, respeitando `ring_order` para ordenar a exibição e `receives_calls`.
|
||||
|
||||
O primeiro que atender vence; os demais recebem cancelamento da notificação — deixar notificação órfã tocando depois de resolvido é ruído que faz o morador desativar o app.
|
||||
|
||||
### 2. Fila de operador
|
||||
|
||||
Ativa apenas com `tenant_features.operator_queue` habilitado. Sem o módulo, `ESCALONADA` vai direto para `RECADO_EM_VIDEO`.
|
||||
|
||||
Fila ordenada por tempo de espera, com prioridade para `VISITA` sobre `ENTREGA`. `POST /operator/queue/claim` usa `FOR UPDATE SKIP LOCKED` — dois operadores nunca pegam a mesma visita.
|
||||
|
||||
Posição na fila enviada ao visitante por `QUEUE_POSITION` no WebSocket. Esperar é tolerável; esperar sem saber quanto falta, não.
|
||||
|
||||
`VISIT_QUEUE_TIMEOUT` (120s) → `RECADO_EM_VIDEO`.
|
||||
|
||||
### 3. App do operador
|
||||
|
||||
Mesmo binário Compose Multiplatform, perfil decidido por `GET /me`. Layout mais denso que o do morador.
|
||||
|
||||
**Fila** à esquerda: foto, tipo, unidade, tempo de espera e **por que escalonou** (ninguém atendeu / quiet hours / unidade inexistente).
|
||||
|
||||
**Contexto** à direita, antes de assumir: histórico da unidade, visitas recentes, regra de entrega, nomes dos moradores, telefone. **O operador nunca atende sem contexto** — atender às cegas é o que faz a portaria remota parecer pior que a física.
|
||||
|
||||
Presença: `DISPONIVEL` / `EM_ATENDIMENTO` / `OFFLINE`, com heartbeat. Operador que perde conexão volta para `OFFLINE` automaticamente e suas visitas retornam à fila.
|
||||
|
||||
Ao resolver, o operador registra `resolution_reason` — obrigatório, e é o que alimenta a análise de por que o modelo autônomo falhou naquele caso.
|
||||
|
||||
### 4. Recado em vídeo
|
||||
|
||||
`RECADO_EM_VIDEO`: o visitante grava até 30s. Salvo em `media_assets` com `kind = RECADO_VIDEO` e retenção de 30 dias.
|
||||
|
||||
Notificação ao morador **sem urgência** — não é CallKit, não é full-screen intent. É pendência, não chamada.
|
||||
|
||||
Aparece no topo da home do app até ser resolvida. O morador pode autorizar retroativamente (gerando `access_grant` com validade estendida), negar, ou apenas marcar como visto.
|
||||
|
||||
### 5. Quiet hours
|
||||
|
||||
Dentro de `condominiums.quiet_hours`, visitas não pré-autorizadas **pulam o toque ao morador**: vão direto para `FILA_OPERADOR` (se o módulo estiver ativo) ou `RECADO_EM_VIDEO`.
|
||||
|
||||
É a defesa contra tocar em todos os apartamentos de madrugada, e precisa ser óbvia para o visitante: "Fora do horário. Transferindo para a portaria."
|
||||
|
||||
### 6. Cancelamento de notificações
|
||||
|
||||
Ao resolver a visita, cancele as notificações pendentes em todos os dispositivos que foram acionados. Android usa `notificationId` estável por visita; iOS usa `CXProvider.reportCall(with:endedAt:reason:)`.
|
||||
|
||||
Uma chamada CallKit que continua tocando depois de resolvida é bug grave de percepção — o morador acha que o app está quebrado.
|
||||
|
||||
### 7. Métricas de escalonamento
|
||||
|
||||
`portaria.escalation.ratio` (quantas visitas escalaram) · `portaria.queue.wait_time` · `portaria.queue.abandoned` · `portaria.message.recorded` · `portaria.message.unresolved_24h`.
|
||||
|
||||
`escalation.ratio` é a métrica que decide se um condomínio fica no plano Autônomo ou precisa migrar para o Assistido.
|
||||
|
||||
## Critérios de aceite
|
||||
|
||||
- [ ] Escalonamento toca para os demais moradores após 20s
|
||||
- [ ] Primeiro a atender vence; notificações dos outros são canceladas
|
||||
- [ ] Sem `operator_queue`, escalonada vai direto para recado
|
||||
- [ ] Dois operadores não pegam a mesma visita (teste concorrente)
|
||||
- [ ] Posição na fila chega ao visitante em tempo real
|
||||
- [ ] Operador que perde conexão devolve a visita à fila
|
||||
- [ ] Recado gravado, armazenado e notificado sem urgência
|
||||
- [ ] Recado aparece como pendência na home até resolver
|
||||
- [ ] Quiet hours desvia sem tocar em ninguém, com aviso ao visitante
|
||||
- [ ] CallKit não continua tocando após resolução
|
||||
- [ ] Fluxo completo sem morador algum: QR → escalonamento → fila → operador → autorizado
|
||||
|
||||
## Não faça nesta fase
|
||||
|
||||
- Pré-autorização e recorrentes (v2)
|
||||
- WhatsApp (FASE 8)
|
||||
- Escalação para telefone/PSTN
|
||||
126
prompts/FASE-8-planos-e-lgpd.md
Normal file
126
prompts/FASE-8-planos-e-lgpd.md
Normal file
@@ -0,0 +1,126 @@
|
||||
# FASE 8 — Planos, módulos, LGPD e observabilidade
|
||||
|
||||
## Objetivo
|
||||
|
||||
Fechar o que torna o produto operável e vendável: módulos ligáveis por tenant, canal de WhatsApp preparado (interface, não integração), retenção e expurgo automáticos, direitos do titular, e os SLOs medidos de verdade.
|
||||
|
||||
## Pré-requisitos
|
||||
|
||||
FASES 0–7 concluídas. Ler `docs/06-LGPD-E-SEGURANCA.md` §3 e §4, e `docs/05-INFRA-DOCKER.md` §6.
|
||||
|
||||
## Tarefas
|
||||
|
||||
### 1. Canais de notificação plugáveis
|
||||
|
||||
```kotlin
|
||||
interface NotificationChannel {
|
||||
val key: FeatureKey? // null = sempre ativo
|
||||
suspend fun notify(alvo: NotificationTarget, evento: NotificationEvent): Result<Unit>
|
||||
}
|
||||
```
|
||||
|
||||
`PushChannel` (FCM + APNs) sempre ativo. `WebSocketChannel` sempre ativo.
|
||||
|
||||
`NotificationDispatcher` resolve os canais habilitados para o tenant e despacha **em paralelo**, tolerando falha individual — um canal fora do ar não pode impedir os demais.
|
||||
|
||||
### 2. WhatsApp — interface e flag, sem integração
|
||||
|
||||
O WhatsApp é **módulo pago opcional**; o app é o notificador principal. Nesta fase entrega-se apenas:
|
||||
|
||||
- `WhatsAppChannel` com `key = FeatureKey.WHATSAPP_NOTIFICATIONS`, implementação **stub** que registra o que seria enviado
|
||||
- Toggle no painel admin, com o efeito descrito em uma frase
|
||||
- Estrutura de templates prevista
|
||||
- Documentação em `docs/anexos/whatsapp-v2.md` com o que falta: WABA verificada, templates `utility` aprovados, custo (~US$0,004/msg no Brasil, grátis dentro da janela de 24h)
|
||||
|
||||
**Não integre com a Meta agora.** Verificação de negócio e aprovação de template levam dias e bloqueariam o lançamento. E registre no documento a limitação de fundo: a latência do WhatsApp não serve para *tocar* uma chamada — ele é aviso paralelo com deep link, nunca o canal primário.
|
||||
|
||||
### 3. Feature flags
|
||||
|
||||
```kotlin
|
||||
@Service
|
||||
class FeatureService {
|
||||
fun habilitado(tenantId: UUID, key: FeatureKey): Boolean
|
||||
fun dentroDaQuota(tenantId: UUID, key: FeatureKey, uso: Int): Boolean
|
||||
}
|
||||
```
|
||||
|
||||
Cache curto (60s) com invalidação ao alterar. Toda mudança de flag vai para `audit_log` com autor.
|
||||
|
||||
Quotas relevantes: minutos de vídeo por mês, gravações armazenadas, operadores simultâneos.
|
||||
|
||||
### 4. Gravação de chamada
|
||||
|
||||
Módulo `video_recording`. LiveKit Egress grava para o MinIO ao entrar em `EM_CHAMADA`, se habilitado.
|
||||
|
||||
**Aviso reforçado obrigatório:** ao gravar, o visitante vê aviso explícito antes do início, e a versão desse aviso é registrada. Gravar sem avisar é violação direta.
|
||||
|
||||
Retenção de 90 dias. Cada gravação soma banda no SFU — ative com quota.
|
||||
|
||||
### 5. Retenção e expurgo
|
||||
|
||||
Job diário `MEDIA_PURGE`:
|
||||
|
||||
1. Seleciona `media_assets` com `expires_at < now()` e `storage_key IS NOT NULL`
|
||||
2. Apaga o objeto no MinIO
|
||||
3. `UPDATE media_assets SET storage_key = NULL, purged_at = now()`
|
||||
4. **Preserva a linha**
|
||||
|
||||
O passo 4 é o ponto: fica provado que existiu uma foto e que ela foi eliminada no prazo — exatamente o que se demonstra numa fiscalização.
|
||||
|
||||
Prazos configuráveis por tenant, com **teto** definido pela plataforma. Um condomínio querendo guardar foto por 5 anos configuraria um risco que a plataforma não aceita hospedar.
|
||||
|
||||
Job diário de limpeza de `idempotency_keys` vencidas.
|
||||
|
||||
### 6. Direitos do titular
|
||||
|
||||
`POST /admin/lgpd/export` — busca por CPF ou telefone, devolve JSON com todas as visitas, mídias (URLs assinadas) e registros de aviso.
|
||||
|
||||
`POST /admin/lgpd/erase` — apaga mídia, anonimiza `visitor_name` para `[REMOVIDO]`, limpa documento e telefone, **preserva a linha da visita** com data, unidade e resultado. Justificativa: obrigação de guarda de registro de acesso para segurança patrimonial. A linha vira estatística, não identificação.
|
||||
|
||||
Ambas registram em `audit_log` e devolvem comprovante em PDF — o condomínio precisa provar que atendeu no prazo de 15 dias.
|
||||
|
||||
### 7. Versionamento do aviso de privacidade
|
||||
|
||||
Editor no admin. Alterar o texto **cria nova versão**; as visitas antigas continuam apontando para a versão que foi realmente exibida. Sem isso, não há como provar o que o visitante leu naquele dia.
|
||||
|
||||
### 8. SLOs medidos
|
||||
|
||||
Instrumentar as métricas de `01-ARQUITETURA.md` §5.7:
|
||||
|
||||
```
|
||||
portaria.visit.ring_latency QR → push entregue no dispositivo
|
||||
portaria.visit.answered_ratio atendidas pelo morador / total
|
||||
portaria.delivery.resolution_latency QR → resolvida
|
||||
portaria.delivery.abandoned_ratio canceladas ou expiradas
|
||||
portaria.entry_flow.availability caminho crítico
|
||||
```
|
||||
|
||||
`ring_latency` mede até a **entrega** do push, não até o envio. A diferença entre os dois é justamente onde a falha acontece.
|
||||
|
||||
Dashboards e alertas conforme `05-INFRA-DOCKER.md` §6, incluindo os dois alertas de negócio (taxa de atendimento baixa e tokens mortos) que viram tarefa de customer success, não plantão.
|
||||
|
||||
### 9. Runbooks
|
||||
|
||||
`docs/anexos/runbook-incidente.md` — vazamento, indisponibilidade, perda de dados; com prazos ANPD.
|
||||
`docs/anexos/runbook-operacao.md` — restore testado, rotação de segredo, invalidação de QR, adição de condomínio.
|
||||
`docs/anexos/LIA-modelo.md` — modelo de Legitimate Interest Assessment para preenchimento por condomínio.
|
||||
|
||||
## Critérios de aceite
|
||||
|
||||
- [ ] Dispatcher despacha em paralelo e tolera falha de um canal
|
||||
- [ ] `WhatsAppChannel` desligado não é invocado; ligado, registra o que enviaria
|
||||
- [ ] Toggle de módulo tem efeito imediato e fica em `audit_log`
|
||||
- [ ] Gravação só ocorre com módulo ativo e aviso reforçado exibido
|
||||
- [ ] `MEDIA_PURGE` apaga o objeto e preserva a linha
|
||||
- [ ] Exportação LGPD devolve todos os dados de um titular
|
||||
- [ ] Eliminação anonimiza e preserva a linha da visita
|
||||
- [ ] Alterar o aviso cria versão; visitas antigas mantêm a original
|
||||
- [ ] Todos os SLOs visíveis no Grafana com dados reais
|
||||
- [ ] Alertas disparam em condição simulada
|
||||
- [ ] Restore de backup testado e cronometrado
|
||||
|
||||
## Não faça nesta fase
|
||||
|
||||
- Integração real com a Meta (v2)
|
||||
- Billing automatizado (v2)
|
||||
- Ativar multi-tenancy
|
||||
126
prompts/FASE-9-hardening.md
Normal file
126
prompts/FASE-9-hardening.md
Normal file
@@ -0,0 +1,126 @@
|
||||
# FASE 9 — Hardening e produção
|
||||
|
||||
## Objetivo
|
||||
|
||||
Provar que o sistema aguenta o mundo real: testes ponta a ponta, carga no SFU, injeção de falhas, revisão de segurança e deploy documentado.
|
||||
|
||||
## Pré-requisitos
|
||||
|
||||
FASES 0–8 concluídas. Ler `docs/06-LGPD-E-SEGURANCA.md` §5 e `docs/07-REQUISITOS-DE-CAMPO.md`.
|
||||
|
||||
## Tarefas
|
||||
|
||||
### 1. Testes ponta a ponta
|
||||
|
||||
Playwright para os webs, Maestro para os apps. Cenários obrigatórios:
|
||||
|
||||
1. Visita completa: QR → aviso → permissões → foto → unidade → morador atende → autoriza → PIN
|
||||
2. Entrega em um toque, resolvida pela notificação, em menos de 10s
|
||||
3. Escalonamento completo: ninguém atende → outros moradores → fila → operador → autorizado
|
||||
4. Recado: ninguém atende, sem módulo de operador → grava → morador resolve depois
|
||||
5. Unidade inexistente: visita criada, ninguém notificado, expira — **resposta indistinguível de unidade válida**
|
||||
6. Fora do geofence: bloqueado com mensagem acionável
|
||||
7. Quiet hours: desvia sem tocar em ninguém
|
||||
8. Dois moradores respondendo juntos: um vence, o outro vê quem resolveu
|
||||
9. LiveKit fora: degrada para áudio, depois foto+texto
|
||||
10. Backend reiniciado no meio de uma visita: escalonamento acontece mesmo assim
|
||||
|
||||
O cenário 10 é o que valida o padrão de timeouts persistidos. O 5 valida a busca cega.
|
||||
|
||||
### 2. Teste de carga
|
||||
|
||||
**SFU:** 50 chamadas simultâneas de 2 participantes, medindo banda, CPU e tempo até o primeiro frame. Confronte com o dimensionamento de `05-INFRA-DOCKER.md` §4.
|
||||
|
||||
**API:** 100 visitas/minuto sustentadas — muito acima do real (~0,3 req/s), de propósito.
|
||||
|
||||
**Fila:** 10 mil jobs agendados para o mesmo instante, verificando que nenhum é perdido ou executado duas vezes.
|
||||
|
||||
**Banco:** tabela de auditoria com 1 milhão de visitas; a tela do admin precisa continuar fluida.
|
||||
|
||||
### 3. Injeção de falhas
|
||||
|
||||
Derrube cada peça, uma por vez, com uma visita em andamento, e verifique o comportamento:
|
||||
|
||||
| Peça derrubada | Comportamento esperado |
|
||||
|---|---|
|
||||
| LiveKit | Degrada para áudio → foto+texto → operador |
|
||||
| Redis | WebSocket degrada para polling; fila continua (está no Postgres) |
|
||||
| MinIO | Visita prossegue sem foto, com aviso; upload entra em retry |
|
||||
| FCM | WebSocket assume se o app está aberto; falha vai para o dashboard |
|
||||
| Uma réplica do backend | Outra assume; jobs pendentes executam |
|
||||
| Postgres (breve) | Erro claro ao visitante e contingência física acionada |
|
||||
|
||||
**Teste do relógio:** atrase o relógio de uma réplica em 30s e confirme que ShedLock não permite execução dupla.
|
||||
|
||||
### 4. Revisão de segurança
|
||||
|
||||
Percorra o modelo de ameaças de `06-LGPD-E-SEGURANCA.md` §5 e prove cada mitigação:
|
||||
|
||||
- [ ] **A.1 Busca cega** — tempo de resposta constante medido estatisticamente, não no olho. Este é o teste mais importante da fase
|
||||
- [ ] **A.2 Geofence** — rejeita fora do raio; GPS falsificado é registrado
|
||||
- [ ] **A.3 DoS social** — rate limit e quiet hours funcionam; varredura gera alerta
|
||||
- [ ] **A.4 Mídia** — nenhuma URL pública; assinada expira; acesso auditado
|
||||
- [ ] **A.5 Auditoria** — `UPDATE`/`DELETE` em `audit_log` falham no banco
|
||||
- [ ] **A.6 Tokens** — JWT de visitante não serve para outra visita nem outra portaria
|
||||
- [ ] **A.7 QR** — incrementar `qr_version` invalida os códigos antigos
|
||||
|
||||
Rode também: `pnpm audit`, `gradle dependencyCheck`, varredura de imagem Docker (Trivy), e headers verificados por observatório de segurança.
|
||||
|
||||
**Grep obrigatório:** nenhuma biblioteca de detecção ou reconhecimento facial em `build.gradle.kts` ou `package.json`. A tentação aparece disfarçada de UX ("só para enquadrar o rosto") e tem consequência jurídica direta.
|
||||
|
||||
### 5. Checklist de produção
|
||||
|
||||
**Infraestrutura**
|
||||
- [ ] Faixa UDP 50000–60000 aberta (esquecer isso conecta a chamada e nunca mostra vídeo)
|
||||
- [ ] TLS válido e renovação automática testada
|
||||
- [ ] Backup automático com **restore testado e cronometrado**
|
||||
- [ ] Segredos fora do repositório e fora das imagens
|
||||
- [ ] Healthchecks respondendo em todos os serviços
|
||||
- [ ] Alertas chegando em canal monitorado de verdade
|
||||
|
||||
**Aplicação**
|
||||
- [ ] Perfil `prod`, sem stack trace exposto
|
||||
- [ ] Rate limits ativos
|
||||
- [ ] CORS restrito aos domínios reais
|
||||
- [ ] Logs sem dado pessoal (nome, foto, coordenada) — **verificar linha a linha**
|
||||
- [ ] Migrations aplicadas em ordem em banco limpo
|
||||
|
||||
**Apps**
|
||||
- [ ] Builds de release assinados
|
||||
- [ ] Testados em dispositivo físico, Android e iOS
|
||||
- [ ] Ficha de privacidade da App Store e do Google Play preenchida
|
||||
- [ ] Justificativa de VoIP documentada para a App Review
|
||||
- [ ] Crash reporting **sem PII**
|
||||
|
||||
**Conformidade** — checklist completo de `06-LGPD-E-SEGURANCA.md` §9.
|
||||
|
||||
**Campo** — checklist de ativação de `07-REQUISITOS-DE-CAMPO.md` §2 para o condomínio piloto.
|
||||
|
||||
### 6. Deploy
|
||||
|
||||
`docs/anexos/deploy.md` com provisionamento, primeiro deploy, atualização sem downtime (rolling com healthcheck), rollback, aplicação de migration com dado em produção, e o procedimento de crescer para HA.
|
||||
|
||||
### 7. Piloto assistido
|
||||
|
||||
Antes da ativação definitiva, **duas semanas com o módulo de operador ligado**, monitoramento diário e reunião semanal com o síndico.
|
||||
|
||||
O que se mede: taxa real de atendimento, tempo médio de resolução, abandono de entregador, incidentes, e feedback qualitativo de moradores e entregadores.
|
||||
|
||||
**É esse número — não a expectativa comercial — que define se o condomínio fica no plano Autônomo ou no Assistido.**
|
||||
|
||||
## Critérios de aceite
|
||||
|
||||
- [ ] Os 10 cenários E2E passam em CI
|
||||
- [ ] Carga do SFU dentro do dimensionamento previsto
|
||||
- [ ] Toda injeção de falha se comporta como na tabela
|
||||
- [ ] Todas as mitigações do modelo de ameaças verificadas
|
||||
- [ ] Busca cega com tempo constante comprovado estatisticamente
|
||||
- [ ] Checklist de produção 100% marcado
|
||||
- [ ] Restore de backup executado e cronometrado
|
||||
- [ ] Runbooks testados por alguém que não os escreveu
|
||||
- [ ] Piloto de duas semanas concluído com métricas coletadas
|
||||
|
||||
## Não faça nesta fase
|
||||
|
||||
- Novas funcionalidades — se aparecer necessidade, vira v2
|
||||
- Otimização sem métrica que a justifique
|
||||
Reference in New Issue
Block a user