Dashboards de IA (embed)
Micro-BI do Copilot: widgets salvos com SQL determinística (WidgetSqlGuard), favoritos, tela Meus Dashboards e os endpoints /embed/v1/{widgets,dashboards,favorites}.
O micro-BI do embed é a camada em que o AI Copilot
para de ser só chat: o agente monta um widget com SQL determinística, o
usuário aprova, o widget é salvo e passa a ser reexecutado sem IA
nenhuma — no chat (favoritos) ou na tela "Meus Dashboards" (um iframe React
servido pelo MadBuilder, apontado por [ai] dashboards_frontend_url).
O ciclo completo:
pergunta no chat
-> db_schema (tabelas/colunas reais — nunca chutar nome)
-> preview_widget (SQL validada + executada; bloco aparece no chat)
-> save_widget (persiste o spec em mad_ai_widget, por usuario)
-> save_dashboard (monta a grade com os widgets salvos)
-> tela "Meus Dashboards" re-executa o MESMO spec, 0 chamada de LLM
Habilitando
// config/mad.php — dentro de 'ai'
'widgets_enabled' => (bool) env('MAD_AI_WIDGETS_ENABLED', true),
'widgets_full_schema' => (bool) env('MAD_AI_WIDGETS_FULL_SCHEMA', false),
'widget_global_filter_hook' => env('MAD_AI_WIDGET_GLOBAL_FILTER_HOOK', ''),
'dashboards_frontend_url' => env('MAD_AI_DASHBOARDS_FRONTEND_URL', 'http://localhost:7400/dashboard.embed.html'),
widgets_enabled herda [ai] enabled — com o Copilot desligado nada disso
registra. Desligado, as tools de BI somem do agente e as rotas de
widgets/dashboards/favoritos não são registradas.
WidgetSqlGuard — o freio da SQL do modelo
A SQL do widget vem do modelo e fica persistida em
mad_ai_widget.spec_json; a exibição do dashboard a re-executa depois, sem
supervisão. Por isso Mad\Ai\WidgetSqlGuard é fail-closed:
- uma instrução só, começando em
SELECTouWITH(tolera um;final); - comentários e literais são removidos antes do scan de palavras-chave —
não dá para esconder um
UPDATEdentro de string nem gerar falso-positivo com um rótulo "última atualização"; - denylist de escrita/DDL/execução (
insert,update,delete,drop,alter,create,truncate,grant,exec,call,into,outfile,load_file,sleep,pg_sleep,lock,shutdown, entre outras); - exige
FROM— widget sem tabela é dado fabricado em literal, que o modelo já tentou emitir quando a tabela pedida não existia; - o resultado é sempre envelopado em
SELECT * FROM ( … ) __mad_w LIMIT N(WidgetSqlGuard::MAX_ROWS = 1000), portável entre SQLite/MySQL 8/PostgreSQL; - filtros de dashboard entram como placeholders
:nomeinterpolados com o quote do driver PDO, e a validação roda depois da interpolação (defesa dupla).
Atenção: SQL crua não passa pelo masker de PII do motor MCP. O freio é
db_schemaanunciar por default só as entidades do manifesto MCP — ligarwidgets_full_schemaabre a introspecção do banco inteiro ao modelo.
Onde os dados ficam
| Tabela | Papel |
|---|---|
mad_ai_widget |
widget salvo: id, user_id, title, spec_json (type/sql/map/style + display), timestamps — privado por usuário (WidgetStore) |
mad_ai_dashboard |
dashboard: id, user_id, title, layout_json, filters, is_home (DashboardStore) |
mad_ai_dashboard_share |
compartilhamento por usuário/grupo — quem não é dono só lê |
mad_ai_favorite |
bloco do chat salvo como favorito (título, nota, snapshot do bloco, conversa de origem) |
Todo acesso é escopado pelo user_id do token MCP (McpCurrentUser): as
queries dos stores carregam WHERE ... AND user_id = ?, e listar/abrir um
dashboard alheio só funciona via mad_ai_dashboard_share.
Endpoints
Todos sob /embed/v1, com o mesmo Bearer (token MCP) e o mesmo
McpManifestAuthMiddleware do chat.
| Método | Rota | O quê |
|---|---|---|
GET |
/favorites |
metadados dos favoritos do usuário |
POST |
/favorites |
salva {title, note?, block, conversationId?} |
DELETE |
/favorites/{id} |
remove (só o dono) |
POST |
/favorites/{id}/run |
replay SSE do bloco salvo — sem LLM |
GET |
/widgets |
lista os widgets salvos (metadados) |
GET |
/widgets/{id}/render |
executa o spec → bloco fresco (0 IA) |
PATCH |
/widgets/{id} |
título e/ou display (tipo/formato de exibição) |
DELETE |
/widgets/{id} |
exclui |
GET |
/dashboards |
meus + compartilhados comigo |
POST |
/dashboards |
cria {title, layout?} |
GET |
/dashboards/{id} |
meta + layout (+ shares, para o dono) |
PATCH |
/dashboards/{id} |
title/layout/filters (dono) |
DELETE |
/dashboards/{id} |
exclui (dono) |
POST |
/dashboards/{id}/home |
marca como tela inicial (dono) + frontpage |
PUT |
/dashboards/{id}/share |
{users:[], groups:[]} (dono) |
GET |
/share-targets |
usuários + grupos para o modal de compartilhamento |
Exibição: display e filtro global
Mad\Ai\WidgetDisplay guarda o override de exibição do usuário em
spec_json.display (gravado por PATCH /embed/v1/widgets/{id}): o mesmo
WidgetSpec pode ser mostrado como barra, linha ou tabela sem reescrever a
SQL nem chamar o modelo de novo.
Filtros do dashboard que a fonte do widget não consome caem no slicer de
linhas (corta o resultado pela coluna). Quando isso não basta — porque o
filtro precisa mudar a agregação —, [ai] widget_global_filter_hook
aponta para uma classe app-level com apply(array $filters): void, que
traduz os filtros em escopo de dados de verdade. Vazio = só o slicer.
Veja também
- AI Copilot (embed) — o chat que origina os
widgets, as tools
preview_widget/save_widget/save_dashboarde as cotas de custo. - Servidor MCP — o manifesto que define o que
db_schemaanuncia e o token que autentica estes endpoints.