Workspaces: dashboards do DATTABI e filtro por contexto
Visão geral
Cada workspace do DATTA agrupa um conjunto de funcionalidades habilitadas, uma lista de painéis vinculados e uma lista de contextos (domínios/bases). Esta entrega adiciona duas capacidades ao workspace:
- Dashboards do DATTABI no gerenciador e no menu — os dashboards de BI criados no DATTABI passam a aparecer no catálogo de painéis, podem ser vinculados a um workspace pelo gerenciador e viram botões no grupo do workspace no menu lateral.
- Contextos filtrando os dados das funcionalidades — ao abrir uma funcionalidade ciente de contexto a partir do grupo de um workspace, o shell propaga os contextos do workspace para a funcionalidade, que passa a filtrar seus dados por esses contextos.
Problema
- Os dashboards do DATTABI viviam isolados na página a tela do DATTABI; não havia como expô-los como atalho dentro do menu de um workspace nem reutilizá-los junto das demais funcionalidades (chat, painel, investigação, triagem).
- Um workspace pode ter vários contextos, mas as funcionalidades abertas a partir dele mostravam todos os dados de todos os contextos. O usuário precisava filtrar manualmente o domínio em cada tela, sem ganho do agrupamento por workspace.
Solução
- O catálogo de painéis (
GET /api/config/panels) passou a incluir, além dos painéis nativos e de canvas, os dashboards do DATTABI de cada workspace, obtidos via chamada service-to-service (S2S) aodattabi-service. - O gerenciador de workspaces (
/configurar/workspaces) exibe esses dashboards na ação Vincular existentes, e o menu lateral renderiza como botões os dashboards cujopanelRefestá persistido (abrindo a tela do DATTABI). A persistência do vínculo do dashboard viapanelReftem uma limitação de backend conhecida — ver Limitações conhecidas. - O shell propaga os contextos do workspace via querystring
?domain=ctx1,ctx2ao abrir uma funcionalidade ciente de contexto, e cada funcionalidade lê esse parâmetro para filtrar/pré-selecionar seus dados.
Como funciona (arquitetura + fluxo de dados)
Item 2 — Dashboards do DATTABI no catálogo, no gerenciador e no menu
Backend (catálogo de painéis). A API do catálogo de painéis do config-service serve GET /api/config/panels — um catálogo achatado dos painéis da plataforma visíveis ao usuário (ver Visibilidade por membros). Além dos painéis nativos (native-view) e de canvas de cada workspace, ele agora agrega os dashboards do DATTABI:
- Para cada workspace, faz uma chamada S2S ao
dattabi-serviceemGET /api/dattabi/dashboards?workspaceId=<id>usandoWebClient. - As chamadas por workspace rodam em paralelo, com concorrência 6 (
Flux.fromIterable(workspaces).flatMap(this::dattabiDashboards, 6), seguindo as Diretrizes do Projeto §19. - Cada chamada é best-effort: timeout de 6s e
onErrorResumeque devolve lista vazia, de modo que a indisponibilidade dos dashboards de um workspace não derruba o catálogo inteiro. - Cada dashboard não-arquivado vira uma entrada do catálogo no formato:
{
"workspaceId": "<id-do-workspace>",
"workspaceName": "<nome>",
"panelId": "<dashboardId>",
"title": "<titulo>",
"description": "<opcional>",
"kind": "dattabi-dashboard",
"dashboardId": "<dashboardId>",
"url": "/dattabi.html?dashId=<dashboardId>",
"native": false
}.
Backend (fonte dos dashboards). O dattabi-service expõe GET /api/dattabi/dashboards?workspaceId=<id>. O endpoint exige DATTABI_VIEW, lista os dashboards do workspace (parâmetro includeArchived=false por padrão) e, defensivamente, retorna lista vazia quando o workspaceId vem ausente/em branco.
Frontend (gerenciador). No gerenciador de workspaces (screens/settings-workspaces.html, aba Painéis → Vincular existentes), o catálogo é carregado e filtrado para a lista de opções vinculáveis:
// screens/settings-workspaces.html
const options = catalog.filter(
c => c.workspaceId !== selected.id || c.kind === 'dattabi-dashboard'
);Ou seja: painéis de canvas continuam linkáveis apenas de outros workspaces (o próprio já aparece), mas dashboards do DATTABI (kind === 'dattabi-dashboard') ficam linkáveis de qualquer workspace, inclusive o próprio. A seleção é salva como panelRef via PUT /api/config/workspaces/{id}/panel-refs (screens/settings-workspaces.html).
Como esses vínculos são referências (panelRef), eles aparecem na seção Painéis vinculados da lista, com o badge genérico Vinculado e o botão Abrir (que chama openRef → abre /dattabi.html?dashId=<id> em nova aba) (screens/settings-workspaces.html). O badge Dashboard BI e o botão Abrir dashboard (screens/settings-workspaces.html) são usados apenas para painéis próprios do workspace cujo config.kind já é dattabi-dashboard — um caminho distinto do vínculo por panelRef.
Frontend (menu lateral). O shell (compiled/index.js) carrega /api/config/workspaces e /api/config/panels em paralelo com Promise.all (index.js:746-756) e indexa o catálogo por chave workspaceId::panelId. Ao montar o grupo de cada workspace, resolve os panelRefs no catálogo e renderiza os do tipo dattabi-dashboard como botões:
// index.js:2105-2107
const dashRefs = (ws.panelRefs || [])
.map(r => panelCatalog[r.workspaceId + '::' + r.panelId])
.filter(p => p && p.kind === 'dattabi-dashboard');Clicar no botão abre /dattabi.html?dashId=<dashboardId> em uma nova aba (index.js:2117-2126).
Item 9 — Contextos do workspace filtram as funcionalidades
Propagação no shell. O shell mantém um estado panelDomain (index.js:705). Ao clicar em uma funcionalidade dentro do grupo de um workspace, ele define panelDomain com os contextos do workspace concatenados por vírgula e navega para a funcionalidade:
// index.js:2115
onClick: () => { setPanelDomain((ws.contexts || []).join(',')); navigateTo(fk); setSidebarOpen(false); }Quando a funcionalidade é renderizada como iframe, o shell adiciona o parâmetro domain na src apenas se a funcionalidade for ciente de contexto e houver panelDomain:
// index.js:2602
src: WS_CONTEXT_AWARE.has(key) && panelDomain
? src + (src.includes('?') ? '&' : '?') + 'domain=' + encodeURIComponent(panelDomain)
: src,O conjunto de funcionalidades cientes de contexto é fixo (index.js:639-642):
var WS_CONTEXT_AWARE = new Set([
'dashboard', 'chat', 'investigate', 'upload', 'analytics',
'triagem-lote', 'regras-triagem'
]);Consumo nas funcionalidades. Cada funcionalidade lê ?domain da URL e usa os contextos para filtrar seus dados:
- Painel/Processos (
dashboard), Chat (chat), Investigação (investigate), Upload (upload) e Casos (analytics) já liam?domaine passaram a filtrar/usar os contextos do workspace. - Regras (
regras-triagem): filtra as regras pelo primeiro contexto do workspace; sem?domainmostraTodos(compiled/regras_triagem.js:788-791):
const wsCtx = (() => { try {
return (new URLSearchParams(window.location.search).get('domain') || '').split(',')[0].trim();
} catch (e) { return ''; } })();
const [contexto, setContexto] = useState(wsCtx || 'Todos');- Triagem em Lote (
triagem-lote): pré-seleciona apenas os contextos do workspace (intersecção com os contextos disponíveis); sem?domainseleciona todos, mantendo o comportamento padrão (compiled/triagem_lote.js:132-137):
const ws = (new URLSearchParams(window.location.search).get('domain') || '')
.split(',').map(s => s.trim()).filter(Boolean);
const wsValid = ws.filter(c => ctxs.includes(c));
setSelectedContexts(wsValid.length ? wsValid : ctxs);APIs / contratos
GET /api/config/panels (config-service)
Catálogo achatado dos painéis da plataforma visíveis ao usuário (workspaces públicos + aqueles de que ele é membro; admin vê todos — ver Visibilidade por membros). Resposta: array de objetos. As entradas de dashboard do DATTABI têm kind: "dattabi-dashboard" com dashboardId e url (ver formato acima). Permissão: PANEL_VIEW ou CONFIG_VIEW (any-of).
GET /api/dattabi/dashboards?workspaceId=<id> (dattabi-service)
Lista os dashboards de um workspace. Parâmetros:
| Parâmetro | Tipo | Default | Descrição |
|---|---|---|---|
workspaceId | string | — | Obrigatório (semanticamente); vazio → [] |
includeArchived | boolean | false | Inclui dashboards arquivados |
Permissão: DATTABI_VIEW. Chamado em S2S pelo config-service.
PUT /api/config/workspaces/{id}/panel-refs (config-service)
Define a lista de painéis existentes vinculados ao workspace. Corpo:
{ "panelRefs": [ { "workspaceId": "<id>", "panelId": "<id>" } ] }Permissão: CONFIG_EDIT. O serviço valida cada referência: aceita apenas painel nativo (API do catálogo de painéis) ou painel canvas existente em outro workspace (store.find(workspaceId).panels()); qualquer outra referência retorna 400 Bad Request com {"error":"Painel referenciado não existe: ..."}. Além disso, descarta silenciosamente referências cujo workspaceId seja o do próprio workspace.
Atenção (gap atual): dashboards do DATTABI vêm do catálogo via S2S e não são
Workspace.Panelpersistidos — logo osetPanelRefsatual não os reconhece como referência válida (rejeita os de outro workspace com 400 e descarta os do próprio). Veja Limitações conhecidas.
RBAC / permissões
| Ação | Permissão exigida | Onde é checada |
|---|---|---|
| Ler o catálogo de painéis | PANEL_VIEW ou CONFIG_VIEW | API do catálogo de painéis — @RequirePermission via checagem de permissão declarada (retorno Mono, ramo reativo). O SecurityWebFilterChain só exige .authenticated() para esse GET. |
| Listar dashboards de um workspace | DATTABI_VIEW | API de dashboards — @RequirePermission via checagem de permissão declarada (retorno Mono). O filtro do dattabi-service só exige .authenticated() em /api/dattabi/**. |
| Vincular painéis/dashboards ao workspace | CONFIG_EDIT | Dupla camada: o filtro de segurança exige CONFIG_EDIT ou ROLE_ADMIN em todo PUT /api/config/**, e a permissão declarada CONFIG_EDIT é cobrada de novo na vinculação de painéis. |
Nota sobre o ponto de enforcement. Nesses três endpoints a checagem granular de permissão roda na checagem de permissão declarada (
datta-common), que só enforça quando o método retornaMono/Flux(ramo reativo, viaReactiveSecurityContextHolder). Os três métodos retornamMono, então a anotação vale. Para métodos de retorno blocking em WebFlux o aspecto não bloqueia (thread-local vazio) e a autorização recai noSecurityWebFilterChain— por isso as mutações de config também têm o path-matcher por permissão noSecurityConfig.
A chamada S2S do config-service ao dattabi-service usa um JWT interno (InternalJwtProvider) emitido com as authorities DATTABI_VIEW e ADMIN, satisfazendo tanto o .authenticated() do filtro do dattabi-service quanto o @RequirePermission("DATTABI_VIEW") checado pelo aspecto. O frontend é apenas dica visual; a barreira de segurança permanece no backend (Diretrizes do Projeto §13).
Visibilidade por membros (escopo, Diretrizes do Projeto §13)
Ter CONFIG_VIEW ou PANEL_VIEW autoriza ler workspaces, não autoriza ler todos os workspaces. Além da permissão, todo acesso passa pelo escopo de membership (WorkspaceAccess, config-service):
| Quem | O que enxerga |
|---|---|
Administrador (ADMIN/SYSTEM_ADMIN) e serviços internos (S2S) | Todos os workspaces |
| Qualquer usuário autenticado com permissão de leitura | Workspaces sem membros — que são públicos por definição (é o caso dos seeds Processos e Assistência Social) |
| Usuário listado na aba Membros, e o dono do workspace | Os workspaces restritos de que participa, além dos públicos |
O membro é reconhecido tanto pelo e-mail quanto pelo id do usuário: a aba Membros grava o e-mail quando ele está disponível, e o token do usuário carrega o id — o backend aceita os dois, sem diferenciar maiúsculas de minúsculas.
Um workspace restrito de que o usuário não participa responde 404, e não 403: confirmar a existência do recurso já seria um vazamento (o usuário descobriria os nomes/ids dos workspaces sigilosos das outras equipes). Por isso a mensagem na interface é deliberadamente ambígua — "Workspace não encontrado ou sem acesso". A negação é registrada na auditoria como AUTHZ.PERMISSION_DENIED com permission=WORKSPACE_MEMBERSHIP.
O escopo vale para todas as rotas do workspace, não só a leitura: alterar, excluir, mexer em painéis, contextos e membros de um workspace invisível também responde 404, mesmo com CONFIG_EDIT. A validação da ação Vincular existentes (PUT .../panel-refs) também é escopada: um painel de workspace invisível é recusado exatamente como um id inexistente, com a mesma mensagem — caso contrário a diferença entre as duas respostas revelaria, por tentativa e erro, quais workspaces restritos existem. E o catálogo de painéis (GET /api/config/panels) achata apenas os workspaces visíveis — sem isso, os títulos dos painéis e dashboards de um workspace restrito vazariam pela galeria e pelo popover da home. Painéis nativos da plataforma continuam visíveis a todos.
Consequência prática. Enquanto a aba Membros de um workspace estiver vazia, ele continua visível para toda a plataforma — é o comportamento histórico e é o que mantém os workspaces de demonstração acessíveis. Para restringir um workspace, basta adicionar o primeiro membro: a partir daí ele some para quem não estiver na lista (exceto administradores).
Auditoria
- A vinculação de painéis usa o fluxo existente de mutação de workspace (
PUT .../panel-refs), coberto pela auditoria de mutações de configuração doconfig-service. - A criação/alteração/arquivamento de dashboards no DATTABI segue a auditoria do próprio
dattabi-service(versionamento via o serviço de versões de dashboards e invalidação de caches de copilot no save/archive/delete). A leitura (GET /api/dattabi/dashboards) não gera evento de mutação.
Cache
- A listagem de dashboards por workspace é consultada a cada montagem do catálogo de painéis. O caminho de leitura
GET /api/dattabi/dashboardsapoia-se no repositório do DATTABI; o catálogo em si não adiciona uma camada Valkey própria — a chamada S2S é paralelizada e protegida por timeout/best-effort. - O DATTABI mantém caches próprios de narrativa e sugestões por dashboard, invalidados em save/archive/delete (API de dashboards).
Observabilidade (OTel)
Backend e frontend já são instrumentados pelo padrão da plataforma (Diretrizes do Projeto §11):
- As chamadas HTTP de
GET /api/config/panels,GET /api/dattabi/dashboardsePUT .../panel-refsgeram spans de entrada (controllers) e a chamada S2S doconfig-serviceaodattabi-servicegera span de cliente HTTP, todos exportados aos índicesotel-v1-apm-span-*. - O shell (página
index.html) carrega o OTel browser SDK e propagatraceparentnas chamadasfetch()de/api/config/workspacese/api/config/panels.
Como usar (passo a passo do usuário)
O menu lateral mostra o workspace ativo
O menu exibe as funcionalidades do workspace selecionado, e trocar o workspace no card do topo substitui a lista. Não existe hub nem listagem de workspaces na navegação: o card é o controle.
Ficam sempre visíveis, independentemente do workspace: Chat, Painéis, DATTA Captain, Documentação, Sistema e o perfil. São plataforma, não trabalho de equipe — e filtrar o hub Sistema poderia trancar o administrador fora da própria configuração.
Um grupo cujo conteúdo inteiro esteja desabilitado não aparece. Quando ainda não há workspace resolvido — durante o carregamento, se a chamada falhar, ou se o usuário não tem acesso a nenhum — o menu mostra tudo: um menu vazio não teria saída nem explicação.
Para escolher o que cada workspace mostra, use a aba Funcionalidades da tela de gestão.
Criar um workspace
- Abra .
- Clique em Novo workspace, no topo da lista à esquerda.
- Informe o nome (obrigatório) e uma descrição (opcional) e confirme em Criar workspace.
O workspace nasce vazio e já fica selecionado: as abas à direita — Funcionalidades, Contextos de Dados, Painéis e Membros — são onde ele ganha conteúdo. Enquanto não tiver membros, ele é público para quem enxerga a tela; ao ganhar o primeiro membro, passa a aparecer só para membros e para o dono.
Exige a permissão CONFIG_EDIT.
Criar um painel próprio do workspace
- Selecione o workspace e abra a aba Painéis.
- Clique em Novo painel, informe o título e confirme em Criar e abrir.
O painel é criado vazio e abre no construtor de painéis em outra aba, onde os widgets e a fonte de dados são definidos. É diferente de Vincular existentes, que apenas traz para este workspace um painel que já existe em outro lugar — vincular não copia nem duplica.
Vincular um dashboard do DATTABI a um workspace
- Acesse Sistema → Workspaces (
/configurar/workspaces). - Selecione o workspace desejado e abra a aba Painéis.
- Clique em Vincular existentes.
- Na lista, marque os dashboards do DATTABI (e/ou painéis de outros workspaces) que deseja exibir e confirme. Os dashboards aparecem com a indicação de origem; os do próprio workspace também podem ser selecionados na UI.
- Os vínculos persistidos aparecem na seção Painéis vinculados com o badge Vinculado e o botão Abrir.
Limitação atual: o backend
setPanelRefsainda não reconhece dashboards do DATTABI como referência válida (rejeita os de outro workspace com400e descarta os do próprio). Até o backend aceitarkind == 'dattabi-dashboard', esse vínculo pode não persistir e o atalho no menu não se materializa. Ver Limitações conhecidas.
Abrir um dashboard pelo menu
- No menu lateral, expanda o grupo do workspace.
- Os dashboards cujo
panelRefestá efetivamente persistido aparecem como botões (com o título do dashboard) — ver a limitação acima. - Clique para abrir o dashboard (
/dattabi.html?dashId=<id>) em uma nova aba.
Usar os contextos do workspace para filtrar dados
- Defina os contextos do workspace na aba de contextos do gerenciador.
- No menu lateral, expanda o grupo do workspace e clique em uma funcionalidade.
- O escopo aplicado depende da funcionalidade:
- Chat direciona para uma tela própria escopada ao workspace: a busca (Pesquisa e Investigação) considera somente os contextos do workspace, resolvidos no servidor pelo id do workspace com a identidade do usuário, e os nomes dos contextos aparecem esmaecidos ao lado do seletor de modo. Ver Chat do workspace.
- Painel (
dashboard), Upload e Triagem em Lote recebem os contextos do workspace via?domain=e abrem já filtrados (Triagem em Lote vem com os contextos pré-selecionados).
Como operar (admin)
- A descoberta de dashboards depende do
dattabi-serviceestar acessível peloconfig-servicena URL configurada emdatta.dattabi-service.url(defaulthttp://dattabi-service:8210). Se indisponível, o catálogo de painéis continua respondendo, apenas sem as entradas de dashboard (degradação graciosa, logada em níveldebug). - O JWT interno usado na chamada S2S exige
datta.jwt.secret/JWT_SECRETpopulado noconfig-service; sem segredo válido, o cabeçalhoAuthorizationnão é enviado e odattabi-servicepode recusar a leitura. - A permissão
DATTABI_VIEWdeve estar mapeada nas roles que precisam ver os dashboards, eCONFIG_EDITnas roles que administram os vínculos do workspace.
Limitações conhecidas
- Cobertura de contexto parcial: apenas as funcionalidades em
WS_CONTEXT_AWARE(dashboard,upload,triagem-lote) consomem?domain. O Chat tem escopo próprio (view dedicadachat-ws, resolvida no servidor pelo id do workspace). Investigação, Regras, Lineage, Notebook, Explorar Dados e Catálogo ainda não filtram por contexto do workspace — extensão prevista para o futuro (Regras é chaveada por contexto legal — CPC/CDC/CPP — e abre em "Todos", não pelos contextos de dados do workspace). - Vínculo de dashboard do DATTABI via
panelRefnão persiste hoje: a UI do gerenciador permite marcar dashboards do DATTABI (inclusive do próprio workspace), mas osetPanelRefsatual só aceita referências a painéis nativos ou a painéis canvas existentes em outro workspace. Dashboards do DATTABI vêm do catálogo via S2S e não sãoWorkspace.Panelpersistidos, então:- referência a dashboard de outro workspace é rejeitada com
400("Painel referenciado não existe"); - referência a dashboard do próprio workspace é descartada silenciosamente.
- referência a dashboard de outro workspace é rejeitada com
O menu lateral (index.js:2105-2107) só renderiza um dashboard quando o panelRef correspondente foi efetivamente persistido e resolve no catálogo pela chave workspaceId::panelId. Enquanto o backend não reconhecer dashboards do DATTABI como referência valida (ex.: aceitar kind == 'dattabi-dashboard' na validacao), o atalho no menu via panelRefs não se materializa de forma estável — extensão pendente.
- A propagação de
?domainocorre ao navegar a partir do grupo do workspace; ao acessar a funcionalidade por outro caminho (sem workspace), o filtro de contexto não é aplicado automaticamente.
Galeria "Painéis" do workspace (cards com screenshot)
Cada grupo de workspace no menu lateral tem um item "Painéis" que abre a galeria de painéis escopada àquele workspace (?workspaceId=<id>). A galeria substitui a antiga listagem de dashboards individuais por uma grade de cartões de ~3,5 polegadas (336px @96dpi), cada um com um "screenshot" do painel:
A galeria mostra apenas painéis publicados: dashboards do DATTA BI (kind == 'dattabi-dashboard'), o painel nativo do tipo do contexto do workspace (ver abaixo) e painéis vinculados (panelRefs). Painéis de canvas (declarativos) entram quando estão publicados — só o published os separa, como qualquer outro tipo. (Eles já foram filtrados por kind !== 'canvas', quando ainda não existia passo de publicação de canvas; ele existe, e o filtro saiu.)
Painel nativo: um por tipo de contexto
O cartão nativo do workspace é escolhido pelo tipo do contexto de dados (tipoContexto) que o workspace agrupa, não por uma constante da plataforma:
| Tipo do contexto | Cartão nativo | Tela aberta |
|---|---|---|
processo_judicial | Painel de Processos | painel de processos (view dashboard) |
dossie_cadastral | Painel de Onboarding PJ | lista de dossiês PJ por CNPJ (view kyb-onboarding) |
| demais tipos | nenhum | — |
- O catálogo de painéis resolve, por workspace, quais painéis nativos se aplicam (campo
workspaceIdsde cada entrada nativa) — a galeria não precisa saber o tipo do contexto, e um workspace multi-contexto recebe um cartão por tipo. - Título e descrição do cartão vêm do catálogo, não da página: o cartão de dossiê não herda mais o texto "Triagem e auditoria de processos judiciais".
- O vínculo gravado (
panelRefscomworkspaceId == "native") é reconciliado ao trocar os contextos do workspace e na subida da plataforma — troca, nunca duplica; workspace sem contexto conhecido (não cadastrado ou sem o tipo informado) fica intacto. - O cartão de dossiê cadastral exige a permissão
KYB_VIEW— a mesma que esconde a entrada "Onboarding PJ" no menu lateral. O catálogo não devolve a entrada a quem não a tem, então o cartão some de todas as superfícies ao mesmo tempo, em vez de abrir em 403.
Cada cartão tem um "screenshot" do painel:
- Dashboard do DATTA BI: usa a miniatura de
GET /api/dattabi/dashboards/{id}/thumbnail.png, que prioriza o screenshot real capturado no browser: quando um editor abre/salva/publica o dashboard, odattabi.jscompõe um PNG fiel (pixels reais dos canvases ECharts + textos de KPIs/tabelas) e envia viaPUT .../thumbnail; o backend persiste no Neo4j e cacheia no Valkey. O render sintético server-side ficou apenas como fallback para dashboards nunca abertos. (Render "ao vivo" via iframe do BI foi tentado e revertido — carregar o app inteiro por cartão era pesado/lento demais para thumbnail.) - Painel nativo ("Painel de Processos" ou "Painel de Onboarding PJ", conforme o tipo do contexto): usa miniatura "ao vivo" — um
iframenão-interativo do painel real, carregado no shell embutido (/index.html?view=<view>&embed=1&domain=<contextos do workspace>, que esconde menu lateral e barra do topo), escalado (largura lógica 1280,transform: scale, recorte no topo), lazy (IntersectionObserver) + escala responsiva (ResizeObserver),sandbox="allow-scripts allow-same-origin",pointer-events: none. Não há renderizador de screenshot server-side para nativos — a miniatura ao vivo é a abordagem 100% client-side e on-prem. (Até a migração das telas para fragmentos o alvo era a página standalone/dashboard.html; ela não existe mais, e a view do shell a substituiu.) - Painel de canvas (declarativo, feito no Panel Builder): mesma miniatura "ao vivo" do nativo, apontando para
/index.html?view=painel&ws=…&panel=…&mode=view&embed=1. Oembed=1esconde a toolbar (pb-embed) e implicaviewOnly, então o que aparece na miniatura é o painel pronto, nunca o construtor.
A miniatura sempre nasce de carregar o painel em background, e todo painel pronto tem uma — seja painel da plataforma (nativo ou declarativo) ou dashboard do DATTABI. O que muda entre os tipos é só onde o resultado para: o painel da plataforma renderiza o iframe direto no cartão, e o DATTABI guarda a PNG no servidor porque abrir o app inteiro do BI por cartão seria pesado demais para repetir a cada carga da galeria.
O construtor não aparece em superfície nenhuma da galeria — nem na miniatura (
embed=1o esconde) nem na trilha (ver abaixo). Ele é ferramenta interna; a galeria mostra painéis, não a ferramenta que os monta. A captura do DATTABI é enfileirada pordatta-thumb-regen.js(um iframe oculto por vez,?thumb=1, teto de capturas e timeout por captura), compartilhado com a Galeria do DATTA BI.
Ao clicar num cartão:
- Dashboard de BI → abre a tela do DATTABI travada em modo somente-visualização (
?view=dattabi&mode=view&dashId=<id>; ver DATTABI — Guia do Usuário §15.2). OdashIdviaja na querystring, e não como parâmetro de navegação interna, porque é de lá que o DATTABI o lê. - Painel nativo → navega para a view nativa do tipo do contexto (
dashboardpara processo judicial,kyb-onboardingpara dossiê cadastral), já filtrada pelos contextos do workspace, sem recarregar a aplicação. - Painel de canvas → abre o painel em modo visualização (
?view=painel&mode=view&ws=…&panel=…), com "Abrir no construtor" como ação separada — a galeria não entra no construtor por conta própria.
Todo cartão é um link: Ctrl/Cmd+clique abre em nova aba, e o teclado o alcança sem ARIA extra. A trilha de navegação reflete o caminho percorrido (Painéis › painel), e não a posição do item no menu — sem isso o painel de dossiês abriria como "Investigação › Onboarding PJ", correto para quem chegou pelo menu e errado para quem clicou no cartão.
O construtor não aparece nem na URL. A view chama-se
painel, nãopanel-builder: o id da view fica visível na barra de endereços, e nomear ali uma ferramenta interna para quem só abriu um painel pela galeria contradiz a mesma regra que o tira da trilha. A view é uma só para ver e editar — o que muda émode=view, não a tela. O id antigo (?view=panel-builder&…) segue como alias, para não quebrar link já gravado.
Num painel declarativo aberto como visualizador, a folha da trilha é o título do painel, nunca "Construtor de Painéis": DATTA › Painéis › Onboarding PJ — Pendências. O gatilho é o mode=view da URL, e não a origem da navegação — origem é estado de tela e some ao recarregar, e o deep-link do cartão é justamente uma carga nova. O título vem do que a própria tela publica (BREADCRUMB_SET). Dentro do construtor (sem mode=view) a trilha volta a nomeá-lo, e o título entra como nível extra dizendo qual painel está aberto: DATTA › Painéis › Construtor de Painéis › Onboarding PJ — Pendências.
URLs externas eventualmente configuradas num painel passam por validação (safePanelUrl: só http(s)/relativa) antes de window.open — bloqueia javascript:/data: (Diretrizes do Projeto §2).
Limpeza do menu
- O item "Painel" (a view
dashboard= "Painel de Processos") foi removido da lista de funcionalidades por-workspace no menu lateral, por ser redundante com a galeria "Painéis" — esse painel continua acessível pelo cartão NATIVO da galeria. A viewdashboardpermanece roteável (deeplink?view=dashboardsegue funcionando). - O construtor de painéis (Gerenciador de Painéis em Sistema › Contextos) foi mantido — a galeria/menu "Painéis" apenas não expõe o construtor.
Seleção múltipla de painéis + popover de painéis na home
Esta seção cobre duas capacidades complementares ao catálogo de painéis descrito acima:
- Vincular múltiplos painéis existentes a um workspace numa única operação (multi-seleção), por referência — sem duplicar a definição do painel.
- Popover de painéis na home (chat): o botão ao lado do enviar consulta deixou de navegar cegamente para o painel de processos e passou a abrir um popover que lista, system-wide, os painéis prontos para consumo.
Vincular existentes (multi-seleção) no gerenciador de workspaces
Na aba Painéis do gerenciador (/configurar/workspaces), o botão Vincular existentes abre um dialog M3 com checkboxes — o usuário marca vários painéis do catálogo (GET /api/config/panels) de uma só vez e confirma. O estado de seleção é um conjunto (Set) inicializado com os vínculos já persistidos, de modo que abrir o dialog novamente mostra o que já está vinculado (screens/settings-workspaces.html).
As opções oferecidas são o catálogo achatado, filtrado assim (screens/settings-workspaces.html):
const options = catalog.filter(
c => c.workspaceId !== selected.id || c.kind === 'dattabi-dashboard'
);Ou seja: painéis de canvas de outros workspaces (o próprio já aparece sozinho na lista de painéis do workspace) mais os dashboards do DATTABI (kind === 'dattabi-dashboard') de qualquer workspace, inclusive o próprio. Cada opção mostra o título e a origem (Plataforma para nativos, ou o nome do workspace de origem).
A confirmação persiste a lista completa de referências via PUT /api/config/workspaces/{id}/panel-refs. Os vínculos aparecem na seção Painéis vinculados, cada um com um badge de origem — Plataforma para painéis nativos, Vinculado para os demais — e as ações Abrir (abre o painel de origem) e Remover vínculo (screens/settings-workspaces.html). O contador do card do workspace na listagem soma painéis próprios + vinculados: (w.panels || []).length + (w.panelRefs || []).length (screens/settings-workspaces.html).
Referência, não cópia. O vínculo aponta para o painel de origem — editar o painel no workspace dono reflete em todos os que o vinculam; remover o vínculo não apaga o painel de origem (CA-2). A fonte de verdade permanece única.
Validação do backend (setPanelRefs)
A API de workspaces valida, deduplica e normaliza a lista antes de gravar. Regras atuais:
- Dedupe por chave
workspaceId::panelId(refs repetidas são descartadas). - Refs com
workspaceId/panelIdnulos são ignoradas. - Auto-referência redundante: um painel de canvas do próprio workspace já aparece sozinho, então a ref a ele é descartada silenciosamente.
- Aceita: painel nativo (API do catálogo de painéis), painel canvas de qualquer workspace visível ao usuário, ou qualquer referência a um workspace existente e visível (
knownWorkspace— inclui"native"e os workspaces do store que passam porWorkspaceAccess.canSee; um workspace invisível é recusado com a mesma mensagem de um id inexistente, para não virar oráculo de existência). Isso cobre os dashboards do DATTABI (que vêm do catálogo S2S e não sãoWorkspace.Panel): eles são aceitos pela existência do workspace de origem e resolvidos no menu/galeria viaGET /api/config/panels. - Rejeita apenas quando a ref não é nativa, não é canvas e o workspace de origem não existe:
400 Bad Requestcom{"error":"Painel referenciado não existe: <ws>/<panel>"}(CA-4).
Nota (evolução vs. seção anterior). A seção "Limitações conhecidas" mais acima (herdada da entrega dos dashboards do DATTABI no workspace) descreve um gap em que refs a dashboard do DATTABI eram rejeitadas/descartadas. O
setPanelRefsatual é mais permissivo: valida a ref pela existência do workspace de origem, então dashboards do DATTABI de outros workspaces e do próprio são aceitos como referência; refs que não resolvem no catálogo são ignoradas graciosamente na renderização (não quebram o catálogo nem o menu).
Permissão: CONFIG_EDIT, em dupla camada — path-matcher no SecurityConfig (pathMatchers(PUT,"/api/config/**").hasAnyAuthority("CONFIG_EDIT","ROLE_ADMIN")) mais @RequirePermission("CONFIG_EDIT") no método (que retorna Mono, ramo reativo da checagem de permissão declarada).
Popover de painéis na home (chat)
Na home (chat), o botão ao lado do enviar consulta virou um toggle de popover (mesmo padrão visual do picker de agentes JDBC). O primeiro clique abre o popover e carrega o catálogo uma única vez (lazy); o clique num item fecha o popover e navega ao painel escolhido (CA-3).
O catálogo vem de GET /api/config/panels e é filtrado, na home, para somente painéis prontos para consumo — nativos da plataforma e dashboards BI publicados (chat.js:367-372):
fetch(API + '/api/config/panels')
.then(r => r.ok ? r.json() : [])
.then(d => setPanelsList((Array.isArray(d) ? d : [])
.filter(p => p && (p.native || p.kind === 'dattabi-dashboard'))))
.catch(() => setPanelsList([]));Por que canvas fica de fora da home (ajuste de escopo, feedback do usuário em 2026-06-11): painéis de canvas estão em construção no construtor e são acessados pelo Gerenciador de Painéis/Workspaces — incluí-los na home duplicava a entrada "Painel de Processos" (canvas seedado vs. nativo) e expunha um link para o builder na home.
Cada item mostra o título e a origem do painel. Estados em PT-BR: "Carregando painéis..." (loading) e "Nenhum painel disponível para o seu usuário." (empty). Se o fetch falhar, a lista cai para vazia — o popover degrada para o empty state amigável, sem erro bruto (Diretrizes do Projeto §5).
Roteamento do clique (chat.js:openPanelEntry, chat.js:384-403):
- Nativo (
p.native): navega no próprio shell (SPA) viawindow.parent.postMessage({type:'NAVIGATE_TO', payload: p.view || 'dashboard'}). - Dashboard BI (
kind === 'dattabi-dashboard'): abrep.url(/dattabi.html?dashId=<id>) em nova aba, após passar pelosafePanelUrl. - Canvas (não aparece na home hoje, mas o roteamento existe):
?view=painel&ws=<ws>&panel=<panel>&mode=viewem nova aba.
Segurança de URL. URLs vindas do config do painel passam por safePanelUrl, que só aceita http/https (ou relativa) — bloqueia javascript:/data: graváveis por quem tem CONFIG_EDIT (Diretrizes do Projeto §2).
Permissão PANEL_VIEW
A leitura do catálogo e a abertura de painéis em modo visualização passaram a aceitar a permissão dedicada PANEL_VIEW (categoria Config, registrada no catálogo central Permission) ou CONFIG_VIEW — semântica any-of via @RequirePermission({"PANEL_VIEW", "CONFIG_VIEW"}) (a anotação foi ampliada para aceitar String[]). Isso evita exigir CONFIG_VIEW (admin-leaning) do usuário comum só para ver/consumir painéis.
Gates any-of atuais:
| Endpoint | Permissão any-of |
|---|---|
GET /api/config/panels (API do catálogo de painéis) | PANEL_VIEW \ |
GET /api/config/workspaces/{id} (API de workspaces) | PANEL_VIEW \ |
POST /api/config/panel/{domain}/aggregate e .../preview (a API de dados de painéis) | PANEL_VIEW \ |
Para os endpoints de dados do painel (aggregate/preview), que são leitura de renderização apesar de serem POST (parâmetros só no body), há um matcher dedicado no SecurityConfig antes do gate de mutação, para que PANEL_VIEW libere a leitura sem conceder escrita.
PANEL_VIEW está mapeada nas roles padrão ADVANCED_USER, ANALISTA e READ_ONLY (RolePermissions + BuiltInRoleSync). Usuários não-admin precisam re-logar para o JWT carregar a permissão nova.
Como usar (passo a passo)
Vincular vários painéis de uma vez:
- Acesse Sistema → Workspaces e selecione o workspace.
- Na aba Painéis, clique em Vincular existentes.
- Marque, com os checkboxes, todos os painéis desejados (dashboards BI e/ou painéis de outros workspaces) e confirme.
- Eles aparecem em Painéis vinculados com o badge de origem (Plataforma/Vinculado) e as ações Abrir e Remover vínculo.
- Remover o vínculo tira apenas a referência — o painel de origem permanece.
Descobrir e abrir painéis pela home:
- Na home (chat), clique no botão ao lado do enviar consulta.
- O popover Painéis lista os painéis acessíveis (nativos + dashboards BI), independentemente de workspace.
- Clique em um item para abrir o painel escolhido.