Sitemap · Nommand Loadbalance

Arquitetura de informação do dashboard

Mapa completo do produto: as rotas do app, o menu lateral, os links entre telas, o fluxo diário por persona, os atritos de acesso e a regra de permissões da API. Inventário conferido contra o código — cada linha aponta para o arquivo que a declara.

28Rotas
13Itens de menu
4Seções
3Personas
8Bloqueantes
10Assimetrias
9Divergências do mapa

Mapa geral

A árvore completa do produto. /app/* = área autenticada (MainLayout) · /auth/* e /login = sessão · o resto = sem shell.

Loadbalance ├── sem shell · público │ ├── / → /app/overview │ ├── /login · cartão único "Entrar com a Conta Nommand" │ ├── /auth/logged-out · confirmação de logout │ ├── /auth/no-access · login recusado (motivo) │ └── * · 404 (mostra o endereço pedido) │ ├── /app · MainLayout + AuthGuard │ ├── Monitoramento · leitura do tráfego │ │ ├── /app/overview · Visão geral [todos] │ │ ├── /app/requests · Logs de requisições [todos] │ │ ├── /app/metrics · Métricas [todos] │ │ └── /app/insights · Insights [todos] │ │ │ ├── Configuração · como o proxy roteia │ │ ├── /app/keys · Chaves de API [ADMIN] │ │ ├── /app/usage-limits · Limites de uso [ADMIN] │ │ ├── /app/configs · Configs de URL [todos, escrita ADMIN] │ │ ├── /app/client-keys · Chaves de URL [ADMIN] │ │ ├── /app/groups · Grupos de chaves [ADMIN] │ │ │ ├── /app/groups/new · novo grupo │ │ │ └── /app/groups/:id · editar grupo │ │ ├── /app/config-groups · Grupos de config [ADMIN] │ │ └── /app/oauth · OAuth [ADMIN] │ │ │ ├── Nomad · área da plataforma │ │ └── /app/platform · Plataforma [platform admin] │ │ │ ├── Diagnóstico · leitura do próprio app │ │ └── /app/arquitetura · Arquitetura [todos] │ │ │ ├── /app/account · Minha conta ÓRFÃ — sem menu, sem link │ │ │ └── removidas · ainda respondem, caem em outra tela │ ├── /app/settings → /app/account │ ├── /app/organization → /app/client-keys (admin) | /app/overview │ └── /app/brand → /app/overview │ └── redirects legados · 15 segmentos × 2 formas └── /overview, /requests, /keys, /usage-limits, /client-keys, /configs, /groups, /config-groups, /oauth, /metrics, /insights, /settings, /organization, /account, /arquitetura → /app/ (preserva query e hash)

Rotas

Fonte: frontend/src/app/router.tsx. Guard = quem passa. Menu = se aparece na lateral.

PathTelaGuardNo menuO que a pessoa faz aqui
/app/overviewVisão geraltodos✓ MonitoramentoPainel do dia: KPIs, saúde do pool, atividade, top modelos e chaves.
/app/requestsLogs de requisiçõestodos✓ MonitoramentoCada requisição que passou pelo proxy, com filtros, exportação e feed ao vivo.
/app/metricsMétricastodos✓ MonitoramentoLatência, custo, atividade e saúde em série temporal.
/app/insightsInsightstodos✓ MonitoramentoLeitura narrativa do período: o que mudou, onde errou, quem consome.
/app/keysChaves de APIADMIN✓ ConfiguraçãoCredenciais dos providers: criar, conectar, desabilitar e inspecionar.
/app/usage-limitsLimites de usoADMIN✓ ConfiguraçãoJanelas de uso dos planos (5h/semanal) por conta de provider.
/app/configsConfigs de URLtodos*✓ ConfiguraçãoAs URLs do proxy: cada uma aponta para um conjunto de chaves.
/app/client-keysChaves de URLADMIN✓ ConfiguraçãoAs chaves sk-lb- que o cliente manda para chamar uma Config de URL.
/app/groupsGrupos de chavesADMIN✓ ConfiguraçãoEstratégia de balanceamento: como as chaves de um grupo são escolhidas.
/app/groups/newNovo grupoADMIN—Criar um grupo de chaves: nome, estratégia e chaves que entram nele.
/app/groups/:idEditar grupoADMIN—Editar um grupo: membros, ordem das chaves e estratégia de escolha.
/app/config-groupsGrupos de configADMIN✓ ConfiguraçãoAgrupamentos de Configs de URL, para organizar por ambiente ou cliente.
/app/oauthOAuthADMIN✓ ConfiguraçãoContas de provider que entram por autorização externa em vez de chave manual.
/app/platformPlataformaplatform✓ NomadVisão da Nomad: todas as orgs, uso agregado, saúde, auditoria e pool compartilhado.
/app/arquiteturaArquiteturatodos✓ DiagnósticoO mapa do loadbalance: rotas, menu, links, fluxo diário e onde o acesso é ruim.
/app/accountMinha contatodos✗ órfãPerfil (leitura), chaves pessoais sk-lb- e aparência do app.
/app/settingsConfigurações (removida)——→ /app/account · config de instância é env no deploy.
/app/organizationOrganização (removida)——→ /app/client-keys (admin) ou /app/overview.
/app/brandDesign System (removido)——→ /app/overview.
/app/brand/*Design System (removido, wildcard)——→ /app/overview. não documentado no mapa router.tsx:239

Rotas fora do shell

PathTelaPapel
/Raiz → Visão geralpúblico
/loginEntrar com a Conta Nommandpúblico
/auth/logged-outVocê saiupúblico
/auth/no-accessSem acesso ao Load Balancerpúblico
/signup · /invite/:token · /accept-invite→ /login (removidas, LB-CONTA-ONLY-01)público
*Página não encontradapúblico

Telas

O que cada tela faz e o que consome. Ações em negrito são o que a pessoa sai dali tendo feito.

TelaAções principaisAPIs consumidasEstados tratados
Visão geralVer KPIs · troca de período · saúde das chaves · top modelos/api/stats · /api/metrics/advanced · /api/keys/usagecarregando · vazio · zerado sem aviso (MEMBER)
Logs de requisiçõesFiltrar · abrir detalhe · exportar · feed ao vivo · excluir/api/requests · /summary · /facets · /export · /ws/requestscarregando · vazio · 403 no excluir (MEMBER)
MétricasLatência por período · custo por modelo · atividade por bucket/api/metrics/latency · /costs · /healthcarregando · painel de saúde ausente (MEMBER)
InsightsLer narrativa · blocos de erro · comparar por chave/URL/modelo/api/metrics/insightscarregando · vazio
Chaves de APICriar provider · testar conexão · desabilitar · duplicar · rotacionar segredo/api/keys · /api/providerscarregando · vazio · segredo novo descartado em silêncio
Limites de usoVer janela 5h · janela semanal · editar corte por chave/api/keys/usage-limitscarregando · vazio
Configs de URLCriar Config · vincular chaves e grupos · ativar · duplicar/api/configs · /api/config-groupscarregando · botões somem sem aviso (MEMBER)
Chaves de URLEmitir chave de serviço · emitir chave pessoal · revogar · copiar segredo/api/organizations/:id/service-keyscarregando · vazio
Grupos de chavesCriar · editar · estratégia de balanceamento · reordenar/api/groupscarregando · vazio
Grupos de configCriar · vincular Configs · excluir/api/config-groupscarregando · vazio
OAuthIniciar autorização · acompanhar status · conectar GitHub/Codex/Copilot/api/oauth/* · /api/copilot/*carregando · polling
PlataformaPanorama · listar orgs · saúde dos serviços · auditar ações/api/platform/*carregando · vazio
Minha contaPerfil · criar/revogar chaves pessoais · tema e paleta/api/account · /api/organizations/:id/service-keyscarregando · órfã: sem caminho de navegação

Fluxo diário

Três personas. O que cada uma alcança, o que não alcança, e onde trava.

PersonaAlcançaNão alcançaOnde trava
ADMIN da org12 telas—O caminho "chave com erro → cliente quebrado" atravessa 4 telas sem atalho. E o admin da org não destrava uma chave presa no limite (a ação é da plataforma).
MEMBER6 telas7 telas de adminTrês painéis aparecem ZERADOS sem explicação. A única tela onde pode agir (/app/account) não tem caminho de navegação. O botão "Excluir" dos logs aparece e leva 403.
Platform admin5 telas + Plataforma—O guard da Plataforma devolve "Página não encontrada" em vez de "sem permissão".

O dia do ADMIN — 13 passos

#AçãoOndeAtrito
1Entrar/loginSó pela Conta Nommand: 1 clique e um salto para fora da SPA.
2Olhar os KPIs/app/overview/api/metrics/advanced = 9 agregações em polling de 60s. O primeiro paint demora.
3Ver "Estado das chaves" zerado/app/overviewMEMBER vê o painel vazio sem aviso.
4Ir aos Logs/app/requestsA Visão geral não linka uma requisição específica. Começa do zero.
5Filtrar e abrir o detalhe/app/requests5 dos 13 filtros não chegam ao servidor (latência, tokens, streaming, IP).
6Ir para a chave que falhou/app/keysSalto de contexto: copiar o nome, sair, achar "Chaves de API".
7Desabilitar a chave/app/keys/api/stats traz TODAS as chaves inteiras — "dezenas de MB", admite o backend.
8Conferir o limite do plano/app/usage-limitsNão há atalho do log nem do card de status. Beco sem saída.
9Criar a chave nova/app/keysCriar a chave não resolve: falta Config de URL, grupo e sk-lb-. E rotacionar o segredo é aceito e descartado.
10Criar a Config/app/configsA tela bifurca por papel dentro do render: MEMBER vê a lista sem botões e sem aviso.
11Emitir a sk-lb-/app/client-keysNome ambíguo ao lado de "Chaves de API".
12Copiar a configuração de uso/app/configsCopia um PLACEHOLDER. A chave real só apareceu uma vez, em outra tela.
13Testar/app/requestsNão há "testar" na tela de configs.

Contagem: ~12 cliques para levar um cliente novo do zero à primeira requisição passando, com 1 troca de contexto obrigatória (configs → chaves de URL).

Atritos

8 bloqueantes, 10 assimetrias entre tela e permissão. Ordenados por gravidade. A tabela original do app diz 6 bloqueantes — a auditoria por eixo achou 2 a mais.

6 bloqueantes

#AtritoSeveridadeComo se manifesta
1Rotacionar o segredo de uma chave de providerbloqueanteO form oferece "Nova chave (deixe vazio para manter)", o schema Zod aceita key e a rota repassa o body inteiro — mas o backend descarta o segredo em silêncio (filtra por lista branca sem key). O toast diz que salvou, a chave continua com o segredo antigo e o cliente continua falhando. Para rotacionar de verdade é preciso revogar e criar de novo — e nada na interface diz isso.
2Filtrar os logs por latência, tokens, streaming ou IPbloqueanteA UI monta 15 parâmetros (filters.ts:125-150) e a lista envia só 9 (useRequests.ts:32-47). Ficam de fora 6: minLatency, maxLatency, maxTokens, streaming, ip e sort. O backend aceita os 5 de dados (routes/requests.ts:140-145) — é o front que não manda. O badge conta-os como ativos ("3 filtros ativos"), mas a lista não muda. Os KPIs do topo e a exportação respeitam os filtros, então o resumo e a lista discordam entre si.
3Apagar requisições selecionadasbloqueanteA barra de ação em lote aparece para qualquer pessoa que selecione linhas — nunca checa o papel. Mas DELETE /api/requests exige ADMIN. O MEMBER vê o botão, clica e leva 403 com toast genérico: "Excluir" simplesmente não funciona, sem dizer por quê.
4Destravar uma chave presa em limite de usobloqueanteA ação chama POST /api/platform/keys/:id/usage-limit/reset — rota da PLATAFORMA. O admin da organização não tem esse papel, então a opção some do kebab (e a rota devolve 404, não 403). Na prática, um incidente de produção depende de escalar para a Nomad.
5Criar uma chave pessoal sk-lb- para meu clientebloqueanteA única tela onde o MEMBER pode MUTAR chaves é /app/account — e ela não está no menu. O item "Minha conta" do topbar depende do backend mandar accountLinks, e ele não manda. Três telas apontam para lá — todas levando a uma página que ninguém alcança.
6Entender por que o painel mostra "Estado das chaves" zeradobloqueanteBlocos ADMIN no backend vivem em telas abertas ao MEMBER. correção O painel de chaves não aparece zerado — ele é omitido: OverviewPage.tsx:138 passa showKeys={isAdmin} e ModelsAndKeysSection.tsx:140 retorna só a lista de modelos. O front desliga a query — não há 403, não há erro, e nem um aviso de que a área é restrita. O usuário conclui que o dado sumiu. O mesmo padrão em Métricas: o painel de balanceamento é omitido, mas a descrição da página adapta o texto ("...e tokens das suas URLs" em vez de "...distribuição entre chaves") — ali existe um aviso implícito.

10 assimetrias entre tela e permissão

TelaO que a interface prometeO que o backend permite
Visão geralPainel de estado das chavesMEMBER não vê o painel (é omitido, não zerado) — sem aviso, sem 403, sem erro.
MétricasPainel de saúde e balanceamentoMEMBER não vê o painel de balanceamento, mas a descrição da página muda ("...e tokens das suas URLs"). correção Não é o "buraco silencioso" que a tela afirma.
Configs de URLEditar, ativar, duplicarMEMBER vê a lista e nenhum botão — sem mensagem de que a edição é restrita.
Logs de requisiçõesFiltro por chaveO filtro some para MEMBER, sem aviso.
Logs de requisiçõesBotão "Excluir"Aparece e leva 403 — um botão que sempre falha, sem mensagem.
Chaves de APICampo "Nova chave"Aceita o segredo novo e o backend o descarta em silêncio.
Chaves de API"Liberar limite de uso"Admin da org não consegue — a opção some e a rota devolve 404.
Minha contaCriar chave pessoalA única ação do MEMBER fica sem caminho de navegação.
Membros da organizaçãoComentário diz que MEMBER lêComportamento real: 403. Contradição documental.
Auditoria—"Quem desabilitou minha chave?" não tem resposta: a API está pronta e ninguém consome.

Contraste: o que a tela diz e o que a medição dá

A tela afirma "contraste do texto AA aprovado nas 11 paletas × 2 temas (axe, 195 verificações)" e "foco visível uniforme (ratio 4.86)". contestado A auditoria recalculou: 440/440 combinações de token de texto × superfície passam 4.5:1 — o número real é 440, não 195. E o foco visível varia de 3.39:1 a 15.66:1 entre paletas, não 4.86 uniforme. Já o text-accent sobre bg-accent-muted (uso real em Sidebar, dashboard, KeyStatsCards, OverviewTab) passa AA em 88/88 combinações de paleta × tema × superfície.

Achados adicionais da auditoria por eixo

#AtritoSeveridadeEvidência
7Não há ação de "testar" na tela de Configs — o ciclo de verificação fecha em outra seção do menu (Monitoramento → Logs)bloqueanteA rota POST /api/test-connection existe e faz teste real (routes/testConnection.ts:16), mas o frontend não consome. Quem configura precisa atravessar o menu e esperar a requisição chegar.
8O platform admin que é MEMBER na própria org não alcança /app/usage-limits — a tela que ele precisa justamente para liberar limitesbloqueanteMenu exige ADMIN na org e a rota também — mas a fonte de dados troca para plataforma quando isPlatformAdmin (useUsageLimits.ts:20-24).
9OWNER é um papel sem distinção prática — nenhuma rota ou tela o exige, apesar de o texto do guard prometer exclusividademédioRequireRole.tsx:41-43 diz "Só o dono da organização acessa esta página", mas nenhum requireRole('OWNER') existe no backend, e hasRole compara por ranking (tenancy/context.ts:244).
10Endpoint de limpeza de registros antigos sem consumidormédioPOST /api/cleanup/run e GET /api/cleanup/stats existem (routes/cleanup.ts:17,45) e o frontend não chama. A manutenção de registros depende de chamar a API na mão.
11Ordenação da tabela é client-side e vale só para as 25 linhas da página — mas a URL carrega um sort que o backend entendemédioRequestsTable.tsx:100-116 vs. routes/requests.ts:153-155. "A requisição mais lenta do dia" vem errada, sem aviso.
12Criar uma chave sk-lb- tem 2 caminhos — e um deles é a tela órfãmédioMyKeysSection.tsx (dentro de /app/account, a tela sem item de menu) e ServiceKeyForm.tsx (em /app/client-keys). O MEMBER só tem acesso ao primeiro — justamente o inalcançável. O ADMIN chega ao segundo por um caminho que não se chama "criar chave pessoal".
133 telas de Configuração não tratam erro — falha de rede vira tela vazia, sem mensagem e sem "tentar de novo"médioConfigsPage.tsx, GroupsPage.tsx, ConfigGroupsPage.tsx: 0 ocorrências de isError. O padrão correto já existe em UsageLimitsPage.tsx:112-117.

API e permissões

A regra do backend, em uma frase: escrita sempre exige ADMIN; leitura só exige ADMIN quando o caminho está na lista negativa ADMIN_READ_PATHS — tudo que não está nela é MEMBER por padrão. O menu esconde por lista positiva (minRole) e a rota re-checa com RequireRole. Duas listas diferentes — é daí que nasce a assimetria.

Fluxo da requisição

HTTP request
  → middleware de autenticação (sessão / token)
    → middleware de tenancy (contexto: org, usuário, papel)
      → guard de papel (escrita: sempre ADMIN)
        → handler da rota
          → service (Prisma + Redis)

Endpoints por domínio

DomínioEndpointsPapelConsumido por
auth/api/auth/login · /me · /logout · /modepúblicoAuthGuard, Header, useAuthStore
keys/api/keys (POST) · /usage · /options · /:id (PUT/DELETE) · /:id/toggle (PUT) · /:id/reset (PUT) · /bulk-*ADMINChaves de API, Visão geral
key-groups/api/groupsADMINGrupos de chaves
configs/api/configsleitura todos / escrita ADMINConfigs de URL
config-groups/api/config-groupsADMINGrupos de config
requests/api/requests · /summary · /facets · /export · /:idleitura todos / DELETE ADMINLogs de requisições
stats/api/statsADMINVisão geral
metrics/api/metrics/advanced · /latency · /costs · /health · /models · /insightsparte ADMINMétricas, Insights, Visão geral
providers/api/providers · /capabilitiesADMINChaves de API
oauth/api/oauth/* · /api/copilot/* · /api/github/*ADMINOAuth
platform/api/platform/*platform adminPlataforma
audit/api/organizations/:id/auditADMINNINGUÉM — a rota existe e funciona (routes/audit.ts:29), só não há tela
account (pessoal)/api/account/keys (GET/POST) · /:id (DELETE)qualquer papelMinha conta — única mutação liberada ao MEMBER (auth.ts:77-79), e a tela é órfã

Fidelidade do mapa

A tela /app/arquitetura se declara "espelho verificado do código". Auditoria cruzada: o que ela afirma vs. o que o código faz. Classificação: MENTIRA (afirma algo que o código não faz) · DESATUALIZADO (era verdade, mudou) · IMPRECISO (direção certa, detalhe errado) · OK.

TipoOndeO que se afirmaO que o código faz
IMPRECISOArchitecturePage.tsx:55,74Badge "6 sem item de menu" na aba RotasConta só shell === 'app' (6 rotas). A aba "Sem item de menu" conta sobre as 28 e dá 15 (inclui 9 de sessão/sem-shell). Dois números, dois recortes, mesma etiqueta — parece contradição e não é.
OKrouter.tsx:33-49,249-25615 segmentos legados com redirect para /app/*Confere. platform e brand não estão na lista — lacuna real, já anotada.
OKroutes-app.ts:12 … routes-shell.ts:122"28 rotas"9 + 10 + 9 = 28 entradas de RouteEntry. Contagem bate exatamente com o router.
OKnav-items.ts:45-79"13 itens de menu"4 seções × itens = 13 (4+7+1+1), todos com minRole/platformOnly conferidos contra os guards do router.
INCOMPLETOrouter.tsx:23928 rotas no totalO router tem 20 filhos sob /app, mas o mapa documenta 19: falta /app/brand/* (o wildcard), que o router declara e o mapa omite. O teste de guarda só checa /brand e /app/brand — por isso não quebra com essa rota faltando. A contagem "28" está certa por coincidência: 9+10+9 = 28 no mapa, mas o app tem uma rota a mais.
ERRADOapi/routes.ts:9API_ROUTES.auth.login = '/api/auth/login'Essa rota não existe no backend — o login é 100% SSO (Conta Nommand); o backend só tem /api/auth/me, /logout, /mode e as de OIDC. A constante é morra: nenhuma referência fora do próprio arquivo. Não quebra nada hoje, mas é uma armadilha para quem for usá-la.
ERRADOapi-data.ts:61,93,109,253,261,2776 endpoints da tabela de API com o verbo erradoCruzamento automático dos 84 endpoints da tela contra as 111 rotas reais do backend: 74 conferem, 6 têm o verbo errado (a rota existe, o método não). GET /api/keys não existe — a tela inventou um GET de listagem (real: POST, keys.ts:69). POST /api/keys/:id/toggle é PUT (keys.ts:126); POST /api/keys/:id/reset é PUT (keys.ts:245); PUT e DELETE de service-keys são GET+POST (proxyKeys.ts:100,111); DELETE /api/organizations/:id/api-keys não existe sem :keyId (proxyKeys.ts:87).
DESATUALIZADOrouter.tsx (todo o mapa)"Toda afirmação traz arquivo:linha" — as linhas seriam verificáveisO mapa foi escrito contra a versão do router.tsx anterior ao commit que o criou. O commit 73de8b3 (que criou a tela) adicionou 14 linhas ao próprio router (a rota /app/arquitetura + o segmento legado), deslocando tudo abaixo. Exemplo medido: o mapa cita router.tsx:64 para REMOVED_BRAND_PATHS — na linha 64 da versão nova só há um comentário; era exatamente essa linha na versão antiga (247 linhas vs 261 hoje). As citações não apontam para o que dizem apontar.
FANTASMAroutes-app.ts, routes-app2.ts, routes-shell.tsDado de navegação com 37 pares (de → para) declarados, na seção "Grafo de navegação: quem leva a quem"Dois problemas. (1) O dado é em parte falso: 12 dos 37 pares apontam para um destino que nenhum arquivo do app navega. As 4 telas de Monitoramento (/app/overview, /app/requests, /app/metrics, /app/insights) só se alcançam pelo menu; /app/oauth, /app/client-keys e /app/config-groups também. (2) A seção não mostra nada disso: RouteMap.tsx:212 filtra por l.external, então renderiza só os 3 links externos (todos para a Conta Nommand). O "grafo" que a tela mostra não é o grafo que ela declara — e o que o app tem de fato, 13 navegações reais, não aparece em lugar nenhum.
FANTASMAfriction-data.ts:105Sugere criar GET /api/platform/audit "equivalente"Essa rota não existe no backend (nenhuma ocorrência em backend/src/). A sugestão aponta para um endpoint imaginário como se fosse real.
INCOMPLETOrouter.tsx:33-49,249-256"28 rotas"O router declara 61 rotas: 10 fixas na raiz + 21 sob /app + 30 legadas (15 segmentos × 2 formas: /seg e /seg/*). O mapa documenta 28 — faltam as 30 legadas e o índice /app. Para um sitemap do produto isso é aceitável; para uma auditoria de navega��ão, é uma lacuna.
IMPRECISOfriction-data2.ts:106Paleta ⌘K "indexa exatamente as 12 rotas do menu"Indexa as 13: CommandPalette.tsx:47 chama visibleNavSections() e navCommandItems faz flatMap sem nenhum filtro (CommandPalette.tsx:14-15). O número está errado por um. O mesmo erro está no .uxmap/01-rotas-e-menu.md:106.

O teste de guarda não protege o que promete

O teste se chama "o mapa cobre o app" e tem 15 casos. Rodado agora: 15/15 passam — com GET /api/keys inexistente, 6 verbos errados, 12 links fantasmas e citações deslocadas. Motivo: ele importa allRoutes do próprio map-data (a cópia que deve validar) e só faz toContain de uma lista de paths digitada à mão — nunca importa o router real. O guard é testado por string não-vazia, o source por regex, e api/links/actions não são testados contra nada.

A auditoria completa dos 8 eixos (rotas, menu, telas, API, fluxo, design, a11y) está nos documentos da pasta /workspace/.sitemap-out/.

Shell e navegação

Como o app se comporta como casca em volta do conteúdo.

Sidebar

  • · Recolhível no desktop: 248px aberto, 60px recolhido
  • · Persiste em localStorage['lb-sidenav-collapsed']
  • · Drawer no mobile (<1024px): 84% de largura, máx 320px, com backdrop
  • · Fechado fica inert; aberto prende o foco (Esc fecha, foco volta ao hambúrguer)
  • · Item ativo por prefixo, com aria-current="page"

Header / breadcrumb

  • · Topbar com busca, tema e links da conta (accountLinks, do backend)
  • · Breadcrumb usa ROUTE_LABELS — fonte paralela ao menu, e diverge em 8 pares
  • · Paleta ⌘K reusa visibleNavSections() — mesma fonte do menu
  • · ConnectivityBanner avisa queda de WebSocket

Design system

Tokens e componentes compartilhados. @nomad/ui + Tailwind 4 + tokens próprios.

CamadaO que éOnde vive
PrimitivosBadge, Card, Page, PageHeader, Tabs, Spinner (de @nomad/ui)node_modules/@nomad/ui
TokensSuperfície, texto, borda, acento, status — com variantes light/darksrc/index.css
Componentes locaisKpiCard, Panel, EmptyState, SideSheet, ConfirmDialog, SearchableSelect, ActivityFeed, charts/src/components/
GráficosÁrea, barra, donut + paleta categórica de sériessrc/styles/chartCategorical.ts
PaletasTroca de paleta persistida em localStorage['loadbalance:palette-vars'], aplicada no boot — 11 paletas (não 10), × claro/escuro@nomad/ui/src/theme/palettes.ts
TemaClasse light/dark no <html> + boot sem flash (themeBootPlugin), com retrato das duas variantes em localStoragevite.config.ts · src/lib/themeBoot.ts
GráficosTabela categórica de 8 matizes × 11 paletas, 6 fatias + "Other" — validada por OKLab/Machado (ΔE ≥ 8 sob daltonismo, ≥ 15 em visão normal). No dark só muda o degrau, não a ordemsrc/styles/chartCategorical.ts
ConsistênciaZero hex hardcoded no app fora da tabela categórica; zero rgb()/hsl() fora do utilitário de conversão. O design system é 100% externo — o index.css só importa o tema do @nomad/uisrc/index.css

Acessibilidade

O que foi medido e o que ainda dói.

Bug de token que reprova contraste (verificado no bundle)

AchadoGravidadeOnde
text-text-on-accent não gera CSS — o token existe no @theme como --color-text-onAccent (camelCase), mas o app usa o nome em kebab-case. No bundle compilado: text-text-onAccent = 1 ocorrência, text-text-on-accent = 0críticasrc/features/groups/components/GroupStrategySelect.tsx:51

O ícone do card de estratégia fica sobre bg-accent sem cor de texto própria, então herda a cor do card. Medido na paleta nommand (dark): a cor correta daria 7.36:1; herdada sobre #60a5fa, o contraste é 1.86:1 — reprovando AA. E o test:a11y não cobre /app/groups, então o axe nunca enxerga a tela.

AchadoGravidadeOnde
recontado Gráficos sem nome acessível: a tela diz 38, a auditoria conta 17 (o app tem 3 arquivos que importam recharts, não 38 gráficos)altasrc/components/charts/
recontado Tab stops para 4 linhas de tabela: a tela diz 51, a auditoria conta 25altasrc/pages/requests/
Tabela HTML crua sem <caption> e sem scope="col" — enquanto o app tem um Table do kit (com caption) usado em outros lugaresaltasrc/features/insights/components/ModelInsightsPanel.tsx:73
Feed e tabela ao vivo sem aria-live — a atualização chega sem ser anunciadamédiaActivityFeed.tsx:139 · RequestsPage.tsx:179
Drawer móvel sem role="dialog" / aria-modal; fundo não fica inertmédiasrc/components/layout/Sidebar.tsx:213-222
6 elementos com outline-hidden ficam sem foco visívelmédiasrc/components/
Checkbox de 14×14 px (abaixo do alvo mínimo)altasrc/components/ui/table
Busca ausente em 390/360px: botão fica com 0×0 px — focável e invisívelaltasrc/components/layout/Header.tsx
lb:session-expired emitido e ninguém escuta — sessão expirada sem saídamédiasrc/api/http.ts