diff --git a/.env.example b/.env.example index f4950cb..4d4d288 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,82 @@ +# Copie pra `.env` (gitignored) e preencha. Ver docs/QA_SETUP.md pro passo a +# passo completo de como subir o ambiente do zero numa máquina nova. + +# Token de acesso ao repositório de pacotes .deb do FreeSWITCH +# (freeswitch.signalwire.com) — só existe durante o build da imagem Docker +# (BuildKit secret, nunca vira camada da imagem nem chega no container em +# runtime). Peça este valor a quem já tem uma conta SignalWire; não é gerável +# localmente. FREESWITCH_PAT= -DATABASE_URL=postgresql://user:password@localhost:5432/b2bcall?schema=public -POSTGRES_APP_USER= + +# apps/api roda direto no host (fora do Docker) — fixado aqui pra nunca +# depender de quem inicia primeiro disputando a porta padrão 3000 com o +# apps/frontend. +API_PORT=3000 + +# --- Postgres (container b2bcall-postgres, gerar senhas com `openssl rand -base64 24`) --- +POSTGRES_USER=b2bcall +POSTGRES_PASSWORD= +POSTGRES_DB=b2bcall +POSTGRES_HOST=postgres +POSTGRES_PORT=5432 + +# --- Redis (container b2bcall-redis) --- +REDIS_PASSWORD= +REDIS_HOST=redis +REDIS_PORT=6379 + +# --- Segredos da aplicação (gerar cada um com `openssl rand -hex 32`) --- +JWT_SECRET= +JWT_REFRESH_SECRET= +ENCRYPTION_KEY= + +# Event Socket do FreeSWITCH — só apps/api (systemd, no host, fora do Docker) +# lê isto. "freeswitch" é um nome DNS que só existe na rede interna do +# Docker; os serviços que RODAM dentro do Docker (fs-events, fs-config, +# predictive-dialer) têm ESL_HOST=freeswitch fixo no próprio +# docker-compose.yml, nunca leem esta variável. Precisa ser 127.0.0.1 (a +# porta 8021 é publicada só em loopback, ver docker-compose.yml) — nunca +# "freeswitch" aqui. +ESL_HOST=127.0.0.1 +ESL_PORT=8021 +ESL_PASSWORD= + +# Segurança de chamada real (agente.md secao 186) — as DUAS precisam estar +# explicitamente na posição de risco pra originar uma chamada PSTN de +# verdade. Deixe como está pra um ambiente de QA/laboratório. +DIALER_SIMULATION=true +ALLOW_REAL_OUTBOUND_CALLS=false + +# --- Gravações (agente.md secao 90-93) --- +STORAGE_PROVIDER=LOCAL +LOCAL_STORAGE_ROOT=/opt/b2bcall/data/object-storage-local + +# --- Prompts de áudio do IVR (PHASE 59) --- +IVR_PROMPTS_HOST_ROOT=/opt/b2bcall/data/ivr-prompts + +# --- S3/MinIO (só usado se STORAGE_PROVIDER=s3; deixe em branco pro padrão LOCAL) --- +S3_ENDPOINT= +S3_REGION= +S3_ACCESS_KEY_ID= +S3_SECRET_ACCESS_KEY= +S3_BUCKET= +S3_FORCE_PATH_STYLE=true + +# --- Connection strings derivadas dos valores acima (preencher com os +# mesmos POSTGRES_PASSWORD/REDIS_PASSWORD já definidos) --- +DATABASE_URL=postgresql://b2bcall:@localhost:5432/b2bcall?schema=public +REDIS_URL=redis://:@localhost:6379 + +# Role restrito (não-superuser) que a aplicação usa em runtime pra RLS +# funcionar de verdade (agente.md secao 32) — a role em si é criada por uma +# migration do Prisma, mas a senha nunca fica num arquivo de migration +# commitado (ver scripts/db-setup-app-role.sh). +POSTGRES_APP_USER=b2bcall_app POSTGRES_APP_PASSWORD= -APP_DATABASE_URL=postgresql://user:password@localhost:5432/b2bcall?schema=public -REDIS_URL=redis://:password@localhost:6379 +APP_DATABASE_URL=postgresql://b2bcall_app:@localhost:5432/b2bcall?schema=public + +# Credenciais que b2bcall-fs-config usa pra autenticar no Event Socket +# quando o PRÓPRIO FreeSWITCH chama de volta (mod_xml_curl) — role de app +# dedicado, nada a ver com FS_CONFIG_USER de um tenant. FS_CONFIG_USER= FS_CONFIG_PASSWORD= diff --git a/TODO.md b/TODO.md index 3e0c088..762afe3 100644 --- a/TODO.md +++ b/TODO.md @@ -2337,6 +2337,86 @@ a funcionalidade de verdade) sobreviveu ao F5. Confirmado ainda que corrigir a Hydration eliminou o erro do console sem quebrar nenhum dos dois cenários +## PHASE 65 — 3 achados reais reportados pelo usuário numa mensagem só: +edição de rota de entrada faltando, telas de Infraestrutura sempre com +erro de ESL, e ramal externo registrando sem áudio +- [x] Rotas de Entrada não tinha edição depois de criada (só criar/ + remover) — mesmo padrão de Filas (form inline por linha, + `Fragment`-wrapped). DID não pode ser trocado depois de criado + (é a chave de roteamento — trocar quebraria o vínculo com o + tronco/operadora). Testado ponta a ponta com Playwright: criar, + editar descrição, F5, confirma que persistiu. +- [x] Telas de `Platform > Infraestrutura` sempre mostravam "Timeout + conectando no ESL" nesta VM — suposição registrada até então + (incorreta) era que isso seria permanente (`apps/api` fora do + Docker, porta nunca publicada). Causa real: `ESL_HOST=freeswitch` + no `.env` é um nome DNS que só existe dentro da rede do Docker, + nunca resolve a partir do host. O host SEMPRE alcança o IP de + qualquer container na rede bridge diretamente, com ou sem porta + publicada — só outras máquinas são bloqueadas sem `ports:`. Fix: + publicar 8021 só em loopback (`127.0.0.1:8021:8021`) + trocar + `ESL_HOST` pra `127.0.0.1`. Ver docs/FREESWITCH.md ("Achados na + sessão de PHASE 65") pro segundo bug achado na mesma investigação + (`show gateways as json` não é um comando válido nesta versão do + FreeSWITCH). Verificado com curl + Playwright nas 3 telas. +- [x] "Registrei o ramal e não passou áudio" — diagnosticado com + contadores de pacotes do `iptables` (não suposição): SIP chegava, + RTP nunca chegava. Causa: esta VM está atrás de um roteador (NAT) + sem port-forward configurado pra faixa de RTP — infraestrutura de + rede fora do controle desta aplicação, não um bug de código (a + configuração de NAT-detection do próprio FreeSWITCH já estava + correta, testado direto via `fs_cli -x "acl nat.auto"`). Ver + docs/NETWORK_ARCHITECTURE.md pro diagnóstico completo e o + checklist de portas pra encaminhar no roteador. + +## PHASE 66 — Softphone WebRTC embutido (pedido do usuário: "integre o +widget handphone.js, é um webrtc que vai conectar em outro proxy [OpenSIPS, +já em produção] que depois vai vir via sip comum ao b2bcall") +- [x] O FreeSWITCH deste projeto nunca fala WebRTC — quem faz a ponte + WebRTC↔SIP é um OpenSIPS externo já em produção. Cada ramal + continua um registro SIP puro; só o NAVEGADOR do agente conecta + via WebRTC no OpenSIPS. +- [x] Código-fonte do widget (`handphone.js`, já commitado sem histórico + neste repo) achado em `git.falehandix.com.br/Handix/handphone-2.0` + (branch `main`, a mais atualizada — a branch default `handphone- + 2.0-alpha` está desatualizada). Nesse código, o endereço do proxy + (`server`) vinha de `VITE_SIP_SERVER`, uma env var de BUILD-TIME — + incompatível com "Super Admin configura em runtime, sem redeploy". + Patch mínimo de 2 linhas em `widgetStorage.mergeConfig`/ + `widgetConfig.parseWidgetConfig` (só nesta cópia local, nunca + enviado pro repo externo do usuário): `data-sip-server`/ + `window.HandphoneConfig.server` agora tem prioridade sobre o env + var de build, mantendo o comportamento antigo como fallback. + Rebuildado (`npm run build:widget`) e vendorizado em + `apps/frontend/public/handphone.js` (era um arquivo solto na raiz + do repo antes desta fase). +- [x] `PlatformSetting` (tabela chave-valor genérica, sem RLS — mesmo + padrão de Role/Permission) pra guardar o endereço WSS do proxy + OpenSIPS, configurável em `Platform > Infraestrutura > Softphone + WebRTC` (permissão `freeswitch.configure`, mesma família das + outras telas de infra). +- [x] `GET /agents/me/softphone-config` (novo, `agents-me.controller.ts`) + — credenciais SIP do PRÓPRIO ramal vinculado ao agente logado + (nunca um `extensionId` arbitrário) + o endereço do proxy. Sem + `@RequirePermission` (mesmo padrão dos outros endpoints de + `agents/me`) — é o próprio agente pegando a própria senha, não uma + ação administrativa tipo `POST /extensions/:id/reveal-password` + (que exige `extensions.manage`, permissão que um agente comum + nunca tem). Cada acesso fica no audit log. +- [x] `SoftphoneWidget` (novo componente client, ao lado do + `AgentStatusWidget` na topbar do tenant) injeta um `