Phantom Unit OS · auditoria geral

O motor do dinheiro está certo.
A porta da frente está aberta.

Seis frentes em paralelo — pentest de código, integridade do banco, design system, paridade React, fluxo de trabalho e sondagem ao vivo. Tudo somente leitura: nada foi editado, nenhum POST em produção, nenhuma escrita no D1.

30/07/2026 app.phantomunit.com.br D1 phantom_releases_db migrations 001–022 6.813 linhas em src/ · 230 KB em app.html 16 URLs medidas · 35 screenshots

13crítico
16alto
26médio
1prova fecha
176portas abertas

00 O fluxo que você desenhou × o sistema que existe

Esta seção entrou depois. A auditoria original mediu o app contra o que estava escrito nos documentos. O fluxograma de 30/07 é a régua certa — e contra ela o buraco é maior e tem outro formato.

crítico

O motor que faz a bolinha andar é WhatsApp. O sistema só sabe e-mail.

src/lib/avisos.js:46 canal = "email" · :83 if (a.canal === "email") · wrangler.toml EVOLUTION_URL = ""

canal é parâmetro, mas e-mail é o único caso implementado — qualquer outro valor entra na fila e nunca sai. Os 13 disparos, os três de prazo, o feedback da recusa, o aviso de saque: todos e-mail.

A sua regra é "moveu a bolinha, avisa o próximo", e o aviso que a galera vê é o zap — você mesmo escreveu isso. Hoje o Lutz e o Bagre só descobrem que entrou trabalho se abrirem o e-mail ou o painel. Isso tira o WhatsApp da fase 6 e joga pra fase 1: sem ele, a esteira anda mas ninguém sente.

crítico

O contrato — que no seu desenho é o gatilho de tudo — não existe

public/submit.html passo 5 · contratos/ é só um prefixo no R2

O passo 5 tem duas caixinhas: cpf_aceite e declaration_security. É um checkbox. Nenhum contrato é gerado, guardado, versionado ou assinado, e nada é escrito em contratos/.

No seu fluxograma a assinatura é o momento em que a bola passa do artista pra staff, em que os perfis nascem, em que o grupo é criado e em que os disparos saem. Se ela não existe como evento, não há de onde pendurar nada disso — é o nó que sustenta o leque inteiro.

crítico

O grupo no zap não é criado — o código só sabe receber de um grupo que já existe

src/routes/api/whatsapp.js:81 const ehGrupo = remetenteJid.endsWith("@g.us")

Existe tratamento pra mensagem vinda de grupo (pega o remetente real em participant). Não existe uma linha que crie grupo.

No seu desenho o grupo é onde o acerto fino acontece — o artista fala com o Lutz, a Amanda combina a divulgação, o agente participa. É a camada que corre por fora da esteira inteira, e ela não tem como nascer sozinha.

alto

O disparo não carrega a ferramenta — só um link genérico pra aba

src/lib/avisos.js:219, 231, 241 — ${linkDoApp(env)}#musicas

Você desenhou: "o masterizador recebe o link do wav pra download por mensagem, o designer já recebe o briefing por mensagem". O que o aviso manda é …/#musicas — a aba inteira, sem apontar o lançamento, sem o arquivo.

E o briefing existe e se perde no caminho: o formulário coleta designer_concept e designer_ref (submit.html:562,566), o ficha.js:403 até salva em conceito_capa — e o texto do aviso nunca lê esse campo.

O Bagre é avisado de que tem trabalho e precisa caçar o briefing dentro do app. O aviso que não carrega a ferramenta vira só mais uma notificação pra ignorar.

alto

O financeiro não tem como marcar pago — o verde só nasce por SQL na mão

nenhuma rota escreve payout_status='paid'

O saque.js grava 'requested' (🟡, bola com o financeiro). Daí pra frente, nada: financeiro.js o status. Não existe rota de confirmar pagamento, nem de devolver pro artista quando o PIX está errado.

Os dois últimos passos do seu ciclo do dinheiro — "confere, paga e aprova" — não têm botão nem rota. O trimestre trava em amarelo pra sempre, e o artista fica olhando "aguardando" sem fim.

O fluxograma inteiro, nó por nó

O que você desenhouO que existe hoje
Cadastro em 3 níveis encadeadoso formulário coleta os três, mas o envio morre em 404 nao_encontrada
Contrato assinadodois checkboxes. nada é gerado nem guardado
Cria perfil do artista + página públicaexiste, mas 14 de 21 páginas quebram por title:null
Cria perfil da música + página pré-saveexiste; o pré-save é maquete e "confirma" sem salvar nada
Cria o grupo no zap automaticamentezero linhas. só sabe receber de grupo existente
Dispara pro master com o link do wave-mail com link genérico pra aba #musicas
Dispara pro designer com o briefingo briefing é salvo e nunca lido pelo aviso
Bolinha = de quem é a vezo banco tem os 4 estados; a tela lê como progresso, não como posse
Staff faz ou avalia, e sobeo upload move a etapa por SQL direto, sem avisar ninguém
Rejeita → feedback por zap e e-maila rota existe e exige nota; não há botão, e o canal zap não existe
Artista refaz → bola volta pro laranjaconsequência do anterior: o ciclo nunca fecha
2 verdes destravam mkt e distrofunciona, na mesma transação, e avisa os dois
Mkt baixa, cria o pack, sobemkt/ nasceu público e o registro estoura no CHECK; o APROVAR é inclicável
Distro baixa, distribui, sobesó aprova; não define o que sobe como prova
Fecha o upload no lançamentonão existe trava. dá pra trocar arquivo depois de distribuído
Dia do lançamento → todas as lojasa data existe e guia; a publicação é manual, por fora
Pré-save vira smartlink com embedszero ocorrências de smartlink no projeto
Divulgação D−3 · D0 · D+não existe nem como aviso nem como tarefa
+30 dias: wav e capa pro armáriodesenhado até o custo em reais, nada construído
Trimestre → calcula → avisao cálculo fecha ao centavo; a importação é script manual
Artista solicita com o PIXfunciona — dono conferido, valor recalculado no servidor, PIX não guardado no perfil
Financeiro confere, paga e aprovanenhuma rota escreve paid
Financeiro devolve quando está erradonão existe
Botão de chamar no zapexiste; um deles ainda aponta pro número de teste
o que essa comparação muda na ordem de construção

O WhatsApp sobe da fase 6 pra fase 1. Era "melhoria de canal"; virou pré-requisito — sem ele a bolinha se move e ninguém fica sabendo, que é o oposto da regra que você escreveu.

