Docs›IA & MCP›Dashboards de IA (embed)
IA & MCP

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 SELECT ou WITH (tolera um ; final);
  • comentários e literais são removidos antes do scan de palavras-chave — não dá para esconder um UPDATE dentro 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 :nome interpolados 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_schema anunciar por default só as entidades do manifesto MCP — ligar widgets_full_schema abre 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_dashboard e as cotas de custo.
  • Servidor MCP — o manifesto que define o que db_schema anuncia e o token que autentica estes endpoints.