Files
DeskcommCRM/docs/architecture/marca-propria.architecture.json
T

211 lines
23 KiB
JSON
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.
{
"schema_version": 1,
"diagram_type": "architecture",
"meta": {
"title": "Marca própria — a fachada resolve em runtime, e o PDF de LGPD fica FORA dela",
"subtitle": "uma imagem Docker serve todas as marcas: banco acima do .env, e três identidades que não se misturam",
"output": "marca-propria.html",
"quality_profile": "standard"
},
"lanes": [
{ "id": "config", "label": "Fora do app (kit + .env + GoTrue)" },
{ "id": "humano", "label": "Telas, portas e quem recebe" },
{ "id": "escrita", "label": "Escrita (server actions)" },
{ "id": "banco", "label": "Banco" },
{ "id": "resolver", "label": "Resolvedor (lib/branding)" },
{ "id": "saida", "label": "Saídas — com DOM e sem DOM" }
],
"mainPath": [
"adminmarca",
"updatebranding",
"platformbranding",
"instalacao",
"resolver",
"css",
"layout",
"interface"
],
"nodes": [
{
"id": "installsh",
"lane": "config",
"col": 0,
"type": "service",
"label": "install.sh",
"sublabel": "envq APP_NAME/APP_LOGO_URL/APP_ACCENT_HEX/SUPPORT_EMAIL/RESEND_* (:1392-1414) · a cor tem validador v_hex na entrevista"
},
{
"id": "env",
"lane": "config",
"col": 1,
"type": "service",
"label": "lib/env.ts — .env",
"sublabel": "semente e PISO DE ROLLBACK: agent.sh reverte a imagem, nunca o banco"
},
{
"id": "marcaemails",
"lane": "config",
"col": 6,
"type": "service",
"label": "hostgator-setup-kit/marca-emails.sh",
"sublabel": "sem SUPABASE_ACCESS_TOKEN imprime o passo manual e SAI 0 — nunca derruba instalação"
},
{
"id": "gotrue",
"lane": "config",
"col": 7,
"type": "external",
"label": "GoTrue (Supabase Auth)",
"sublabel": "templates de confirmação e recuperação — processo de terceiro, nenhum TypeScript nosso roda ali"
},
{ "id": "registry", "lane": "humano", "col": 0, "type": "frontend", "label": "lib/navigation/registry.ts", "sublabel": "a PORTA: /app/settings/marca no grupo Configurações, sidebar:false (tarefa de uma vez)" },
{ "id": "adminmarca", "lane": "humano", "col": 1, "type": "frontend", "label": "/admin/marca", "sublabel": "marca da INSTALAÇÃO · tira de tons + estado do degrade (fallback_at/fallback_reason)" },
{ "id": "orgmarca", "lane": "humano", "col": 1, "type": "frontend", "label": "/app/settings/marca", "sublabel": "marca da ORGANIZAÇÃO · admin do tenant, sem platform_admin" },
{ "id": "telatenant", "lane": "humano", "col": 1, "type": "frontend", "label": "/app/settings/tenant", "sublabel": "campo Razão social — o único lugar onde legal_name deixa de ser cópia do nome fantasia" },
{ "id": "aba", "lane": "humano", "col": 8, "type": "frontend", "label": "A aba do navegador", "sublabel": "título (generateMetadata) + ícone (/icon) + cor da barra (themeColor)" },
{ "id": "interface", "lane": "humano", "col": 8, "type": "frontend", "label": "A interface pós-login", "sublabel": "sidebar, logo, botões, anel de foco — tudo pelo accent derivado" },
{ "id": "painelaudit", "lane": "humano", "col": 8, "type": "frontend", "label": "/admin/audit", "sublabel": "quem repintou isto, e quando — as duas ações estão nas DUAS listas de código" },
{ "id": "caixaentrada", "lane": "humano", "col": 8, "type": "frontend", "label": "A caixa de entrada do cliente", "sublabel": "convite, confirmação de conta, recuperação de senha, aviso de SLA" },
{ "id": "autenticador", "lane": "humano", "col": 8, "type": "frontend", "label": "O app autenticador, no celular", "sublabel": "issuer=<marca>. Trocar o issuer NÃO reescreve fator já cadastrado" },
{ "id": "titular", "lane": "humano", "col": 8, "type": "frontend", "label": "O titular de dados (Art. 18 II)", "sublabel": "recebe o link do export e o relatório — e nele lê o CONTROLADOR, não a marca de quem vende" },
{ "id": "updatebranding", "lane": "escrita", "col": 2, "type": "api", "label": "updateBranding (server action)", "sublabel": "gate is_platform_admin + admin client + invalidarMarcaDaInstalacao()" },
{ "id": "updateorg", "lane": "escrita", "col": 2, "type": "api", "label": "updateMarcaDaOrganizacao (server action)", "sublabel": "RPC atômica: SELECT→spread→UPDATE no mesmo jsonb perderia visibility_mode" },
{ "id": "rotalogo", "lane": "escrita", "col": 2, "type": "api", "label": "POST/DELETE /api/v1/marca/logo", "sublabel": "gate POR ESCOPO · tipo pelos BYTES (file.type só no details) · 512 KB por arte · claro/escuro opcionais · sobe → grava → só então apaga, com o prefixo asseverado" },
{ "id": "bucketlogos", "lane": "banco", "col": 3, "type": "database", "label": "storage/brand-logos (PÚBLICO)", "sublabel": "o 1º bucket público do repo · zero policy em storage.objects · <prefixo>/<uuid>.<png|jpg> · a URL é DERIVADA do caminho, nunca gravada" },
{ "id": "fnlogoorg", "lane": "banco", "col": 4, "type": "database", "label": "fn_definir_logo_da_organizacao (0158)", "sublabel": "assevera o prefixo contra p_org DENTRO do banco · merge no CAMPO · a 0157 ganhou forward-fix para não apagar logo_path" },
{ "id": "platformbranding", "lane": "banco", "col": 3, "type": "database", "label": "public.platform_branding", "sublabel": "singleton id=1 · sem organization_id · revoke de anon (o baseline dá GRANT ALL por default)" },
{ "id": "orgbranding", "lane": "banco", "col": 3, "type": "database", "label": "organizations.settings.branding", "sublabel": "jsonb com envelope {format, algo, semente_hex} · schema LOOSE: código velho sobre schema novo não pode lançar" },
{ "id": "legalname", "lane": "banco", "col": 3, "type": "database", "label": "organizations.legal_name + dpo_email", "sublabel": "identidade JURÍDICA — já existia, ninguém usava. DIRC respondeu I (Integrar), não campo novo" },
{ "id": "auditlog", "lane": "banco", "col": 4, "type": "database", "label": "api_audit_log", "sublabel": "platform_branding.updated (sem org) · org.branding_updated (com org)" },
{ "id": "instalacao", "lane": "resolver", "col": 4, "type": "service", "label": "lib/branding/instalacao.ts", "sublabel": "marcaDaInstalacao() · memo TTL 30s · sementeDoAmbiente() · registrarEstadoDaMarca() · NUNCA LANÇA" },
{ "id": "organizacao", "lane": "resolver", "col": 4, "type": "service", "label": "lib/branding/organizacao.ts", "sublabel": "resolverMarcaDaOrganizacao(organizationId)" },
{ "id": "resolver", "lane": "resolver", "col": 5, "type": "service", "label": "lib/branding/resolve.ts", "sublabel": "pilha org → instalação → .env → padrão do produto; cor recusada NÃO apaga a camada de baixo" },
{ "id": "rampa", "lane": "resolver", "col": 5, "type": "service", "label": "rampa.ts + contraste.ts", "sublabel": "1 hex → 11 stops nos 2 temas; piso por PAPEL × SUPERFÍCIE, não pela semente" },
{ "id": "css", "lane": "resolver", "col": 6, "type": "service", "label": "lib/branding/css.ts", "sublabel": "serialização com allowlist: emite --color-brand e a família do accent — NUNCA --color-bg" },
{ "id": "saida", "lane": "resolver", "col": 6, "type": "service", "label": "lib/branding/saida.ts", "sublabel": "marcaDaSaida(orgId|null) → um hex + uma frente legível · tema CLARO sempre · NUNCA LANÇA" },
{ "id": "barra", "lane": "resolver", "col": 6, "type": "service", "label": "lib/branding/barra-do-navegador.ts", "sublabel": "os 2 hexes do themeColor, importados de REGUA_DO_PRODUTO (eram cópia em 3 arquivos)" },
{ "id": "layout", "lane": "saida", "col": 7, "type": "frontend", "label": "app/layout.tsx", "sublabel": "<EstiloDaMarca/> no <head> + generateMetadata (título com template) + viewport" },
{ "id": "applayout", "lane": "saida", "col": 7, "type": "frontend", "label": "app/app/layout.tsx", "sublabel": "<EstiloDaMarcaDaOrganizacao/> escopado em [data-marca-org] — não alcança /login" },
{ "id": "marcacliente", "lane": "saida", "col": 8, "type": "frontend", "label": "lib/branding/contexto.tsx", "sublabel": "a marca atravessa por PROP do servidor (useMarcaDaInstalacao) — era branding(), que lê window.__PUBLIC_ENV__ no navegador e process.env no SSR: com logo no banco e APP_LOGO_URL vazio, o servidor desenhava <span> e o cliente <img> (React #418)" },
{ "id": "iconroute", "lane": "saida", "col": 7, "type": "api", "label": "app/icon.tsx", "sublabel": "ImageResponse em runtime · force-dynamic · /icon em PUBLIC_PATHS (sem isso, 307 para /login)" },
{ "id": "invite", "lane": "saida", "col": 7, "type": "service", "label": "lib/email/templates/invite.ts", "sublabel": "assunto + corpo + botão com a marca de quem convidou" },
{ "id": "lgpdemail", "lane": "saida", "col": 7, "type": "service", "label": "lib/lgpd/email-delivery.ts", "sublabel": "diz quem PROCESSOU a solicitação — era 'DeskcommCRM' em 100% dos casos, não em borda" },
{ "id": "slaalarm", "lane": "saida", "col": 7, "type": "service", "label": "lib/lgpd/sla-alarm.ts", "sublabel": "marca resolvida no cabeçalho; o #dc2626 do alerta FICA — é semântica de urgência, não marca" },
{ "id": "enrollmfa", "lane": "saida", "col": 7, "type": "api", "label": "app/actions/auth/enrollMfa.ts", "sublabel": "issuer (o campo que o celular grava), não friendlyName — medido contra GoTrue v2.188.1" },
{ "id": "resend", "lane": "saida", "col": 7, "type": "service", "label": "lib/email/resend.ts", "sublabel": "nome de exibição = marca; endereço = do OPERADOR (domínio verificado). Vazio = not_configured" },
{ "id": "lgpdworker", "lane": "saida", "col": 6, "type": "service", "label": "workers/lgpd-export-worker.ts + export-collector", "sublabel": "monta o ExportPayload; roda pelo scheduler (event-log-drain), nunca no contêiner worker" },
{ "id": "pdf", "lane": "saida", "col": 7, "type": "service", "label": "lib/lgpd/pdf-renderer.tsx", "sublabel": "DESLIGADO da marca de propósito — ver o card vermelho" }
],
"edges": [
{ "id": "e1", "from": "installsh", "to": "env", "label": "grava a marca no .env — e o arquivo é escrito com truncamento: chave que o script não grava some no update seguinte" },
{ "id": "e2", "from": "env", "to": "platformbranding", "label": "sementeDoAmbiente(): a 1ª leitura semeia a tabela e marca seeded_from_env" },
{ "id": "e3", "from": "env", "to": "resolver", "label": "camadaDoAmbiente() — a camada mais baixa da pilha, e a ÚNICA porta por onde um hex torto entra: lib/env.ts:201 não valida formato (o formulário e o CHECK barram os outros dois caminhos)" },
{ "id": "e4", "from": "registry", "to": "orgmarca", "label": "a porta declarada; sem entrada aqui, navegacao-completude.test.ts reprova o build" },
{ "id": "e5", "from": "registry", "to": "telatenant", "label": "porta de 1ª classe do grupo Configurações" },
{ "id": "e6", "from": "registry", "to": "interface", "label": "a sidebar inteira é renderizada a partir do registro" },
{ "id": "e7", "from": "adminmarca", "to": "updatebranding", "label": "Salvar (desabilitado enquanto o hex for inválido)" },
{ "id": "e8", "from": "orgmarca", "to": "updateorg", "label": "Salvar" },
{ "id": "e9", "from": "telatenant", "to": "legalname", "label": "Razão social — legal_name NASCE igual a display_name (bootstrap-owner.ts), então 'se vazio use X' nunca dispara" },
{ "id": "e10", "from": "updatebranding", "to": "platformbranding", "label": "update do singleton + invalidação do memo" },
{ "id": "e11", "from": "updateorg", "to": "orgbranding", "label": "RPC atômica (o jsonb é compartilhado com visibility_mode)" },
{ "id": "e12", "from": "updatebranding", "to": "auditlog", "label": "audit('platform_branding.updated') — resourceId null: a tabela tem UMA linha e resource_id é uuid" },
{ "id": "e13", "from": "updateorg", "to": "auditlog", "label": "audit('org.branding_updated') — com organization_id e resource_id = uuid da org" },
{ "id": "e14", "from": "auditlog", "to": "painelaudit", "label": "quem repintou isto? A resposta só existe aqui — não há event_log (nenhum handler consumiria)" },
{ "id": "e15", "from": "platformbranding", "to": "instalacao", "label": "leitura + memo com TTL de 30s" },
{ "id": "e16", "from": "orgbranding", "to": "organizacao", "label": "leitura por organizationId" },
{ "id": "e17", "from": "instalacao", "to": "resolver", "label": "camadaDaInstalacao()" },
{ "id": "e18", "from": "organizacao", "to": "resolver", "label": "camadaDaOrganizacao() — só depois da sessão; /login nunca a recebe" },
{ "id": "e19", "from": "resolver", "to": "rampa", "label": "semente → rampa, com reconciliação contra as cores semânticas do produto" },
{ "id": "e20", "from": "rampa", "to": "css", "label": "tokens derivados dos dois temas" },
{ "id": "e21", "from": "resolver", "to": "css", "label": "cssDaMarca(): allowlist de tokens" },
{ "id": "e22", "from": "css", "to": "layout", "label": "<style id=\"marca-instalacao\"> no <head>" },
{ "id": "e23", "from": "css", "to": "applayout", "label": "<style id=\"marca-organizacao\">, escopado" },
{ "id": "e24", "from": "barra", "to": "layout", "label": "themeColor claro/escuro a partir de uma fonte só" },
{ "id": "e25", "from": "barra", "to": "aba", "label": "a cor da barra do navegador é a do PRODUTO em toda marca — medido: a marca não emite --color-bg", "route": "highlight" },
{ "id": "e26", "from": "layout", "to": "aba", "label": "título com template, herdado por toda página" },
{ "id": "e27", "from": "applayout", "to": "interface", "label": "a cor da organização pinta /app e não vaza para o login" },
{ "id": "e28", "from": "layout", "to": "instalacao", "label": "registrarEstadoDaMarca(motivo): a recusa VOLTA para o banco — este é o laço de retorno", "route": "highlight" },
{ "id": "e29", "from": "instalacao", "to": "platformbranding", "label": "grava/limpa fallback_at + fallback_reason" },
{ "id": "e30", "from": "instalacao", "to": "adminmarca", "label": "a tela mostra o estado do degrade em português, não um log que ninguém abre" },
{ "id": "e31", "from": "resolver", "to": "saida", "label": "MarcaResolvida → MarcaDeSaida: um hex, uma frente legível, tema claro" },
{ "id": "e32", "from": "saida", "to": "iconroute", "label": "cor + inicial. NUNCA busca logo_url: seria requisição de saída disparada pelo <head> de toda página" },
{ "id": "e33", "from": "iconroute", "to": "aba", "label": "/icon responde 200 para quem NÃO logou — é a primeira tela que um comprador vê" },
{ "id": "e34", "from": "saida", "to": "invite", "label": "marcaDaSaida(orgId): classe A, a org já está resolvida nos dois chamadores" },
{ "id": "e35", "from": "saida", "to": "lgpdemail", "label": "marcaDaSaida(orgId) vindo do worker" },
{ "id": "e36", "from": "saida", "to": "slaalarm", "label": "marcaDaSaida(orgId) vindo do cron lgpd-sla-watcher" },
{ "id": "e37", "from": "saida", "to": "enrollmfa", "label": "marcaDaSaida(null): classe B, MFA é anterior à escolha de organização" },
{ "id": "e38", "from": "saida", "to": "resend", "label": "fromName = marca resolvida" },
{ "id": "e39", "from": "env", "to": "resend", "label": "RESEND_API_KEY + RESEND_FROM_EMAIL (agora dentro do Zod, não process.env cru)" },
{ "id": "e40", "from": "invite", "to": "resend", "label": "sendEmail()" },
{ "id": "e41", "from": "lgpdemail", "to": "resend", "label": "sendEmail()" },
{ "id": "e42", "from": "slaalarm", "to": "resend", "label": "sendEmail()" },
{ "id": "e43", "from": "resend", "to": "caixaentrada", "label": "sem RESEND_FROM_EMAIL não há envio silencioso: EmailNotConfigured vira pending_review e accept_url na tela" },
{ "id": "e44", "from": "enrollmfa", "to": "autenticador", "label": "otpauth://…?issuer=<marca da instalação>" },
{ "id": "e45", "from": "env", "to": "marcaemails", "label": "lê a marca do .env e substitui __APP_NAME__/__ACCENT__/__ACCENT_FG__ nos HTML" },
{ "id": "e46", "from": "installsh", "to": "marcaemails", "label": "chamado após o provision; e por update.sh, para o clone que já roda" },
{ "id": "e47", "from": "marcaemails", "to": "gotrue", "label": "PATCH /v1/projects/{ref}/config/auth — assunto, corpo, site_url e uri_allow_list" },
{ "id": "e48", "from": "gotrue", "to": "caixaentrada", "label": "confirmação de conta e recuperação de senha: o PRIMEIRO artefato que o cliente do revendedor recebe" },
{ "id": "e49", "from": "legalname", "to": "lgpdworker", "label": "select legal_name, display_name, dpo_email" },
{ "id": "e50", "from": "lgpdworker", "to": "pdf", "label": "ExportPayload com o controlador e o DPO resolvido (dpo_email || LGPD_DPO_EMAIL)" },
{ "id": "e51", "from": "lgpdworker", "to": "lgpdemail", "label": "link assinado do export" },
{ "id": "e52", "from": "legalname", "to": "pdf", "label": "o rodapé imprime o CONTROLADOR — a ÚNICA identidade do épico que não é marca", "route": "highlight" },
{ "id": "e53", "from": "pdf", "to": "titular", "label": "Relatório do Art. 18 II (Lei 13.709/2018)" },
{ "id": "e54", "from": "lgpdemail", "to": "titular", "label": "o e-mail com o link, dizendo quem OPEROU" },
{ "id": "e55", "from": "adminmarca", "to": "rotalogo", "label": "CampoDeLogo: o arquivo sobe na hora, FORA do Salvar — o ponteiro tem escritor próprio" },
{ "id": "e56", "from": "orgmarca", "to": "rotalogo", "label": "mesmo componente, escopo=organizacao — o gate troca, a ordem das verificações não" },
{ "id": "e57", "from": "rotalogo", "to": "bucketlogos", "label": "upload dos bytes FAREJADOS; o anterior só é apagado DEPOIS de gravar, e só sob o próprio prefixo (podeApagar)" },
{ "id": "e58", "from": "rotalogo", "to": "platformbranding", "label": "escopo=instalacao: upsert de logo_path (CHECK ^platform/<uuid>.(png|jpg)) + seeded_from_env=false" },
{ "id": "e59", "from": "rotalogo", "to": "fnlogoorg", "label": "escopo=organizacao: RPC com row_count de volta — a catraca da issue #144" },
{ "id": "e60", "from": "fnlogoorg", "to": "orgbranding", "label": "jsonb_set em {branding} preservando app_name/accent_hex — merge no CAMPO, não no objeto" },
{ "id": "e61", "from": "bucketlogos", "to": "resolver", "label": "logoDaCamada(): o ARQUIVO vence a URL colada; par de logos não mistura marcas entre camadas; urlPublicaDoLogo deriva a URL do caminho + host em runtime" },
{ "id": "e62", "from": "rotalogo", "to": "auditlog", "label": "reusa platform_branding.updated / org.branding_updated — código de ação novo não responderia pergunta nova" },
{ "id": "e63", "from": "layout", "to": "marcacliente", "label": "a MESMA marcaResolvida() do título e do CSS desce por prop, no payload do RSC — mesma rota de user/activeOrg" },
{ "id": "e64", "from": "marcacliente", "to": "interface", "label": "useMarcaDaInstalacao(): SSR e hidratação leem o mesmo objeto, então o logo do revendedor não troca de elemento no meio do caminho" }
],
"cards": [
{
"dot": "rose",
"title": "A não-ligação declarada: o PDF de LGPD NÃO recebe marca, e isso é decisão, não esquecimento",
"items": [
"não existe aresta saida → pdf, e a ausência é o conteúdo: `lib/branding/saida.ts` alcança e-mail, remetente, ícone e autenticador, e para no relatório do titular",
"o motivo é jurídico, não estético: o revendedor é OPERADOR; quem responde pelos dados é o CONTROLADOR, que é a organização (organizations.legal_name)",
"trocar 'DeskcommCRM' pela marca do revendedor ali PIORARIA o defeito — hoje o nome é obviamente do software; depois pareceria a declaração de quem responde legalmente",
"consequência boa e deliberada: a armadilha do @react-pdf deixa de existir para nós. Ele aceita var(--x) e oklch(), NÃO lança, e devolve PDF válido com a cor descartada (1514 bytes contra 1538 do hex) — qualquer prova do tipo 'gerei e abriu' passaria com a marca perdida",
"e o e-mail de LGPD (e54) leva marca DE PROPÓSITO: ele diz quem OPEROU. São dois papéis distintos na mesma jornada, e é por isso que as duas saídas carregam nomes diferentes",
"armadilha aberta: legal_name NASCE igual a display_name (scripts/bootstrap-owner.ts) — o caso ruim é o valor plausível e errado, não o nulo. Quem resolve é a tela (e9), não um fallback"
]
},
{
"dot": "amber",
"title": "Invariantes que a forma não mostra",
"items": [
"a imagem Docker é UMA SÓ para todas as marcas — por isso nada de NEXT_PUBLIC_*, nada de favicon em public/, nada de imagem por revendedor (update.sh grava APP_IMAGE incondicionalmente e substituiria a do revendedor em silêncio)",
"o BANCO está acima do .env: mudar a marca não pede reinício de contêiner. O .env é semente e piso de rollback, porque o agent.sh reverte a imagem e nunca o banco",
"resolver NUNCA LANÇA, em nenhuma das duas pontas (instalacao.ts, saida.ts): branding() é chamado em app/layout.tsx, e um throw ali é 500 em TODAS as telas — e um e-mail de LGPD tem SLA legal de D+7",
"o schema do jsonb é LOOSE com catchall, exceção declarada ao .strict() do repo: rollback põe código velho sobre schema novo POR CONSTRUÇÃO (update.sh aplica o baseline antes de puxar a imagem)",
"cor recusada não apaga a camada de baixo — a pilha continua descendo até o padrão do produto, e o motivo fica escrito em fallback_reason",
"app/icon.tsx precisa de force-dynamic: sem isso o build congela o ícone dentro da imagem pré-buildada, e o defeito é invisível em dev, em teste e na Vercel — só aparece na VPS do revendedor",
"/icon precisa estar em PUBLIC_PATHS: o matcher do proxy só exclui caminho COM extensão, então GET /icon respondia 307 para /login (medido em produção)",
"as duas ações de auditoria da marca estão nas DUAS listas de código (union + painel), e desde 2026-08-14 há gate cobrando que a divergência não cresça"
]
},
{
"dot": "sky",
"title": "Fronteiras deliberadas — o que continua NÃO sendo configurável, com a razão medida",
"items": [
"DOMÍNIO por organização: fora. Zero coluna no schema, o proxy é NOOP nesse eixo, e no Edge não há banco para consultar antes de decidir o host",
"FONTE por tenant: fora. next/font resolve em BUILD time e a imagem é pré-buildada — um seletor de fonte seria controle decorativo",
"X-Deskcomm-Signature e sb-deskcomm-auth NÃO são renomeados: o primeiro é contrato de fio com receptores de terceiros, o segundo é a sessão de quem está logado. 'Completar o whitelabel' ali derruba integração de cliente em silêncio",
"lib/email/templates/ai-budget-alarm.tsx segue com a nossa marca: não tem rota de cron, não tem linha no docker/scheduler/entrypoint.sh e, desde a limpeza do teto de orçamento (0159), não tem chamador nenhum — workers/ai-budget-checker.cron.ts, o único, foi apagado. Dívida D1, com a condição de saída escrita",
"quem envia e-mail é o APP (via event-log-drain, batido pelo scheduler), NUNCA o contêiner worker — que roda o agent-engine e não importa e-mail nenhum. Quem for provar reiniciando 'o worker' reinicia o processo errado"
]
}
]
}