Entram três nós que eu não tinha mapeado: o contrato como evento (não como checkbox), a criação do grupo, e o disparo carregando link e briefing. Os três moram no mesmo instante — a assinatura — e é o instante que hoje não existe.

E o ciclo do dinheiro não fecha. Falta a última rota, a que vira o amarelo em verde.

01 O que mudou desde ontem

Duas coisas aconteceram hoje que reordenam a fila inteira, e nenhuma das duas está escrita em documento nenhum.

agora

O primeiro artista de verdade entrou no app

id 6 · GreenTech · criado 30/07 17:25:56 · último acesso 17:25:57 · projeto greentech
Lutz, Basso e Bagre: ultimo_acesso = NULL  (nunca abriram)
fichas = 0 linhas  (ele entrou e não terminou o cadastro)

A pendência "ninguém logou e conversou ainda" está vencida. Todo bug de tela de artista deixou de ser hipótese — tem uma pessoa real esbarrando neles agora. E o que ele encontra hoje é o roster inteiro dos 227, os 350 lançamentos da label com botão ⚡ GERENCIAR em cada um, e a aba do próprio dinheiro travada em "carregando…".

alto

A janela de saque do Q1 fecha amanhã

src/routes/api/financeiro.js:45 · janelaDeSaque('2026-Q1') → limite 31/07/2026

dias_restantes = 1. Existem 13 tipos de aviso no sistema e nenhum é sobre janela de saque prestes a vencer — nem em avisos.js, nem no relogio.js.

Ninguém logou pra ver, e ninguém vai ser avisado. A janela fecha em silêncio.

02 Vazando agora, na internet aberta

Tudo neste bloco foi confirmado por requisição anônima, sem login, hoje. Nenhum é teórico.

críticono ar

O app velho tem 176 portas sem Access, ligadas no banco de produção

Cloudflare Pages phantom-release-hub · 176 deploys · mesmo D1 e mesmo R2

O Access protege phantom-release-hub.pages.dev — o domínio exato. Mas todo deploy do Pages ganha uma URL permanente com hash, e a política não cobre nenhuma delas. São 176 deploys, cada um uma porta. Testei três, as três abertas:

https://a6abb9fd.phantom-release-hub.pages.dev/api/financeiro   200
  totais           GBP 13.252,58 · 48.223 linhas · 27 trimestres
  lojas            Spotify 4.619,85 · YouTube · Beatport …
  artistas         top 15 com o valor de cada um (Sonic Massala 3.401,19)
  janela/saque     2026-Q2 · bruto 986,52 · aberta até 31/10

.../api/financeiro?artist=sonic-massala   200  extrato individual
.../api/roster                            200  228 artistas com royalty_gbp e split_pct

O detalhe que faz isso crescer sozinho: aquele código faz SELECT a.* (functions/api/roster.js:202). O último deploy foi 28/07. A migration que criou artists.split_pct foi 30/07. Ninguém publicou nada — a taxa confidencial entrou sozinha no ar dois dias depois. Enquanto esses deploys existirem, toda coluna futura de artists vai pra internet no dia em que for criada.

E é caminho de escrita. functions/api/saque.js não importa quemE, não tem checagem nenhuma: onRequestPost direto no banco, artista vindo do corpo, royalty_gbp * 0.5 chumbado, rateio por lançamento — e INSERT INTO royalties na tabela de produção. Não exercitei; é escrita. O código prova.

Um POST anônimo registra pedido de saque de qualquer projeto com a chave PIX de quem pediu, calculado com as duas regras revogadas. O app novo então mostra 🟡 AGUARDANDO PAGAMENTO e o aviso chega pra diretoria com cara de pedido legítimo — porque é o caminho legítimo, só que o velho. Os hashes não estão em log de Certificate Transparency, então ninguém acha por varredura. Mas URL não é senha: ela vive no histórico do navegador, na saída do wrangler, no painel, e em qualquer link já colado. E é permanente.

críticono ar

Os 227 press kits públicos dizem "LEVE UTTARI PARA O SEU EVENTO"

public/artist-page.html:441,446,405,413,421,429,460,449 · public/release-page.html:180,204

Uttari nunca existiu no roster — era nome de maquete. Um arquivo serve os 227 artistas, então está em todos. Confirmado ao vivo agora:

$ curl -s .../artistas/sonic-massala
LEVE UTTARI PARA O SEU EVENTO
wa.me/5555999999999
alert('Baixando fotos oficiais HD...')      alert('Baixando logos vetoriais...')
alert('Baixando Tech Rider PDF...')         alert('Baixando Biografia PDF...')
soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3
booking@phantomunit.com   ← domínio errado, é .com.br

$ curl -s .../musicas/pachamama
PRÉ-SAVE REALIZADO COM SUCESSO!   ← não salva nada, pro público
soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3

A seção PRESS KIT & ASSETS PARA CONTRATANTES E IMPRENSA — o motivo da página existir — é inteira falsa: os quatro botões de download são alert(). O "▶ PLAY PREVIEW · Qualidade Master 24-bit" toca uma faixa genérica de demonstração. E a bio que o artista escreve no /bemvindo.html nunca aparece: artist-page.html:583 só lê influences e goals.

É o material que a label manda pra contratante e pra imprensa. Um produtor abre a página do Sonic Massala, lê o nome de outro artista, clica num WhatsApp que não existe e escuta uma música que não é dele. E no release, um fã clica em pré-save e recebe uma confirmação de sucesso de algo que não aconteceu.

críticono ar

O faturamento por lançamento e o WhatsApp da equipe, sem login

src/routes/api/roster.js:97 (SELECT r.*), :122, :132, :140, :228 — no app novo

$ curl -s "https://app.phantomunit.com.br/api/roster?release=pachamama"
release.royalty_gbp = 1368.6263
faixas  [{"Pachamama", 1587.79}, …]
lojas   [{"Spotify",754.64},{"YouTube",296.06},{"Beatport",70.12}, …]
etapas.capa.whatsapp   5548988379562   Bagre
etapas.wav.whatsapp    5551995511691   Lutz
etapas.mkt.whatsapp    5548996203352   Amanda
etapas.distro.whatsapp 5555991297488   Basso

Vale pros 350 (?all_releases=1 entrega os slugs prontos pra varrer). É a terceira repetição do mesmo bug no mesmo arquivo: o bloco VITRINE fechou o nível do artista, a subconsulta do ?id= foi fechada em 30/07 com a lição escrita em caixa alta logo acima — e o ramo ?release=, 150 linhas antes, nunca foi olhado.

