Brava Dashboard

Painel administrativo da Cidade Brava, um servidor de roleplay em FiveM. Treze telas para quatro pessoas. A parte que vale contar não é a interface: é o que ficou entre uma página na Vercel e um servidor de jogo que roda numa máquina doméstica, sem IP público, em Lua.

Papel
Painel, API e integração
Contexto
Cliente, servidor de um time de três
Situação
Em uso interno, com limites conhecidos
Acesso
Privado, atrás de login

Quatro usuários, e isso muda tudo

A Cidade Brava é um servidor de GTA roleplay. Quem administra precisa saber quem está online, quanto dinheiro circula na economia do jogo, quais organizações estão ativas, e o que a equipe de moderação andou fazendo. Tudo isso já existia dentro do jogo, espalhado por comandos de console, um tablet in-game e o F8. Nada disso se consulta do celular, nem de fora do PC do servidor.

O público é de quatro administradores. Esse número é a premissa mais importante do projeto, e está escrita no plano como coisa a não revisitar. Ele derruba de uma vez cargos, permissões granulares, convite por e-mail, onboarding, suporte a navegador antigo e otimização mobile. Um painel para quatro pessoas conhecidas, em desktop, é um problema diferente de um painel para mil desconhecidos, e fingir o contrário custaria semanas.

Metade do plano é uma lista do que não seria feito: chat da equipe, mapa de calor, assistir jogador ao vivo, catálogo de comandos, chamados, empregos, busca global, notas de moderação sobre jogador. Tudo isso ou já funciona melhor dentro do jogo, ou é operação de suporte que ninguém ali pediu. A lista de vetos é mais longa que a de telas.

O servidor é de um time de três. Este case cobre o painel web, a API que o alimenta e a integração entre os dois, que foram meus. Os scripts de jogo em Lua que já existiam no servidor não são meus e não estão contados aqui, com uma exceção que aparece na seção IV, onde precisei mexer em código de outra pessoa para a integração funcionar.

Um traço vale mais que um número bonito

Quando um dado não existe, a tela mostra um traço. Não mostra zero, não cai no dado de exemplo, não estima. E três indicadores da home continuam assim hoje, à vista de todo mundo.

O painel nasceu como maquete visual: treze telas com dados de exemplo escritos à mão, para validar layout e navegação antes de tocar no servidor. Isso funcionou bem, e deixou uma armadilha. Quando os dados reais chegaram, a home continuava com sete blocos de números fixos no código, herdados da maquete. A tela parecia ligada e não estava.

A saída fácil seria manter o dado de exemplo como reserva: se a API não responder, mostra o que a maquete mostrava. A tela nunca fica vazia e ninguém reclama. É exatamente por isso que é ruim. Um painel que sempre tem número não pode ser usado para decidir nada, porque quem olha não distingue o dado do enfeite.

Então os sete blocos saíram e o traço entrou. Sessões da semana, jogadores ativos na semana e receita em real aparecem como traço até hoje: os dois primeiros dependem de um registro de sessão que o servidor não guarda, o terceiro depende de um meio de pagamento que ainda não existe. O painel diz que não sabe, em vez de inventar. É a mesma regra que o resto deste portfólio segue, e aqui ela está dentro do produto.

Um adapter fino, de propósito

O servidor de jogo já tinha cerca de setenta funções administrativas escritas em Lua, com as consultas ao banco prontas. A questão era como alcançá-las de fora.

  1. 01 Nada de regra de negócio na API O recurso Lua que criei só faz autenticação, roteamento e a chamada da função correspondente. Não duplica uma linha de SQL. Se o administrativo do servidor evoluir, o painel herda de graça, e não existe uma segunda versão da mesma consulta divergindo em silêncio.
  2. 02 Um túnel no lugar de um IP O servidor de jogo roda numa máquina doméstica atrás de NAT, sem IP público e sem TLS, porque o HTTP do FiveM é texto puro. Um túnel da Cloudflare resolve os três de uma vez: DNS, certificado e travessia de NAT, sem abrir porta no roteador de ninguém.
  3. 03 Um hook por endpoint, com reserva Toda tela lê por um hook próprio, e o hook conhece três caminhos: modo demonstração, dado real, e cache em memória quando a API falha. Permitiu migrar tela por tela em vez de trocar tudo de uma vez, e continuar desenvolvendo com o servidor desligado.
  4. 04 Gráficos escritos à mão Seis tipos de gráfico em SVG, sem biblioteca. Não é bravata: são séries pequenas com estilo muito específico, e a menor das bibliotecas conhecidas pesaria mais que os seis juntos.

Três bugs que só existem na junção

No dia de ligar o painel em produção, nada apareceu. As três causas estavam encadeadas, e nenhuma delas aparece testando os dois lados separados.

