Files
eden/docs/progress.md
Matheus (Handix) abe056c8b6 feat(infra): proxy same-origin /api/* dentro do nginx dos 3 frontends
A API não tem domínio público próprio (decisão do operador — só IP
interno). O proxy externo que termina SSL nos 3 domínios públicos é
gerenciado fora deste repositório; a peça que este repo controla é o
nginx que já roda dentro de cada container de frontend.

infra/docker/nginx-spa.conf ganha um location /api/ com proxy_pass para
eden-api:8080 (mesma rede eden_net) — resolver 127.0.0.11 (DNS
embutido do Docker) + proxy_pass via variável, para não cachear o IP do
upstream permanentemente. Com isso o browser chamando /api/v1/... no
domínio público (já roteado para a porta 3001/3002/3003 pelo proxy
externo do operador) completa o fluxo same-origin sem precisar de CORS
nem de um domínio próprio para a API.

Testado ao vivo: login real via POST /api/v1/auth/login funcionando
através dos 3 containers reconstruídos (eden-core:3001,
eden-parceiros:3002, eden-assinante:3003).

Claude-Session: https://claude.ai/code/session_01BcWWuKfykGtUweXmHD6cKx

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-03 18:48:11 -03:00

52 KiB
Raw Blame History

EDEN — Progresso

Fase 0 — Descoberta e Arquitetura: CONCLUÍDA

Data: 2026-09-03.

Entregáveis

  • docs/project-inventory.md, docs/glossary.md, docs/architecture.md, docs/assumptions.md, docs/security/threat-model.md, docs/implementation-plan.md, docs/progress.md (este arquivo).
  • 12 ADRs em docs/adr/ (0001–0012), cobrindo: modular monolith, topologia de containers (Postgres isolado + eden-core/parceiros/assinante em containers/portas distintas — decisão explícita do operador), auth/sessões, modelo de permissões, criptografia/segredos, contrato first-class, stock ledger, separação billing/finance/fiscal, outbox transacional, adapter SaperX, adapter Focus NFe, três apps com identidade compartilhada.
  • 14 subagentes em .claude/agents/: eden-architect, eden-database, eden-security, eden-commercial, eden-finance, eden-fiscal, eden-inventory, eden-telecom, eden-support, eden-hr-timeclock, eden-frontend, eden-api-integrations, eden-qa, eden-code-reviewer.
  • 7 skills em .claude/skills/: eden-domain, eden-security, eden-finance, eden-fiscal, eden-inventory, eden-telecom, eden-timeclock — cada uma com SKILL.md + references/.
  • Hooks em .claude/settings.json + .claude/hooks/*: PreToolUse (guarda de comandos Bash destrutivos/externos e de acesso a arquivo fora do projeto/.ssh//etc/segredo), PostToolUse (lint/typecheck incremental, fail-open enquanto pnpm não existir), Stop (checagem advisória de segredo/TODO crítico no diff, silenciosa até existir repositório git). Testados via pipe com payloads sintéticos — todos os cenários (allow/ask/deny/fail-open) se comportaram como esperado.

Revisão cruzada (self-review contra os critérios dos agentes eden-architect / eden-security / eden-database, criados nesta mesma fase)

eden-architect: bounded contexts (docs/architecture.md §2) têm direção de dependência consistente (nenhum ciclo); HR/Timeclock corretamente isolado; candidatos a extração futura (Billing, Fiscal) identificados sem extração prematura. Nenhuma feature de negócio foi implementada antes deste gate — consistente com o Master Prompt §18.

eden-security: threat model cobre as superfícies do legado (rotas públicas, auth, autorização, integrações, upload, PDF, auditoria, infraestrutura) mais os invariantes de teste obrigatórios (§15.3). Hooks PreToolUse já impõem, a nível de ferramenta, várias regras de §2.1/§2.2 (fora do projeto, .ssh//etc, impressão de .env, SSH/SCP externo, remoção ampla de volume/prune). Pendência real registrada (não bloqueante): política de retenção/eliminação LGPD ainda não definida pela Handix (docs/assumptions.md #1).

eden-database: princípios de modelagem (§11) já refletidos nas ADRs de contrato/estoque/billing (nunca saldo editável, nunca CASCADE indiscriminado, JSONB só para snapshot/metadata). Nenhum schema físico foi criado ainda nesta fase — corretamente adiado para a Fase 1, quando o Postgres 18 realmente subir.

Nota: esta revisão foi feita pela própria sessão orquestradora aplicando os critérios definidos nos arquivos de agente recém-criados. Os subagentes dedicados (eden-architect, eden-security, eden-database) ficam disponíveis para revisões reais em sessões futuras assim que o Claude Code os carregar como subagent types.

Pendências não bloqueantes (ver docs/assumptions.md)

  1. Política de retenção/eliminação de dados pessoais (LGPD) — Handix ainda não definiu.
  2. Docker/Node/pnpm precisam ser provisionados no ambiente antes da Fase 1.
  3. Portas dos containers definidas via .env (não fixas) — ver ADR-0002.
  4. Git ainda não iniciado no projeto — planejado para o início da Fase 1.
  5. Inventário fino do tema_do_Eden.zip (extração de componentes) adiado para o início de eden-frontend.

Gate de saída da Fase 0: VERDE

Todos os itens do checklist da seção 22 do Master Prompt (“Primeira Execução”) estão completos.


Fase 1 — Plataforma: EM ANDAMENTO

Data de início: 2026-09-03.

Ambiente provisionado

  • Docker CE 29.7.2 + Compose v5.5.0 (repositório oficial Docker, não o docker.io do Debian) instalados via apt.
  • Node.js 22.23.2 LTS (NodeSource) + pnpm 11.25.0 (via corepack).
  • Git 2.47.3. Repositório inicializado (main), remoto origin = https://git.falehandix.com.br/Matheus/eden.git (nenhum push feito ainda).

Monorepo

  • pnpm workspaces + Turborepo. 5 apps (api, worker, core-web, reseller-web, subscriber-web) + 9 packages, todos com package.json/tsconfig.json válidos.
  • pnpm install, pnpm turbo run typecheck e pnpm turbo run build passam 100% (15/15 e 14/14 tarefas, respectivamente) em todo o monorepo.
  • apps/api: NestJS mínimo com /health/live e /health/ready (este último checando Postgres de verdade via @eden/database).
  • apps/worker: placeholder que confirma conectividade com o banco; sem BullMQ/Redis ainda (conforme ADR-0002 — só entra quando houver job real).
  • apps/core-web, apps/reseller-web, apps/subscriber-web: Vite + React + TS + Tailwind, cada um com favicon oficial do EDEN.
  • packages/database: migrations via node-pg-migrate (formato .cjs), client de query (pg Pool).

Banco de dados

  • Migration baseline aplicada com sucesso contra Postgres 18.6 real (container eden-postgres): roles, role_permissions, applications, users, user_applications, sessions, audit_log — cobrindo Identity & Access (ADR-0003/ADR-0004) e um audit log append-only com hash-chain (testado: UPDATE/DELETE são efetivamente bloqueados pelo trigger, só liberados via SET LOCAL eden.allow_audit_mutation).
  • Seeds: 4 papéis do sistema (pesos idênticos ao legado) + as 3 aplicações (core/reseller/subscriber).
  • Organization (empresas/entidades legais), Commercial e demais bounded contexts ainda não têm migration — ficam para quando cada fase começar, por design (ver ADR-0001).

Docker Compose — topologia ADR-0002 validada de ponta a ponta

docker compose --env-file .env up -d sobe os 6 serviços com sucesso:

Serviço Status observado
eden-postgres healthy — porta dev 55432, nunca 5432 direto
eden-api healthy — único serviço com DATABASE_URL; /health/ready confirma Postgres via rede interna
eden-worker up — conecta ao Postgres, sem porta exposta
eden-core (:3001) healthy — HTTP 200
eden-parceiros (:3002) healthy — HTTP 200
eden-assinante (:3003) healthy — HTTP 200

Bugs de bootstrap corrigidos nesta sessão (documentados aqui para não se repetirem):

  1. Imagem oficial postgres:18-alpine mudou o layout do volume — precisa montar /var/lib/postgresql (não .../data).
  2. apps/api/package.json tinha "type": "module" incompatível com o tsconfig (saída CommonJS) — Node falhava com exports is not defined.
  3. node-pg-migrate --envPath não carregava o .env corretamente nesta versão — trocado por bash -c 'source .env; ...' nos scripts migrate:up/migrate:down.
  4. Healthcheck dos 3 frontends usava wget http://localhost/..., que resolve para ::1 (IPv6) dentro do container — nginx só escuta IPv4; trocado para 127.0.0.1.

Auth, permissões, Companies e OpenAPI — implementados e testados ao vivo

  • Auth (apps/api/src/auth/): Argon2id (parâmetros versionados, ADR-0005), JWT de acesso de 15 min, refresh token opaco (256 bits) com rotação e detecção de replay (reuso de token já rotacionado revoga toda a cadeia de sessões do usuário). Endpoints POST /api/v1/auth/{login,refresh,logout} e GET /api/v1/auth/me.
  • Permissões (apps/api/src/auth/permissions.ts, permission.guard.ts): resource+action+scope (ADR-0004), super_admin com bypass hardcoded (nunca consulta role_permissions, espelhando a trava também existente no banco). Guard resolve permissões do banco a cada request (nunca cacheado no JWT) — mudança de papel tem efeito imediato, mesma filosofia do legado.
  • Companies (apps/api/src/companies/ + migration 1788437478189_companies): CRUD com leitura aberta a qualquer autenticado e escrita restrita a companies:manage; validação de CNPJ (dígito verificador, packages/domain-shared) testada com CNPJ válido/inválido conhecidos.
  • Seed automático: SeedService cria o primeiro super_admin a partir de EDEN_SUPERADMIN_EMAIL/PASSWORD só quando users está vazia — testado (log super_admin inicial criado).
  • OpenAPI/Swagger: /api/docs (UI) e /api/docs-json, cobrindo todos os endpoints atuais.
  • Validação/segurança de request: ValidationPipe global com whitelist+forbidNonWhitelisted (proteção contra mass-assignment) — testado (id/isActive extras num POST de company são rejeitados com 400).

Testes manuais realizados contra a stack real (não só typecheck): login com senha errada → 401 genérico; refresh rotaciona e o token antigo some; reuso do token antigo → 401 + toda a cadeia revogada; logout revoga e é idempotente; audit log com a sequência completa de eventos (auth.login, auth.refresh, auth.refresh_replay_detected, company.created).

CRUD de Usuários/Papéis, Products, Resellers, adapters S3/SMTP — implementados via 4 subagentes especializados em paralelo (worktrees isolados) + trabalho direto

Estratégia: 4 subagentes (general-purpose, atuando com a persona eden-commercial/eden-api-integrations definida em .claude/agents/, já que os subagent types customizados ainda não carregaram nesta sessão) rodaram em paralelo, cada um em git worktree isolado, enquanto o CRUD de Usuários/Papéis foi feito diretamente (por tocar peso hierárquico e segurança). Cada agente foi instruído a não editar app.module.ts nem packages/database/src/index.ts — a integração final (merge das branches, wiring, resolução de conflitos, rebuild, teste ao vivo) foi feita manualmente.

  • Users/Roles (apps/api/src/users/, apps/api/src/roles/): convite com senha temporária, troca de senha self-service, reset administrativo, grant/revoke de aplicação — tudo com checagem de peso hierárquico (assertCanAssignRole/assertCanActOnUser, eden.md §1.6). CRUD de papéis e role_permissions exclusivo super_admin (SuperAdminGuard), deliberadamente não generalizado para o modelo de permissão (raiz do próprio sistema de autorização).

  • Products (apps/api/src/products/ + migration products): catálogo com as 5 faixas de preço fixas, product_code sequencial, flags de rastreamento (estoque/serial/patrimônio/MAC). Leitura aberta, escrita via products:edit.

  • Resellers (apps/api/src/resellers/ + migration resellers): CRUD operacional + vínculo usuário↔revenda (users.reseller_id). Leitura aberta, escrita via resellers:edit — desvio deliberado do legado (que travava em requireAdmin), documentado em comentário.

  • Adapters S3/SMTP (packages/integrations/): portados fielmente do legado — S3 falha alto (erro claro se não configurado), SMTP falha silencioso (nunca quebra um fluxo de negócio por causa de e-mail).

  • CRM (apps/api/src/leads/, apps/api/src/opportunities/ + migration crm): leads e oportunidades com conversão transacional (convertLeadToOpportunity, FOR UPDATE + withTransaction). Diferente de Companies/Products, leitura também exige permissão (leads:view/opportunities:view) por conter PII de prospect.

4 bugs reais encontrados em teste manual e corrigidos antes do commit final:

  1. assertCanAssignRole/assertCanActOnUser pulavam a validação de existência do papel quando o ator era super_admin (o bypass de peso pulava a checagem inteira) — convidar usuário com role_key inexistente estourava a constraint FK do banco como 500 não tratado em vez de 400 claro.
  2. PATCH /v1/resellers/:id/assign-user aceitava um resellerId diferente no corpo, que na prática era o valor usado — o :id da URL virava só uma checagem de existência, não o alvo real da atribuição. Corrigido para o :id da URL ser a única fonte de verdade; desatribuição virou endpoint próprio (POST /v1/resellers/unassign-user).
  3. Os controllers de Leads/Opportunities liam req.user?.id em vez de usar @CurrentUser()/request.edenUser (o padrão real do JwtAuthGuard) — todo evento de auditoria de lead/oportunidade estaria gravando actorId: null sempre. O agente que os escreveu não tinha o módulo de auth disponível no worktree dele para testar isso de verdade (só stubou e reverteu antes de commitar).
  4. As chamadas appendAuditEvent() de leads/opportunities omitiam o campo obrigatório actorType (6 ocorrências).

Também precisou renomear o timestamp da migration de CRM, que o agente gerou com valor anterior ao da migration de resellers (já aplicada por outro agente/merge) — node-pg-migrate recusa migration fora de ordem.

Todos os módulos testados ao vivo contra a stack real (não só typecheck): convite de usuário, papel inexistente (400 após o fix), CRUD de produto com preços NUMERIC corretos, CRUD de revenda, atribuição/desatribuição de usuário à revenda, lead→oportunidade transacional.

Suíte de testes de integração (agente eden-qa)

Vitest + supertest + @nestjs/testing (bootstrap real do Nest, sem mock) contra o Postgres real — 5 arquivos, 15 testes, todos verdes: auth.spec.ts (anti-enumeração, rotação de refresh, detecção de replay fim-a-fim, logout idempotente), role-weight.spec.ts (guarda a regressão do bug #1), companies.spec.ts (CNPJ), mass-assignment.spec.ts (whitelist), reseller-assignment.spec.ts (guarda a regressão do bug #2). Achado técnico do agente: o esbuild (transform padrão do Vitest) não implementa emitDecoratorMetadata, quebrando toda injeção de dependência do Nest silenciosamente sob teste — corrigido trocando para SWC (unplugin-swc), abordagem padrão documentada pelo próprio NestJS. Rodar com pnpm --filter @eden/api test.

Resumo do lote de agentes paralelos

6 subagentes no total (4 + 1 QA + Users/Roles feito diretamente), rodando em git worktree isolados, revisados e mesclados manualmente um a um (nunca merge automático sem verificação). 4 bugs reais encontrados só porque cada módulo foi testado contra a stack viva antes do commit — nenhum deles teria aparecido só com typecheck/build, que todos os agentes já reportavam verde. Padrão validado: delegar implementação para agentes especializados funciona bem para módulos CRUD bem especificados, mas a integração final (merge, wiring, teste ao vivo, revisão) precisa ficar com quem tem visão do sistema inteiro.

Pendências da Fase 1 (não iniciadas)

  • MFA/TOTP — implementado na leva seguinte (ver Fase 4+ abaixo).
  • Extração do Design System a partir do tema DreamsERP (packages/ui ainda é placeholder).
  • Wiring dos adapters S3/SMTP em algum módulo consumidor real (hoje só existem como biblioteca em packages/integrations, nada os chama ainda).
  • Nota de dado de teste: a empresa "Handix Telecom LTDA" e a revenda "Handix (revenda própria)" criadas durante os testes usam dados de exemplo, não os dados reais da Handix — substituir/remover antes de qualquer ambiente que não seja dev local.

Fase 2/3 — Ofertas (Quotes), Cadastro de Cliente (Customer 360) e esqueleto de Contratos

Data: 2026-09-03.

Ofertas / Quotes (apps/api/src/quotes/ + migration quotes) — implementado diretamente

Núcleo comercial mais sensível a erro de arredondamento/regra de negócio (eden.md §7/§8), por isso feito sem delegar a subagente:

  • pricing.ts: funções puras porteadas literalmente do legado — monthlyTotal = Σitem.total; totalImpl = implFromProducts + implGeral; hasSpecialPrice/isDiscount/isMarkup por comparação com proposedMonthlyTotal; contractTotal = effectiveMonthly * (contractPeriod||1) + totalImpl; needsApproval = isDiscount || hasReducedFidelity; fidelidade só tem efeito legal com approval_status === 'approved'. Faixas de condição de pagamento (À vista ≤1500; +1+1 ≤3000; +1+2 acima) replicadas do legado.
  • Preço sempre derivado server-side do produto vivo no banco (nunca confia em preço vindo do cliente).
  • Fluxo completo: criar oferta → decidir aprovação (PATCH :id/approval) → fechar negócio vinculando a um cadastro de cliente (POST :id/close, bloqueado por client_registration_id IS NULL — não fecha duas vezes).

Customer 360 / Cadastro de Cliente (apps/api/src/client-registrations/ + migration client-registrations) e Contratos (apps/api/src/contracts/ + migration contracts) — subagentes + salvamento manual

Lançados 2 subagentes em paralelo (worktrees isolados) para Cadastro de Cliente e esqueleto de Contratos. Ambos falharam duas vezes: primeiro com rate limit de sessão (retomado após o usuário habilitar cobrança de excedente), depois com limite de gasto mensal da conta — um bloqueio de nível de conta que não se resolve tentando de novo. Em vez de insistir, os dois worktrees foram inspecionados (git status, diff arquivo a arquivo); o trabalho não commitado encontrado em ambos era substancial e de boa qualidade — foi copiado diretamente para main (sem passar por branch/merge, já que nada tinha sido commitado) e o que faltava foi completado manualmente:

  • Cadastro de Cliente: repositórios e camada de API salvos do agente praticamente inalterados; só faltava o client-registrations.module.ts, escrito manualmente. MVP com escopo explícito (eden.md §3): só fluxo autenticado, sem token de autoatendimento público, sem upload de anexo, sem integração CNPJ/CEP — tudo documentado em comentário no código, não descartado silenciosamente.
  • Contratos: repositório salvo do agente (com um import não usado removido); toda a camada de API (DTOs, service, controller, module) escrita manualmente, pois o agente não tinha chegado lá. Máquina de estados (ADR-0006) draft→{pending_signature,cancelled}, pending_signature→{active,cancelled}, active→{suspended,cancelled,terminated,expired}, suspended→{active,cancelled,terminated} — imposta via SELECT...FOR UPDATE + tabela de transições, com signed_at/activated_at carimbados na transição correta e histórico completo em contract_status_history. Escopo: 4 das 11 tabelas do ADR-0006 (contracts, contract_items, contract_parties, contract_status_history); as outras 7 explicitamente adiadas.

Wiring final feito manualmente: barrel (packages/database/src/index.ts) e app.module.ts atualizados; 3 migrations aplicadas ao Postgres real; pnpm turbo run typecheck build (28/28 tasks) e pnpm --filter @eden/api test (15/15 testes) verdes; container eden-api reconstruído e todo o fluxo testado ao vivo via curl contra a stack real:

  • Oferta sem preço especial (sem aprovação) e oferta com desconto + fidelidade reduzida (approval_status: pending) — valores conferidos manualmente contra a fórmula.
  • Aprovação da oferta, fechamento vinculando a um cadastro de cliente criado via POST /client-registrations/direct, e guarda de fechamento duplo (409 na segunda tentativa).
  • Contrato criado a partir da oferta fechada, transições de status válidas e inválidas (draft→active direto rejeitado com 409; draft→pending_signature→active aceito, signed_at/activated_at carimbados corretamente), adição de item/parte, FK de produto inexistente rejeitada com 400.
  • 401 sem token; trilha de auditoria (audit_log) conferida no Postgres com a sequência completa de eventos do lote (quote.created, quote.approval_approved, quote.closed, client_registration.created, contract.created, contract.status_changed ×2, contract.party_added, client_registration.updated).

2 bugs reais encontrados e corrigidos antes do commit (nenhum via subagente — achados na própria implementação/revisão):

  1. Primeiro rascunho do updateQuote (escrito por mim) usava COALESCE para os campos financeiros (fidelity_period, proposed_monthly_total, approved_by_id) — como undefined do JS também vira NULL no SQL, qualquer update parcial que omitisse esses campos os apagaria silenciosamente. Corrigido tornando esses campos obrigatórios em UpdateQuoteInput (não mais opcionais), forçando a camada de serviço a sempre fornecer um valor deliberado.
  2. packages/database/src/repositories/types.ts — a interface User estava sem reseller_id/can_approve_discount, mesmo essas colunas já existindo no banco desde as migrations de Resellers/Quotes (ninguém tinha atualizado o tipo compartilhado). Descoberto ao ligar o uso desses campos em Quotes; corrigido no types.ts e propagado para EdenAuthenticatedUser em jwt-auth.guard.ts.

Pendências deste lote

  • FKs cruzadas entre as 3 tabelas — fechadas na leva seguinte (ver abaixo).
  • Fluxo de autoatendimento público (token) de Cadastro de Cliente, upload de anexo, integração CNPJ/CEP — todos fora do escopo desta MVP.
  • 7 das 11 tabelas do ADR-0006 de Contratos (aditivos, faturas, documentos/e-signature, etc.) ainda não criadas.

Fase 4/9 — MFA/TOTP, Estoque (Inventory), Suporte (Service Desk) e fundação de Ponto Eletrônico (HR/Timeclock)

Data: 2026-09-03.

Pedido do usuário: "pode seguir na criacao do app ao menos mais 20 etapas use subagentes". Estratégia: fechei a pendência de MFA/TOTP da Fase 1 diretamente (autenticação é sensível demais para delegar) e lancei 3 subagentes em paralelo (eden-inventory, eden-support, eden-hr-timeclock) em worktrees isolados, seguindo o mesmo padrão validado nas levas anteriores. Além das FKs cruzadas de Quotes/Contracts/ClientRegistrations, deixadas pendentes na leva anterior.

FKs cruzadas (pendência da leva anterior) — fechada

Migration 1788450538883_cross-reference-fks.cjs: quotes.client_registration_id → client_registrations(id), client_registrations.quote_id → quotes(id), contracts.quote_id → quotes(id), contracts.client_registration_id → client_registrations(id), todas ON DELETE RESTRICT (nenhum desses agregados tem rota DELETE hoje; perder o vínculo silenciosamente numa exclusão não relacionada seria pior que bloquear a exclusão). Verificado zero órfãos antes de aplicar.

MFA/TOTP (apps/api/src/auth/mfa.service.ts, field-encryption.service.ts) — implementado diretamente

  • FieldEncryptionService (apps/api/src/crypto/): criptografia reversível genérica AES-256-GCM versionada (ADR-0005), formato v1:iv:tag:ciphertext base64, chave de EDEN_FIELD_ENCRYPTION_KEY (hasheada para 32 bytes via SHA-256). Primeira implementação real desse padrão no projeto — vai ser reaproveitada por outros segredos reversíveis (ex.: credencial de equipamento Control iD, feito na mesma leva).
  • Fluxo de login em duas etapas quando mfa_enabled=true: POST /auth/login retorna {mfaRequired: true, mfaChallenge} (JWT de propósito único, 2 min, claim purpose distintivo) em vez de tokens; POST /auth/mfa/challenge troca o desafio + código TOTP pelos tokens reais. POST /auth/mfa/enroll (gera secret + otpauthUrl, não habilita ainda) → POST /auth/mfa/confirm (primeiro código correto habilita) → POST /auth/mfa/disable (exige código válido, não só sessão autenticada).
  • Biblioteca otplib v13 (API funcional generateSecret/generateURI/verify, não mais o antigo authenticator singleton de v12).

Bug de segurança real encontrado e corrigido em teste ao vivo (não hipotético — explorado de verdade contra a stack rodando): o token de desafio MFA (mfaChallenge, emitido logo após a senha, antes do código TOTP) era aceito pelo JwtAuthGuard como um access token comum — verifyAccessToken() só validava assinatura/expiração, nunca checava a claim purpose. Resultado: um atacante que só tivesse a senha (sem o segundo fator) conseguia acesso total à API por 2 minutos usando o token de desafio como Bearer normal — MFA completamente contornável. Corrigido em token.service.ts::verifyAccessToken rejeitando qualquer token com claim purpose presente. Teste de regressão adicionado em test/mfa.spec.ts (GET /auth/me com o challenge token como Bearer → 401). Explorado e depois fechado ao vivo via curl contra o container real antes de seguir adiante.

Estoque / Inventory (apps/api/src/inventory/ + migration inventory) — subagente eden-inventory

5 tabelas (de 15 possíveis no Master Prompt §6.8, mesmo approach de MVP-coerente do módulo Contracts): warehouses, stock_movements (ledger append-only, saldo sempre derivado via SUM, nunca coluna editável — reforçado por trigger que bloqueia UPDATE/DELETE sem exceção), serialized_assets, asset_assignments, stock_reservations. Invariante "mesmo serial/patrimônio/MAC nunca duas vezes ativo" (Master Prompt §15.3) reforçada por 3 índices únicos parciais no banco, não só validação de aplicação. Fluxo completo reserva→instalação(comodato)→devolução implementado com SELECT...FOR UPDATE + transação, erros tipados mapeados para 404/409.

Subagente entregou trabalho de excelente qualidade na primeira tentativa — nenhum bug encontrado na revisão nem no teste ao vivo.

Suporte / Service Desk (apps/api/src/support/ + migration support) — subagente eden-support

4 tabelas: support_tickets (protocolo humano TCK-2026-000001, mesmo padrão de quote_number/contract_number), ticket_comments (com is_internal, nunca exposto fora do escopo all), ticket_status_history, sla_timers (1:1). Máquina de estados new→triage→in_progress⇄{waiting_customer,waiting_third_party}→resolved→closed (+cancelled), mesmo padrão de CONTRACT_STATUS_TRANSITIONS. Motor de SLA implementado como funções puras testáveis (apps/api/src/support/sla.ts) — pausa/despausa atomicamente em SQL na transição de status, cálculo de violação subtrai corretamente o tempo pausado (incluindo a pausa corrente). root_cause/resolution_notes só persistidos na transição para closed, não resolved (decisão documentada, confirmada em teste ao vivo).

Também de excelente qualidade — nenhum bug encontrado.

Fundação de Ponto Eletrônico / HR-Timeclock (apps/api/src/hr-timeclock/ + migration hr-timeclock) — subagente eden-hr-timeclock

Escopo desta leva deliberadamente limitado a fundação (modelo de dados + Control iD device + parser/import de AFD com CRC-16/KERMIT) — apuração, fechamento de período, ajustes, banco de horas e relatórios (eden.md §5-10) explicitamente adiados. 4 tabelas: timeclock_devices (senha do equipamento cifrada via FieldEncryptionService, nunca retornada em nenhum GET — só hasCredential:boolean), timeclock_employees (cadastro separado de users, decisão documentada: o Control iD identifica por CPF, e users não tem CPF), afd_import_batches, afd_records. afd_records é imutável sem exceção, nem para super_admin — trigger que bloqueia UPDATE/DELETE sem nenhuma válvula de escape (diferente de audit_log, que tem bypass controlado via SET LOCAL). Idempotência em duas camadas: hash SHA-256 do arquivo inteiro (retorna alreadyImported:true em reimportação) e UNIQUE(device_id, nsr) por registro. NSR ausente/não-numérico nunca vira 0 — fica null, preservando o dado bruto mesmo inválido.

Parser CRC-16/KERMIT (poly 0x1021 refletido/0x8408, init 0x0000, sem XOR final) verificado contra o vetor de teste conhecido ("123456789" → 0x2189) — confirmado tanto no script ad-hoc do agente quanto na suíte permanente test/afd-parser.spec.ts (11 testes).

1 bug real encontrado e corrigido antes da integração: a migration PENDING_hr-timeclock.cjs tinha uma crase (`) solta dentro de um comentário SQL (-- separate from \users`) — como a migration inteira é um template literal JS (`` pgm.sql(...) ``), essa crase fechava o literal prematuramente e quebrava o resto do SQL em código JS inválido, causando SyntaxError: missing ) after argument listao tentar rodarpnpm db:migrate`. Corrigido trocando a crase por aspas simples no comentário. Encontrado e corrigido antes de qualquer teste ao vivo, na primeira tentativa de aplicar a migration.

Padrão de salvamento seletivo ao integrar (novo, deste lote)

Diferente da leva anterior (onde os 2 agentes falharam por limite de gasto e o trabalho foi resgatado de worktrees não commitados), desta vez os 3 agentes completaram normalmente, mas todos os 3 worktrees foram criados a partir do commit único de bootstrap (antes de Auth/Companies/Products/etc. existirem nesse checkout isolado) — sem acesso ao código real de apps/api/src/auth/, packages/database/src/repositories/{audit,users,roles,types}.ts etc. Cada agente relatou isso com transparência e, para conseguir cumprir a etapa de verificação (pnpm typecheck limpo), recriou stubs/placeholders type-compatíveis desses arquivos dentro do próprio worktree — e dois deles (eden-support, eden-hr-timeclock) deixaram esses placeholders (audit.ts, users.ts, roles.ts, types.ts, config, guards de auth) como arquivos genuínos no diff final, não revertidos, por acreditarem (corretamente, do ponto de vista deles, isolados no worktree) que essa infraestrutura ainda não existia.

Na integração, copiei seletivamente só os arquivos de domínio real de cada agente (migration, repositório específico do módulo, camada de API do módulo) e descartei os placeholders duplicados de Auth/Config/Crypto/audit/users/roles, mantendo as versões reais já existentes em main. Wiring do barrel (packages/database/src/index.ts) e app.module.ts feito manualmente para os 3 módulos, como nas levas anteriores.

Verificação

pnpm turbo run typecheck build verde em cada integração incremental; suíte de testes cresceu de 15 para 30 testes, todos verdes (auth.spec.ts, mfa.spec.ts novo, afd-parser.spec.ts novo com 11 testes, role-weight.spec.ts, reseller-assignment.spec.ts, companies.spec.ts, mass-assignment.spec.ts). Container eden-api reconstruído e todo o lote testado ao vivo via curl contra a stack real:

  • MFA: enroll→confirm com código TOTP real gerado via otplib→login exige desafio→código errado rejeitado→código certo completa login→disable exige código válido; exploração do bug de segurança repetida após o fix, confirmando 401.
  • Inventory: armazém→entrada de ativo→serial duplicado rejeitado (409, não 500)→reserva→instalação (comodato)→instalação duplicada rejeitada (409)→devolução→ledger conferido linha a linha (purchase_in, reservation, comodato_out, return_in).
  • Support: ticket criado com SLA (+4h/+24h)→transições válidas→comentário público marca first_response_met→waiting_customer pausa o timer→retorno a in_progress acumula o tempo pausado→resolved→closed (root_cause só persiste aqui)→transição inválida rejeitada (409).
  • HR-Timeclock: device criado com senha cifrada (confirmado no Postgres, formato v1:iv:tag:ciphertext, nunca no GET)→import de arquivo AFD válido (2 linhas, CRC correto)→reimport do mesmo arquivo retorna alreadyImported:true sem duplicar→UPDATE direto em afd_records via SQL bloqueado pelo trigger mesmo fora da API.
  • Trilha de auditoria (audit_log) conferida no Postgres com a sequência completa de todos os eventos do lote.

Pendências deste lote

  • Inventory: warehouse_locations, stock_items (cache de saldo), stock_lots, inventory_counts, transfers/receipts/issues/returns formais, rma, asset_maintenance — deferidos, tipos de movimento já modelados no vocabulário do ledger para não exigir nova migration.
  • Support: anexos reais (S3), CSAT, OS dedicada, filas com roteamento automático, calendário de atendimento configurável, SLA por categoria — hoje é uma política única fixa (4h/24h).
  • HR-Timeclock: apuração, fechamento de período, ajustes manuais, banco de horas, relatórios, sync ao vivo com o Control iD, upload multipart real (hoje aceita o conteúdo como string JSON no body).
  • MFA: sem rate-limiting/bloqueio de tentativas no mfa/challenge (mesma lacuna que já existe no login por senha hoje — nenhum endpoint de auth tem rate limit ainda, ficou registrado como débito técnico geral, não específico do MFA).

Fase 5/6 + Design System — Billing, Fiscal e primeira tela real (login + shell autenticado)

Data: 2026-09-03.

Continuação do mesmo pedido do usuário ("pelo menos mais 20 etapas, use subagentes"). 3 subagentes em paralelo (eden-finance, eden-fiscal, eden-frontend), mesmo padrão de worktree isolado + integração seletiva manual.

Billing / Faturamento recorrente (apps/api/src/billing/ + migration billing) — subagente eden-finance

MVP de 4 tabelas (ADR-0008 lista 9; billing_accounts/billing_cycles/charge_components/usage_charges/invoice_adjustments/billing_run_logs deferidos, documentado): billing_subscriptions (derivada de contrato, monthly_value é snapshot — nunca recalculado automaticamente), billing_runs, invoices, invoice_items. Fluxo subscription→billing_run→invoice completo e idempotente: rodar o mesmo run duas vezes em draft substitui as invoices (não duplica) — reforçado por índice único parcial WHERE status='draft' (no máximo um run em rascunho por período); depois de consolidate, run e invoices ficam imutáveis pela aplicação (409 em qualquer tentativa de gerar/alterar). ADR-0008 respeitado à risca: Billing só calcula o que é devido, nunca toca em AR/AP/boleto/cobrança (Finance) nem em documento fiscal (Fiscal) — bounded contexts continuam sendo desenvolvidos separadamente.

Fiscal (apps/api/src/fiscal/ + migration fiscal) — subagente eden-fiscal

eden.md módulo 5 tem ~19 tabelas de catálogo + importadores automáticos (Siscomex/IBGE) + importador manual de arquivo — MVP portou 5 tabelas fielmente ao shape do legado: fiscal_ncm/fiscal_cfop (catálogo simples), fiscal_municipality (shape distinto, sem valid_from/valid_until), fiscal_operation_nature (seed fixo com as 11 naturezas do legado), product_fiscal_profiles (liga produto à classificação fiscal). Invariante do eden.md §2.5 implementada de verdade: um produto tem no máximo um profile ativo por billing_component — índice único parcial + camada de aplicação que inativa o profile anterior antes de criar o novo (nunca update destrutivo, histórico preservado). Todo o importador automático/manual e as ~14 outras tabelas de catálogo ficam deferidos (documentado com a seção exata do eden.md para cada item).

Design System + login real (packages/ui, apps/core-web) — subagente eden-frontend

Primeira leva de frontend do projeto. Achado que mudou o escopo para melhor: o tema_do_Eden.zip (comprado) não é só Angular — tem uma versão pronta em React 18 + Vite + Tailwind dentro (react.zip, 42.5MB), a mesma stack que o EDEN já usa. Em vez de portar de Angular, o agente extraiu os tokens visuais reais (cores, tipografia, sombras, border-radius, larguras de layout) desse React/Tailwind — confirmando inclusive uma divergência da nossa própria documentação: docs/project-inventory.md citava fonte Nunito/Poppins, a fonte real do tema é Geist.

  • packages/ui deixou de ser placeholder: preset Tailwind exportável (@eden/ui/tailwind-preset) com os tokens reais extraídos, mais componentes React reais (não stub) — Button, Input, Card, Badge, Alert, PageHeader, AppShell (sidebar+topbar+conteúdo).
  • apps/core-web (EDEN Core — prioridade, os outros 2 apps ficam para leva futura): trocou a tela estática "Bootstrap da Fase 1" por um fluxo real com react-router-dom — página de login chamando de verdade POST /api/v1/auth/login (com suporte ao fluxo mfaRequired/POST /mfa/challenge da leva de MFA), sessão em localStorage, rota protegida, e um shell autenticado (AppShell real, nome do usuário via GET /api/v1/auth/me, logout).

Achado real fora do escopo do agente, corrigido por mim em seguida: a API não tinha CORS configurado (app.enableCors ausente em main.ts) — um browser real rodando core-web num domínio/porta diferente da API teria toda chamada de login bloqueada no preflight, mesmo com tudo implementado corretamente nos dois lados. Corrigido: novo EDEN_CORS_ORIGINS (env, lista de origens separadas por vírgula, nunca wildcard — a API emite bearer token) em packages/config/src/env-schema.ts, app.enableCors({origin, credentials:true}) em main.ts, propagado em compose.yaml/.env/.env.example já com os 3 domínios públicos reais (dev-edencore/dev-parceirohandix/dev-portalassinate.falehandix.com.br) mais as portas de dev locais. Testado ao vivo: preflight de https://dev-edencore.falehandix.com.br aceito com os headers corretos; origem não autorizada (evil.example.com) corretamente sem Access-Control-Allow-Origin na resposta; login completo simulando o Origin real de browser, 200 com o header de CORS certo.

Pendência de infra identificada, não resolvida (decisão do operador, não técnica): os 3 domínios públicos hoje só apontam para os containers de frontend (3001/3002/3003) — não há um domínio público equivalente para a API. VITE_API_BASE_URL do build de produção dos 3 apps ainda aponta para localhost:8080 (só funciona em dev local); falta decidir e implementar como o browser em produção alcança a API (domínio próprio para a API? proxy reverso roteando um path tipo /api para o container eden-api no mesmo domínio do frontend?) antes do login funcionar nos domínios públicos de verdade.

Verificação

pnpm turbo run typecheck build (28/28) e pnpm --filter @eden/api test (30/30, sem regressão) verdes após cada integração. Containers eden-api/eden-core reconstruídos; testado ao vivo via curl:

  • Fiscal: naturezas de operação semeadas (11), NCM criado, invariante de profile único ativo por componente confirmada (segundo profile RECURRING criado → primeiro automaticamente active:false).
  • Billing: subscription criada → billing run gerado (invoice de R$499,90, vencimento no billing_day) → regeneração do mesmo período reaproveita o mesmo run (idempotente, sem duplicar invoice) → consolidação → segunda consolidação rejeitada (409) → regeneração de período já consolidado rejeitada (409).
  • CORS: preflight aceito do domínio público real, rejeitado de origem não cadastrada, login completo com Origin de browser real.
  • core-web: build de produção servido pelo container real não contém mais o texto "Bootstrap da Fase 1" — contém o formulário de login real (E-mail/Senha/Entrar).
  • Trilha de auditoria conferida no Postgres com a sequência completa do lote.

Pendências deste lote

  • Billing: billing_accounts, billing_cycles como tabela própria, consumo/telefonia (charge_components/usage_charges), invoice_adjustments, pró-rata — tudo Finance/AR-AP (títulos, boletos, dunning, conciliação) fica para quando esse bounded context for iniciado, por decisão da ADR-0008.
  • Fiscal: as ~14 tabelas de catálogo restantes, fiscal_catalog_sync, importador automático (Siscomex/IBGE) e manual (CSV/XLSX/JSON com proteção de inativação em massa), fiscal_rules (schema pronto mas sem consumo mesmo no legado).
  • Frontend: reseller-web/subscriber-web ainda no bootstrap estático; telas de negócio (Quotes/Contracts/Inventory/etc.) no core-web; rotação automática de refresh token; fluxo de ENROLL de MFA na UI (só login com MFA já habilitado está implementado).

Decisão do operador sobre topologia de API pública + 3 bugs de infra encontrados e corrigidos na sequência

Usuário decidiu: a API nunca tem domínio público próprio, só IP/nome interno — o browser sempre chama a API pelo mesmo domínio do frontend (caminho relativo), e o proxy externo (gerenciado pelo operador, fora deste repo) roteia esse caminho internamente para eden-api (por nome de serviço Docker, /etc/hosts ou IP interno — detalhe de infra do operador, não deste código).

Implementado: infra/docker/Dockerfile.web agora aceita ARG VITE_API_BASE_URL (default /api/v1, Vite bake em build-time); compose.yaml passa esse build arg fixo (/api/v1) para os 3 serviços web; EDEN_CORS_ORIGINS simplificado para só as portas de dev local (o fluxo de produção é same-origin via proxy, não precisa de CORS).

3 bugs reais de infraestrutura encontrados e corrigidos nessa verificação (nenhum era o objetivo original, todos achados testando o resultado de ponta a ponta, não só lendo o código):

  1. turbo.json não declarava VITE_API_BASE_URL como input da task build — o Turborepo cacheava o build do Vite sem saber que essa env var afeta o output, então reaproveitava um bundle antigo (com a URL absoluta de dev) mesmo depois de mudar a env var. Corrigido adicionando "env": ["VITE_*"] à task build em turbo.json.
  2. compose.yaml usava ${VITE_API_BASE_URL:-/api/v1} para o build arg do Docker — parecia razoável, mas colidia com a variável de mesmo nome no .env (usada para pnpm dev local, valor absoluto http://localhost:8080/api/v1) que o docker compose carrega automaticamente; o "default" nunca era usado porque a variável já estava setada. Corrigido fixando o valor diretamente (/api/v1, sem interpolação de env) nos 3 serviços web, já que essa é uma decisão arquitetural fixa, não uma configuração por ambiente.
  3. Não existia .dockerignore no projeto — todo COPY . . do Dockerfile.web/Dockerfile.node estava copiando node_modules/ (315MB), .git/ (5.8MB) e .claude/worktrees/ (345MB, os worktrees de subagentes desta mesma sessão) para dentro do contexto de build a cada vez. Além do desperdício óbvio, isso é parte de por que o bug nº1 passou despercebido no primeiro teste: caches .turbo/ de dentro de cada pacote também eram copiados para dentro da imagem. Corrigido criando .dockerignore (exclui node_modules, .git, dist, .turbo, .claude/worktrees, .tmp, .env*, o zip do tema).

Cada correção foi testada isoladamente e a última confirmada abrindo o JS de produção servido pelo container real (docker compose build --no-cache) e conferindo que o literal "/api/v1" (não mais "http://localhost:8080/api/v1") está no bundle.


Fase 8 (parcial) — Portais Parceiros e Assinante replicam o shell autenticado do Core

Data: 2026-09-03. Subagente eden-frontend replicou o mesmo padrão de login/sessão/shell do core-web (leva anterior) para apps/reseller-web e apps/subscriber-web — mesma estrutura de arquivos, mesmo cliente HTTP, mesmo AuthContext, só trocando applicationKey (reseller/subscriber), título, e a chave de localStorage da sessão (eden.reseller.session/eden.subscriber.session, isolamento explícito mesmo já sendo origens/portas diferentes).

Bug de integração (não do agente) encontrado e corrigido: ao copiar src/lib/ e src/routes/ do worktree para main, o cp -r criou diretórios aninhados (src/lib/lib/, src/routes/routes/) porque os diretórios de destino já existiam vazios em main — mesmo padrão de erro já visto na integração do HR-Timeclock nesta sessão. Corrigido achatando a estrutura antes do typecheck.

Verificado: pnpm turbo run typecheck build (28/28) após a correção; containers eden-parceiros/eden-assinante reconstruídos — títulos corretos (EDEN Parceiros/EDEN Assinante), texto antigo "Bootstrap da Fase 1" ausente do bundle de produção, formulário de login real presente; login de ponta a ponta testado contra a API real com applicationKey: "reseller" e "subscriber" — ambos aceitos e retornam tokens válidos.

Pendências

Idênticas às do core-web (rotação de refresh token, enroll de MFA na UI, telas de negócio) — nenhuma tela além do shell autenticado existe em nenhum dos 3 apps ainda.


Fase 3 (parcial) — Fundação de Templates de Documento (sem Assinatura Eletrônica ainda)

Data: 2026-09-03. Subagente construiu a metade "templates + geração" do eden.md módulo 4 (§1.1-1.3) — a metade de Assinatura Eletrônica (§1.4-1.9: envelopes, signatários, OTP, cadeia de auditoria hash-chain, 9 tabelas satélite) foi deliberadamente deixada de fora, por ser criptograficamente/legalmente sensível e grande demais para dividir espaço com esta leva — fica para uma leva própria e dedicada.

document_templates + document_template_versions (ciclo draft→publish, invariante de no máximo 1 rascunho por template via índice único parcial, versão publicada é imutável) + document_generations (idempotente por (template_version_id, origem), CHECK garantindo exatamente uma origem). Dependência circular (document_templates.current_version_id ↔ document_template_versions) resolvida com ALTER TABLE depois que as duas tabelas existem. Motor de merge de variáveis ({{chave}} → valor do contexto, nunca lança exceção, reporta variáveis ausentes) implementado como função pura testável — o parser Tiptap real e a renderização de PDF via Playwright/Chromium ficam para quando o editor visual e essa infra existirem (simplificação documentada: editor_content.plainText como stand-in).

Verificado: pnpm turbo run typecheck build (28/28), pnpm --filter @eden/api test (30/30, sem regressão). Container eden-api reconstruído; testado ao vivo via curl: template criado → draft criado → segundo draft rejeitado (409, invariante confirmada) → draft editado com placeholders → publicado (current_version_id atualizado) → documento gerado com merge de variáveis funcionando ({{empresa}}/{{cliente}}/{{valor}} substituídos corretamente) → regeneração com os mesmos parâmetros reaproveita o registro existente (idempotência confirmada, mesmo id). Trilha de auditoria conferida no Postgres.

Pendências

Toda a Assinatura Eletrônica (eden.md §1.4-1.9/§5/§6/§7 — envelopes, signatários, OTP, hash-chain, verificação pública), geração real de PDF (Playwright/Chromium) + sanitização de HTML, editor visual Tiptap real (renderToHtml/resolveDocument com condicionais e repetição de tabela), seed dos 7 templates reais de produção da Handix, regra de idempotência completa com force/extra_fields.


Fase 2/8 — Telas de negócio reais no EDEN Core (Empresas, Produtos, Revendas, CRM, Ofertas)

Data: 2026-09-03. Usuário pediu explicitamente para priorizar a parte web ("ja faca a parte web para eu ja ver como esta ficando"). 2 subagentes eden-frontend em paralelo, mesma sessão, construindo sobre o shell autenticado + Design System já prontos.

Correção de instrução no meio da leva: o usuário interrompeu para dizer "não esqueça de seguir o template" / "melhor usar ele" — as telas deviam seguir de perto o LAYOUT REAL do tema DreamsERP comprado, não só os tokens visuais abstratos já extraídos. Extraí do tema_do_Eden.zip as páginas React reais mais relevantes (sales/salesQuotes*.tsx, crm/leads.tsx/deals*.tsx, inventory/products*.tsx, purchase/vendors.tsx, system/userManagement.tsx, ui-interface/{tables,forms}) para dentro do worktree de cada agente (.tmp/theme-reference/, nunca versionado) e mandei mensagem de correção para os dois agentes já em execução — via SendMessage, não relançando a tarefa (no meio do processo cometi o erro de usar Agent em vez de SendMessage para a primeira correção, criando um agente duplicado; percebido e corrigido na hora com TaskStop antes de qualquer trabalho real ser feito por ele). Ambos incorporaram a referência do tema (estrutura de header/breadcrumb/tabela/formulário em cards) reescrita com @eden/ui + Tailwind puro — sem copiar JSX nem trazer dependências do tema (antd/Preline) para o projeto.

Empresas / Produtos / Revendas (apps/core-web/src/routes/{companies,products,resellers}/)

CRUD completo (lista + criar/editar) para os 3 módulos, layout modelado em purchase/vendors.tsx (empresas/revendas) e inventory/products.tsx (produtos, com máscara de moeda para as 5 faixas de preço + implantação). Novo componente Table<T> em packages/ui (tabela genérica reutilizável, sem dependência de terceiro).

CRM e Ofertas/Quotes (apps/core-web/src/routes/{crm,quotes}/)

Leads (lista, criar/editar, converter em oportunidade) e Oportunidades (lista, criar/editar) modelados em crm/leads.tsx/dealsDetails.tsx. Ofertas — o módulo mais rico: QuotesListPage, QuoteFormPage (revenda + cliente + período de contrato + fidelidade reduzida opcional + lista dinâmica de itens produto/quantidade + desconto opcional + condição de pagamento, mostra os totais calculados pelo servidor antes de ir para o detalhe) e QuoteDetailPage (resumo financeiro completo, aprovar/rejeitar quando pendente, fechar negócio quando ainda é orçamento) — modelados em sales/salesQuotes.tsx/addSalesQuotes.tsx/salesQuotesDetails.tsx. Novo componente Select em packages/ui.

Achado real do próprio agente, corrigindo meu brief: eu tinha dito no brief que as respostas de Leads/Opportunities eram snake_case (copiando o padrão do resto da API) — o agente verificou contra o código-fonte real (packages/database/src/repositories/leads.ts) e descobriu que esse módulo específico mapeia para camelCase na resposta (mapLead), diferente de Quotes/Companies/Products/Resellers (snake_case direto da linha do banco). Confirmado ao vivo via curl antes de integrar.

Integração manual (os 2 agentes trabalharam em paralelo no mesmo core-web)

Um dos agentes não conseguiu git merge dentro do próprio sandbox e reconstruiu manualmente (leitura+escrita) o estado já existente do shell autenticado — comparei arquivo por arquivo contra main (diff -rq) para confirmar que a restauração foi fiel (nenhum componente restaurado divergiu) antes de aceitar qualquer coisa como "nova". App.tsx, AuthenticatedLayout.tsx (nav unificada, lógica de "ativo" por prefixo de path com borda de / para nunca casar /companies com /companies-x) e packages/ui/src/index.ts foram reconciliados manualmente juntando as rotas/exports dos dois agentes; lib/format.ts teve as duas versões mescladas (formatDocument de um lado, formatDateBR do outro).

Verificado: pnpm turbo run typecheck build (28/28). Containers eden-api/eden-core reconstruídos (o build em background travou duas vezes por algum motivo de recurso do ambiente — refeito em primeiro plano, funcionou); bundle de produção confirmado contendo todas as novas telas; login simulando requisição real de browser (Origin: http://localhost:3001) funcionando; listagens de Empresas/Produtos/Revendas retornando dados reais; criação de lead confirmando o formato camelCase ao vivo.

Pendências

Client Registration (Cadastro de Cliente) ainda sem tela — QuoteDetailPage hoje exige colar manualmente um UUID para fechar negócio. Nenhuma tela de Contracts/Inventory/Support/HR-Timeclock/Fiscal/Billing/Documents ainda. reseller-web/subscriber-web continuam só com o shell de login (sem telas de negócio).


Proxy same-origin dentro do próprio nginx dos 3 frontends

Data: 2026-09-03. Usuário pediu para "configurar o proxy". Como a API não tem domínio público próprio (decisão do operador) e o proxy externo que termina SSL nos 3 domínios é gerenciado fora deste repositório (sem acesso daqui, e o guard de segurança do projeto bloqueia acesso a caminhos fora do projeto//etc), a solução implementada foi dentro do que o repo já controla: infra/docker/nginx-spa.conf (usado pelos 3 containers de frontend) ganhou um location /api/ que faz proxy_pass para eden-api:8080 — os 2 containers já estão na mesma rede Docker (eden_net), então o roteamento é interno, sem tocar em nada externo. resolver 127.0.0.11 (DNS embutido do Docker) usado com proxy_pass via variável para evitar o problema clássico de nginx cachear o IP do upstream para sempre.

Com isso, o browser batendo em https://dev-edencore.falehandix.com.br/api/v1/... (uma vez que o proxy externo do operador já roteia o domínio inteiro para a porta 3001, como já estava configurado) chega ao nginx do container, que repassa para a API interna — fluxo 100% same-origin do ponto de vista do browser, sem CORS.

Testado ao vivo: POST /api/v1/auth/login com credenciais reais funcionando através da porta 3001 (eden-core), 3002 (eden-parceiros) e 3003 (eden-assinante) — os 3 containers reconstruídos e confirmados.