E o agente entrega isso de bandeja. cerebro.js:388 dá a ferramenta catalogo pra todo mundo, descrita como "informação pública, a mesma do press kit", e ela chama exatamente essa rota. Um artista pergunta no chat "quanto rendeu o Pachamama?" e o robô responde £1.368,63 do Sonic Massala. A premissa que sustenta a permissão do agente é falsa.

Quatro celulares pessoais reais em texto puro numa rota aberta — spam, engenharia social, e é o mesmo número que aparece no aviso de saque. O faturamento item por item do catálogo inteiro sai com um curl.

alto

Qualquer pessoa logada baixa a planilha de pagamento do trimestre

src/routes/api/financeiro.js:141 (portão) · :149 (CSV) · :208 (ehDono)

if (!artist && !podeVerALabel) return 403;              // 141 — só exige artist não-vazio
if (period && p.get("formato") === "csv") { …CSV… }     // 149 — sem ehDono
if (artist) { const id = await resolverArtista(…); ehDono(…) }   // 208 — tarde demais

?artist=qualquercoisa&period=2026-Q2&formato=csv devolve uma linha por artista do trimestre: nome, streams, vendas, bruto e repasse. É a planilha que a Amanda usa pra pagar.

Dividindo repasse por bruto sai a taxa de cada projeto — e split_pct é acordo comercial confidencial. O ehDono do extrato foi colocado em 29/07; o CSV, dez linhas acima, ficou. Mesma classe do achado anterior: fechar uma consulta não fecha o arquivo.

alto

Staff comum vira admin por uma porta que foi aberta de propósito

src/routes/api/convite.js:109 · artista.js:152 · auth/retorno.js:94

O revincular: true foi removido em 29/07 com a justificativa escrita de que "o convite vai pro e-mail da própria pessoa". A premissa não se sustenta: o link, com o código inteiro, volta na resposta HTTP pra quem criou o convite. Quem cria não precisa que o e-mail chegue em ninguém.

1. designer faz POST /api/convite {"email":"<e-mail do admin>"}
2. o código acha a pessoa e grava vincula_pessoa_id sozinho, sem flag
3. a resposta devolve o link
4. abre o link com uma conta Google descartável
   → UPDATE pessoas SET email = <do atacante> WHERE id = <admin>

Ele passa a ser o admin — CPF, fichas, financeiro inteiro, liquidação. E o titular fica de fora, porque o e-mail dele não existe mais em pessoas. Alcance hoje: Lutz, Basso e Bagre. A trava de "papel admin só por admin" é inútil enquanto isso existir — não precisa pedir admin, é só virar um.

críticono ar

14 dos 21 artistas do casting têm o press kit quebrado — e o que sobra na tela é a maquete

public/artist-page.html:584 esc(r.title.toUpperCase()) · :502 → corDe(null) na :554

39 dos 350 releases têm title: null. Dois TypeError distintos, mesma causa. O script que troca a maquete por dado real morre no meio — e o que fica na tela é o que estava embaixo.

O Basso é o caso mais caro. Ele tem Spotify, Instagram, YouTube, SoundCloud e press kit cadastrados. A página mostra seis pílulas falsas apontando pras homepages de spotify.com, apple.com e beatport.com, um destaque anunciando "COSMIC PARADIGM — 138 BPM", e de capa o wallpaper do macOS Ventura. Karma renderiza inteiro — é a prova de controle: ela é uma das que não tem release sem título.

Não é "a página fica feia". A página fica convincente e errada: um contratante lê uma discografia que não é do artista, clica em links que vão pra loja genérica, e o que a label realmente cadastrou nunca aparece. Consertar os 39 títulos nulos no banco resolve mais do que qualquer conserto de front-end.

críticono ar

A home pública não fala com o banco — e lista "Uttari" como artista da label

public/index.html — servido em / do site público e em /musicas

$ curl -s .../index.html | grep -c "picsum.photos"     11
$ curl -s .../index.html | grep -oE "/api/[a-z]+"      (nenhum)

artistas listados:  Uttari · Shamanix & Zortek · Sonic Massala · Basso

Zero chamada de API. Roster e "últimos lançamentos" são conteúdo estático inventado, e as 11 imagens vêm do picsum.photos — um gerador de foto aleatória. A label tem 327 capas reais no R2.

A vitrine da label mostra fotos sorteadas por um serviço de placeholder e apresenta um artista que nunca existiu ao lado de três reais. Cada visita carrega imagem de terceiro.

alto

Slug inexistente devolve 200 e uma página convincente — e a discografia falsa aponta pra ela

artist-page.html — if (!id) return; // sem id, mantem a maquete

  • /artistas/naoexiste → maquete UTTARI inteira, HTTP 200
  • /artist-page.html?id= → nem o título muda
  • /release-page.html?id=naoexistecountdown falso rodando, e os botões internos MODO PRÉ-SAVE / MODO PÓS-LANÇAMENTO visíveis ao público

Fecha o ciclo com o achado anterior: a discografia falsa do Basso aponta pra ?id=PHANTOM-8421, que cai exatamente nessa tela. Erro nunca vira erro — vira outra página falsa. Junto, um detalhe que quebra navegação: o link do logo e o "VER PRESS KIT DE MÍDIA" usam href relativo numa URL com pasta virtual, então resolvem pra /artistas/index.html — o manual de marketing nunca abre a partir da página de artista.

alto

O royalty já está no HTML público — só um bug o esconde

public/release-page.html — #section-live chega ao anônimo com display:none

Em /musicas/lsdance, o bloco #section-live vem populado com £467,21, £6,24 e o detalhe por loja (Spotify £172,22, Beatport £35,25…) — alimentado pelo ?release= do achado acima. O que segura isso na tela é um bug: o script esconde o seletor de modo e nunca chama setMode('live').

Consertar o modo sem antes tirar a coluna ROYALTY publica o faturamento por faixa. Os dois têm que andar juntos, e nessa ordem — é o tipo de conserto que parece melhoria e vira vazamento.

