feat(marca): marca própria — cor, nome e favicon por instalação e por organização - #248
Merged
Conversation
`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
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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, ""); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
É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=1sob essa autorização.A premissa do pedido estava parcialmente errada, e isso mudou o plano
lib/branding.tsjá existia (nome + logo por.env, em runtime) edocs/white-label.mdjá 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
platform_branding+ tela/admin/marcasettings.branding+ tela/app/settings/marca, gravada por uma instrução SQL/favicon.icodevolvia 404 — para nós e para o revendedorissuer), remetente, suporteTrês defeitos pré-existentes que o épico achou e consertou
Sant'Anaparava o Docker Compose inteiro. Oenvqdo instalador gravava o.envcom aspas simples e escape'\''; o Compose recusa ler o arquivo inteiro —config,psepulltodos rc=1. Nomes brasileiros com apóstrofo são comuns, eAPP_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ó cobriaapp/ecomponents/, só.tsx. Escapava pela extensão ofriendlyNamedo autenticador, que fica no celular do usuário para sempre.O produto nasce com colisão de cor:
--color-successdo 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
organizationsjá separalegal_namededisplay_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.settingstem três donos com gates diferentes, e um save de cor podia revertervisibility_modedeownparaall— campo lido dentro da RLS, emfn_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_countporque 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_updatedfaltando 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 →
42501em todas as negativas, cross-tenant comsettingsda outra org byte a byte intacto); vazamento deEXECUTEpelos 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
typecheck0 ·lint0 ·lint:channels0 ·test:unit390 files / 4436 tests ·test:shell265 ✓ / 0 ✗ ·test:db102 / 753 cominstall okeupdate ok·build0.1.
RESEND_FROM_EMAILvazio agora significa não-configurado (antes caía emnoreply@deskcomm.app). Se a produção tiverRESEND_API_KEYsem essa chave, os convites deixam de sair por e-mail e passam a mostrar oaccept_urlna 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.RedirectTojá chega com?type=./auth/confirmcai emlink_invalido, sintoma que se lê como "link expirado". Não foi tocado (é produção, precisa do seu aval). Omarca-emails.shdeste 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 noHANDOFF-marca-propria.md.🤖 Generated with Claude Code
https://claude.ai/code/session_01TCiffR3ugjQbxsfceQE2GB