# SeConectai Vendas — Controle de Progresso do Frontend Este arquivo é o **estado oficial do frontend**. Segue exatamente a mesma lógica do `09-controle-de-progresso.md` (backend), mas aplicado às etapas 11–19. ## Instruções obrigatórias para a IA 1. **Antes de iniciar qualquer etapa**, peça este arquivo e leia a seção "Status Geral" para confirmar que a etapa anterior está 100% concluída. 2. **Não inicie uma etapa** com critério de aceite pendente na etapa anterior. Avise o usuário e pergunte se deve terminar o que falta antes de avançar. 3. **Ao concluir uma etapa**, marque com `[x]` cada item realmente implementado e testado, atualize "Status Geral" e preencha "Observações" com decisões relevantes para as próximas etapas. 4. **Devolva este arquivo atualizado** ao usuário ao final de cada etapa. 5. Esta etapa numerada 11–19 pressupõe que o **backend já está funcionando** conforme `09-controle-de-progresso.md`. Se o backend ainda não estiver pronto, o frontend pode ser construído contra uma API mockada, mas isso deve ficar registrado em "Observações" da Etapa 11. ## Status Geral | # | Etapa | Status | Concluída em | |---|-------|--------|---------------| | 11 | Setup do Projeto Frontend | Concluída | 2026-08-15 | | 12 | Sistema de Design e Componentes Base | Concluída | 2026-08-15 | | 13 | Autenticação e Tema Dinâmico | Concluída | 2026-08-15 | | 14 | Layout e Navegação | Concluída | 2026-08-15 | | 15 | Área Master — Telas de Negócio | Concluída | 2026-08-15 | | 16 | Área Tenant — Dashboard, Clientes e Produtos | Concluída | 2026-08-15 | | 17 | Área Tenant — Vendas e Financeiro | Concluída | 2026-08-15 | | 18 | Área Tenant — Integrações e Perfil | Concluída | 2026-08-16 | | 19 | Polimento, Acessibilidade e Testes E2E | [ ] Não iniciada | | Valores possíveis para "Status": `Não iniciada`, `Em andamento`, `Concluída`, `Concluída com pendências`. --- ## Etapa 11 — Setup do Projeto Frontend - [x] `npm run dev` sobe o projeto sem erro - [x] Todas as rotas do mapa de rotas resolvem, com placeholder, sem 404 - [x] `tokens.css` contém todas as variáveis do schema, aplicadas corretamente - [x] Classes utilitárias do Tailwind funcionam e refletem os tokens - [x] `/diagnostico` mostra o retorno real de `GET /health` do backend - [x] `npm run lint` roda sem erro - [x] Estrutura de pastas idêntica à definida no schema de frontend **Observações:** - Projeto criado em `frontend/` (subpasta deste mesmo repositório, não um repo separado). Next.js 15.5.23 + React 19.1 + TypeScript, pinado deliberadamente em vez do Next.js 16 (que o `create-next-app@latest` instala por padrão hoje): o próprio scaffold do Next 16 gera um `AGENTS.md` avisando que a versão tem mudanças de API/estrutura ainda não documentadas de forma estável — risco desnecessário para uma build de 9 etapas. Next 15 já satisfaz o requisito "Next.js 14+" da etapa. - Tailwind vem em v4 (`@tailwindcss/postcss`), que já era o padrão do `create-next-app` mesmo pinando `next@15`. Diferença relevante para as próximas etapas: **não existe `tailwind.config.ts`** — os tokens em `styles/tokens.css` são expostos como utilities (`bg-primary`, `text-danger` etc) via bloco `@theme inline` (CSS-first, forma idiomática do Tailwind v4), não via `theme.extend.colors` em JS. Isso cumpre o critério de aceite (utilities funcionando e refletindo os tokens) sem precisar de um arquivo de config duplicando os mesmos valores. A Etapa 12 (shadcn/ui) deve considerar isso — versões recentes do shadcn CLI já suportam Tailwind v4 CSS-first. - Fonte: Inter via `next/font/google`, exposta como `--font-inter` e mapeada para `font-sans` em `tokens.css`. - `lib/api/client.ts`: wrapper sobre `fetch` nativo (não axios — não há necessidade de interceptors globais além do gancho de refresh, que o próprio client já expõe via `authInterceptor`, preenchido na Etapa 13). Já usa `credentials: 'include'` para o cookie `httpOnly` de refresh token que a Etapa 13 vai configurar no backend. - Husky + lint-staged: como o `.git` fica na raiz do repo (acima de `frontend/`) e o backend não usa Husky, o hook fica em `.husky/` **na raiz do repo** (`git config core.hooksPath .husky`, já aplicado), com `pre-commit` fazendo `cd frontend && npx lint-staged` — isso evita interferir no fluxo de commit do backend. Não usei `npx husky init` (assume que o hook mora ao lado do `package.json`, o que não é o caso aqui) — o hook foi criado manualmente. - Conflito descoberto e corrigido: o `tsconfig.json`/`tsconfig.build.json` do **backend** (raiz do repo) não tinham `exclude`, então o `nest start --watch` tentava compilar os `.tsx` de `frontend/` e quebrava com "Cannot use JSX unless the '--jsx' flag is provided". Adicionado `"frontend"` ao `exclude` de ambos os tsconfig do backend, e ao `ignores` do `eslint.config.mjs` do backend. Necessário para qualquer etapa futura que rode `docker compose up` com as duas pastas coexistindo. - `.gitignore` gerado pelo `create-next-app` ignorava `.env*` (incluindo `.env.local.example`, que precisa ser versionado) — ajustado para `.env*` + `!.env*.example`. - Verificação ponta a ponta feita com o backend real via `docker compose up` (app+worker+postgres+redis) e o frontend em `npm run dev` (porta 3001 — 3000 já ocupada pelo backend; `CORS_ORIGINS` do `.env` do backend já incluía `localhost:3001`). `/diagnostico` confirmado funcional via `curl` com header `Origin` (retornou `Access-Control-Allow-Origin: http://localhost:3001` e o JSON real do `/health`) — não há navegador gráfico disponível neste ambiente para captura visual, então a verificação final "no navegador" fica pendente para quem tiver acesso a um browser local. - `npm audit` acusa 3 vulnerabilidades "high" em `postcss`/`sharp` (dependências transitivas do `next@15.5.23`), só corrigidas subindo para `next@16` (breaking, descartado acima). Reavaliar quando o Next 16 estabilizar. --- ## Etapa 12 — Sistema de Design e Componentes Base - [x] Todos os componentes listados existem, tipados, com as variantes descritas - [x] `Table` alterna corretamente entre tabela e cards empilhados - [x] `Modal/Sheet` alterna corretamente entre modal e tela cheia/bottom sheet - [x] `StatusPill` cobre todos os status de negócio, com cor e label em português - [x] Navegação por teclado percorre todos os componentes com foco visível - [x] Nenhum componente quebra em 375px - [x] Página de showcase acessível apenas em desenvolvimento **Observações:** - shadcn/ui inicializado com **Radix UI** como base de primitivas (a CLI atual oferece Base UI/React Aria/Radix — escolhido Radix por ser exatamente o que a Etapa 12 cita nominalmente e o que tenho mais domínio para customizar com segurança). Preset visual "Nova" usado só como ponto de partida — todas as cores foram remapeadas para os tokens do projeto (ver abaixo), fonte continua Inter (não Geist, que o preset tentou injetar; removido). - **Bug real encontrado e corrigido**, não cosmético: `styles/tokens.css` era importado no `layout.tsx` **antes** de `globals.css`. Como `tokens.css` tem seu próprio bloco `@layer utilities { .tabular-nums }`, isso registrava a layer `utilities` no CSS *antes* de `@import 'tailwindcss'` (dentro de `globals.css`) declarar a ordem canônica `theme, base, components, utilities`. Resultado: a ordem das cascade layers ficava invertida — `utilities` virava a de **menor** prioridade do documento inteiro, então qualquer classe utilitária (`px-3`, `py-2` etc) perdia silenciosamente para regras da layer `base` em conflito. Sintoma visível: padding de `TableCell`/`TableHead` renderizando como `0px` mesmo com a classe certa aplicada no DOM. Corrigido movendo o `@import '../styles/tokens.css'` para dentro do próprio `globals.css` (logo após `@import 'tailwindcss'`), removendo o import solto do `layout.tsx`. Isso também corrigiu de quebra um problema correlato: `--color-neutral-*` dos nossos tokens estava perdendo para a paleta cinza *default* do Tailwind (mesmo nome de variável, tema padrão vencendo por causa da mesma inversão de ordem). **Vale a pena qualquer IA futura desconfiar de "build passou mas o layout não bate" — nem sempre é a classe errada, pode ser ordem de import de CSS.** - Detectado só porque o build de produção e o `tsc`/`eslint` passam limpo mesmo com esse bug (é um erro de cascata, não de sintaxe/tipo) — só apareceu medindo `getComputedStyle` real no navegador headless. Reforça por que essa etapa foi validada com Playwright real, não só build. - Verificação: Playwright headless (imagem oficial `mcr.microsoft.com/playwright`, já que Alpine/musl não roda o Chromium do Playwright) nos três breakpoints — 0 overflow horizontal, screenshots conferidas visualmente. Tab por teclado percorrido 15 vezes: todo elemento focável tem `box-shadow` de foco visível (ring), nenhum `outline: none` sem substituto. - Touch target 44px: `Button` (`md`/`lg`) e `Input`/`Select`/`Textarea` agora têm `h-11` (44px) por padrão — o `sm` do `Button` continua abaixo de 44px **de propósito**, documentado no código como reservado para contexto denso (ex: ação "Ver" na linha da `Table`), nunca a única forma de disparar uma ação. Corrigidos durante a auditoria: botão "Ver mais" dos cards mobile da `Table` (estava com 20px de altura) e os `TabsTrigger` (estavam fixos em 32px, sem variante de tamanho — agora 44px). Grade de dias do `Calendar` ficou pequena de propósito (~28px): é a exceção padrão do setor para grids de data-picker densos, aumentar quebraria o calendário inteiro em mobile. - `Pagination` gerada pelo shadcn (baseada em ``) foi **removida** — construí `DataPagination` própria (botões controlados, compatível com `page`/`limit`/`meta.total` que o backend já retorna). - `Modal/Sheet`: decisão tomada por bottom sheet no mobile (não tela cheia) — a maioria dos formulários do projeto é curta o bastante para caber sem ocupar a tela inteira; `max-h-[85vh]` com scroll interno cobre os casos mais longos (documentado em `responsive-dialog.tsx`). - `Select` com busca implementado como `Combobox` separado (Command + Popover), já que o `Select` nativo do Radix não suporta busca — o `Select` padrão continua disponível para listas curtas (ex: papel de usuário). - Toast usa `sonner` (padrão atual do shadcn, substituiu o componente `Toast` clássico) — `success`/`error`/`warning`/`info` nativos, empilhável, auto-dismiss configurável. Reexportado em `lib/toast.ts`. - Dark mode **não** foi implementado (fora de escopo — não citado em nenhum lugar do schema de frontend). O bloco `.dark { ... }` que o shadcn gera foi removido do `globals.css`; classes `dark:` que aparecerem em componentes shadcn adicionados em etapas futuras são inofensivas (ficam inertes sem a classe `.dark` no ``). --- ## Etapa 13 — Autenticação e Tema Dinâmico - [x] Login master e tenant funcionam ponta a ponta - [x] Sessão expira e renova automaticamente via refresh - [x] Logout limpa sessão e revoga refresh token no backend - [x] Middleware bloqueia acesso não autenticado a `/master/*` e `/app/*` - [x] Master não acessa `/app/*` e vice-versa sem redirecionamento - [x] Fluxo completo de esqueci/redefinir senha funciona - [x] Login aplica cor/logo do tenant certo; fallback para tema padrão - [x] Cor de baixo contraste ajustada automaticamente - [x] Telas testadas em 375px, 768px, 1280px **Observações:** - **Mudança no backend (Etapa 2, já "concluída")**, com autorização explícita do usuário antes de fazer: o backend devolvia o refresh token só no corpo JSON, nunca em cookie — contrariava o requisito do schema ("refresh token em cookie httpOnly"). Adicionado suporte a cookie httpOnly de forma aditiva (não quebra os clientes existentes): - `cookie-parser` instalado; `security.util.ts` ganhou `setRefreshTokenCookie`/`clearRefreshTokenCookie` (`httpOnly, secure: true, sameSite: 'none', path: '/auth'` — `Secure` funciona em `localhost` por exceção do browser; qualquer deploy real precisa estar atrás de HTTPS) e `app.enableCors({ credentials: true })`. - `POST /auth/master/login`, `/auth/tenant/login`, `/auth/refresh` setam o cookie; `/auth/refresh` e `/auth/logout` agora aceitam `refresh_token` no corpo **ou** no cookie (corpo tem prioridade, então os testes e2e antigos continuam passando sem alteração) — `RefreshDto`/`LogoutDto.refresh_token` viraram opcionais. - 4 testes novos em `test/auth.e2e-spec.ts` cobrindo o cookie (Set-Cookie no login, refresh só com cookie, logout limpa o cookie). Suíte completa rodada: 24/24 em `auth.e2e-spec.ts`, 133/136 no total dos e2e (ver nota de teste pré-existente abaixo), lint/tsc limpos. - **Gotcha de infra**: `docker-compose.override.yml` monta `/app/node_modules` como volume anônimo — depois de adicionar uma dependência nova (`cookie-parser`), `docker compose up --build` sozinho **não** basta, o volume anônimo antigo (sem a dependência) sobrevive à recriação do container e esconde o `node_modules` da imagem nova. Precisa de `--renew-anon-volumes` (`-V`). Vale para qualquer etapa futura que adicione dependência ao backend. - **Nota — achado incidental, não corrigido (fora do escopo desta etapa)**: rodando a suíte e2e completa, `test/alertas.e2e-spec.ts` tem 3 testes falhando por timeout do Jest (5000ms padrão), não relacionados a nada desta etapa. Causa raiz aparente: o helper `aguardarFilasOciosas` do próprio arquivo já assume até 15000ms de espera (retry/backoff de job BullMQ), mais que o timeout padrão do Jest — só os testes que exercitam o caminho de falha/retry (que demoram mais) estouram. Fica registrado para quem for revisar a Etapa 7. - **Estratégia de login master/tenant**: toggle explícito ("Sou cliente" / "Sou administrador") na tela de login e em "esqueci minha senha", não detecção automática — o backend expõe dois endpoints separados (`/auth/master/login` e `/auth/tenant/login`) sem nenhum jeito de descobrir o tipo a partir só do e-mail sem tentar os dois (o que duplicaria contagem de tentativas de força bruta). "Cliente" é o padrão selecionado. - **Sinalização de sessão para o middleware**: o cookie httpOnly de refresh token pertence ao **backend** (`Path=/auth`, origem/porta diferente do frontend) — o middleware do Next.js não tem acesso a ele, nem deveria. Criado um segundo cookie, `sc_session` (não sensível, só `master`/`tenant`), setado pelo próprio frontend via `document.cookie` depois de um login bem-sucedido (`lib/auth/session-cookie.ts`). É uma checagem otimista (evita "flash" de conteúdo protegido), não uma fronteira de segurança — a autorização real acontece no backend, em toda chamada de API, via o access token de verdade. - **Identificação do tenant antes do login**: o schema não tem `slug`/subdomínio (confirmado no backend), e não existe endpoint de "descobrir tenant pelo e-mail". Então a resolução de tema pré-login só funciona via `?tenant=` explícito na URL de `/login` — sem esse parâmetro, cai no tema padrão da plataforma (comportamento já previsto pelo próprio schema como fallback). Pós-login, o tema é resolvido a partir do `tenant_id` decodificado do JWT. - **Ajuste de contraste**: `--color-primary` nunca é alterada (fundos continuam com a cor exata do tenant); criado um token novo, `--color-primary-on-light` (`lib/theme/contrast.ts` + `text-primary-on-light`), só para texto/ícone sobre fundo claro. Testado de ponta a ponta com uma cor de tenant propositalmente clara (`#FCD34D`) → resolveu para `#8b742a` automaticamente. - Access token confirmado em memória (`useRef` dentro do `AuthProvider`) via Playwright: `localStorage`/`sessionStorage` vazios depois do login. - `lib/api/client.ts` ganhou `fetchRefreshToken()`, deliberadamente **fora** do `request()`/`authInterceptor` — a chamada de refresh não pode passar pelo mesmo interceptor que ela alimenta, ou um 401 nela mesma causaria recursão infinita. - Sem botão de logout na UI ainda (a função `logout()` existe e funciona, testada isoladamente) — o gatilho visual entra na Etapa 14 junto com sidebar/topbar. --- ## Etapa 14 — Layout e Navegação - [x] Layout master com sidebar/topbar (desktop) e drawer+bottom-nav (mobile) - [x] Layout tenant com cor/logo do tenant aplicada corretamente - [x] "Equipe" só visível para `tenant_admin` - [x] Navegação destaca item ativo em todos os formatos (desktop/drawer/bottom-nav) - [x] Drawer fecha ao selecionar item ou tocar fora - [x] Bottom tab bar com 4 itens + "Mais" - [x] Sidebar colapsável no `md`, preferência persistida - [x] Todas as rotas dentro do layout correto - [x] Testado em 375px, 768px, 1280px **Observações:** - Componentes novos em `components/layout/`: `Sidebar`, `Topbar`, `Drawer`, `BottomTabBar`, `Breadcrumb`, `NavList` (lista de navegação com estado de item/grupo ativo, compartilhada entre `Sidebar` e `Drawer` para não duplicar essa lógica), `AppShell` (orquestra os cinco acima — composto uma vez e reaproveitado pelos dois shells, em vez de duplicar toda a montagem), `UserFooter` (avatar+identidade+logout do rodapé) e os shells finais `MasterShell`/`TenantShell` (client components que buscam `useAuth()`/tema e montam os itens de menu certos). Mapa de itens em `lib/layout/nav-items.ts` (`MASTER_NAV_ITEMS`/`TENANT_NAV_ITEMS` + `*_BOTTOM_HREFS` + `filterByRole`). `app/master/layout.tsx`/`app/app/layout.tsx` viraram server components (`async`, leem o cookie de sidebar via `next/headers` e passam pro shell) — antes eram só placeholders. - **Achado de segurança/consistência visual corrigido durante o teste manual desta etapa:** o destaque de item ativo em `NavList`/`BottomTabBar` usa os tokens `--color-primary`/`--color-primary-on-light` — dinâmicos por tenant (Etapa 13, aplicados via `document.documentElement.style.setProperty` no `:root`, global). Como o painel master tem identidade **fixa** (seção 6 da arquitetura, `--brand-master`), usar os mesmos tokens no master faria a cor de destaque do master mudar dependendo do que ficou setado no `:root` por uma sessão de tenant anterior na mesma aba (o `TenantThemeProvider` só reseta esses tokens quando `tenantId` é `null`, e o master nunca passa por ele). Corrigido em `MasterShell`: um `
` envolve o ``, sobrescrevendo os tokens só para essa subárvore (não toca no `:root`) — `Sidebar`, `Drawer`, `BottomTabBar` e o conteúdo herdam a cor certa sem precisar saber se estão no master ou no tenant. Confirmado visualmente (ver screenshots do teste manual abaixo) e via `getComputedStyle(document.documentElement).getPropertyValue('--color-primary')` continuando no valor padrão/tenant no `:root` (prova de que a sobrescrita é local, não vazou). - **Grupo expansível para "Alertas":** o prompt lista "Alertas (Templates/Logs)" como um item de menu só, mas as rotas já existentes desde a Etapa 11 são duas (`/master|app/alertas/templates` e `.../logs`), sem página índice em `/alertas`. Modelado como `NavItem.children` (só `href`+`label`, sem ícone próprio) — clique no item pai expande/recolhe a lista com Templates/Logs; com a sidebar recolhida (ícones-only) ou no bottom-tab, vira link direto pro primeiro filho (Templates). `href` do item pai é `.../alertas/templates`, então a home de alertas nunca 404. - **Decisão de comportamento do colapso da sidebar, documentada conforme pedido na tarefa 4:** o colapso é **só manual** (botão na base da sidebar), sem auto-colapso automático ao entrar no breakpoint `md`. A sidebar aparece a partir de `md` (`hidden md:flex`) já no estado persistido (cookie `sc_sidebar_collapsed`, lido no server component do layout via `next/headers` antes do primeiro paint — sem "flash" de sidebar expandida depois recolhendo). Escolhido em vez de um auto-colapso heurístico por breakpoint por ser mais previsível e mais simples de testar/explicar; o critério de aceite ("colapsável no breakpoint md, com botão pra expandir/recolher manualmente também no desktop") permite as duas leituras — essa ficou registrada aqui como a interpretação adotada. - **Rodapé do usuário mostra o papel, não o nome:** o payload do access token (`lib/auth/jwt.ts`, espelhando `src/modules/auth/types/jwt-payload.type.ts` no backend) só carrega `sub`/`tipo`/`tenant_id`/`role` — nunca nome ou e-mail. `UserFooter` (usado tanto no master quanto no tenant) mostra "Administrador master" (fixo) ou o papel do `tenant_user` traduzido (`Administrador`/`Vendedor`/`Financeiro`/`Visualizador`), com a inicial dessa palavra como avatar — não um nome de pessoa. Trazer o nome/e-mail reais exigiria um endpoint de perfil mais rico (a Etapa 18, que constrói a tela `/app/perfil` de verdade, é o lugar natural pra decidir se isso vale a pena). - `TenantBrand` usa `next/image` pro logo do tenant (mesmo padrão já usado em `app/(public)/login/login-form.tsx` na Etapa 13, `width`/`height` fixos, sem `unoptimized`) — sem cair pra `` puro, por consistência com o resto do código já existente. - Breadcrumb gerado automaticamente em `AppShell` (`findBreadcrumb`) a partir do item de menu ativo (casa `pathname` contra `items`/`children`) — não é por página manual como o prompt desta etapa cogitava como alternativa aceitável; ficou simples o suficiente por já ter a árvore de itens disponível. Fica limitado a no máximo 2 níveis (grupo → filho); páginas de detalhe (`/app/clientes/[id]`, `/app/vendas/[id]` etc., que só chegam nas Etapas 16/17) vão cair no mesmo breadcrumb da listagem-mãe até que uma etapa futura estenda `findBreadcrumb` com um 3º nível — registrado aqui como a limitação esperada da "primeira versão" que a tarefa 3 já autorizava. - **Teste manual desta etapa, já que não há Playwright configurado no projeto ainda:** sem Node/npm no host (só Docker), rodei `tsc --noEmit`/`eslint .`/`next build` via `docker run node:20-alpine` (mesmo padrão das etapas de backend) — todos limpos. Para o teste visual real, subi o `next dev` num container (`--network host`, porta 3001, `NEXT_PUBLIC_API_URL` apontando pro backend já rodando via `docker compose` na 3000) e dirigi um Chromium headless via Playwright (instalado ad-hoc num container separado, sem ficar como dependência do projeto) cobrindo: login master e tenant reais contra a API, screenshot em 375/768/1280px, abrir/fechar drawer mobile, expandir o grupo "Alertas", colapsar a sidebar e confirmar que o item ativo e a preferência sobrevivem a um reload de página. Também criei dados de teste no banco de dev via API/SQL direto: um plano, um tenant novo ("Etapa14 Demo", cor `#7C3AED`) com seu `tenant_admin` e um `tenant_user` role `vendedor` adicional — confirmando visual e programaticamente que "Equipe" aparece só pro admin. **Ação que precisa de atenção do usuário:** pra testar o login master, sobrescrevi a senha do `master_admin` já existente (`admin@seconectai.com.br`) sem guardar o hash anterior — a senha antiga desse usuário foi perdida; se ela importava, é preciso redefini-la manualmente (ex: direto no banco ou, se existir, via o fluxo de "esqueci minha senha"). Os outros dados criados (`Teste Etapa14` em `planos`, `Etapa14 Demo` em `tenants` + seus `tenant_users`) são só aditivos, não destrutivos — posso removê-los se não forem úteis como dado de demonstração pras próximas etapas. ## Etapa 15 — Área Master: Telas de Negócio - [x] Todas as telas master funcionam ponta a ponta com o backend real - [x] Dashboard com números corretos - [x] Criação de tenant exibe senha provisória com aviso claro - [x] Mudança de status de tenant exige confirmação - [x] Erros de negócio da API aparecem como mensagem clara - [x] API key da Evolution nunca aparece em texto completo após salva - [x] Editor de template mostra e insere placeholders corretamente - [x] Listagens com paginação, busca/filtro, loading/vazio - [x] Testado em 375px, 768px, 1280px **Observações:** - Infra nova para esta etapa: `@tanstack/react-query` instalado e plugado em `app/layout.tsx` via `lib/query/query-provider.tsx` (`QueryClient` com `retry` desligado pra `ApiError` — erro de negócio não se resolve tentando de novo, só falha de rede/infra vale 1 retry). `recharts` **não** foi instalado — ver limitação do dashboard abaixo. Camada de API por domínio nova em `lib/api/{planos,tenants,faturas,dashboard, evolution-config,alert-templates,alert-logs,master-admins}.ts` (tipos + funções `apiClient.*`, espelhando os DTOs/respostas do backend campo a campo) e schemas zod em `lib/validators/{planos,tenants, evolution-config,alert-templates}.ts`, mesmo padrão de `lib/validators/auth.ts` da Etapa 13. `lib/format.ts` novo (`formatCurrency`/`formatDate`/`formatDateTime`, pt-BR). - **Limitação de backend, registrada conforme a própria tarefa 1 já previa:** `GET /master/dashboard` (`dashboard.service.ts#resumo`) só devolve o snapshot atual (contagem por status, MRR, recebido no mês, em aberto) — nenhuma série histórica de faturamento/novos tenants. Sem dado pra alimentar, o gráfico Recharts da tarefa 1 não foi construído (nem a dependência instalada); o dashboard ficou só com os cards de resumo (8 cards: 5 de status + MRR/recebido/em aberto), como a tarefa já autorizava nesse cenário. - **Limitação de backend, relevante pra `/master/configuracoes`:** não existe endpoint de perfil/senha do master admin (só `GET /auth/master/admins`, somente leitura, listando todos os admins — sem filtro por usuário nem rota de update). A tela resolve "meus dados" filtrando essa lista pelo `sub` do JWT decodificado (`useAuth().user.sub`) e mostra só leitura; a troca de senha reaproveita o fluxo genérico `POST /auth/password/forgot` (`usuario_tipo: 'master'`) com o e-mail já preenchido — não uma troca "senha atual → nova senha" direta na tela, porque essa rota não existe no backend. Registrado como gap de backend caso o produto queira uma troca de senha in-app mais direta numa etapa futura. - **Placeholders de template**: `src/modules/alerts/placeholders.ts` (`PLACEHOLDERS_POR_TIPO`) não é exposto por nenhum endpoint — é uma constante interna do backend. Replicada manualmente em `lib/api/alert-templates.ts` para alimentar o painel de ajuda do editor (clicar num placeholder insere `{{chave}}` na posição do cursor do textarea, via manipulação direta de `selectionStart`/`selectionEnd` + `form.setValue`). Se a lista mudar no backend, esta cópia precisa ser atualizada junto — fica documentado no comentário do arquivo. - Campos numéricos de formulário (`valor_mensal`, `limite_customers`, `dia_vencimento_mensalidade` etc) usam `z.number()` puro nos schemas (sem `z.coerce`/`z.preprocess`) — a conversão string→number do `` acontece só no `register(...)` do react-hook-form (`valueAsNumber`/`setValueAs`). Misturar coerção no schema com `zodResolver` faz o tipo de entrada do formulário divergir do tipo de saída e quebra a inferência de tipos entre `useForm`/ `zodResolver` (erro real de TS encontrado e corrigido nesta etapa). - Modal de criação de tenant (`/master/tenants/novo`) mostra a senha provisória num `ResponsiveDialog` que só fecha levando para o detalhe do tenant (não é dismissível "por engano") — com botão de copiar (`navigator.clipboard`) e aviso de que não será exibida de novo, como pedido no critério de aceite. - Mudança de status de tenant reaproveitada entre a listagem e o detalhe via `components/domain/tenant-status-dialog.tsx` — confirmação explícita com aviso ("bloqueia login mesmo com sessões abertas"), Select só com as transições válidas (`ativo`/`inadimplente`/`suspenso`/`cancelado` — nunca `trial`, que só existe na criação automática). - **Três bugs pré-existentes (Etapas 12/13, não desta etapa) encontrados durante o teste manual ponta a ponta e corrigidos aqui, por serem bloqueadores reais para "todas as telas funcionam com o backend real":** 1. **Race condition em `AuthProvider.refreshSession`** (`lib/auth/auth-context.tsx`): após qualquer reload de página protegida, duas fontes disparavam `POST /auth/refresh` quase ao mesmo tempo (o efeito de montagem do Provider e o `authInterceptor.onUnauthorized` de uma request que saiu antes do token estar pronto) — a 2ª chamada reenviava o cookie já rotacionado pela 1ª, e o backend tratava isso como reuso de token revogado, revogando a família inteira e derrubando a sessão que acabara de ser validada. Corrigido deduplicando chamadas concorrentes via uma `Promise` compartilhada (`refreshPromiseRef`). Sem esse fix, um F5 em qualquer tela de `/master/*` ou `/app/*` podia deslogar o usuário de forma intermitente. 2. **`Button asChild` quebrado** (`components/ui/button.tsx`): o `