Mais 6 achados de permissão e exposição
OndeO quê
roster.js (público)goals de 18 artistas — o do Sonic Massala diz "Sócio da label e curador do V/A South Storm". Estrutura societária, aberta. Junto: membro_inativo marcando 3 pessoas nominalmente, streams e vendas por artista.
upload.js:42mkt/ não está em SO_COM_LOGIN — nasceu público no R2. E arquivos.tipo tem CHECK que recusa "mkt": o PUT no R2 acontece antes do INSERT que estoura. Arquivo público no bucket, zero linha no banco, 500 na tela.
ficha.js:53SELECT * + delete f.cpf. Tira uma coluna; ficam nome_civil, e-mail, whatsapp, premaster_key. A regra da casa põe nome civil na mesma classe do CPF.
artist-page.html:497,574esc() escapa &<>"' mas não valida esquema. presskit_url = "javascript:…" gravado pelo próprio artista executa no origin do app quando staff ou imprensa clica em PRESS KIT.
saque.js:131A chave PIX fica gravada pra sempre em royalties.note e de novo em avisos.corpo. A regra é "digitada na hora, não guardada" — o perfil não guarda, o banco guarda. Hoje 0 linhas: dá pra decidir antes do primeiro saque.
9 telas-protótipoadmin.html, financial-dashboard.html, label-analytics.html, cover-approval.html e mais 5 abrem sem sessão. Não vazam dado real, mas a financial-dashboard se anuncia como "🔒 ACESSO RESTRITO: ADM" — é página de phishing pronta, no domínio verdadeiro da label.

03 O dinheiro

Aqui é onde o projeto está mais forte. A prova de conservação foi refeita do zero, com as expressões do rateio.js, e fecha ao centavo.

fecha

Prova de conservação — refeita e confirmada

bruto total (sales)               GBP  13.252,58
bruto rateado (via JOIN_DONOS)    GBP  13.252,58   nada perdido no JOIN
  artistas (VALOR_ARTISTA)        GBP   7.580,54
  label    (VALOR_LABEL)          GBP   5.672,04
  soma                            GBP  13.252,58   
órfão (contado com NOT IN)        GBP       0,00   
chave de faixa nula                       0 linhas 
sale_type: só Track e Release · zero 'Totals' · uma label só

Artistas ficam com 57,20% do bruto. Quem rodar a prova antiga (== bruto × 50%) vai vê-la acusar GBP 954,25 de erro que não existe — vale avisar quem for conferir depois.

armadilha dentro da própria prova

NULL NOT IN (...) devolve NULL, não TRUE — chave nula não aparece como órfão. Quem só rodar o NOT IN acha que provou e não provou. Conferi separado com IS NULL: zero linhas.

crítico

A tabela que carrega 100% do dinheiro não tem escritor

grep INSERT … faixa_artistas em src/nenhum resultado

faixa_artistas — que o CLAUDE.md chama de "o coração de tudo" — foi preenchida uma vez, por um script Python. Nenhuma rota do app escreve nela. O formulário até coleta faixas[] com colaborador por faixa (submit.html:1376), mas achata() (ficha.js:376) só lê track.titleo array inteiro é jogado fora.

Todo release nascido no app sai sem dono de faixa. Quando o Label Worx reportar a venda, um trimestre depois, ela vira dinheiro órfão — e órfão é o erro bom, o visível. O ruim seria cair no artista errado. De qualquer forma: o app não consegue criar o dado de que o próprio motor de dinheiro depende.

alto

GBP 378,10 fantasma no razão — e a porta continua aberta

tabela royalties · sem índice único em (artist_id, period)

sonic-massala · 2024-Q4 · 3 linhas · GBP 567,15  (189,05 × 3)

saque.js faz SELECT e depois INSERT, sem nada no banco garantindo. Dois cliques simultâneos, ou os dois logins do mesmo projeto, passam os dois pela checagem. Não é hipótese — já aconteceu. É o único grupo duplicado no banco.

O trimestre mostra 627,49 no lugar de ~249,39, e a porta pra dois PIX no mesmo período segue aberta.

alto

"PAGO" sem prova em 1.878 das 2.015 linhas

D1 royalties · public/app.html:2511

2.015 linhas paid, 205 artistas, GBP 6.163,86 — e só 137 têm paid_at. As outras vieram de backfill: 1.488 "histórico — distribuidora anterior, pago fora" e 390 "histórico — trimestre que faltou na importação". Junto: o paid_at que existe guarda trimestre, não data ('2024-Q4'), então o semáforo vai renderizar "PAGO · 2024-Q4".

O artista abre o extrato, vê a história inteira verde sem data e sem a nota, e não tem como perguntar por quê. E pendente_gbp vira ~0: a própria label acha que não deve nada.

alto

Três resolvedores de artista, dois sem apelido

ArquivoNome oficialApelidoSlugSe não achar
rateio.js:62 (canônico)null
financeiro.js:103null
saque.js:65fabrica um id

financeiro.js importa cinco coisas do rateio.js e não importa resolverArtista — declara a própria. E tem um comentário logo abaixo explicando que resolve depois "porque comparar nome digitado erraria com apelido (Essence (Br))" — enquanto o resolvedor acima nunca consulta a tabela de apelidos.

São 24 apelidos, incluindo os maiores da label. /api/dashboard?artist=Essence (Br) funciona; /api/financeiro?artist=Essence (Br) devolve 404. Duas rotas, dois veredictos sobre a mesma pessoa.

Mais 8 achados de banco e integridade
OndeO quê
artists.royalty_gbpInflado em GBP 2.846,81 (16.099,39 contra 13.252,58 real). Hoje só ordena o roster público — errado. Qualquer tela nova que leia como dinheiro mente 21%.
releases.royalty_gbpCongelou antes do Q2 (12.266,06, falta 986,52). E é o número que vaza publicamente: na mesma resposta, release.royalty_gbp = 1368,63 (cache velho) convive com faixas[0].gbp = 1587,79 (soma viva). Dois números pra mesma coisa, na mesma resposta.
migrations/O banco não é reconstruível a partir do repo. O README aponta pra um schema.sql que não existe lá, e faltam as migrations 001, 007, 008 e 009. sales, releases, artists, royalties e mais sete tabelas não têm CREATE versionado em lugar nenhum.
004 (duas)Existem duas migrations 004. A de pessoas cria pessoa_funcao com CHECK ('adm','capa','wav','mkt','distro'); o banco real tem ('admin','designer','master','manager','marketing'). Rodar as migrations em ordem hoje produz um schema diferente do de produção. Junto: índice duplicado em releases(data_lancamento).
dashboard.js:101Cópia inline da CHAVE_FAIXA, carregando o sale_type='Track' que o rateio.js documenta como errado e removido. Hoje dá o mesmo número — é dívida latente, não erro.
salesSem índice em isrc, catalog, store_name. A prova de conservação lê 228.803 linhas pra uma tabela de 48.223; a view mapa_catalogo (migration 022) leu 771.943 pra 8 linhas — e o D1 cobra por linha lida.
022O cabeçalho da migration proíbe agrupar por catalog e o corpo faz isso duas vezes. Não dispara hoje (nenhum PU duplicado vendeu); dispara no trimestre em que um vender.
histórico × fórmulaRegistrado em royalties pro Sonic: 3.130,77. Devido pela fórmula de hoje: 3.339,88. Diferença de 209,11 — não é bug, é o efeito da taxa ter virado dado em 30/07. Retroagir ou não é decisão sua.

