Skip to content

feat(marca): marca própria — cor, nome e favicon por instalação e por organização - #248

Merged
melgarafael merged 21 commits into
mainfrom
feat/marca-propria
Aug 14, 2026
Merged

feat(marca): marca própria — cor, nome e favicon por instalação e por organização#248
melgarafael merged 21 commits into
mainfrom
feat/marca-propria

Conversation

@melgarafael

Copy link
Copy Markdown
Owner

Épico de whitelabel: o revendedor que instala numa VPS troca nome, cor e favicon pela tela, e o cliente final dele tem marca própria dentro de /app — sem que a cor vaze para a tela de login.

Autorização de merge: Rafael autorizou explicitamente este épico a seguir até o merge de forma autônoma (2026-08-13, "pode seguir de forma 100% autônoma até a fase 6… mergeado na main"). O push usou DESKCOMM_GOV_PHASE_MERGE=1 sob essa autorização.


A premissa do pedido estava parcialmente errada, e isso mudou o plano

lib/branding.ts já existia (nome + logo por .env, em runtime) e docs/white-label.md já prometia whitelabel em três idiomas, listando os buracos: cores não configuráveis, marca por instalação e não por organização, textos e e-mails seguem o padrão. O trabalho não foi criar do zero — foi fechar exatamente esses três buracos e tirar a marca do .env.

O que este PR entrega

Cor derivada de 1 hex rampa de 11 stops em OKLCH, ancorada por papel (a semente é o stop 600), com piso de contraste por papel × superfície e separação de matiz medida sob dicromacia simulada
Marca da instalação platform_branding + tela /admin/marca
Marca da organização settings.branding + tela /app/settings/marca, gravada por uma instrução SQL
Favicon /favicon.ico devolvia 404 — para nós e para o revendedor
Saídas sem DOM e-mail, autenticador (issuer), remetente, suporte
E-mails de auth subidos pela Management API; nenhum script os subia antes

Três defeitos pré-existentes que o épico achou e consertou

Sant'Ana parava o Docker Compose inteiro. O envq do instalador gravava o .env com aspas simples e escape '\''; o Compose recusa ler o arquivo inteiroconfig, ps e pull todos rc=1. Nomes brasileiros com apóstrofo são comuns, e APP_NAME é a última pergunta da entrevista, sem validador. O comprador terminava com Supabase provisionado, schema aplicado, admin criado, e todo comando do kit falhando. Controles: sem apóstrofo rc=0; cifrão e cerquilha rc=0.

O gate de marca estava verde enquanto a marca vazava. /Deskcomm/ era case-sensitive, e a varredura só cobria app/ e components/, só .tsx. Escapava pela extensão o friendlyName do autenticador, que fica no celular do usuário para sempre.

O produto nasce com colisão de cor: --color-success do tema escuro é a mesma string de --color-accent-400.

O que a tela provou, e que teste nenhum pegaria

Um bug de contraste só apareceu no browser: com marca escura, o anel de foco caía a 2,86:1 contra o fundo (piso WCAG 1.4.11 = 3,0) — e a causa não era o foco, era o bloco emitido contradizer o globals.css: 68 pares abaixo do piso em 9 das 16 sementes. O conserto é uma linha.

E dois defeitos de prosa, que gate nenhum pega: a tela dizia "usamos um tom mais escuro da sua cor" quando o botão pousa exatamente na cor da pessoa; e dizia "sua cor ficou parecida com sucesso" sobre uma cor que não pinta nada.

A decisão de produto mais importante

O PDF de LGPD não leva marca nenhuma. Ele imprime o controlador (organizations.legal_name) e o DPO resolvido. O relatório do Art. 18 II responde a um direito legal do titular; nomear ali o revendedor — que é operador, não controlador — inverteria os papéis num documento jurídico. Trocar o nome pela marca não seria "completar o whitelabel": seria piorar.

Isso saiu de graça porque organizations já separa legal_name de display_name — a identidade jurídica sempre esteve modelada, e ninguém a usava. Zero campos novos; DIRC responde "Integrar".

Escrita e isolamento

A marca da organização é gravada por uma instrução SQL, não por read-modify-write. A perda foi medida: organizations.settings tem três donos com gates diferentes, e um save de cor podia reverter visibility_mode de own para all — campo lido dentro da RLS, em fn_can_view_conversation. Um write de identidade visual virava mudança de política de leitura de dado de cliente.

