Files
b2bcall/TODO.md
B2BCall Bootstrap 6273b32214 feat: add frontend, nginx reverse proxy and monitoring/reports extras
- apps/frontend: Next.js 15 (App Router) + Tailwind v4 + componentes estilo
  shadcn/ui sobre Radix UI + TanStack Query. Tema light/dark, logo
  processada. Menu completo (secao 52) com gating por permissao real.
  Todas as telas do checklist de aceite (secao 90) conectadas a endpoints
  reais (nao mockup): login, usuarios, perfis/permissoes, ramais/troncos,
  dialplan, filas/agentes, console do agente, campanhas (CPS/CSV/
  iniciar/pausar), monitoramento ao vivo (polling, nao WebSocket real),
  TME/TMA, busca/export de chamadas, administracao do Asterisk, auditoria
- infrastructure/nginx: reverse proxy colocando frontend+API na mesma
  origem (porta 80), antecipado da Fase 9 pois a API nao publica porta
  propria
- apps/api: GET /api/monitoring/agents (estado corrente real via
  agent_state_events em aberto) e filtro queueId em GET /api/reports/calls

Pendencia registrada: tela de Callbacks nao implementada (schema existe
desde a Fase 6, mas nunca houve controller/service — construir a tela sem
API real seria mockup). Verificacao visual em navegador nao foi possivel
neste ambiente headless; validado via tsc/eslint/next build limpos + curl
reproduzindo as chamadas do navegador (middleware de auth, 24 paginas
protegidas via Nginx, endpoints de dados com cookie de sessao).
2026-08-27 17:35:34 -03:00

355 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# B2BCall — TODO Operacional
Checklist vivo. Marcar `[x]` somente após testar. Não deixar item concluído sem
validação. Ver critérios de aceite completos em `docs/ARCHITECTURE.md` e no prompt
mestre original (`agente.md`, seções 90-93).
## Fase 0 — Infraestrutura base
- [x] Levantamento do servidor (SO, recursos, rede)
- [x] docs/ARCHITECTURE.md
- [x] TODO.md
- [x] Git init + .gitignore + primeiro commit
- [x] Instalar Docker Engine + Compose plugin (29.7.2 / Compose v5.5.0)
- [x] Estrutura de diretórios do monorepo
- [x] .env.example (+ scripts/generate-secrets.sh, .env real gerado localmente)
- [ ] nftables (firewall base, default-deny administrativo, sem bloquear SSH) —
adiado para a Fase 9, quando houver serviços/portas reais a proteger
## Fase 1 — Dados e cache
- [x] docker-compose: serviço postgres 17-alpine (schemas application/asterisk) — testado, healthy
- [x] docker-compose: serviço redis 7-alpine (auth, AOF, maxmemory 100mb) — testado, healthy
- [x] Health checks postgres/redis
- [ ] packages/database: schema inicial + migrations tool
## Fase 2 — Asterisk
- [x] Dockerfile Asterisk 22.10.1 LTS compilado do fonte (host network) —
Debian trixie não tem mais o pacote `asterisk` nos repos oficiais
- [x] PJSIP (transport UDP:5060; chan_sip nem existe mais a partir do Ast 21)
- [x] AMI habilitado — testado login via 127.0.0.1:5038, bloqueado na LAN via nftables
- [x] ARI habilitado — testado GET /ari/asterisk/info via 127.0.0.1:8088, 401 sem auth
- [x] Realtime ODBC -> Postgres schema `asterisk` (sorcery.conf + extconfig.conf
+ res_odbc.conf) — `pjsip show endpoints` consulta o Postgres sem erro
- [x] CDR via cdr_adaptive_odbc — testado, grava em asterisk.cdr (ANSWERED,
duration/billsec corretos). Nota: coluna `end` renomeada para
`end_time` (palavra reservada no Postgres, cdr_adaptive_odbc não
quota identificadores)
- [x] CEL via cel_odbc — testado, grava em asterisk.cel (CHAN_START/ANSWER/
HANGUP/CHAN_END)
- [x] queue_log habilitado (padrão do app_queue, arquivo em /var/log/asterisk)
- [x] Dialplan inicial de teste (contexto `b2bcall-healthcheck`, exten 600,
Answer/Playback/Echo) — testado via `channel originate ... extension`
- [x] nftables protegendo AMI/ARI/SIP na interface da LAN (ens18), trazido da
Fase 9 para já — não testado a partir de outra máquina na LAN (só
localmente, onde tráfego para o próprio IP vai por `lo` e não
exercita a regra; revisar quando OpenSIPS/outro host estiver disponível)
### Pendências conhecidas da Fase 2 (não bloqueiam, revisitar depois)
- [ ] `res_pjsip_outbound_registration` recusa carregar no boot (mensagem
"no existing documentation" para o tipo 'registration' do sorcery) —
carregamento manual via `module load` funciona mas é desfeito por uma
flag interna de "declined at boot". Só afeta troncos com registration
outbound; endpoints/aors funcionam normalmente. Investigar na Fase 4
ao implementar cadastro de troncos com registration.
- [ ] `res_config_ldap` gera ERROR de log no boot (sem LDAP configurado,
não usamos). Cosmético — considerar `noload` em modules.conf.
- [ ] `cdr_pgsql`/`cel_pgsql` não compilaram (falta libpq-dev na imagem);
usamos `cdr_adaptive_odbc`/`cel_odbc` como alternativa funcional via
ODBC — decisão adequada, não precisa dos módulos pgsql nativos.
## Fase 3 — Backend base
- [x] apps/api (NestJS 11 + Fastify) bootstrap — containerizado, healthy
- [x] packages/database (Prisma) + packages/shared (catálogo de permissões)
— decisão de ORM documentada em docs/ARCHITECTURE.md 3.6.1
- [x] Migration inicial aplicada (users/sessions/roles/permissions/
user_roles/role_permissions/audit_logs/password_reset_tokens)
- [x] Seed (40 permissões, 4 perfis, bootstrap super_admin com senha
aleatória em FIRST_LOGIN.txt chmod 600) — testado
- [x] Autenticação (Argon2id, access JWT + refresh opaco com rotação,
cookies HttpOnly/SameSite=Lax, refresh_token restrito a /api/auth) —
testado: login, /me, refresh, change-password, revogação de sessão
- [x] Rate limiting de login progressivo via Redis (5/60s, bloqueio
1min→2min→4min...até 1h por IP) — testado com 7 tentativas seguidas
- [x] RBAC completo (users/roles/permissions/user_roles/role_permissions)
reforçado no backend via PermissionsGuard — testado (nega sem
permissão, permite com todas as permissões) + unit tests
- [x] Proteção contra auto-elevação de privilégio (usuário não altera os
próprios roleIds) — testado, HTTP 403
- [x] Auditoria (audit_logs) chamada explicitamente em cada ação sensível
(login/login_failed/logout/password_changed/user_created/
user_updated/role_*) com redação de campos sensíveis — testado via
GET /api/audit (roles.manage/audit.view), paginação server-side
- [x] Health checks /api/health, /api/health/live, /api/health/ready
(Postgres + Redis reais, sem dado fake) — testado
- [x] Swagger/OpenAPI em /api/docs, desabilitável via SWAGGER_ENABLED=false
— testado (200 com flag default true)
- [x] Logs estruturados JSON (nestjs-pino) com request_id de correlação e
redação de senha/tokens/cookies
- [x] Filtro global de exceções: nunca expõe erro cru, sempre requestId
- [x] Lint (eslint --fix) e testes unitários (PermissionsGuard) passando
- [ ] Recuperação de senha por e-mail: lógica completa (token com
expiração, hash, uso único), mas envio real via SMTP é um stub que
só loga — falta credencial SMTP real (.env SMTP_*), meramente
externo — trocar MailerService por nodemailer quando houver
## Fase 4 — Telefonia (camada de aplicação)
- [x] packages/telephony: cliente AMI próprio (TCP raw, sem dependência de
terceiros) + TelephonyProvider + AsteriskTelephonyProvider — testado
contra o Asterisk real: login, PJSIPShowEndpoints/QueueStatus (lista
vazia tratada corretamente), Command (parsing do formato real do
Asterisk 22: múltiplos headers "Output:", não mais "Follows"/"--END
COMMAND--"), Originate, stream de eventos
- [x] apps/asterisk-events (worker dedicado, containerizado) — conecta via
host.docker.internal (rede bridge -> Asterisk em host network),
normaliza eventos relevantes, persiste ExtensionState (Postgres),
publica em Redis pub/sub (b2bcall:events:asterisk e :extensions),
heartbeat para health check — testado ponta a ponta com chamada real
- [x] Schema Prisma: Trunk, Extension, ExtensionState + criptografia de
segredos AES-256-GCM (packages/shared/secret-crypto.ts)
- [x] CRUD Troncos (IP/AUTH/REGISTRATION, CPS máximo, ACL, secret cifrado
AES-256-GCM, teste de status) — testado: criado via API, verificado
que o Asterisk enxerga o endpoint via realtime SEM reload, status
consultado via PJSIPShowContacts, exclusão em cascata confirmada
- [x] CRUD Ramais (senha SIP gerada automaticamente, nunca reexibida exceto
na criação/reset, reset endpoint) — testado igual aos troncos
- [x] GET /api/monitoring/extensions — status com prioridade de cor
(offline/available/busy; pausa e agente logado chegam na Fase 5)
- [ ] Painel visual de Ramais (WebSocket) — backend (gateway WS) ainda
pendente; ExtensionState/pub-sub e o endpoint de monitoramento já
prontos como base (frontend consumirá via WS na Fase 8)
- [x] Dialplan estruturado e versionado (dialplan_entries + dialplan_versions)
— testado ponta a ponta: criar entradas -> publicar -> gera
b2bcall-dialplan.conf (volume compartilhado api<->asterisk) -> AMI
"dialplan reload" -> "dialplan show" confirma ativo, SEM restart.
Validação estrita de caracteres em cada campo (nunca aceita entrada
capaz de injetar linhas de config). Rollback automático se a
verificação pós-reload detectar erro; rollback manual para qualquer
versão anterior via endpoint dedicado. Limitação conhecida: detecta
falhas estruturais/sintáticas, não erros semânticos como nome de
aplicação Asterisk inexistente (só se manifesta em tempo de chamada)
- [x] Administração do Asterisk: status (AMI + heartbeat asterisk-events),
module show, e diagnóstico com ALLOWLIST ESTRITA de comandos exatos
(core show uptime/channels/version, pjsip show *, queue show, module
show, dialplan show) — testado: comando permitido executa, comando
arbitrário rejeitado com 400. Abas de configuração fina (pjsip/rtp/
cdr/cel/logs como formulário editável) ficam para quando o fluxo de
"editor avançado com backup/validação" for generalizado além do
dialplan — no momento config geral do Asterisk é só leitura via
status/diagnóstico, não tem tela de edição de asterisk.conf/rtp.conf
## Fase 5 — Call Center
- [x] Agentes (agents, separado de users 1:1) + agent_sessions +
agent_state_events + agent_pause_events — schema Prisma completo
- [x] Máquina de estados do agente (LOGGED_IN→AVAILABLE→PAUSED→AVAILABLE,
logout) com histórico início/fim por estado — testado ponta a ponta
contra o Asterisk real: login cria sessão, "disponível" faz AMI
QueueAdd (confirmado via `queue show`: membro dinâmico aparece),
"pausar" faz QueuePause com motivo (confirmado: "paused:Almoco"
aparece ao vivo), "despausar" reverte, "logout" faz QueueRemove
(confirmado: fila volta a "No Members")
- [x] Motivos de pausa (CRUD) — sem permissão dedicada no catálogo da
seção 11, tratado sob settings.manage
- [x] Filas (CRUD + 8 estratégias do enum QueueStrategy) — gera
queues.conf via o mesmo padrão do dialplan (arquivo compartilhado +
reload), mas SEM membros estáticos: membership é 100% dinâmica via
AMI (login/logout do agente), evitando dessincronia entre duas
fontes de verdade
- [x] Tela do agente (backend completo: login/available/pause/unpause/
logout via /api/agent-console/*) — disposição de chamada fica para
quando existir uma chamada de verdade para dispor (Fase 6)
- [ ] Disposições de chamada (CRUD + ações: callback, DNC) — adiado para
Fase 6 junto com Campanhas/Leads, já que disposição só faz sentido
quando há chamadas de campanha reais para classificar
- [ ] Callback (agendamento + scheduler) — idem, depende de leads/campanhas
- [x] Monitoramento de Filas (tempo real): GET /api/monitoring/queues
(chamadas aguardando, maior espera, agentes logados/pausados/
disponíveis) via AMI QueueStatus ao vivo. TME/TMA/SLA/taxa de
abandono HISTÓRICOS ficam para a Fase 7 (dependem de consolidação
de CDR/CEL/queue_log) — não inventamos número aqui
## Fase 6 — Campanhas e discador preditivo
- [x] CRUD Campanhas (todos os campos da seção 24) — testado: criação com
defaults corretos, máquina de estados (DRAFT/READY/RUNNING/PAUSED/
DRAINING/STOPPED/COMPLETED) rejeitando transições inválidas (ex.:
PAUSED->PAUSED = 400), edição bloqueada com campanha RUNNING
- [x] Import CSV streaming (detecção automática de delimitador — vírgula/
ponto-e-vírgula/tab —, mapeamento de coluna por nome de header,
validação+normalização, dedupe dentro do arquivo E contra leads já
existentes na campanha, CSV de rejeitados com motivo, modo dryRun
para preview sem persistir) — testado com CSV real (5 linhas: 3
válidas, 1 inválida, 1 duplicada — todos os contadores bateram)
- [x] Normalização de telefone (packages/shared, BR — E.164, DDD/celular
validados) — 7 testes unitários passando
- [x] Lista de supressão (DNC): CRUD + import CSV + remoção sempre exige
motivo e é auditada — checagem obrigatória pré-originação
(isSuppressed) implementada e usada pelo dialer-worker (ver abaixo)
- [x] CPS limiter (token bucket via Lua atômico no Redis, dois buckets
independentes campanha+tronco, min() dos dois — multi-worker seguro)
- [x] Reserva concorrente de leads (SQL único com FOR UPDATE SKIP LOCKED —
atômico; timeout de 90s libera reservas órfãs após queda de worker)
- [x] Idempotência de originação (DialAttempt.id é o attempt_id de negócio,
nunca reoriginado; UNIQUEID nunca usado como PK — seção 97)
- [x] PredictiveDialerEngine (EWMA de answerProbability/avgTalkTimeSeconds/
abandonRate, previsão de liberação de agentes via TMA histórico +
tempo decorrido, ajuste de pacing) — apps/dialer-worker/src/
predictive-engine.ts, 100% lógica pura testável, 14 testes unitários
- [x] Controle de abandono (pacing cai imediatamente acima da meta; nunca
sobe "porque nada de ruim aconteceu" durante período ocioso — bug
real encontrado e corrigido durante o teste do cenário da seção 66)
- [ ] AMD opcional por campanha — campo existe no schema/DTO, detecção real
(app AMD do Asterisk) ainda não integrada à originação (ver
docs/PREDICTIVE_DIALER.md seção 7)
- [ ] Wrap-up time — campo existe, mas transição automática do agente para
WRAP_UP via eventos reais (AgentComplete) não implementada; hoje só
funciona no modo simulação e nas transições manuais da Fase 5 (ver
docs/PREDICTIVE_DIALER.md seção 7)
- [x] Retry engine (regras por causa configuráveis por campanha via JSON,
default seção 79, nunca rediscagem infinita via maxAttempts) —
testado ponta a ponta (NO_ANSWER agendou retry corretamente)
- [x] Horário de campanha (timezone via Intl.DateTimeFormat, dias da
semana, HH:MM, WAITING_SCHEDULE computado) — 6 testes unitários
cobrindo timezone, dias, startDate/endDate
- [x] Lock distribuído por campanha (Redis SET NX PX + token de posse +
renovação/liberação seguras via Lua — nunca libera lock de outro dono)
- [x] docs/PREDICTIVE_DIALER.md — algoritmo, decisões, bugs encontrados e
corrigidos durante o teste, limitações conhecidas documentadas
**Testado ponta a ponta contra containers reais** (Postgres/Redis/Asterisk):
campanha completa criada → agente logado/disponível via API real → leads
importados → campanha iniciada → reserva atômica → CPS respeitado →
simulação de NO_ANSWER (retry agendado) e ANSWERED (AGENT_CONNECTED, EWMA
atualizada ao vivo no Redis) → parada sem derrubar chamadas em andamento.
## Fase 7 — CDR, métricas e relatórios
- [x] Modelo consolidado de chamadas — `DialAttempt` já concentra os timestamps
reais (started/ringing/answered/queued/agentConnected/ended) e
hangupCause; CDR/CEL do Asterisk seguem gravados via `cdr_adaptive_odbc`/
`cel_odbc` na base `asterisk` (não duplicados no domínio da app). Não há
correlação automática CDR↔DialAttempt para chamadas reais (não
simuladas) além do timeout de segurança — ver nota em
docs/PREDICTIVE_DIALER.md.
- [x] Reconciliação de estados órfãos após restart — `reconciliation.ts`
(`reconcileOrphanedAttempts`), rodando a cada 60s via `main.ts`; força
`DialAttempt`s presos há >10min para `FAILED`/`RECONCILED_ORPHAN` e
devolve o lead para `READY`. Testado por unit tests; não exercitado
neste ciclo via kill real do worker (fica para Fase 10).
- [x] TME / TMA — cálculo real em `reports.service.ts#calculateMetrics` e
`dashboard.service.ts#overview/campaignDashboard`, a partir de
`queuedAt`/`agentConnectedAt`/`endedAt`. Validado end-to-end: campanha
de teste "Campanha F7" retornou `tmeSeconds: 0.023`, `tmaSeconds: 136.6`
batendo com os timestamps reais da chamada simulada conectada.
- [x] Relatório de Chamadas — `GET /api/reports/calls` (filtros
campaignId/agentId/dispositionId/phone/state/from/to, paginação
server-side) e `GET /api/reports/calls/export` (CSV via
`csv-stringify`, teto de 100k linhas). Testado contra as 10 tentativas
reais da campanha de teste — paginação e filtros funcionando.
- [x] Relatório de Agentes — `GET /api/reports/agents/:agentId`, tempo real
por estado a partir de `agent_state_events`/`agent_pause_events`.
Validado: agente de teste retornou `timeByStateSeconds` com
LOGGED_IN/AVAILABLE/IN_CALL/WRAP_UP batendo com o ciclo real observado.
- [x] Dashboard geral — `GET /api/dashboard` e `/api/dashboard/calls-by-hour`,
cards/gráficos 100% derivados de `dial_attempts`/`agent_state_events`
reais (sem números inventados, seção 72). Validado com dados reais.
- [x] Dashboard do discador — `GET /api/dashboard/campaigns/:id`, combina
contagens Postgres com snapshot EWMA do Redis
(`dialer:stats:{campaignId}`). Validado: `abandonRate` e
`pacingFactor` refletiram corretamente o abandono real ocorrido no
teste.
- [x] Compliance de Chamadas — `ComplianceSettings` (singleton configurável:
limiar de chamada curta, limites de tentativas por número/dia/mês,
threshold de alto volume) + `GET/PATCH /api/compliance/settings` e
`GET /api/compliance/indicators` (contadores reais de chamadas
hoje/mês, chamadas curtas, abandonadas, números acima do limite
diário, alertas). Validado com dados reais da campanha de teste.
**Teste E2E executado (2026-08-27):** criados trunk/fila/ramal/agente/usuário/
campanha de teste ("Campanha F7"), 10 leads importados via CSV, campanha
rodada em `DIALER_SIMULATION=true`. Confirmado em produção real (não só no
harness isolado): `DialAttempt.agentId` populado corretamente ao conectar
(bug da Fase 7 corrigido em `agent-call-binding.ts`); ciclo completo de
`AgentStateEvent` `AVAILABLE → IN_CALL → WRAP_UP → AVAILABLE`; `abandonRate`
no Redis atualizado de `0` para `0.05` após abandono real (bug do EWMA não
conectado ao `campaign-worker.ts` corrigido); todos os endpoints de
relatórios/dashboard/compliance retornando dados reais e coerentes com o
banco. Todos os fixtures de teste foram removidos/desativados ao final.
## Fase 8 — Frontend completo
- [x] Bootstrap Next.js 15 (App Router) + Tailwind v4 + componentes estilo
shadcn/ui (escritos à mão sobre Radix UI, sem CLI interativo) +
TanStack Query. WS client não implementado (ver nota abaixo) —
telas ao vivo usam polling via `refetchInterval` (515s conforme a
tela), suficiente para o volume atual mas não é WebSocket real.
- [x] Logo processada (`b2bcall.png` copiada, original nunca modificada) +
tema light/dark com toggle manual + `prefers-color-scheme`.
- [x] Menu completo (seção 52) com gating por permissão real
(`RequirePermission` + itens de menu ocultos por `useAuth().can()`),
Dashboard/Discador/Call Center/Telefonia/Monitoramento/
Relatórios/Sistema + Console do Agente.
- [x] Reverse proxy Nginx (`infrastructure/nginx/`) colocando frontend e
API na mesma origem (porta 80, único serviço publicado à LAN) —
antecipado da Fase 9 (seção 87) porque sem isso não havia como um
navegador de verdade alcançar a API (que não publica porta própria).
- [x] Todas as telas do checklist de aceite (seção 90) implementadas e
conectadas a endpoints reais: login, usuários, perfis/permissões,
ramais, status de ramal, troncos, dialplan, filas, agentes,
associação agente↔fila, motivos de pausa, console do agente
(login/pausa/retirar pausa), campanhas (CPS, importação CSV,
iniciar/pausar), monitoramento (discagem/agentes/fila) ao vivo,
TME/TMA, busca de chamadas com filtro por fila/agente/estado/
telefone/data, exportação CSV, administração do Asterisk
(status + diagnóstico allowlist + reload), auditoria.
Adicionado `queueId` a `GET /api/reports/calls` (não existia antes)
para permitir o filtro por fila explicitamente pedido na seção 90.
- [ ] Callbacks (menu da seção 52) — **não implementado nesta fase**: o
modelo `Callback` existe no schema desde a Fase 6, mas nunca houve
controller/service de API para ele, e a seção 90 (critério literal de
aceite) não exige essa tela. Criar uma tela sem a API por trás seria
construir um mockup, o que a diretriz do projeto proíbe. Fica
registrado como pendência explícita, não descartado silenciosamente.
- [ ] **Verificação visual em navegador não foi possível nesta sessão**
ambiente é um servidor Debian headless sem display/browser. A
verificação feita foi: `tsc --noEmit` limpo, `eslint` limpo, build de
produção (`next build`) gerando as 27 rotas sem erro, e testes
funcionais via `curl` reproduzindo exatamente as chamadas que o
navegador faria — login via `/api/auth/login`, cookie de sessão
validado pelo middleware (`/` redireciona para `/login` sem cookie,
libera com cookie), todas as 24 páginas protegidas retornando HTTP
200 através do Nginx, e os endpoints de dados (`/api/dashboard`,
`/api/reports/*`, `/api/compliance/*` etc.) retornando dados reais
com o mesmo cookie de sessão. O que **não** foi verificado: renderização
visual real, interações de clique/formulário no DOM, responsividade,
tema escuro na prática. Recomenda-se ao usuário abrir
`http://10.10.32.142/` em um navegador para essa validação final.
## Fase 9 — Segurança e produção
- [ ] Criptografia de segredos de trunk (AES-256-GCM)
- [ ] HTTP security headers, CORS, CSRF, Helmet
- [ ] nftables final revisado
- [ ] Logs estruturados JSON (sem segredos)
- [ ] Correlation IDs (request_id/attempt_id/call_id)
- [ ] Métricas Prometheus (/metrics)
- [ ] Bootstrap super_admin (senha aleatória, FIRST_LOGIN.txt, forçar troca)
- [ ] Seed (permissões, perfis, pausas, disposições) sem dados fake em produção
- [ ] Backup/restore (postgres, asterisk config, .env seguro)
- [ ] scripts/install.sh, update.sh, backup.sh, restore.sh, healthcheck.sh
- [ ] Nginx reverse proxy (80/443, WS, HTTPS documentado)
## Fase 10 — Testes e aceite
- [ ] Unit tests (predictive engine, CPS limiter, permissions, phone norm, retry,
state machines, TME/TMA, scheduling)
- [ ] Integration tests (postgres, redis, repositories, API, AMI mock)
- [ ] E2E (login → ... → RBAC, conforme seção 64)
- [ ] Modo simulação (DIALER_SIMULATION=true) + testes do predictive engine
- [ ] Prova de CPS respeitado / concorrência máxima / sem discagem dupla /
pausa efetiva / recuperação após restart / pacing reage a abandono /
supressão respeitada / horário respeitado
- [ ] Quality gate (lint, typecheck, unit, integration, e2e, compose config,
compose ps, health checks) — tudo verde
- [ ] Aceite de segurança (seção 92)
- [ ] Aceite Asterisk (seção 93, comandos documentados)
- [ ] README final completo
- [ ] Relatório final da implementação
---
**Nota de ambiente:** VM atual com 1.9 GiB RAM / 2 vCPU — adequada para dev e
simulação, não para carga real de produção. Ver docs/ARCHITECTURE.md seção 1.