04 O painel de trabalho nunca foi ligado na tomada

a missão, na sua letra

"Mais do que um painel de gestão dos ativos, é um painel de trabalho da equipe. O app não mostra o que a label TEM — mostra o que a equipe PRECISA FAZER."

A parte de painel de métrica está pronta e é boa. A parte de painel de trabalho está arquitetada, escrita, e nunca rodou uma vez.

fichas                                    0   ninguém submeteu
etapa_eventos                             0   nenhum "quem aprovou e quando"
release_etapas com data_alvo   0 de 1.428     o prazo nunca disparou
avisos de entrega enviados                0   subir arquivo não avisa ninguém

Todo estado que existe hoje é backfill de 28/07 — retrato, não fluxo. E o prazo foi a razão declarada de sair do Pages pro Workers.

crítico

A submissão de música não funciona — devolve nao_encontrada

submit.html:1373   const releaseId = 'PHANTOM-' + Math.floor(1000 + Math.random()*9000)
                   ↓ vira body.id do POST
ficha.js:88        SELECT … FROM fichas WHERE id = 'PHANTOM-4823'  → não existe
                   return json({ erro: "nao_encontrada" }, 404)

O artista preenche cinco passos, assina o contrato, clica SUBMETER e lê "nao_encontrada" — código interno cru, em inglês de banco. Nenhum lançamento pode nascer no app. O GreenTech já está logado; se clicar em "+ SUBMETER MÚSICA" hoje, é isso que ele vê.

crítico

O artista não vê o próprio extrato — vê "carregando…" para sempre

app.html:2671    VENDO vem  de ?artista= na URL, nunca de /api/eu
app.html:3541    artista logado → VENDO = null → chama /api/financeiro sem ?artist
financeiro.js:141 → 403 "O fechamento da label é da diretoria"
app.html:3543    if (d.status !== 'success') return;
                 CARREGADA['financeiro'] = true → nunca tenta de novo

O /api/dashboard impõe o projeto pela sessão; o /api/financeiro recusa. Duas rotas irmãs com filosofias opostas, e a tela só sabe lidar com uma.

Não é um erro na cara — é a palavra "carregando…" parada indefinidamente, na tela do dinheiro dele. E o agente do chat responde o valor certo, porque meu_extrato preenche o artist pelo servidor. O app diz duas coisas diferentes sobre o mesmo dinheiro, e a que funciona é a do robô.

crítico

Metade da máquina de estado não tem botão

app.html:3090, 3108, 3125, 3141 — as 4 chamadas de moverEtapa, todas 'aprovar'

As transições de etapa.js:29 estão certas e completas (entregar · aprovar · recusar · reabrir), lendo o estado de origem do banco e não da tela. Só que RECUSAR e REABRIR não existem em lugar nenhum da interface. A rota exige nota obrigatória pra recusar, o texto do aviso existe, o FLUXO-E-AVISOS chama isso de "recusa abre espaço de feedback" — e a única coisa que a equipe pode fazer é aprovar.

Junto, três defeitos que se somam:

  • upload.js:131 move release_etapas por SQL direto — sem gravar etapa_eventos e sem chamar o aviso. O artista sobe a capa e o Bagre só descobre se abrir o painel. E aceita sair de bloqueado: dá pra subir o ZIP de marketing e pular capa e master.
  • app.html:2939temMkt = est('mkt')==='concluido' mede o estado, não o arquivo (capa usa !!r.cover_url, wav usa !!r.tem_master). O botão APROVAR do marketing é impossível de clicar: se não está concluído, desliga com "precisa de pack enviado" mesmo com o ZIP no R2; se está, diz "já aprovado". A coluna inteira da Amanda está morta na tela. Causa raiz: o roster.js devolve tem_master e tem_mp3, mas não tem_mkt.
  • Sem recusa não existe ciclo de correção. Sem ciclo de correção não existe "ajudar o lançamento a acontecer da melhor forma" — existe carimbo.
alto

92 bolinhas vermelhas de trabalho que ninguém vai fazer

D1 release_etapas · app.html:2397 "ONDE ESTÁ PARADO"

capa pendente = 24, wav pendente = 68 — todas de releases PU que já saíram (PU001 BASSO & F5 de 2025, PU007 3 CORPOS de 2026), vários com title = NULL. Mais 76 em mkt/distro cinza.

A primeira tela do painel de trabalho cobra 92 tarefas inexistentes. Isso ensina a equipe a ignorar vermelho — que é a única coisa que um painel de trabalho não pode perder. Quando a esteira de verdade começar, o trabalho real nasce no meio desse ruído.

alto

O perfil ARTISTA não existe na prática

app.html:1728 — setRole(eu.adm ? 'adm' : 'staff'). 'artist' nunca é passado.

setRole('artist') existe e é código morto: quem chamaria é switchUserAccount(), que ninguém chama. O que o GreenTech viu hoje:

AbaO que ele encontrou
Paineldashboard da label com os números dele — /api/dashboard impõe o projeto ✓
Artistaso roster inteiro, 227 artistas — não "meu perfil"
Lançamentosos 350 releases da label, com botão ⚡ GERENCIAR em cada um
Financeiro"carregando…"
Chatsala projeto:greentech
Configescondida ✓

Clicar GERENCIAR num release alheio abre a ficha com botões APROVAR, que o servidor barra corretamente — e a tela mostra a recusa num alert(). É oferecer trabalho que não é dele e depois dar um pop-up de "não pode".

alto

O filtro PH/GM sobreviveu num quarto lugar — e esconde 64 lançamentos do artista

public/app.html:2318 — if (VENDO) lista = lista.filter(x => /^(PH|GM)/.test(x.catalog))

Sua decisão de 29/07 ("são lançamentos VÁLIDOS, só tem outra nomenclatura") tirou o filtro de três lugares. Ele sobreviveu aqui, escondido atrás de if (VENDO) — só aparece quando alguém escopa por artista, que é justamente o caso em que ninguém confere o total. Medido: 65 releases PU, 64 com artista vinculado, todos invisíveis. São os de 2025–2026.

Mais 9 achados de fluxo — entrada, chat e avisos
OndeO quê
bemvindo.html:210"deixar pra depois" não deixa pra depois. salvar(false) faz o mesmo POST+PUT: a ficha vira enviada e trava. retorno.js:252 só manda pro /bemvindo.html quem não tem ficha — a pessoa nunca mais vê aquela tela. CPF é opcional ali e não existe segunda porta pra digitar.
retorno.js:82,172Convite vencido pra quem já tem conta: o convite é zerado, a pessoa entra normalmente e o INSERT INTO pessoa_projeto nunca roda. O projeto novo simplesmente não aparece. Ninguém recebe erro.
login.html:120A tela sabe que o convite morreu ({ok:false}) e mantém o texto genérico. A pessoa faz o OAuth inteiro pra só então ler o erro. E nenhuma das 6 mensagens de recusa tem próxima ação — "fale com a label" sem dizer como, com o contato@ em lugar nenhum da tela.
convite.js:124O WhatsApp digitado no modal de convite do artista é descartado — o INSERT não tem a coluna. O /api/artista grava; o /api/convite não. Dois caminhos, dois comportamentos.
app.html:3956O chat faz setInterval(buscar, 3000) no DOMContentLoaded, independente da aba aberta, e não existe clearInterval no arquivo. 1.200 requisições/hora por aba. 5 pessoas × 8h ≈ 48.000/dia com o chat vazio — metade do free tier do Workers.
app.html:3812Sessão vencida = chat mudo. if (!r.ok) return; a cada 3 segundos, sem nada na tela. A pessoa vê conversa vazia e conclui que quebrou.
avisos.js:75entregar() sem trava de posse — confirmado. SELECT WHERE estado='fila' e só depois o UPDATE: cron e rota podem pegar a mesma linha. Não mordeu porque só há 7 avisos. Morde no primeiro trimestre com movimento.
artista.js:115/api/artista cria com status='ativo' e zero releases; relogio.js:125 pega exatamente ultimo IS NULL. O primeiro artista cadastrado pela tela vira "parado há mais de um ano" no dia seguinte. Bomba armada, ainda não explodida.
cover.jsEscreve UPDATE submissions (0 linhas, sempre), CORS *, id padrão "PHANTOM-8421", e sempre responde "Capa registrada com SUCESSO no banco Cloudflare D1!". Sucesso categórico sobre um UPDATE que não tocou nada.

05 Design system — a gramática não tem token

O veredito curto: não existe design system, existem 11 tokens. Todos numa camada só, cada um guardando um hex cru. Zero token de espaçamento, tamanho de texto, raio, sombra ou duração.

25espaçamentos
20tam. de fonte
10raios
12sombras
289style= inline

E 52,6 KB de CSS inline nas 10 páginas contra 17 KB no phantom-system.css — 3,1× mais CSS fora do sistema do que dentro. É por isso que "o painel parece três produtos", que é um comentário do próprio código.

crítico

Verde é a cor da marca e a cor de "concluído"

app.html — 67 var(--green), 20 btn-green, 11 text-green

btn-green é a ação primária do app inteiro: SOLICITAR SAQUE, CADASTRAR, ENVIAR CONVITE, GERENCIAR, SALVAR, SUBMETER MÚSICA. A regra diz "verde = pode esquecer". O app usa verde pra dizer "faça isso agora".

O caso que resume tudoapp.html:3584, tabela do financeiro:

🔴 não solicitado   …   [ SOLICITAR  ← botão VERDE ]  │
        mesma linha

Quem foi treinado a ler a bolinha aprende, na mesma tela, que verde também significa o contrário. A regra não morre nas bolinhas — morre no botão. O conserto de raiz é separar --brand-accent de --estado-concluido: hoje é o mesmo token fazendo as duas coisas.

alto

Vermelho virou julgamento na aba ARTISTAS

app.html:2098 — VINCULO = { membro:'concluido', recente:'andamento', frio:'pendente' }

frio = "sem lançamento há mais de um ano" cai em pendente, que é vermelho. São 207 artistas em historico contra 21 ativos.

A aba abre como uma parede vermelha onde nenhuma bolinha tem dono nem próxima ação — o oposto da sua lei. E quando 80% da tela é vermelha, o vermelho para de puxar o olho em todas as outras telas. Artista que saiu da label em 2019 não é trabalho pendente; é o cinza "não se aplica", que existe exatamente pra isso.

crítico

A página pública do casting está com os três estados quebrados

public/casting.html:7 (carrega o sistema) + :68 (redeclara .sem)

phantom-system.css   .sem { display:inline-block; width:11px; height:11px; border-radius:50% }
casting.html:68      .sem { color: var(--muted); font-size:13px; padding:14px 0 }
                     ↑ só sobrescreve cor, fonte e padding — os 11px sobrevivem

<p class="sem">carregando…</p>                          → bolinha de 11px
<p class="sem">Erro: …</p>                              → bolinha de 11px
<p class="sem">Nenhum release vinculado…</p>            → bolinha de 11px

É a única página onde carregando, erro e vazio aparecem, e os três estão assim. .sem é nome genérico colidindo com componente do sistema — a classe da página é que tem que mudar.

alto

"REPASSE (50%)" numa conta que não é mais 50%

app.html:3578, 3621, 3631 · web/src/telas/Financeiro.jsx:103, 157, 172 — nos dois front-ends

O número vem certo (SUM(VALOR_ARTISTA), lendo split_pct). O rótulo é fixo. Dois problemas de uma vez: pro Sonic Massala o número é 70% e o rótulo diz 50% — o extrato se contradiz na mesma linha. E na visão da label, os artistas levam 57,2%, não 50.

Não é caso de trocar "50%" por "70%": a taxa é confidencial, e escrevê-la no cabeçalho é publicá-la. É caso de tirar a porcentagem da tela — "SUA FATIA" e "REPASSE AOS ARTISTAS" já dizem tudo e não vazam acordo comercial.

alto

A pendência do seu print continua aberta

app.html:2422 — <span class="text-mono text-xs text-muted">

