Indicadores embutidos — SmartMetabase
O SFA não desenha gráfico. Ele hospeda painéis do SmartMetabase em iframe e assina, no
servidor, o token que define de quem são os dados. Decisão e alternativas descartadas em
ADR-037 (.claude/docs/decisions.md).
Consequência prática: indicador novo é trabalho de BI, não release do SFA. Quem monta a pergunta é o analista, na ferramenta; no SFA se registra apenas um ponteiro (tipo + id do item).
Como o recorte por organização é garantido
O embedding do SmartMetabase tem parâmetros em três estados; o que interessa aqui é o locked:
o valor vem dentro do token e o servidor recusa qualquer tentativa de sobrescrevê-lo pela URL
ou pela requisição.
Navegador API do SFA SmartMetabase
│ GET /analytics/panels/:key/embed │ │
│──────────────────────────────────►│ │
│ │ assina HS256: │
│ │ { resource: {dashboard: 3}, │
│ │ params: { org_id: <sessão> },
│ │ exp: agora+600s } │
│◄──────────────────────────────────│ │
│ { url, expiresAt, ttlSec } │ │
│ │ │
│ <iframe src=".../embed/dashboard/<token>"> ────────────────────►│
│ valida assinatura,
│ aplica org_id travado
A chave de assinatura nunca sai do backend: ela assina token para qualquer item com qualquer
recorte, então no navegador seria a base inteira exposta. O token é curto (SMARTMETABASE_EMBED_TTL_SEC,
default 600s) e a tela renova sozinha antes de vencer.
Configuração
1. No SmartMetabase
- Conecte o PostgreSQL do SFA com um usuário somente leitura.
- Monte a pergunta/dashboard.
- Crie um filtro de dashboard que recorte pela organização (a coluna
orgId, presente em toda entidade do SFA) e mapeie-o nos cards. - Publique o item para embedding e marque esse filtro como
locked. - Copie a chave de embedding (Administração · Links públicos).
2. No ambiente da API do SFA
SMARTMETABASE_URL=http://localhost:3070 # URL que o NAVEGADOR alcança
SMARTMETABASE_EMBED_SECRET=<chave de embedding>
SMARTMETABASE_EMBED_TTL_SEC=600
GET /analytics/status diz o que falta — e a tela mostra isso em vez de um iframe quebrado.
3. No SFA
Em Indicadores, "Novo painel": key, nome, tipo (dashboard/pergunta), id do item no
SmartMetabase, e o slug do parâmetro travado que recebe a organização (default org_id).
Marque "Mostrar também no dashboard inicial" para ele subir para a home.
Se o slug informado aqui não estiver locked no SmartMetabase, o embed responde erro — de
propósito. A alternativa (seguir sem o filtro) mostraria a base inteira dentro da organização
errada, que é o pior resultado possível de um bug de integração.
Mandando vários valores ao painel (ADR-038)
O painel raramente quer só a organização. Ele quer o vendedor logado, a carteira, o período que a pessoa escolheu. Esses valores não são todos iguais, e por isso vão por dois canais diferentes:
| Canal | Quem define | Quem vê pode mudar? | |
|---|---|---|---|
| Recorte — quem está perguntando | params do token assinado | a sessão, no servidor do SFA | não |
| Filtro — o que se está olhando | query da URL do iframe | a página, ou quem vê | sim |
Se errar significa mostrar dados demais, vai no token. Se errar significa mostrar outra tela, vai na URL.
Recorte: travados com placeholders da sessão
Em Indicadores → Parâmetros travados (extras), o valor aceita placeholders resolvidos no servidor a cada carregamento:
| Placeholder | Vira |
|---|---|
{{orgId}} | id da organização ativa |
{{orgName}} | nome da organização ativa |
{{userId}} | id de quem está logado |
{{userEmail}} | e-mail de quem está logado |
{{userName}} | nome de quem está logado |
Assim, "este painel é sempre recortado pelo vendedor logado" vira configuração: basta um travado
vendedor = {{userEmail}} (com o filtro vendedor marcado como locked no SmartMetabase e mapeado
na coluna certa). Sem isso, cada recorte novo seria campo novo no modelo e deploy.
{{naoexiste}}, ou um placeholder que a sessão atual não sabe responder, devolve 400 com a lista
dos válidos. Mandar o texto cru — ou vazio — produziria um gráfico vazio com cara de "não há dados",
que é o tipo de falha que ninguém investiga.
O org_id não vai aqui: ele é somado por último, na hora de assinar. Um travado que redefinisse
o slug da organização estaria desligando o isolamento multiorg por configuração de tela.
Filtro: valores iniciais na query
GET /analytics/panels/visao-geral/embed?f.periodo=previous-6-month&f.categoria=A&f.categoria=B
devolve a URL do iframe já com ?periodo=previous-6-month&categoria=A&categoria=B. Repetir a chave
manda vários valores. No front, o <EmbeddedPanel filtros={{ periodo: "previous-6-month" }} /> faz
o mesmo.
Esses filtros precisam estar enabled no SmartMetabase — e quem vê pode mudá-los, porque a URL
do iframe está no HTML. Use-os para o que se está olhando, nunca para recorte.
Contrato
| Rota | Permissão | O que faz |
|---|---|---|
GET /analytics/status | analytics:read | Se está configurado e quais variáveis faltam. |
GET /analytics/panels | analytics:read | Painéis da organização (?enabled=true filtra). |
POST /analytics/panels | analytics:configure | Registra um painel. |
PATCH /analytics/panels/:id | analytics:configure | Edita (a key é imutável). |
DELETE /analytics/panels/:id | analytics:configure | Remove do SFA — não apaga nada no SmartMetabase. |
GET /analytics/panels/:key/embed | analytics:read | URL assinada + validade. ?f.<slug>= pré-preenche filtros editáveis. |
O modelo AnalyticsPanel guarda resource (DASHBOARD/QUESTION), resourceId (id lá),
lockedParams (travados estáticos extras), orgParamSlug, themeSlug, height, showOnHome,
position e enabled — sempre por orgId.
A aparência do painel
O campo Tema guarda o identificador de um tema do SmartMetabase — o mesmo que aparece lá em
Administração · Temas, ao lado de cada tema (ex.: wessel-escuro). Ele vira ?theme=<slug> na
URL do iframe.
Vazio usa o tema padrão do produto, e um identificador que não existe do outro lado também: a regra é a mesma que separa os dois canais — errar o tema muda a cara do painel, nunca os dados, e por isso ele viaja na URL e não no token. Aparência não é motivo para uma tela ficar em branco.
Se o painel tiver um filtro editável chamado theme, quem vence é o tema do painel: quem o
configurou escolheu a aparência, e a query de quem vê não decide isso.
O que ainda não existe
- Escolher o painel por uma lista: hoje se digita o id do item. Listar os dashboards exigiria uma credencial de sessão do SmartMetabase guardada no SFA — outra decisão, com outro risco.
- Escolher o tema por uma lista: mesmo motivo.
GET /api/themesdo SmartMetabase exige sessão; a rota anônima só serve um tema pelo identificador, e não a lista deles. - UI de filtros no SFA. O canal existe (
?f.<slug>=, e a propfiltrosdoEmbeddedPanel), mas nenhuma tela ainda oferece os controles — hoje quem preenche é código. - Recorte numa pergunta avulsa. Filtro é do dashboard, não do card: um gráfico só, recortado por organização, precisa estar dentro de um dashboard (ainda que de um card).
- Cache/telemetria de visualização no lado do SFA. O SmartMetabase já conta visualizações por item embutido.