As requisições ficavam penduradas para sempre. O framework de RPC do servidor tem dois transportes independentes: um para o jogo falar com o servidor, outro para um recurso falar com outro recurso. O administrativo só tinha registrado o primeiro, porque foi escrito pensando só em uso dentro do jogo. Minha API chamava pelo segundo, o evento saía e ninguém escutava. Não dava erro, não dava timeout: a chamada esperava uma resposta que nunca viria. A correção foi uma linha, registrar a mesma tabela de funções nos dois transportes, e o custo foi mexer em código que não era meu.

Resolvido o transporte, tudo respondia negando acesso. Cada função administrativa começa identificando quem chamou e conferindo o cargo. Numa chamada entre recursos não existe jogador do outro lado, então esse identificador vem zerado, e todas elas saíam pela porta do não autorizado. O padrão que apliquei nas dezessete funções usadas pelo painel deixa o caminho do jogador booleanamente idêntico ao original e libera só o caso da origem zero, que é a chamada interna. Ela já tinha sido autenticada antes, na borda HTTP.

Aí o front começou a quebrar ao percorrer as listas. As funções do administrativo já devolvem um objeto com a lista dentro, e meus handlers embrulhavam isso outra vez num objeto com o mesmo nome de campo. O front recebia a lista dois níveis abaixo de onde esperava e tentava iterar sobre um objeto. Só três endpoints tinham o problema: nos outros, o aninhamento era intencional e o front já contava com ele. Corrigir por atacado teria quebrado os que estavam certos.

A lição que ficou escrita no repositório: quando um recurso vai ser consumido por outro recurso, registrar os dois transportes desde o começo, porque custa duas linhas e a falha se manifesta como espera infinita, que é o sintoma mais caro de diagnosticar. E o que eu faria diferente: um teste de fumaça atravessando a pilha inteira, do HTTP até o banco, antes de migrar treze telas. Os três bugs eram de junção, e eu tinha testado os dois lados separados.

Três autenticações, duas jogadas fora

A parte do projeto em que mais escrevi código que não sobreviveu, e a que mais vale contar.

Primeira: o identificador do jogo

Login com o número interno que o jogo usa para o jogador, mais senha. Funcionava, e era ruim para gente: um número opaco não se lembra, não se recupera, e amarra a conta de administrador à existência de um personagem no jogo. Trocado por e-mail e senha.

Segunda: painel próprio, em Lua

Tabela própria de administradores, hash com sal calculado no banco, sessões com token, e um comando de console para cadastrar, resetar senha e revogar. Zero dependência externa. Implementado inteiro e documentado.

A pergunta que derrubou

Logo depois de pronto, uma pergunta: por que console, se existe serviço gerenciado? Esqueci minha senha vira ir até a máquina do servidor e rodar um comando. Convidar alguém vira construir uma tela de cadastro. O custo estava no dia a dia, não na implementação, e a implementação era a única parte que estava pronta.

Terceira: identidade gerenciada

A tabela própria, o comando de console e o arquivo inteiro foram apagados no dia seguinte. Recuperação de senha, cadastro e registro de acesso passaram a ser problema de outra pessoa. O front já mandava e-mail e senha desde a etapa anterior, então não precisou de uma linha.

Verificar sem chave compartilhada

O Lua valida o token perguntando ao provedor, com resposta em cache por cinco minutos, em vez de verificar a assinatura localmente. Verificar local seria um dia de trabalho, exigiria uma chave no arquivo de configuração do servidor e um HMAC escrito à mão. A chamada em cache é mais simples e tem menos superfície para errar.

O preço, dito antes de cobrar

Passou a existir uma dependência externa: se o provedor cair, ninguém entra. O cache de cinco minutos limita o estrago a login novo e renovação de token, quem já está dentro continua. Está escrito como decisão consciente, não descoberto depois.

O que existe hoje

Telas
13
Com dado real
9
Decisões registradas
30
Usuários
4

Também: catorze processos do servidor monitorados ao vivo, seis tipos de gráfico escritos em SVG, e um pacote de 186 KB comprimido. As trinta decisões estão num arquivo do repositório, cada uma com o motivo e, onde cabe, a condição para desfazer.

O que não está pronto, e por quê. O túnel sobe numa janela de terminal aberta na máquina do servidor: instalá-lo como serviço exige privilégio administrativo local que eu não tenho ali. Dois endpoints de baú continuam servidos por dado de exemplo, esperando o lado do servidor. Três indicadores mostram o traço da seção II. Nada disso foi auditado em acessibilidade e o layout responsivo nunca foi testado em aparelho físico: são limites aceitos para um painel de quatro pessoas em desktop, e ficam escritos aqui pelo mesmo motivo que ficam escritos no repositório.

  • React 19
  • TypeScript
  • Vite 6
  • Tailwind CSS 4
  • Lua
  • FiveM
  • MariaDB
  • Supabase Auth
  • Cloudflare Tunnel
  • Vercel

Painel privado, sem link público. Quer ver o resto do trabalho, ou conversar sobre uma vaga?

Atualizado em 5 de agosto de 2026