A função devolve row_count porque a única policy de escrita é orgs_write_platform_admin (FOR ALL): pelo client de sessão, o UPDATE de um admin de tenant casa 0 linhas e o PostgREST responde 204 — a tela diz "salvo" e nada grava (issue #144).

Revisão adversarial antes do PR

Quatro lentes tentaram refutar os commits. Acharam um bloqueador (org.branding_updated faltando na lista do painel de audit — regra que o próprio épico escrevera duas commits antes; consertado) e mataram seis dos onze achados delas mesmas, com evidência.

Não conseguiram refutar: escalação de papel (matriz de 16 chamadas em Postgres real → 42501 em todas as negativas, cross-tenant com settings da outra org byte a byte intacto); vazamento de EXECUTE pelos dois caminhos de instalação; 12 valores hostis contra a allowlist do CSS; rollback (código antigo não lê a tabela nova, e o Zod estripa chave desconhecida em vez de lançar); e re-apply do baseline sobre clone com dados semeados.

Gates, medidos localmente

typecheck 0 · lint 0 · lint:channels 0 · test:unit 390 files / 4436 tests · test:shell 265 ✓ / 0 ✗ · test:db 102 / 753 com install ok e update ok · build 0.

⚠️ Dois pontos que precisam da sua atenção

1. RESEND_FROM_EMAIL vazio agora significa não-configurado (antes caía em noreply@deskcomm.app). Se a produção tiver RESEND_API_KEY sem essa chave, os convites deixam de sair por e-mail e passam a mostrar o accept_url na tela. Degradação visível e reversível por uma variável — mas confira o env. O fallback antigo era pior num clone: o domínio não está verificado na conta Resend do revendedor, então todo envio falhava lá com mensagem opaca.

2. O template de auth do projeto Supabase de produção está quebrado{{ .RedirectTo }}?token_hash=…, com ? duplicando o separador porque .RedirectTo já chega com ?type=. /auth/confirm cai em link_invalido, sintoma que se lê como "link expirado". Não foi tocado (é produção, precisa do seu aval). O marca-emails.sh deste PR sobe a versão correta quando o token for exportado.

O que ficou de fora, declarado

Upload de logo, fonte e tema por tenant, domínio por organização (zero coluna no schema, proxy NOOP, sem banco no Edge), tradução EN/ES do white-label.md, e a cura das 120 divergências das listas de audit — o conserto certo lá é derivar a lista, não copiá-la melhor. Tudo com razão medida no HANDOFF-marca-propria.md.

🤖 Generated with Claude Code

https://claude.ai/code/session_01TCiffR3ugjQbxsfceQE2GB

melgarafael and others added 21 commits August 13, 2026 09:46
`envq` (install.sh) gravava todo valor entre aspas simples com escape `'\''`.
Para `APP_NAME = Sant'Ana Odontologia` isso produz

    APP_NAME='Sant'\''Ana Odontologia'

e o Docker Compose recusa ler o arquivo INTEIRO:

    failed to read .env: line 1: unexpected character "\" in variable name

Medido: `config`, `ps` e `pull` todos rc=1; o mesmo arquivo sem o apostrofo,
rc=0. Cifrao e cerquilha passam — o defeito e especifico do apostrofo.

Onde isso morde o comprador: APP_NAME e a ULTIMA pergunta da entrevista
(install.sh:996), sem validador. O baseline e aplicado e o dono e criado por
caminhos que nao usam compose, entao passam; so entao `dc pull`/`dc up -d`
(:1473) morrem. Ele fica com Supabase provisionado, schema aplicado, admin
criado, e um erro sobre "variable name" que nao aponta para nada que digitou.
E como `dc()` nao passa --env-file, TODO comando do kit passa a falhar depois:
healthcheck.sh, update.sh, backup.sh, e o agente de 5 em 5 minutos.

"Sant'Ana", "D'Avila", "Espaco D'Or" sao nomes de empresa brasileiros comuns.

SAO TRES CONSUMIDORES DO .env, e nenhum encoding obvio serve aos tres:

  1. load_env (_common.sh) — parsing manual com printf -v, nao `source`
  2. docker compose, via `env_file: .env`
  3. `source .env && curl ...` — receita real do README.md:143

Medido, 6 valores x 3 consumidores:

  aspas simples + '\''        -> load_env 6/6  source 6/6  container 4/6
  aspas duplas, load_env atual-> load_env 3/6  source 6/6  container 6/6
  aspas duplas + unescape     -> load_env 6/6  source 6/6  container 6/6

Entao: envq passa a aspas duplas escapando \ " $ e crase, e o ramo de aspas
duplas do load_env desfaz esse escape (com sentinela para nao reprocessar \\).
O ramo de aspas simples FICA — clone que atualiza nao reescreve o .env, e
`update.sh` so mexe em APP_IMAGE e APP_PULL_POLICY.

Residual conhecido e documentado: o parser do Compose nao desfaz \` dentro de
aspas duplas, entao um valor com crase chega feio ao conteiner. Mantido assim
de proposito — a alternativa e o `source .env` do README EXECUTAR o que esta
entre crases.

POR QUE NENHUM GATE PEGAVA: o round-trip do .env em test-validators.sh:303
exercitava so o load_env, e o fixture nao tinha apostrofo interno. Nenhum teste
do kit rodava `docker compose` contra um .env gerado. Ponto cego em dois eixos
ao mesmo tempo — o consumidor e o caractere.

O teste novo cobre os TRES consumidores contra uma lista unica de 8 valores,
mais um controle positivo que prova que o formato ANTIGO e recusado pelo
Compose, mais retrocompatibilidade com .env escrito no formato velho.
`docker compose config` e 100% client-side, entao roda no CI sem daemon; se o
docker faltar, o caso pula em voz alta em vez de passar calado.

Sabotagem, previsto vs medido: 9/9, 9/9, 1/1, 24/24 (guarda de vacuidade).
pnpm test:shell: 201 -> 232 casos, EXIT=0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TCiffR3ugjQbxsfceQE2GB
Fecha a reancoragem do epico de whitelabel. Tres coisas que mudam o plano:

PACKAGING: nao vira monorepo nem pacote npm. A regra de extracao e >=2
consumidores independentes e foi medido UM — os 8 importadores de lib/branding
estao todos em app/ e components/, e o worker nao renderiza marca. O custo do
contrario e concreto: --frozen-lockfile quebra em workspace nos dois
Dockerfiles, output:"standalone" muda o caminho do server.js, e 53 testes de
arquitetura ancorados em caminho top-level tem helper que devolve [] em
diretorio inexistente — metade da suite viraria verde vacuoso.

O packaging que falta e outro: um emissor de build-time multi-formato. A marca
atravessa 4 fronteiras de processo e 3 nao consomem TypeScript (o StyleSheet do
@react-pdf, o HTML inline dos e-mails, os templates Go do Supabase Auth).

IDENTIDADES: quatro papeis, tres identidades de marca, ZERO campos novos.
`organizations` ja separa legal_name (NOT NULL) de display_name — a identidade
juridica ja estava modelada e ninguem a usava. DIRC responde "I - Integrar":
o controlador de dados E a organizacao, e ela ja declara a razao social. Criar
controlador_nome seria duplicacao sem source of truth.

Consequencia: o rodape do PDF de LGPD passa a imprimir legal_name + o DPO
resolvido, nunca a marca. Trocar o nome pela marca do revendedor ali PIORARIA
o defeito — nomearia o revendedor como controlador num documento que responde
a um direito legal, quando o controlador e o cliente dele.

CORRECAO DE UM ERRO MEU: eu afirmei que app/design e rota publica sem auth.
E falso. A sonda procurou em middleware.ts, que nao existe — o Next 16
renomeou para proxy.ts. /design nao esta em PUBLIC_PATHS e proxy.ts:32
redireciona quem nao bate na allowlist. A rota e autenticada; o problema real
e vazamento cross-tenant, e o conserto e notFound() fora de dev.

Achados novos confirmados na medicao:
- install.sh:918 usa ${APP_NAME:-DeskcommCRM} e APP_NAME so e coletado em
  :1024 — 106 linhas depois. Todo projeto Supabase de revendedor nasce
  chamado "DeskcommCRM".
- supabase/templates/{confirmation,recovery}.html tem a marca literal e
  NENHUM script os sobe: o primeiro e-mail que o cliente do revendedor recebe
  diz o nome do nosso produto.
- registry.ts:453 tem porta de 1a classe "Billing" levando a uma tela que
  mostra suporte@deskcomm.app ao cliente do revendedor — desintermediacao na
  tela de dinheiro, dentro de um CRM que ele acha que e de outra marca.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TCiffR3ugjQbxsfceQE2GB
…corrigidas

Nucleo do whitelabel: o cliente escolhe UM hex e o sistema deriva a rampa de 11
stops e os tokens de papel, sem quebrar legibilidade nem o design system.
Funcoes puras, zero dependencia nova (conversoes sRGB/OKLab/OKLCh a mao).

ANCORAGEM POR PAPEL, NAO POR LIGHTNESS. A semente E o stop 600, por definicao;
a escada de L e reescalada em dois segmentos para atravessar a semente no
indice 6. Com ancoragem por lightness, #0f172a (navy — a identidade corporativa
mais comum que existe) cai num indice alto e o accent claro vira #4360a7,
azul-periwinkle: o cliente cola o hex da marca e recebe outra cor.

Catraca: rampaDeSemente('#506d48') reproduz os 11 stops Sage do globals.css com
Dmax = 2/255 por canal (stops 500/600/700/900/950 exatos). Os esperados sao
LIDOS do globals.css, nao copiados — o teste acompanha o design system.

O PISO E POR PAPEL x SUPERFICIE, nao pela semente. Medido na Sage: accent-600
contra o bg da 5,51, mas o anel de foco usa accent-500 e da 3,79 contra o bg e
3,60 contra surface-elevated. Uma semente pousando num piso de 3,0 poe o anel
em ~2,07 COM O GATE VERDE. A regua sai extraida do globals.css (6 papeis por
tema, 44 pares) — lista a mao so pega quem foi inscrito.

SEPARACAO DE MATIZ POR DELTA-E SOB DICROMACIA, NAO POR ANGULO. A regua de
angulo ordena invertido, medido: oliva x warning tem 44,7 graus e DE 0,0231
(pior), verde-agua x success tem 27,2 graus e DE 0,1262 (melhor). Simulacao
Machado 2009 para deuteranopia e protanopia, comparacao em OKLab sobre a cor
simulada, piso 0,05.

QUEM SE MOVE SAO AS NOSSAS SEMANTICAS, NUNCA O ACCENT. O accent e a unica cor
que nao nos pertence — e a marca real de uma empresa. Sem solucao, o algoritmo
nao falha nem distorce: sinaliza que a redundancia nao-cromatica precisa ser
ligada e registra o motivo (laco de retorno, invariante 7).

Cai de graca: o produto JA nasce colidido — globals.css define --color-success
como #82a077 no bloco escuro, a mesma string de --color-accent-400. A Sage pura
dispara a reconciliacao, e isso e controle positivo de que o mecanismo funciona.

DOIS NUMEROS DE CROMA, NAO UM (medido, e a diferenca decide o epico):
#0f172a mede C=0,039824 e #1a1f36 mede C=0,044430 — as duas navies caem em
lados opostos de 0,04 por 0,0046. Com um gatilho unico em 0,04, a navy que
motiva o epico perderia a marca e receberia Sage. Entao LIMIAR_ACROMATICO=0,01
(cinza mede 0,000000 exato, 40x de margem) e o gatilho; PISO_DE_CROMA=0,04
continua como assercao sobre o accent que resta.

37 testes novos: calibracao, ancoragem, fixture adversarial de 16 sementes com
Sage como controle positivo, guarda de vacuidade, ramo degradado alcancado por
rampas sinteticas, marca acromatica.

Sabotagem, previsto vs medido: 4->2, 7->6, 6->6. O deficit da primeira foi
investigado e o modelo mental era do autor, nao do teste: a caminhada e a
reconciliacao sao governadas por LIGHTNESS, e CURVA_C so mexe em croma.
E a sabotagem da ancoragem NAO e pega pela calibracao Sage, porque a semente
Sage tem L exatamente igual a ESCADA_L[6] — por isso os testes de ancoragem
existem separados.

DIVIDA DECLARADA: nada consome estes modulos ainda. O invariante 1 do Sistema
Vivo (nada e ilha) NAO esta satisfeito — o emissor de CSS, a tela e o log
entram nos proximos avancos. Divida declarada nao e defeito; divida presumida e.

typecheck 0, lint 0, test:unit 4182 passed (+37). As 5 falhas de
lib/ai/dispatcher/rate-limit.test.ts sao PRE-EXISTENTES: com a arvore limpa no
HEAD, sem nenhum arquivo novo no disco, o arquivo falha 5/5 igual.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TCiffR3ugjQbxsfceQE2GB
A Fase 1a deixou rampa.ts e contraste.ts sem consumidor — invariante 1 do
Sistema Vivo (nada e ilha) explicitamente nao satisfeito. Este commit fecha:
.env -> resolvedor -> <style id="marca-instalacao"> no <head> -> todo
componente que le --color-accent-*.

Nesta fase a fonte e o .env (a tabela vem na seguinte). E deliberado: entrega
valor antes da migration, e e o caminho que ja existe — lib/branding.ts ja
resolve nome e logo de process.env.

TRES EXCECOES DELIBERADAS A CONVENCAO, com o porque no arquivo:
- .catchall(z.unknown()), nao .strict(). O rollback do kit poe codigo velho
  sobre schema novo POR CONSTRUCAO: o update.sh aplica o baseline antes de
  puxar a imagem, e o agent.sh reverte so APP_IMAGE. Com .strict(), chave
  desconhecida LANCA — e o resolvedor roda no app/layout.tsx, entao um throw
  ali e 500 em todas as telas do produto.
- O envelope {format, algo} nasce agora, sem tela de import/export. E a unica
  superficie que atravessa instalacoes e versoes; retrofit e impossivel, porque
  sem ele toda linha existente vira "algo desconhecido".
- Guarda ENTRADA, nunca saida: {semente_hex, papel_da_semente}, nunca os 11
  stops. Assim melhoria no gerador alcanca quem ja configurou.

APP_ACCENT_HEX e optional().default(""), nunca required(). Precedente medido:
as 2 unicas vars adicionadas desde a v1.1.0 sao ambas opcionais, e a razao e o
modo de falha — required() estoura na PRIMEIRA REQUISICAO, nao no boot, e o
healthcheck do conteiner e probe TCP puro: o Docker mostra healthy com 100%
das requisicoes em 500.

lib/branding/regua-do-produto.ts existe por causa da ancora HostGator: o
Dockerfile copia so .next/standalone, .next/static e public/ (linhas 60-62), e
app/globals.css NAO existe no conteiner do self-hoster. readFileSync no layout
daria ENOENT — 500 em todas as telas, e VERDE em dev, em teste e na Vercel. A
regua e congelada em modulo e um teste a compara com o CSS a cada run.

BUG ACHADO NA TELA, QUE NENHUM TESTE UNITARIO PEGOU:
com APP_ACCENT_HEX="#0f172a", o anel de foco no tema escuro media 2,86 contra
--color-bg e 2,37 contra --color-surface-elevated — abaixo do piso de 3,0 de
WCAG 1.4.11, que o produto CUMPRIA antes do epico.

A causa nao era o foco: era uma classe. O bloco emitido CONTRADIZIA o
globals.css — emitia os 11 --color-accent-NNN crus ao lado dos tokens de papel
ja deslocados, e o globals.css declara --color-accent: var(--color-accent-400).
Quem lia a rampa direto ficava com a cor de ANTES da caminhada. Medido nas 16
sementes: 68 pares abaixo do piso em 9 sementes, em 3 papeis do globals.css
(--ring, :focus-visible, ::selection) mais 7 usos diretos de *-accent-{400,500}
em componentes.

O conserto e uma linha (css.ts:148): a emissao passa a entregar a rampa que o
modelo mediu. O globals.css fica INTACTO — nao ha nada novo para aprender — e
com deslocamento zero (Sage e toda marca que ja cabe) a saida e identica byte a
byte. Preco declarado no comentario: com deslocamento grande as pontas
colapsam, o mesmo preco que --color-accent-soft ja pagava sozinho.

O teste que faltava (branding-pares-pintados, 23 casos) nao chama medirPares de
proposito: le o TEXTO que cssDaMarca produz, monta a cascata em miniatura
(0,2,0 vencendo 0,1,0) e mede o pixel resultante. Instrumento independente —
defeito dentro do resolvedor aparece aqui em vez de ser compartilhado. Os pares
saem de extrairRegua(app/globals.css), o arquivo, nao a copia congelada.
Sabotagem: previsto 12 de 23 vermelhos, medido 12, e a mensagem reproduziu o
browser.

globals.css ganha [data-theme="light"] (medido: 0 ocorrencias antes; :root casa
so o <html>, entao subarvore clara nao recebia token nenhum) e o rider de
forced-colors — 0 ocorrencias no repo e 14 arquivos matam outline dependendo de
ring, entao o indicador de foco SUMIA por completo no Windows High Contrast,
que e WCAG 2.4.7 nivel A.

PROVA EM TELA (dev 3111, getComputedStyle em /login):
                        controle(Sage)   navy #0f172a
  blocos no <head>            0               1
  --color-accent claro     #506d48        #0f172a   <- a semente sobrevive
  foco escuro x bg           6,31           5,28    (era 2,86 antes do conserto)
  foco escuro x surfElev     5,23           4,39    (era 2,37)
  foco claro x bg            3,79          10,77
  alternancia de tema      sobrevive e reversivel nos dois
  colisao accent/success     COLIDE          ok     <- reconciliacao funciona

Sem APP_ACCENT_HEX nao ha bloco nenhum: a mudanca nao vaza para quem nao pediu,
e os numeros da Sage sao identicos aos de antes do epico.

typecheck 0, lint 0, test:unit 378 files / 4274 passed, EXIT=0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TCiffR3ugjQbxsfceQE2GB
tests/unit/branding.test.ts:90 usava /Deskcomm/ — CASE-SENSITIVE. Contraste
medido no mesmo arquivo: /Deskcomm/ devolve false onde /deskcomm/i devolve true.
Passavam pelo gate antigo, todos visiveis ao usuario final:

  app/account-suspended/page.tsx:17,20     support@deskcomm.com.br
  app/app/settings/billing/page.tsx:26,27  suporte@deskcomm.app
  components/auth/RecoveryCodesPanel.tsx   deskcommcrm-recovery-codes.txt

E a varredura nao alcancava onde a marca SAI: cobria so app/ e components/, so
.tsx. Fora do alcance, com vazamento visivel ao usuario final:

  app/actions/auth/enrollMfa.ts:37   vai para o app autenticador e fica no
                                     celular do usuario PARA SEMPRE — esta
                                     dentro de app/ e escapava pela EXTENSAO
  lib/lgpd/pdf-renderer.tsx:277      rodape do relatorio entregue ao titular
  lib/email/templates/invite.ts:32   o primeiro artefato que um usuario recebe
  lib/email/resend.ts:38             o remetente

Agora: /deskcomm/i sobre app/ components/ lib/ workers/, em .ts e .tsx.
1232 arquivos varridos, 24 com ocorrencia, 33 ocorrencias.

O GATE NAO NASCE VERMELHO: a divida existente entra congelada como MULTISET DE
TOKENS POR ARQUIVO, nao como booleano. E isso que faz marca NOVA dentro de um
arquivo JA listado reprovar — provado por sabotagem.

A DISTINCAO QUE PRECISAVA ESTAR ESCRITA: marca nao e protocolo. Categorias:

  PROTOCOLO  6 arq /  9 ocor  contrato de fio com terceiros. X-Deskcomm-Event e
                              X-Deskcomm-Signature vao para o receptor de
                              webhook DO CLIENTE; renomear quebra integracao em
                              producao, em silencio
  INFRA      6 arq /  6 ocor  sb-deskcomm-auth desloga toda a base no primeiro
                              update; deskcomm-theme perde a preferencia de tema
  DIVIDA     9 arq / 13 ocor  vazamento real, Fase 4 (precisa da marca do banco)
  DEV        2 arq /  4 ocor  fixture de teste
  PADRAO     1 arq /  1 ocor  a propria definicao de DEFAULT_APP_NAME

Sem isso escrito, alguem "completa o whitelabel" renomeando um header e derruba
a integracao de um cliente sem nenhum teste ficar vermelho.

Duas medicoes que divergem, com rotulo: 47 linhas casam /deskcomm/i contando
comentario; 32 linhas / 33 ocorrencias sem. O gate ignora linha que ABRE com
comentario — e tem guarda propria provando que `const u = "https://deskcomm.app/x"`
NAO e tratado como comentario, porque procurar // em qualquer posicao esconderia
exatamente um vazamento real.

Consertado 1 dos 3 de case-sensitive: o nome do arquivo de recovery codes passa
a derivar da marca. Os outros 2 sao ENDERECOS DE E-MAIL — branding() nao resolve
isso, endereco nao se deriva de nome, e nao existe SUPPORT_EMAIL em lib/env.ts.
Trocar so o nome PIORARIA: imprimiria a marca do revendedor ao lado do NOSSO
endereco. Ficam como DIVIDA da Fase 4.

Sabotagem, previsto vs medido: 6/6, uma reprovacao cada.
  A  marca nova em arquivo fora da lista            1 failed
  B  marca MINUSCULA (o furo original)              1 failed
  C  marca nova em arquivo JA congelado             1 failed
  D  entrada removida da allowlist                  1 failed
  E  desfeito o proprio conserto do RecoveryCodes   1 failed
  F  workers/ — a unica raiz sem entrada nenhuma    1 failed

typecheck 0, lint 0, test:unit 378 files / 4284 passed (+10), EXIT=0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TCiffR3ugjQbxsfceQE2GB
Trocar a marca exigia SSH e reinicio da stack. Agora vive em
public.platform_branding, e o .env vira SEMENTE + REDE DE SEGURANCA.

O TRIO, indivisivel (doutrina de migrations do CLAUDE.md):
  migrations/20260813090000_0155_marca_da_instalacao_no_banco.sql
  baseline.sql — +95 linhas, antes do notify pgrst
  MANIFEST.md

Numeracao com as DUAS medicoes que a justificam, porque as reguas sao
independentes (neste repo a 0119 tem timestamp MAIOR que a 0142):
  NNNN=0155      maior existente em TODAS as branches locais e 0154
  ts=2026081309  maior timestamp existente e 20260811210000

POR QUE O .env CONTINUA SENDO ESCRITO. O agent.sh do kit, em falha de update,
reverte SO a imagem (APP_IMAGE) — nao o schema, nao o git checkout. O clone
fica com codigo antigo sobre banco novo. Se a marca so existisse no banco, a
imagem antiga nao a leria e a marca SUMIRIA. Com o .env intacto, degrada para o
valor da instalacao.

RLS LIGADA COM ZERO POLICIES — e a forma explicita de dizer que o PostgREST
nunca serve isto. Mais o revoke, que e o achado desta fase:

  revoke all on public.platform_branding from anon, authenticated;
  grant select, insert, update on public.platform_branding to service_role;

baseline.sql tem ALTER DEFAULT PRIVILEGES ... GRANT ALL ON TABLES TO anon, e
ele vale para TODA tabela criada depois dele — isto e, para todo apendice novo.
E o analogo, PARA TABELA, da regra de security definer do item 9 do CLAUDE.md,
e nao estava escrito em lugar nenhum. Medido na sabotagem: sem o revoke, anon
fica com DELETE, INSERT, REFERENCES, SELECT, TRIGGER, TRUNCATE e UPDATE — e o
caso comportamental reprova com o SELECT PASSANDO SEM ERRO, porque a RLS
devolve zero linha calada. Por isso catalogo e comportamento sao dois casos.

CACHE: memo de modulo com TTL de 30s, invalidado pela propria server action.
NAO unstable_cache. Medido: unstable_cache tem ZERO ocorrencias no repo
(controle positivo: revalidatePath em 9 arquivos) — seria primitiva nova num
projeto que nunca a usou. E na VPS ha UM processo de app com os 16 crons
dentro dele, entao o memo e quase perfeito ali. O worker e um segundo processo
e nunca ve a invalidacao; quando ele renderizar marca (e-mail, PDF), le do
banco no envio. Escrito no codigo.

NAO EMITE event_log: register-handlers nao cobre este tipo e o drain deixa
evento sem handler intocado — a linha nasceria pending para sempre em todo
clone. Anti-pattern no 3 do CLAUDE.md. A razao esta escrita no apendice.

O CHECK de accent_hex e REGEX, nao conjunto: o extrator do invariante de
vocabulario so reconhece `= ANY (ARRAY[...])`, entao esta coluna NAO entra na
lista PARES. Dito em voz alta, porque a doutrina "coluna com CHECK -> uma linha
ali" convidaria a um par que faria o extrator estourar.

UM DEFEITO MEU, PEGO POR UM GATE QUE JA EXISTIA: audit-resource-id-e-uuid
reprovou porque eu tinha resourceId "1" e api_audit_log.resource_id e uuid. O
INSERT estouraria com 22P02 e, como audit e fire-and-forget, a trilha perderia
a linha SEM SINTOMA. Corrigido para null.

PROVA EM POSTGRES DESCARTAVEL (pg17), os dois modos que o kit aplica:
  install (banco novo, ON_ERROR_STOP=1)   -> ok
  update  (re-aplicar, sem a flag)        -> ok, idempotente
Linhas citando platform_branding entre os 301 erros tolerados do update: ZERO.
A sabotagem 2 produz exatamente uma (301->302), que e o que prende o delta.

Sabotagem, previsto vs medido: 5/5, 1/1, 2/2, 1/1, 5/5.

typecheck 0 · lint 0 · test:unit 379 files / 4310 passed · test:db 101 files /
732 passed com install e update verdes · next build EXIT=0.

O build exercitou o caminho degradado de verdade: sem banco alcancavel, o
resolvedor registrou "marca da instalacao: nao deu para ler do banco; vale o
.env" em vez de lancar. O contrato "nunca lanca" foi provado, nao declarado.

ACHADO REGISTRADO, NAO CONSERTADO: as duas listas de audit ja divergem em 120
codigos (208 no union contra 88 no painel; o inverso e 0). Nao e divida desta
fase — so acrescentei as minhas nas duas e registrei.

ACHADO SEM GATE: apendice nao-idempotente nao e pego por ninguem. A sabotagem 2
deixou test:db verde e EXIT=0, porque o modo update tolera erro por contrato.
Quem detecta e o diff de stderr, que nenhum job roda.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TCiffR3ugjQbxsfceQE2GB
A tabela e a server action existiam sem chamador. Esta e a superficie: o dono
da instalacao troca o nome e a cor pela tela, em vez de SSH + reinicio da stack.

Gate is_platform_admin com notFound(), nao redirect('/403') — a existencia da
tela nao e assunto de quem nao e da plataforma (precedente: settings/atualizacao).
A porta e o AdminSidebar; NAO entra no registry.ts, que so cobre app/app/**
(confirmado lendo tests/unit/navegacao-completude.test.ts).

O BLOCO QUE ENSINA. O controle primario da cor e a tira dos 11 tons, com tres
marcacoes: "Sua cor", "Botoes no modo claro", "Botoes no modo escuro". Sem ela o
usuario cola um hex e nao entende por que o botao ficou de outro tom. A marcacao
usa frente calculada por luminancia, entao o contorno e visivel em qualquer marca.

LINGUAGEM E METADE DA ENTREGA. O leitor e dono de uma VPS, nao designer. Zero
"OKLCH", "rampa", "stop 600", "WCAG". A tela diz "o texto em cima dos botoes",
"passa em AA", "no modo claro os botoes usam um tom mais escuro que a sua cor —
e o que mantem o texto em cima deles legivel". Um teste guarda isso: a uniao dos
codigos do motor e Record<CodigoDaMarca, Traducao>, entao codigo novo em
lib/branding/ REPROVA o typecheck aqui em vez de vazar cru para a tela.

BLOCO DE ESTADO (invariante 6 — configuracao tem superficie): origem de cada
campo ("padrao do sistema" / "definido nesta tela" / "veio do arquivo de
instalacao"), contraste medido com veredito, o que o sistema ajustou, e o
fallback ativo com data e motivo. Fallback que ninguem ve e indistinguivel de a
feature nunca ter sido instalada.

E a tela DIZ O QUE AINDA NAO FAZ, embaixo do campo: hoje o nome alcanca o titulo
da aba; o menu e a tela de entrada ainda leem o .env. Sem isso o operador conclui
que quebrou.

DOIS DEFEITOS DE PROSA QUE SO A SONDA PEGOU (gates nao pegam texto errado):
1. A frase de deslocamento citava o tom padrao DAQUELE MODO. Medido com #f5c518:
   o modo escuro anda +2 e a tela dizia "um tom mais escuro da sua cor" quando o
   botao pousa EXATAMENTE na cor da pessoa. Agora a distancia e ate a cor dela.
2. Com marca neutra (#808080), a derivacao emite semantica_deslocada porque o
   verde DO PRODUTO colide com o verde de sucesso DO PRODUTO — e a tela dizia
   "sua cor ficou parecida com sucesso" sobre uma cor que nao pinta nada.
Ambos com teste, ambos sabotados para confirmar que reprovam (2/2 e 1/1).

PROVA EM TELA (Playwright, build de producao, login com MFA real):
  estado          swatches   --color-accent   titulo da aba
  inicial              1        #506d48        Marca da instalacao
  digitando #7a5cd6   15        #506d48
  hex invalido        --        Salvar DESABILITADO
  salvo               15        #604aa6        <- a cor muda no produto
  recarregado         15        #604aa6        <- persistiu

E o #604aa6 e exatamente o tom que a tela prometeu como "Botoes no modo claro":
a tela nao mentiu. Banco depois do salvar: accent_hex=#7a5cd6, seeded_from_env=f.
Zero jargao tecnico na tela, zero rolagem lateral. Evidencia em evidence/.

O primeiro run REPROVOU com "Could not find the table 'public.platform_branding'
in the schema cache" — o baseline tinha sido provado num Postgres descartavel,
mas o Supabase local nunca recebera a migration. Nao era bug do codigo; era o
ambiente. Aplicada a 0155 no banco local, o ciclo fechou.

Medido de brinde, nao consertado: `border-error-fg/30` nao gera CSS nenhum no
Tailwind 3.4 (modificador de opacidade sobre cor em var()). Duas telas do repo
usam a classe e estao sem borda — contacts/[id]/_client.tsx:64 e
AnonymizeDialog.tsx:102.

typecheck 0 · lint 0 · test:unit 380 files / 4326 passed · build EXIT=0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TCiffR3ugjQbxsfceQE2GB
…a divida declarada

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TCiffR3ugjQbxsfceQE2GB
Merge da main (19 commits) trouxe os testes de packaging — que nasceram contra o
encoding de aspas SIMPLES do .env. A Fase 0 deste epico trocou o encoding para
aspas duplas, porque aspas simples com escape `'\''` fazem o Docker Compose
recusar o arquivo INTEIRO quando o nome da empresa tem apostrofo (Sant'Ana).

Merge limpo, ZERO conflito de texto, conflito de COMPORTAMENTO. As duas frentes
convergiram sem se ver: uma mudou como o .env e escrito, a outra escreveu
asserções que liam o formato antigo. Sintoma: `APP_IMAGE não foi pinado na
versão resolvida (1.10.0)` com a aspa sobrando no valor impresso — `1.3.0"`.

O conserto NAO e ajustar o regex para aceitar os dois formatos: e parar de casar
formato. As asserções passam a comparar VALOR, lido pelo `load_env` do proprio
kit — o unico leitor que conhece os tres encodings (cru, aspas simples do
legado, aspas duplas de hoje). O que esses testes querem provar e "a imagem esta
pinada na versao resolvida", nunca "o .env usa aspas X".

Fechada a CLASSE, nao as instancias: `grep -qE "^CHAVE='?"` no arquivo devolve
vazio agora. Eram 4 call sites — extracao de APP_IMAGE, o laco das tres imagens,
o laco de pinagem em 1.10.0, e o das tres pull policies.

test:shell 253 ✓ / 0 ✗, EXIT=0 (era 2 falhas depois do merge).
typecheck 0 · lint 0 · test:unit 381 files / 4338 passed.

De brinde, o merge tambem trouxe o conserto de dois achados que este epico havia
levantado e reportado: o `worker` deixou de ser build-only (ganhou
ghcr.io/melgarafael/deskcomm-worker:stable, e o scheduler tambem), e o `srh`
saiu de `:latest` para digest pinado. O epico nao precisa mais tratar disso.

E o HANDOFF passa a CITAR cada evidencia pelo nome, com o que ela prova —
`tests/unit/evidencia-citada.test.ts` reprova imagem versionada sem documento
que a justifique, e eu havia commitado 5 PNGs citando so um glob. O gate estava
certo: imagem citada e lastro de afirmacao, imagem nao citada e artefato de
build, e num projeto aberto todo clone paga o peso para sempre.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TCiffR3ugjQbxsfceQE2GB
organizations.settings tem TRES donos com gates diferentes — a aba Organizacao
(admin), o PATCH de atendimento (manager) e a regua de atrito (manager) — e os
tres leem o jsonb inteiro, espalham em memoria e regravam o objeto inteiro, em
round-trips separados. Sem transacao, sem lock, sem versao.

A perda foi MEDIDA, nao deduzida: o manager restringe visibility_mode de 'all'
para 'own'; o admin salva a aba de identidade com o snapshot velho; o valor
volta para 'all'. Nenhum dos dois recebe erro. E visibility_mode e lido DIRETO
pela RLS, dentro de fn_can_view_conversation e fn_can_view_lead: com 'own' a
funcao devolve f para a conversa de outro dono, com 'all' devolve t.

Um write de COR reverteria, em silencio, uma decisao de exposicao de dado de
cliente. Um quarto escritor com o mesmo padrao era inaceitavel.

Entao: uma instrucao SQL dentro de uma funcao, chamada por RPC. `.select()` nao
resolveria — ele so torna o "0 linhas" DETECTAVEL, nao torna o merge atomico.

A funcao devolve integer, nao void. A unica policy de escrita de organizations e
orgs_write_platform_admin (baseline.sql:3415), SEM clausula FOR, logo FOR ALL:
pelo client de sessao o UPDATE de um admin de TENANT casa 0 linhas e o PostgREST
responde 204 No Content — `error` chega null no supabase-js, a tela diz "salvo"
e nada foi gravado (issue #144). Devolver row_count e o que permite distinguir
"gravou" de "nao gravou".

A autorizacao mora TAMBEM no banco: o gate do chamador usa o papel do SNAPSHOT
de membership, e repetir a checagem no SQL e o que faz a regra valer para
qualquer chamador futuro e impede escalacao se o EXECUTE escapar um dia.

DOIS DEFEITOS DO PLANO QUE OS GATES PEGARAM:

1. O apendice do baseline NAO vai no fim. varredura-anon-e-o-ultimo-bloco
   reprovou: o bloco da varredura anon (0116) e deliberadamente o ultimo — ele
   mede o privilegio EFETIVO de authenticated/service_role antes de revogar e o
   devolve depois. Colar depois dele desarmaria a cura. O bloco novo entra
   imediatamente ACIMA dela.

2. audit-resource-id-e-uuid e TEXTUAL e acusou a PROSA: o comentario que
   explicava por que nao copiar o `null` do updateBranding continha o proprio
   literal e reprovou. Reescrito, com o aviso deixado no comentario.

ACHADO AO ESCREVER O TESTE: "0 linhas" e inalcancavel para admin de tenant — org
inexistente falha antes, no gate (42501). O row_count=0 so existe para o
super-admin de plataforma ou na corrida entre resolveActiveOrg e a chamada. O
caso esta escrito com um platform admin dedicado, com controle positivo de que
ele escreve em org de que nao e membro.

21 casos de invariante, com controle positivo em cada catraca (zero sem o par e
instrumento morto) e guarda de vacuidade (o banco do test:db tem ZERO
organizacoes, entao a org semeada e afirmada antes de qualquer medicao).

Sabotagem, previsto ANTES de rodar vs medido: 5/5, 3/3, 1/1, 0/0. Zero
divergencia. O 0/0 e o que importa relatar: remover so `anon` do revoke nao
reprova nada no caminho do BASELINE, porque quem protege ali e a varredura —
o revoke de anon e a unica defesa no caminho MIGRATION (db push num banco
existente), que nenhum gate exercita.

typecheck 0 · lint 0 · lint:channels 0 · test:unit 382/4354 · test:db 102/753
com install e update verdes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TCiffR3ugjQbxsfceQE2GB
…s duas

resolverMarca ja era variadica, primeiroDefinido ja compunha nome e logo campo a
campo, e MotivoDaMarca.origem ja era string livre com "organizacao" citada no
comentario. Nao havia assinatura a reescrever — so faltava a fabrica da camada.

camadaDaOrganizacao devolve camada SEM a chave `cor` quando o hex e vazio, e nao
`cor: null`: medido em resolverCor, `null` emite cor_ausente e a chave AUSENTE
desce calada. Uma fabrica com `?? null` acenderia aviso em TODA organizacao que
nao configurou marca — o ruido que o proprio resolvedor diz querer evitar, e que
e como um operador aprende a ignorar aviso.

Sem logoUrl de proposito: upload de logo e fase seguinte, e uma camada que
declarasse logoUrl:null nao seria neutra — nao apagaria nada, mas ensinaria a
proxima pessoa a achar que o campo existe.

O `break` da cor NAO foi alterado. Ele compoe nome e logo por campo mas a cor
por objeto inteiro, e uma camada de cima que nao pinta bloqueia a de baixo.
Medido: nesta fase o estado e inalcancavel pelo produto — o unico escritor grava
hex cru e a fabrica monta o envelope com papel default. Cor invalida devolve
null e DESCE para a instalacao, que ja e o comportamento certo. Mexer no break
alteraria a semantica de "falhar fechado" da fase anterior para curar um caso
que so um jsonb editado a mao produz.

TETO NO CACHE, porque e esta fase que o faz crescer. O Map interno nunca podava,
e o comentario ja antecipava "uma por organizacao na fase seguinte" — cada Marca
carrega a rampa de 11 stops nos dois temas mais os motivos, num processo que a
VPS mantem vivo por semanas. Deixar sem limite seria criar o vazamento e
documenta-lo. FIFO com teto de 64.

16 testes de precedencia: org com cor vence; org sem cor cai na instalacao SEM
emitir motivo; org com hex invalido cai na instalacao EMITINDO motivo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TCiffR3ugjQbxsfceQE2GB
Os seletores sao `body:has([data-marca-org])` e o mesmo sob [data-theme="dark"],
com o marcador emitido por app/app/layout.tsx.

ANCORAR NO <body> NAO E DETALHE. Sete componentes de components/ui usam Radix
Portal e NENHUM passa `container` (alert-dialog, dialog, dropdown-menu, popover,
select, sheet, tooltip), entao todos portam para document.body — e 89 arquivos
de app/app e components importam pelo menos um. Numa div, a pagina mostraria a
cor da organizacao e o menu aberto por cima mostraria a da instalacao: sintoma
que passa em revisao e o usuario ve todo dia.

O GATE ESCURO E COMBINADOR DESCENDENTE, nao compound. [data-theme="dark"] so
casa o <html> (medido: 4 ocorrencias de documentElement, 2 sao escrita do
atributo). Um `body[data-theme="dark"]:has(...)` nunca casaria nada — regra
silenciosa, zero erro, cor escura simplesmente ausente.

ESPECIFICIDADE MEDIDA, e ela NAO decide nada: instalacao e (0,2,0)/(0,3,0) e
casa o <html>; organizacao e (0,1,1)/(0,2,1) e casa o <body>. Os dois conjuntos
nunca disputam, porque especificidade so arbitra declaracoes sobre o MESMO
elemento. Quem decide e a proximidade de heranca: o <body> e descendente do
<html>, entao sombreia para o documento inteiro. Os numeros existem para provar
que nao ha empate a resolver por ordem de documento — o bug que css.ts diz nao
querer ter.

O TIPO DO ESCOPO E A UNIAO DOS DOIS LITERAIS, NUNCA string. O seletor entra
direto em montarBloco, que e a unica parte do sistema que escreve texto que o
navegador executa e que nao valida seletor nenhum — a allowlist cobre nome de
token e forma de valor, e a rede final so pega `<` e `;}`. Com `string`, um
slug de organizacao alcancaria o seletor.

A COR DA ORG NAO ALCANCA /login POR TRES CAMINHOS INDEPENDENTES, cada um
suficiente sozinho: (1) o seletor so casa enquanto existir [data-marca-org] no
documento, e ele nasce no layout de /app, que nao esta na arvore de /login;
(2) o <style> nao tem href/precedence, entao o React 19 NAO o iça para o <head>
— e nó normal da subarvore e desmonta com ela (o logout e Server Action +
redirect por startTransition, navegacao client-side, sem full reload);
(3) nenhum caminho de app/(public)/** le settings.branding.

Duas divergencias da letra do plano, com o motivo: o <style> so e emitido quando
origens.cor === "organizacao" (reemitir a cor da instalacao escopada no body
seriam bytes sem efeito, e faria a presenca do bloco parar de responder "esta
org tem cor propria?"); e o nome da org so substitui quando origens.nome vem da
organizacao.

Sabotagem, previsto vs medido: 2/2, 1/1, 2/2, 2/2. Os 6 casos restantes do
describe NAO reprovam na sabotagem do seletor, de proposito: eles leem o bloco
atraves da constante, entao medem "os dois blocos concordam entre si", nao "o
seletor e este". Quem guarda o literal sao os dois primeiros.

typecheck 0 · lint 0 · lint:channels 0 · test:unit 383 files / 4369 passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TCiffR3ugjQbxsfceQE2GB
/app/settings/marca, gate `admin` — nao `manager`. A razao: identidade da
empresa fica na mesma prancheta de billing e api-tokens, que sao admin. E o
gate protege quem chama a rota; quem protege a COLUNA e o RPC, que recusa
manager no proprio banco.

A tira de tons e a linguagem sairam de app/admin/(protected)/marca/ para
components/branding/ e lib/branding/ — as duas telas (instalacao e organizacao)
mostram a mesma coisa, e duas copias divergiriam no primeiro ajuste de texto.
Feito com git mv, imports repontados, teste da linguagem acompanhando.

Entrada no registry com section "Sua empresa" e SEM sidebar:true — medido em
tela que duas telas a mais estouram a dobra em 900px. O card no hub e o ⌘K saem
de graca, porque sidebar, hub e paleta sao projecoes do registro.

A segunda divergencia do plano: ele pedia "aplicar o corpo da resposta", mas a
action devolve so {ok:true}. Entao o bloco de estado deriva origens e motivos AO
VIVO da mesma pilha que o formulario usa — o que ELIMINA a corrida em vez de
mitiga-la — e o router.refresh fica so para o layout (cor e nome no menu).

typecheck 0 · lint 0 · lint:channels 0 · test:unit 383/4369 · build 0, com
/app/settings/marca dinamica.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TCiffR3ugjQbxsfceQE2GB
…meiro

A precondicao do teste falhou antes da medicao, e era a armadilha prevista: o
e2e-admin ERA platform_admin (promovido por seed-e2e-system-update, nunca
revogado). Medido count=1, revogado, reafirmado count=0 — so entao o teste valeu.
Sem isso, verde seria resultado de instrumento morto.

Provado: a cor da org pinta /app (#506d48 -> #b3261e), persiste no reload (a
issue #144 nao se repetiu sob admin de tenant puro), tem valor proprio no escuro
(#f16051), e NAO alcanca /login — la o marcador e o bloco sao ausentes e o accent
volta ao da instalacao.

O ambiente compartilhado saturou no meio (93 tentativas de auth em 10min contra
teto de 60/IP/300s, e o fator TOTP rotacionado por outra sessao). Por isso o caso
de volta foi medido SEM sessao: /login e rota publica e a comparacao usa o valor
ja medido em /app. Mesma propriedade, sem depender de um login que o ambiente
nao entregava.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TCiffR3ugjQbxsfceQE2GB
O commit 0872214 escreveu, no proprio action-codes.ts, a regra: "entra aqui E
em lib/audit/actions.ts — sem isto o filtro do painel nao oferece o codigo e a
linha existiria no banco sem porta na tela". E mediu, no mesmo comentario, a
divida pre-existente de 120 codigos divergentes.

O commit 0513755 acrescentou DOIS codigos ao union e UM a lista. O 121o nasceu
ali — no mesmo epico que escreveu a regra, duas commits depois.

Achado por revisao adversarial antes do merge. Escopo real do dano, medido: a
tela de tenant filtra `action` por campo de texto livre, entao a linha e
filtravel la; o que faltava era a opcao no multi-select do painel de plataforma.

Num repo onde comentario e doutrina, deixar merjar uma frase que o proprio
commit seguinte desmente e ensinar que comentario e decoracao.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TCiffR3ugjQbxsfceQE2GB
…eia o CONTROLADOR

As saidas sem DOM: nao ha CSS, nao ha cascata, nao ha tema. O seam e
lib/branding/saida.ts — um hex e uma frente legivel, tema CLARO sempre (e-mail
nao tem tema, e escolher por prefers-color-scheme num e-mail nao e confiavel).

O piso vem importado de regua-do-produto.ts, nunca constante nova: seria a
quarta copia de um hex do produto. E o contrato e NUNCA LANCAR — um e-mail de
LGPD tem SLA legal de D+7 e nao pode falhar porque a leitura da cor deu erro.

O PDF DE LGPD NAO LEVA MARCA NENHUMA. Ele imprime o CONTROLADOR (legal_name) e
o DPO resolvido. O relatorio do Art. 18 II responde a um direito legal do
titular; nomear ali o revendedor — que e OPERADOR, nao controlador — inverteria
os papeis num documento juridico. Trocar DeskcommCRM pela marca do revendedor
nao seria "completar o whitelabel": seria piorar, porque hoje o nome e
obviamente do software e depois pareceria a declaracao de quem responde pelos
dados. Consequencia boa: o StyleSheet module-level do @react-pdf fica como
esta, zero risco assumido.

O REMETENTE E DO OPERADOR; O NOME DE EXIBICAO E DA MARCA. RESEND_FROM_EMAIL e
endereco de dominio que precisa estar VERIFICADO na conta Resend de quem
instalou — nenhuma resolucao de marca muda isso. O que a marca resolve e o nome
que o destinatario le na caixa de entrada.

E vazio passa a significar NAO CONFIGURADO. O fallback antigo
("noreply@deskcomm.app") era PIOR que nada num clone: o dominio nao esta
verificado na conta do revendedor, entao TODO envio falhava la na Resend com
mensagem opaca, e o operador ia cacar rede, conteiner e chave por causa de uma
variavel em branco. Agora cai no caminho que JA existe e JA e bom —
pending_review no worker de LGPD, accept_url na tela no convite. E o install.sh
passou a perguntar as duas chaves, que estavam fora do .env.example e fora do
Zod (o .env e escrito com truncamento: chave posta a mao sumia no update).

8 DAS 9 DIVIDAS RESOLVIDAS, e a guarda encolheu junto — a lista so encolhe. A 9a
(ai-budget-alarm) foi RECLASSIFICADA com motivo medido: nao tem rota de cron,
nao tem linha no scheduler, nao tem chamador. Um caso novo trava o conjunto PELO
NOME, nao pelo tamanho.

ACRESCIMOS DA REVISAO ADVERSARIAL QUE RODOU EM PARALELO:

- hooks/ entrou na varredura de marca (5 raizes). Medido: 112 arquivos,
  importada por 171 de app/components, 23 toast diretos ao usuario — e ja tinha
  uma ocorrencia (useDebugToggle). Nasceu VERDE. E a FRONTEIRA passou a estar
  escrita: o criterio e "codigo que embarca na imagem e alcanca o usuario";
  scripts/, tests/, evidence/ e loop/ ficam fora por nao embarcarem.
- AAL nas duas server actions. Medido: mfaEmDivida tinha UM call site, que serve
  so /api/v1; o gate do layout e politica de CADASTRO, nao de sessao; e nao
  existe middleware.ts no repo. Para server action nao havia gate de AAL nenhum.
  Docstring corrigido: "quem defende o dado e o banco" vale para PAPEL — o
  Postgres nao enxerga o AAL de uma sessao do GoTrue.
- O aviso de divida da tela /admin/marca estava ERRADO, nao so incompleto: dizia
  que "o menu ainda mostra o nome do arquivo de instalacao", e a Fase 3 tornou
  isso falso. Sao 8 call sites de branding(), nao 2.

ACHADO DO PROPRIO SUBAGENTE, DA MESMA CLASSE: a mudanca dele tornou falso um
item do bloco honesto da tela do tenant. Reescrito.

Sabotagem, previsto vs medido: 1/1, 3/3, 1/1 — e na do PDF ele FORTALECEU o
teste antes de rodar: a primeira versao so checava /deskcomm/i e teria deixado
passar a marca de um revendedor; agora isola o rodape e exige que o texto entre
"Controlador:" e "· Relatorio LGPD" seja EXATAMENTE o legal_name.

typecheck 0 · lint 0 · lint:channels 0 · test:unit 386 files / 4399 passed ·
test:shell 253 ✓ / 0 ✗ · build 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TCiffR3ugjQbxsfceQE2GB
…-mails de auth com a marca

FAVICON. /favicon.ico devolvia 404 — para nos e para o revendedor. A aba nao
tinha marca nenhuma. Agora app/icon.tsx gera em runtime com next/og (zero
dependencia nova, force-dynamic), e letraDoIcone pula emoji, aceita acento e
devolve null em vez de cair na NOSSA inicial. /^\/icon$/ entra em PUBLIC_PATHS.

Sem apple-icon: o plano pedia a regex, mas allowlist para rota inexistente e
config morta.

BARRA DO NAVEGADOR — o defeito nao era o que a medicao anterior dizia. themeColor
NAO e defeito de whitelabel: cssDaMarca emite so --color-brand e a familia do
accent, NUNCA --color-bg, entao #faf9f6/#161510 sao a cor certa para qualquer
marca, e generateViewport() lendo o banco seria uma leitura por request para
devolver sempre o mesmo valor. O defeito real era DUPLICACAO: os dois hexes
viviam em app/layout.tsx, regua-do-produto.ts e globals.css sem nada
sincronizando. Agora sai da regua.

Provado por comportamento, nao por leitura: com app_name='Vendas Turbo' e
accent_hex='#f2c94c' gravados, o icone virou V sobre #6e5c28 (o accent DERIVADO,
nao a semente crua) e o titulo mudou — E o theme-color NAO mudou. E a correcao
acima, medida na tela.

E-MAILS DE AUTH. Os templates tinham a marca literal e NENHUM script os subia:
o primeiro e-mail que o cliente do revendedor recebe dizia o nome do nosso
produto. Agora hostgator-setup-kit/marca-emails.sh sobe pela Management API,
chamado pelo install.sh (sempre) e pelo update.sh (so com token). Custo para o
comprador: ZERO passo novo — com token sobe e confere; sem token, imprime o
passo manual e sai 0.

A MEDICAO QUE DESTRAVOU A DECISAO: PATCH /v1/projects/{ref}/config/auth com
mailer_templates_* e aceito e PERSISTE sem SMTP customizado. Rig num projeto
descartavel, com estado capturado antes e restaurado depois, releitura por GET
byte a byte.

E o achado que so aparece quando se mede: PROJETO PAUSADO responde 400 "Project
is paused." — modo de falha real que um script confiando no 2xx reportaria como
sucesso. Por isso o script RELE o que gravou antes de dizer que deu certo.

--render-em DIR existe porque sem ela eu estaria INTRODUZINDO um defeito:
docs/deploy-selfhost manda o GoTrue self-hosted apontar para supabase/templates/,
que agora carrega placeholder — o cliente receberia __APP_NAME__ literal.

RISCO DO TEMPLATE PADRAO, medido no mecanismo: o verificador PKCE herda as
cookieOptions da sessao, isto e sameSite:"strict", entao clique de webmail e
cross-site e o cookie nao viaja. A primeira versao da tela dizia "abra no mesmo
navegador" e foi REFEITA: cliente de e-mail nativo abre sem iniciador e o cookie
PODE ir — a tela aponta a configuracao como conserto em vez de prometer um
contorno que talvez nao funcione.

A PROSA DA 0155 corrigida: a rede de rollback pelo .env cobre NOME e LOGO; para
COR ela e vazia POR CONSTRUCAO, porque APP_ACCENT_HEX nasceu neste epico e
nenhuma versao anterior pinta accent. So comentario — a migration nao muda.

Sabotagem, previsto vs medido: 1/1, 2/2, 1/1, 2/2, (3+2)/(3+2), 1/1, 1/1, 2/2.
Oito sabotagens, zero divergencia. Restauracao por cp do backup (nunca git
checkout, que levaria trabalho nao commitado), com diff -q confirmando.

typecheck 0 · lint 0 · lint:channels 0 · test:unit 389 files / 4426 passed ·
test:shell 265 ✓ / 0 ✗ · build 0 com /icon dinamico.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TCiffR3ugjQbxsfceQE2GB
A spec nasce no disco e na lista no mesmo commit. Nome errado na lista deixa o
job e2e VERDE sem rodar nada, que e pior que reprovar — e spec fora das tres
listas reprova o job VERIFY, nao o e2e, com mensagem que nao fala de marca.

Nao a pus em FORA_DO_CI: exclusao preventiva e divida entrando pela porta de
servico. O primeiro e2e do PR e quem decide.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TCiffR3ugjQbxsfceQE2GB
…ha a peca

docs/white-label.md era PROMESSA PUBLICA FALSA em duas frases: "cores, fontes e
tema nao sao configuraveis" e "a marca e por instalacao, nao por organizacao".
As duas deixaram de ser verdade neste epico. Reescritas — e o que CONTINUA nao
configuravel saiu com a razao medida, nao com hedge: dominio por organizacao
(zero coluna de dominio no schema; proxy.ts:27 declara a ramificacao por host
como NOOP; no Edge nao ha banco), fonte por tenant (next/font resolve em BUILD e
a imagem e pre-buildada), tema, e logo por organizacao.

OS NUMEROS VELHOS VIVIAM EM QUATRO ARQUIVOS, NAO DOIS. O CLAUDE.md dizia 4
checks obrigatorios (sao 5) e "37 das 39 specs"; os mesmos numeros estavam em
docs/harness-audit.md ("4 das 32, nao-obrigatorio") e docs/current-state.md
("imagens-ok ainda nao esta na branch protection"). Deixa-los seria a regua
errada sobrevivendo nos dois docs que o proprio CLAUDE.md manda ler ANTES de
estimar. Recontado hoje:

  ls tests/e2e/*.spec.ts | wc -l                    46 no disco
  parse por bloco YAML das listas do e2e.yml        45 rodam (23+22), 1 fora
  gh api .../branches/main/protection               5 checks obrigatorios
  git ls-files 'tests/unit/*.test.ts*' | wc -l      257 (o AGENTS.md dizia 221)
  git ls-files 'tests/invariants/*.test.ts' | wc -l 102 (dizia 67)

GATE NOVO CONTRA A DIVERGENCIA DAS DUAS LISTAS DE AUDIT, e ele NASCE VERDE:
medido 209 codigos no union x 89 no painel, 120 divergentes, 0 no sentido
inverso. Congela os 120 e reprova acrescimo novo. Reprova tambem (a) linha
congelada ja curada — senao vira autorizacao permanente para remover de novo —
e (b) opcao de painel sem codigo no union, que e filtro morto.

Curar os 120 NAO e deste epico, e o conserto certo nem e copiar melhor: e
DERIVAR a lista do painel a partir do union. Fica como divida D4.

ACHADO DA PROPRIA CONSTRUCAO DO GATE: o parser inicial recortava o union antes
de tirar comentarios, e um `;` dentro de comentario truncava o match — devolvia
204 e INVENTAVA "5 codigos no painel que nao existem no union". Instrumento
quebrado devolve numero plausivel. Esta escrito no cabecalho do teste.

Mapa vivo: docs/architecture/marca-propria.architecture.json, 37 pecas / 54
arestas / 6 faixas, com o PDF de LGPD entrando como peca deliberadamente
DESLIGADA da marca — e o gate reprova quem ligar saida->pdf.

Tres instrucoes "re-renderize com archify" corrigidas (sistema-vivo.md x2 + a
skill): o archify 2.11.0 recusa o formato, e o proprio README de architecture ja
media isso.

Sabotagem do gate de audit, previsto vs medido: 1/1, 2/2, 1/1, 1/1, e a quinta
(renomear o export do union) produz erro de COLETA em vez de verde silencioso —
que era a previsao. Sabotagem do caso novo do mapa: 1/1.

Checklist do Sistema Vivo respondido para o epico inteiro, com artefato em cada
uma das 10. A 8 (continuidade IA<->humano) e N/A com justificativa escrita:
configuracao de apresentacao, nenhum turno de agente le ou escreve marca.

typecheck 0 · lint 0 · lint:channels 0 · test:unit 390 files / 4436 passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TCiffR3ugjQbxsfceQE2GB
@vercel

vercel Bot commented Aug 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
deskcomm-crm Error Error Aug 14, 2026 4:35am

Request Review

@ecc-tools

ecc-tools Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

ECC bundle files are already tracked in this repository. Skipping generation of another bundle PR.

describe("catraca de marca no que o GoTrue renderiza", () => {
/** Comentário de HTML é um TRECHO, não um prefixo de linha. */
function semComentariosHtml(fonte: string): string {
return fonte.replace(/<!--[\s\S]*?-->/g, "");
@melgarafael
melgarafael merged commit 11d87a1 into main Aug 14, 2026
11 of 13 checks passed
@melgarafael
melgarafael deleted the feat/marca-propria branch August 14, 2026 04:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants