Phantom Unit OS · auditoria geral
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.
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.
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.
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.
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.
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.
nenhuma rota escreve payout_status='paid'
O saque.js grava 'requested' (🟡, bola com o financeiro). Daí pra
frente, nada: financeiro.js só lê 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 que você desenhou | • | O que existe hoje |
|---|---|---|
| Cadastro em 3 níveis encadeados | o formulário coleta os três, mas o envio morre em 404 nao_encontrada | |
| Contrato assinado | dois checkboxes. nada é gerado nem guardado | |
| Cria perfil do artista + página pública | existe, mas 14 de 21 páginas quebram por title:null | |
| Cria perfil da música + página pré-save | existe; o pré-save é maquete e "confirma" sem salvar nada | |
| Cria o grupo no zap automaticamente | zero linhas. só sabe receber de grupo existente | |
| Dispara pro master com o link do wav | e-mail com link genérico pra aba #musicas | |
| Dispara pro designer com o briefing | o briefing é salvo e nunca lido pelo aviso | |
| Bolinha = de quem é a vez | o banco tem os 4 estados; a tela lê como progresso, não como posse | |
| Staff faz ou avalia, e sobe | o upload move a etapa por SQL direto, sem avisar ninguém | |
| Rejeita → feedback por zap e e-mail | a rota existe e exige nota; não há botão, e o canal zap não existe | |
| Artista refaz → bola volta pro laranja | consequência do anterior: o ciclo nunca fecha | |
| 2 verdes destravam mkt e distro | funciona, na mesma transação, e avisa os dois | |
| Mkt baixa, cria o pack, sobe | mkt/ nasceu público e o registro estoura no CHECK; o APROVAR é inclicável | |
| Distro baixa, distribui, sobe | só aprova; não define o que sobe como prova | |
| Fecha o upload no lançamento | não existe trava. dá pra trocar arquivo depois de distribuído | |
| Dia do lançamento → todas as lojas | a data existe e guia; a publicação é manual, por fora | |
| Pré-save vira smartlink com embeds | zero 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ário | desenhado até o custo em reais, nada construído | |
| Trimestre → calcula → avisa | o cálculo fecha ao centavo; a importação é script manual | |
| Artista solicita com o PIX | funciona — dono conferido, valor recalculado no servidor, PIX não guardado no perfil | |
| Financeiro confere, paga e aprova | nenhuma rota escreve paid | |
| Financeiro devolve quando está errado | não existe | |
| Botão de chamar no zap | existe; um deles ainda aponta pro número de teste |
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.
Duas coisas aconteceram hoje que reordenam a fila inteira, e nenhuma das duas está escrita em documento nenhum.
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…".
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.
Tudo neste bloco foi confirmado por requisição anônima, sem login, hoje. Nenhum é teórico.
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.
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.
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.
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.
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.
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.
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.
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=naoexiste → countdown falso rodando, e os
botões internos MODO PRÉ-SAVE / MODO PÓS-LANÇAMENTO visíveis ao públicoFecha 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.
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.
| Onde | O 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:42 | mkt/ 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:53 | SELECT * + 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,574 | esc() 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:131 | A 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ótipo | admin.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. |
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.
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.
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.
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.title — o 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.
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.
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.
| Arquivo | Nome oficial | Apelido | Slug | Se não achar |
|---|---|---|---|---|
| rateio.js:62 (canônico) | ✓ | ✓ | ✓ | null |
| financeiro.js:103 | ✓ | ✗ | ✓ | null |
| saque.js:65 | ✓ | ✗ | ✗ | fabrica 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.
| Onde | O quê |
|---|---|
| artists.royalty_gbp | Inflado 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_gbp | Congelou 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:101 | Có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. |
| sales | Sem í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. |
| 022 | O 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órmula | Registrado 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. |
"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.
nao_encontradasubmit.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ê.
app.html:2671 VENDO vem só 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ô.
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:2939 — temMkt = 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.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.
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:
| Aba | O que ele encontrou |
|---|---|
| Painel | dashboard da label com os números dele — /api/dashboard impõe o projeto ✓ |
| Artistas | o roster inteiro, 227 artistas — não "meu perfil" |
| Lançamentos | os 350 releases da label, com botão ⚡ GERENCIAR em cada um |
| Financeiro | "carregando…" |
| Chat | sala projeto:greentech ✓ |
| Config | escondida ✓ |
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".
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.
| Onde | O 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,172 | Convite 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:120 | A 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:124 | O 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:3956 | O 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:3812 | Sessã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:75 | entregar() 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.js | Escreve 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. |
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.
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.
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 tudo — app.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.
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.
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.
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.
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.
| Onde | O quê |
|---|---|
| app.html:1040,1044,1047 | Aba 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áforo | Existem 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 verdes | Vermelho: #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() fantasma | var(--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--bloqueado | Contraste 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. |
| dinheiro | tabular-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 etapas | CAPA·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-visible | Existe 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.html | 10 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,2881 | Trê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 linhas | Maquete 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. |
/novo → /appRecomendaçã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.
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:
Artistas.jsx:122 manda stage_name; Musicas.jsx:40 compara
com donos, que vem do banco como slug.
"back2beat,parode".includes("Back2Beat") é sempre falso —
"VER DADOS" mostra lista vazia pra todo mundo, sempre./login.html na barra.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.
Testado de verdade, não assumido. Isto é o que está de pé e não precisa de você.
| Frente | Resultado |
|---|---|
| A parede do app novo | 7 telas privadas em 302 pro login, 16 APIs em 401. Variação de caixa, barra sobrando, %2e, // — todas 404 ou 401. Nenhum bypass. |
| Rateio | Toda 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. |
| Saque | ehDono 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. |
| OAuth | state 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. |
| SQL | Tudo em prepared statement com bind. Nenhum LIKE '%nome%' sobreviveu em identidade. Injeção em roster/metrics/convite-info tratada como texto. |
| R2 | Acesso 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 agente | Nenhuma 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=.) |
| Segredos | Nenhuma 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:
| Medida | Resultado |
|---|---|
| Rolagem horizontal no celular | zero nas 16 URLs |
| Imagem quebrada | zero |
null / undefined / NaN vazando no texto | zero — o esc() faz o trabalho |
/login.html | única página sem nenhum achado |
| 404 | real, 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.
Ordenado por "quanto dói se ficar mais uma semana", não por esforço.
| # | O quê | Por quê |
|---|---|---|
| 1 | Apagar o projeto Pages phantom-release-hub | 176 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. |
| 2 | Varrer o roster.js inteiro: lista explícita de colunas no ramo ?release=, tirar gbp de faixas e lojas, tirar whatsapp das duas consultas de funcoes | Terceira repetição do mesmo bug no mesmo arquivo. Faturamento e telefone da equipe, sem login. |
| 3 | Limpar 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. |
| 3b | Preencher os 39 releases com title: null no banco | Uma 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. |
| 4 | Mover a checagem de posse pro topo do handler no financeiro.js, antes de qualquer ramificação por parâmetro | Foi ramificar antes de checar que abriu o CSV. Fecha a classe, não o caso. |
| 5 | Devolver o revincular: true — ou parar de devolver o link na resposta | Staff comum vira admin em 3 passos. Uma das duas basta. |
| 6 | Consertar 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. |
| 7 | CREATE UNIQUE INDEX ON royalties(artist_id, period) — depois de resolver as 2 linhas duplicadas | GBP 378,10 fantasma e a porta pra dois PIX no mesmo trimestre. |
| 8 | Dar 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. |
| 9 | Separar --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. |
| 10 | Versionar 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. |
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 diz | Realidade 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-TRABALHO | Sã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. |
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 bloco VITRINE do roster.js é o padrão certo;
falta aplicá-lo aos outros três SELECT do mesmo arquivo.0.5 chumbado, no filtro PH/GM (quatro lugares) e agora no
resolverArtista (três implementações). O rateio.js provou que a solução
funciona; falta estendê-la a resolverArtista, janelaAberta e à chave de faixa.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.