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.
A árvore completa do produto. /app/* = área autenticada (MainLayout) · /auth/* e /login = sessão · o resto = sem shell.
Fonte: frontend/src/app/router.tsx. Guard = quem passa. Menu = se aparece na lateral.
| Path | Tela | Guard | No menu | O que a pessoa faz aqui |
|---|---|---|---|---|
| /app/overview | Visão geral | todos | ✓ Monitoramento | Painel do dia: KPIs, saúde do pool, atividade, top modelos e chaves. |
| /app/requests | Logs de requisições | todos | ✓ Monitoramento | Cada requisição que passou pelo proxy, com filtros, exportação e feed ao vivo. |
| /app/metrics | Métricas | todos | ✓ Monitoramento | Latência, custo, atividade e saúde em série temporal. |
| /app/insights | Insights | todos | ✓ Monitoramento | Leitura narrativa do período: o que mudou, onde errou, quem consome. |
| /app/keys | Chaves de API | ADMIN | ✓ Configuração | Credenciais dos providers: criar, conectar, desabilitar e inspecionar. |
| /app/usage-limits | Limites de uso | ADMIN | ✓ Configuração | Janelas de uso dos planos (5h/semanal) por conta de provider. |
| /app/configs | Configs de URL | todos* | ✓ Configuração | As URLs do proxy: cada uma aponta para um conjunto de chaves. |
| /app/client-keys | Chaves de URL | ADMIN | ✓ Configuração | As chaves sk-lb- que o cliente manda para chamar uma Config de URL. |
| /app/groups | Grupos de chaves | ADMIN | ✓ Configuração | Estratégia de balanceamento: como as chaves de um grupo são escolhidas. |
| /app/groups/new | Novo grupo | ADMIN | — | Criar um grupo de chaves: nome, estratégia e chaves que entram nele. |
| /app/groups/:id | Editar grupo | ADMIN | — | Editar um grupo: membros, ordem das chaves e estratégia de escolha. |
| /app/config-groups | Grupos de config | ADMIN | ✓ Configuração | Agrupamentos de Configs de URL, para organizar por ambiente ou cliente. |
| /app/oauth | OAuth | ADMIN | ✓ Configuração | Contas de provider que entram por autorização externa em vez de chave manual. |
| /app/platform | Plataforma | platform | ✓ Nomad | Visão da Nomad: todas as orgs, uso agregado, saúde, auditoria e pool compartilhado. |
| /app/arquitetura | Arquitetura | todos | ✓ Diagnóstico | O mapa do loadbalance: rotas, menu, links, fluxo diário e onde o acesso é ruim. |
| /app/account | Minha conta | todos | ✗ órfã | Perfil (leitura), chaves pessoais sk-lb- e aparência do app. |
| /app/settings | Configurações (removida) | — | — | → /app/account · config de instância é env no deploy. |
| /app/organization | Organização (removida) | — | — | → /app/client-keys (admin) ou /app/overview. |
| /app/brand | Design System (removido) | — | — | → /app/overview. |
| /app/brand/* | Design System (removido, wildcard) | — | — | → /app/overview. não documentado no mapa router.tsx:239 |
| Path | Tela | Papel |
|---|---|---|
| / | Raiz → Visão geral | público |
| /login | Entrar com a Conta Nommand | público |
| /auth/logged-out | Você saiu | público |
| /auth/no-access | Sem acesso ao Load Balancer | público |
| /signup · /invite/:token · /accept-invite | → /login (removidas, LB-CONTA-ONLY-01) | público |
| * | Página não encontrada | público |
O que cada tela faz e o que consome. Ações em negrito são o que a pessoa sai dali tendo feito.
| Tela | Ações principais | APIs consumidas | Estados tratados |
|---|---|---|---|
| Visão geral | Ver KPIs · troca de período · saúde das chaves · top modelos | /api/stats · /api/metrics/advanced · /api/keys/usage | carregando · vazio · zerado sem aviso (MEMBER) |
| Logs de requisições | Filtrar · abrir detalhe · exportar · feed ao vivo · excluir | /api/requests · /summary · /facets · /export · /ws/requests | carregando · vazio · 403 no excluir (MEMBER) |
| Métricas | Latência por período · custo por modelo · atividade por bucket | /api/metrics/latency · /costs · /health | carregando · painel de saúde ausente (MEMBER) |
| Insights | Ler narrativa · blocos de erro · comparar por chave/URL/modelo | /api/metrics/insights | carregando · vazio |
| Chaves de API | Criar provider · testar conexão · desabilitar · duplicar · rotacionar segredo | /api/keys · /api/providers | carregando · vazio · segredo novo descartado em silêncio |
| Limites de uso | Ver janela 5h · janela semanal · editar corte por chave | /api/keys/usage-limits | carregando · vazio |
| Configs de URL | Criar Config · vincular chaves e grupos · ativar · duplicar | /api/configs · /api/config-groups | carregando · botões somem sem aviso (MEMBER) |
| Chaves de URL | Emitir chave de serviço · emitir chave pessoal · revogar · copiar segredo | /api/organizations/:id/service-keys | carregando · vazio |
| Grupos de chaves | Criar · editar · estratégia de balanceamento · reordenar | /api/groups | carregando · vazio |
| Grupos de config | Criar · vincular Configs · excluir | /api/config-groups | carregando · vazio |
| OAuth | Iniciar autorização · acompanhar status · conectar GitHub/Codex/Copilot | /api/oauth/* · /api/copilot/* | carregando · polling |
| Plataforma | Panorama · listar orgs · saúde dos serviços · auditar ações | /api/platform/* | carregando · vazio |
| Minha conta | Perfil · criar/revogar chaves pessoais · tema e paleta | /api/account · /api/organizations/:id/service-keys | carregando · órfã: sem caminho de navegação |
Três personas. O que cada uma alcança, o que não alcança, e onde trava.
| Persona | Alcança | Não alcança | Onde trava |
|---|---|---|---|
| ADMIN da org | 12 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). |
| MEMBER | 6 telas | 7 telas de admin | Trê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 admin | 5 telas + Plataforma | — | O guard da Plataforma devolve "Página não encontrada" em vez de "sem permissão". |
| # | Ação | Onde | Atrito |
|---|---|---|---|
| 1 | Entrar | /login | Só pela Conta Nommand: 1 clique e um salto para fora da SPA. |
| 2 | Olhar os KPIs | /app/overview | /api/metrics/advanced = 9 agregações em polling de 60s. O primeiro paint demora. |
| 3 | Ver "Estado das chaves" zerado | /app/overview | MEMBER vê o painel vazio sem aviso. |
| 4 | Ir aos Logs | /app/requests | A Visão geral não linka uma requisição específica. Começa do zero. |
| 5 | Filtrar e abrir o detalhe | /app/requests | 5 dos 13 filtros não chegam ao servidor (latência, tokens, streaming, IP). |
| 6 | Ir para a chave que falhou | /app/keys | Salto de contexto: copiar o nome, sair, achar "Chaves de API". |
| 7 | Desabilitar a chave | /app/keys | /api/stats traz TODAS as chaves inteiras — "dezenas de MB", admite o backend. |
| 8 | Conferir o limite do plano | /app/usage-limits | Não há atalho do log nem do card de status. Beco sem saída. |
| 9 | Criar a chave nova | /app/keys | Criar a chave não resolve: falta Config de URL, grupo e sk-lb-. E rotacionar o segredo é aceito e descartado. |
| 10 | Criar a Config | /app/configs | A tela bifurca por papel dentro do render: MEMBER vê a lista sem botões e sem aviso. |
| 11 | Emitir a sk-lb- | /app/client-keys | Nome ambíguo ao lado de "Chaves de API". |
| 12 | Copiar a configuração de uso | /app/configs | Copia um PLACEHOLDER. A chave real só apareceu uma vez, em outra tela. |
| 13 | Testar | /app/requests | Nã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).
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.
| # | Atrito | Severidade | Como se manifesta |
|---|---|---|---|
| 1 | Rotacionar o segredo de uma chave de provider | bloqueante | O 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. |
| 2 | Filtrar os logs por latência, tokens, streaming ou IP | bloqueante | A 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. |
| 3 | Apagar requisições selecionadas | bloqueante | A 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ê. |
| 4 | Destravar uma chave presa em limite de uso | bloqueante | A 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. |
| 5 | Criar uma chave pessoal sk-lb- para meu cliente | bloqueante | A ú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. |
| 6 | Entender por que o painel mostra "Estado das chaves" zerado | bloqueante | Blocos 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. |
| Tela | O que a interface promete | O que o backend permite |
|---|---|---|
| Visão geral | Painel de estado das chaves | MEMBER não vê o painel (é omitido, não zerado) — sem aviso, sem 403, sem erro. |
| Métricas | Painel de saúde e balanceamento | MEMBER 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 URL | Editar, ativar, duplicar | MEMBER vê a lista e nenhum botão — sem mensagem de que a edição é restrita. |
| Logs de requisições | Filtro por chave | O filtro some para MEMBER, sem aviso. |
| Logs de requisições | Botão "Excluir" | Aparece e leva 403 — um botão que sempre falha, sem mensagem. |
| Chaves de API | Campo "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 conta | Criar chave pessoal | A única ação do MEMBER fica sem caminho de navegação. |
| Membros da organização | Comentá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. |
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.
| # | Atrito | Severidade | Evidência |
|---|---|---|---|
| 7 | Não há ação de "testar" na tela de Configs — o ciclo de verificação fecha em outra seção do menu (Monitoramento → Logs) | bloqueante | A 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. |
| 8 | O platform admin que é MEMBER na própria org não alcança /app/usage-limits — a tela que ele precisa justamente para liberar limites | bloqueante | Menu exige ADMIN na org e a rota também — mas a fonte de dados troca para plataforma quando isPlatformAdmin (useUsageLimits.ts:20-24). |
| 9 | OWNER é um papel sem distinção prática — nenhuma rota ou tela o exige, apesar de o texto do guard prometer exclusividade | médio | RequireRole.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). |
| 10 | Endpoint de limpeza de registros antigos sem consumidor | médio | POST /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. |
| 11 | Ordenação da tabela é client-side e vale só para as 25 linhas da página — mas a URL carrega um sort que o backend entende | médio | RequestsTable.tsx:100-116 vs. routes/requests.ts:153-155. "A requisição mais lenta do dia" vem errada, sem aviso. |
| 12 | Criar uma chave sk-lb- tem 2 caminhos — e um deles é a tela órfã | médio | MyKeysSection.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". |
| 13 | 3 telas de Configuração não tratam erro — falha de rede vira tela vazia, sem mensagem e sem "tentar de novo" | médio | ConfigsPage.tsx, GroupsPage.tsx, ConfigGroupsPage.tsx: 0 ocorrências de isError. O padrão correto já existe em UsageLimitsPage.tsx:112-117. |
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.
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)
| Domínio | Endpoints | Papel | Consumido por |
|---|---|---|---|
| auth | /api/auth/login · /me · /logout · /mode | público | AuthGuard, Header, useAuthStore |
| keys | /api/keys (POST) · /usage · /options · /:id (PUT/DELETE) · /:id/toggle (PUT) · /:id/reset (PUT) · /bulk-* | ADMIN | Chaves de API, Visão geral |
| key-groups | /api/groups | ADMIN | Grupos de chaves |
| configs | /api/configs | leitura todos / escrita ADMIN | Configs de URL |
| config-groups | /api/config-groups | ADMIN | Grupos de config |
| requests | /api/requests · /summary · /facets · /export · /:id | leitura todos / DELETE ADMIN | Logs de requisições |
| stats | /api/stats | ADMIN | Visão geral |
| metrics | /api/metrics/advanced · /latency · /costs · /health · /models · /insights | parte ADMIN | Métricas, Insights, Visão geral |
| providers | /api/providers · /capabilities | ADMIN | Chaves de API |
| oauth | /api/oauth/* · /api/copilot/* · /api/github/* | ADMIN | OAuth |
| platform | /api/platform/* | platform admin | Plataforma |
| audit | /api/organizations/:id/audit | ADMIN | NINGUÉ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 papel | Minha conta — única mutação liberada ao MEMBER (auth.ts:77-79), e a tela é órfã |
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.
| Tipo | Onde | O que se afirma | O que o código faz |
|---|---|---|---|
| IMPRECISO | ArchitecturePage.tsx:55,74 | Badge "6 sem item de menu" na aba Rotas | Conta 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 é. |
| OK | router.tsx:33-49,249-256 | 15 segmentos legados com redirect para /app/* | Confere. platform e brand não estão na lista — lacuna real, já anotada. |
| OK | routes-app.ts:12 … routes-shell.ts:122 | "28 rotas" | 9 + 10 + 9 = 28 entradas de RouteEntry. Contagem bate exatamente com o router. |
| OK | nav-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. |
| INCOMPLETO | router.tsx:239 | 28 rotas no total | O 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. |
| ERRADO | api/routes.ts:9 | API_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. |
| ERRADO | api-data.ts:61,93,109,253,261,277 | 6 endpoints da tabela de API com o verbo errado | Cruzamento 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). |
| DESATUALIZADO | router.tsx (todo o mapa) | "Toda afirmação traz arquivo:linha" — as linhas seriam verificáveis | O 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. |
| FANTASMA | routes-app.ts, routes-app2.ts, routes-shell.ts | Dado 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. |
| FANTASMA | friction-data.ts:105 | Sugere 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. |
| INCOMPLETO | router.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. |
| IMPRECISO | friction-data2.ts:106 | Paleta ⌘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 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/.
Como o app se comporta como casca em volta do conteúdo.
Tokens e componentes compartilhados. @nomad/ui + Tailwind 4 + tokens próprios.
| Camada | O que é | Onde vive |
|---|---|---|
| Primitivos | Badge, Card, Page, PageHeader, Tabs, Spinner (de @nomad/ui) | node_modules/@nomad/ui |
| Tokens | Superfície, texto, borda, acento, status — com variantes light/dark | src/index.css |
| Componentes locais | KpiCard, Panel, EmptyState, SideSheet, ConfirmDialog, SearchableSelect, ActivityFeed, charts/ | src/components/ |
| Gráficos | Área, barra, donut + paleta categórica de séries | src/styles/chartCategorical.ts |
| Paletas | Troca de paleta persistida em localStorage['loadbalance:palette-vars'], aplicada no boot — 11 paletas (não 10), × claro/escuro | @nomad/ui/src/theme/palettes.ts |
| Tema | Classe light/dark no <html> + boot sem flash (themeBootPlugin), com retrato das duas variantes em localStorage | vite.config.ts · src/lib/themeBoot.ts |
| Gráficos | Tabela 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 ordem | src/styles/chartCategorical.ts |
| Consistência | Zero 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/ui | src/index.css |
O que foi medido e o que ainda dói.
| Achado | Gravidade | Onde |
|---|---|---|
| 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 = 0 | crítica | src/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.
| Achado | Gravidade | Onde |
|---|---|---|
| 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) | alta | src/components/charts/ |
| recontado Tab stops para 4 linhas de tabela: a tela diz 51, a auditoria conta 25 | alta | src/pages/requests/ |
| Tabela HTML crua sem <caption> e sem scope="col" — enquanto o app tem um Table do kit (com caption) usado em outros lugares | alta | src/features/insights/components/ModelInsightsPanel.tsx:73 |
| Feed e tabela ao vivo sem aria-live — a atualização chega sem ser anunciada | média | ActivityFeed.tsx:139 · RequestsPage.tsx:179 |
| Drawer móvel sem role="dialog" / aria-modal; fundo não fica inert | média | src/components/layout/Sidebar.tsx:213-222 |
| 6 elementos com outline-hidden ficam sem foco visível | média | src/components/ |
| Checkbox de 14×14 px (abaixo do alvo mínimo) | alta | src/components/ui/table |
| Busca ausente em 390/360px: botão fica com 0×0 px — focável e invisível | alta | src/components/layout/Header.tsx |
| lb:session-expired emitido e ninguém escuta — sessão expirada sem saída | média | src/api/http.ts |