Os cards do topo (CAPA · MASTER · MKT · DISTRIBUIÇÃO) continuam cinza e não usam o phantom-card--labeled — etiqueta verde na borda + subtítulo, que é o padrão da casa. A mesma correção foi aplicada nos KPIs do painel e nos títulos dos módulos do release hub. Só esse card ficou de fora, e é justamente o da foto.

Mais 13 achados de interface, acessibilidade e nomenclatura
OndeO quê
app.html:1040,1044,1047Aba CONFIG com value="sk_live_spotify_999XXXXXX" e value="EAABxxxxxxxxxxxxxxxx" em campos de senha, e botão que faz alert('✅ Chaves de API salvas com sucesso!'). É a única aba que o limparMaquete() não cobre. Um admin que digitar uma chave real recebe o check verde e acha que gravou.
semáforoExistem dois. .status-dot--* (saque, em inglês) e .sem--* (pipeline, em português), mesmos hexes. O do saque não é <button>, não é filtro, e não é alcançado pela regra de toque de 44px — no celular a bolinha do financeiro tem 11px de alvo.
4 vermelhos, 3 amarelos, 3 verdesVermelho: #ff5f56, #f87171, #f66, #ef4444. Amarelo: #ffbd2e, #facc15, #dea123 — os dois primeiros na mesma tela do financeiro, pro mesmo conceito de prazo. "Uma cor um significado" pressupõe que a cor seja a mesma cor.
6 var() fantasmavar(--verde,#2ecc71), var(--red,#f66), var(--surface-2,#1c1c1c) — tokens que não existem. O fallback é o que sempre pinta. O chat inteiro usa #2ecc71, que contra o --green real dá 1,39:1 — são cores visivelmente diferentes na mesma sidebar, parecendo token.
.sem--bloqueadoContraste 1,76:1 contra o card. WCAG pede 3:1. Cinza é um dos quatro estados da gramática, não é ausência — nessa bolinha de 11px, "bloqueado" e "célula vazia" são indistinguíveis. E bolinha que parece informação e não é, é pior que texto.
dinheirotabular-nums aparece uma vez no projeto inteiro, e é no catalogo.html. Nenhuma coluna de £ usa. Os KPIs grandes usam Inter, que é proporcional — coluna de dinheiro que não alinha na vírgula é o erro clássico de leitura de extrato.
catalogo.htmlÚnica página que não carrega o phantom-system.css. Tem :root próprio com --neon:#c026d3 (magenta) e --cyan:#22d3ee, outra fonte, outro raio. O app linka pra ela de dentro do painel verde-limão. Ironia: é a única com tabular-nums.
4 nomes p/ 4 etapasCAPA·WAV·MKT·DISTR (cabeçalho) · CAPA·MASTER·MARKETING·DISTRIBUICAO (cards do topo) · CAPA·AUDIO·… (release hub) · CAPA·WAV·MKT·DISTRIBUIÇÃO (convite). Os dois primeiros ficam a 40px um do outro chamando a mesma coisa de MASTER e WAV. A regra permite abreviar etapa — não obriga a inventar uma abreviação por componente.
0 <label for>53 <label> e 57 <input> no produto inteiro, nenhum associado. Inclui o submit.html, que é onde o artista digita CPF e nome civil.
:focus-visibleExiste uma vez no projeto. E barraFiltro() escreve outline:none nos cinco controles do cabeçalho — os que decidem de quem são os dados na tela — sem substituto.
app.html10 elementos clicáveis que não são botão: <th onclick> pra ordenar as duas tabelas, <tr onclick> pra abrir release, <div onclick> no avatar. As duas ações mais usadas da aba não existem pro teclado.
app.html:3191,3542,2881Três catch (e) { return; } silenciosos — dashboard, financeiro e ficha. Se a API cair, a aba fica em "carregando…" pra sempre. Num painel de dinheiro isso é pior que erro: parece que o valor é zero.
~450 linhasMaquete morta que vai pro navegador em todo carregamento: R$ 16.450, UTTARI, LUNAR RITUALS, chaves PIX falsas, e 4 fotos do Unsplash carregadas por HTTP externo num app atrás de login. A defesa é uma linha de display:none !important.

06 A virada /novo/app

Recomendação: não virar agora — e o motivo não é "faltam telas". É que a premissa da decisão está vencida.

O ESTADO-ATUAL.md diz "o React tem os botões que movem a esteira; o app.html é leitura pura". Isso deixou de ser verdade em 30/07: o commit de ontem pôs +1.252 linhas no app.html (upload por arrastar, convite de equipe, colunas de perfil, chat com marca de leitura, agregação por família de loja) contra +45 no React.

16app.html ganha
6React ganha
8empate

O que se perde na virada hoje: chat e agente, convite (artista e equipe), cadastro de artista, salvar perfil, upload de arquivo, copiar metadados, botão de sair, filtros do dashboard, agregação por família de loja, deep-link e seletor de escopo. O que se ganha: código modular, esqueleto de carregamento de verdade, e a aba CONFIG honesta.

E três coisas quebram no primeiro dia:

dá pra fazer hoje, sem virar nada

Remover o filtro /^(PH|GM)/ do app.html:2318 e matar a maquete de chave falsa da aba CONFIG. As duas estão no ar mentindo agora.

07 O que aguentou

Testado de verdade, não assumido. Isto é o que está de pé e não precisa de você.

FrenteResultado
A parede do app novo7 telas privadas em 302 pro login, 16 APIs em 401. Variação de caixa, barra sobrando, %2e, // — todas 404 ou 401. Nenhum bypass.
RateioToda expressão de dinheiro importa de rateio.js. A cópia com 0.5 chumbado do saque.js foi de fato removida. split_pct é dado, não código.
SaqueehDono antes de gravar, valor recalculado no servidor, idempotência por (artist_id, period), sem mínimo, PIX digitado na hora. O semáforo vem de royalties, nunca de quem olha.
OAuthstate aleatório em cookie e conferido no retorno. Open redirect fechado nos dois lados com /^\/[^/\\]/ — bloqueia //evil, /\evil e esquema. Cookie HttpOnly; Secure; SameSite=Lax. Logout apaga a linha, não só o cookie. Sessão vencida é limpa na leitura.
SQLTudo em prepared statement com bind. Nenhum LIKE '%nome%' sobreviveu em identidade. Injeção em roster/metrics/convite-info tratada como texto.
R2Acesso público desligado, zero domínio customizado. masters/ responde 404, não 401 — não confirma existência. Travessia de diretório 404. O bug do header Range está corrigido de verdade.
O agenteNenhuma ferramenta de escrita existe. A do artista não tem campo onde escrever o nome de outro, e o artist é imposto pela sessão. extrato_de e fechamento_da_label só entram na lista se pessoa.adm. Injeção de prompt não alcança escrita porque escrita não existe. Erro de cota honesto e específico na tela. (A falha do agente é a rota que ele chama — o ?release=.)
SegredosNenhuma chave real servida ao cliente, nenhum nome de secret, nada de .env rastreado. Histórico do git limpo. A chave do Gemini vai no header, não na URL.
Enumeração/api/convite-info devolve {"ok":false} igual pra código inexistente, expirado e com SQLi. Sem oráculo.

E no teste visual — 16 URLs medidas em desktop 1280×800 e mobile 375×812, Chromium headless, anônimo:

MedidaResultado
Rolagem horizontal no celularzero nas 16 URLs
Imagem quebradazero
null / undefined / NaN vazando no textozero — o esc() faz o trabalho
/login.htmlúnica página sem nenhum achado
404real, não soft-404, com noindex e saídas

Três medidas que não passaram e ficam registradas: 18 alvos de toque menores que 44px na home e 12 nas páginas de artista; contraste 1,91:1 na home e 2,16:1 no manual de marketing; e networkidle de 18,5 a 19,1 segundos em 9 páginas — o conteúdo aparece em ~150 ms, o resto é o áudio do SoundHelix carregando.

08 A ordem que eu seguiria

Ordenado por "quanto dói se ficar mais uma semana", não por esforço.

#O quêPor quê
1Apagar o projeto Pages phantom-release-hub176 portas abertas com o financeiro da label e a taxa confidencial. O D1 e o R2 ficam de pé — some só o código velho. É sua decisão, eu não toco.
2Varrer o roster.js inteiro: lista explícita de colunas no ramo ?release=, tirar gbp de faixas e lojas, tirar whatsapp das duas consultas de funcoesTerceira repetição do mesmo bug no mesmo arquivo. Faturamento e telefone da equipe, sem login.
3Limpar as páginas públicas: UTTARI, os 4 downloads falsos, o SoundHelix, o pré-save de mentira, o booking@ errado, e os 11 picsum.photos da homeÉ o material que vai pra contratante e imprensa, em 227 press kits e 350 releases.
3bPreencher os 39 releases com title: null no bancoUma correção de dado que conserta 14 dos 21 press kits do casting de uma vez — sai mais barato que qualquer conserto de front-end. E tirar a coluna ROYALTY do release-page.html antes de consertar o setMode.
4Mover a checagem de posse pro topo do handler no financeiro.js, antes de qualquer ramificação por parâmetroFoi ramificar antes de checar que abriu o CSV. Fecha a classe, não o caso.
5Devolver o revincular: true — ou parar de devolver o link na respostaStaff comum vira admin em 3 passos. Uma das duas basta.
6Consertar a submissão (submit.html:1373 parar de mandar id) e o extrato do artista (VENDO cair pro projeto da sessão)O GreenTech está lá dentro. São as duas telas que ele mais vai tentar usar.
7CREATE UNIQUE INDEX ON royalties(artist_id, period) — depois de resolver as 2 linhas duplicadasGBP 378,10 fantasma e a porta pra dois PIX no mesmo trimestre.
8Dar escritor pro faixa_artistas (o ficha.js já recebe o array e joga fora)Sem isso, todo lançamento novo vira dinheiro órfão um trimestre depois.
9Separar --brand-accent de --estado-concluido e criar os 4 --estado-*São 5 tokens que sozinhos resolvem o verde-ambíguo, os 10 hexes de semáforo, os var() fantasma e o cinza invisível. É a diferença entre a sua gramática ser lei que o CSS cobra ou comentário no topo do arquivo.
10Versionar o schema.sql real dentro de phantom-app/repo/Dez minutos de SELECT sql FROM sqlite_master. Hoje o dado volta do backup; a estrutura, não.

09 Documentação que envelheceu

Cinco pendências que os documentos ainda cobram e que já estão resolvidas — e quatro números que ficaram pra trás. Vale corrigir: pendência falsa custa atenção.

Documento dizRealidade hoje
"a Amanda não tem como ser convidada"Resolvido. A tela existe (app.html:1440), com seletor de papel, sem artist_id, e o texto certo: "Sem projeto musical: é acesso de trabalho".
"formulário mostra erro vermelho de já existe"Resolvido. app.html:1674 trata criado:false em verde: "Associado ao projeto que já existia. Nada foi duplicado."
"app.html é leitura pura"Vencido. moverEtapa existe em app.html:3487, e o upload por arrastar só existe lá.
"ninguém logou e conversou ainda"21 mensagens no chat (5 do agente, 2 de falha real) e um artista logou hoje.
"9 etapas" no PLANO-PAINEL-DE-TRABALHOSão 4 (capa·wav·mkt·distro). Os 9 são os avisos do relogio.js.
"1 release sem dono"São 3: EP Introspectime, SIMIAN e PROG NIGHT.
"2 linhas de sales trocadas"São 6, em 4 trimestres. Somam GBP 0,008.
"migrations até a 016" · "45.688 vendas, GBP 12.266,06"Migrations 022. 48.223 vendas, GBP 13.252,58, 27 trimestres. O ESTADO-ATUAL.md está um dia atrás em tudo que é número; o phantom-app/CLAUDE.md já está certo.

10 A leitura de fundo

Três dos quatro achados graves de segurança são a mesma falha reincidindo: corrigir uma consulta não corrige as outras do mesmo arquivo.

O roster.js já levou duas correções por isso e o ramo ?release= continua aberto. O financeiro.js ganhou ehDono no extrato e o CSV, dez linhas acima, ficou. A lição está escrita em caixa alta em dois documentos e não virou verificação.

O que fecharia a classe inteira, e não os casos:

o que é justo dizer

O motor de dinheiro deste app é bom de verdade. Uma definição só em rateio.js, fatia como dado e não como código, faixa_artistas como camada de identidade antes da soma, prova de conservação fechando ao centavo, ehDono em toda rota de dinheiro, e segurança do agente por ausência de ferramenta em vez de instrução. Isso é engenharia sênior e não vou fingir o contrário.

O que falta não é qualidade — é ligar na tomada o que já está construído, e fechar a porta que ficou aberta atrás. A esteira inteira existe em código e nunca rodou uma vez.