PT EN
Voltar ao site

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:

  1. 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.
  2. 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) ao dattabi-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 cujo panelRef está persistido (abrindo a tela do DATTABI). A persistência do vínculo do dashboard via panelRef tem uma limitação de backend conhecida — ver Limitações conhecidas.
  • O shell propaga os contextos do workspace via querystring ?domain=ctx1,ctx2 ao 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-service em GET /api/dattabi/dashboards?workspaceId=<id> usando WebClient.
  • 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 onErrorResume que 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:
json
  {
    "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éisVincular existentes), o catálogo é carregado e filtrado para a lista de opções vinculáveis:

js
// 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:

js
// 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:

js
// 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:

js
// 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):

js
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 ?domain e passaram a filtrar/usar os contextos do workspace.
  • Regras (regras-triagem): filtra as regras pelo primeiro contexto do workspace; sem ?domain mostra Todos (compiled/regras_triagem.js:788-791):
js
  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 ?domain seleciona todos, mantendo o comportamento padrão (compiled/triagem_lote.js:132-137):
js
  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âmetroTipoDefaultDescrição
workspaceIdstringObrigatório (semanticamente); vazio → []
includeArchivedbooleanfalseInclui 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:

json
{ "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.Panel persistidos — logo o setPanelRefs atual 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çãoPermissão exigidaOnde é checada
Ler o catálogo de painéisPANEL_VIEW ou CONFIG_VIEWAPI 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 workspaceDATTABI_VIEWAPI 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 workspaceCONFIG_EDITDupla 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 enforça quando o método retorna Mono/Flux (ramo reativo, via ReactiveSecurityContextHolder). Os três métodos retornam Mono, 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 no SecurityWebFilterChain — por isso as mutações de config também têm o path-matcher por permissão no SecurityConfig.

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):

QuemO que enxerga
Administrador (ADMIN/SYSTEM_ADMIN) e serviços internos (S2S)Todos os workspaces
Qualquer usuário autenticado com permissão de leituraWorkspaces 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 workspaceOs 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 do config-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/dashboards apoia-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/dashboards e PUT .../panel-refs geram spans de entrada (controllers) e a chamada S2S do config-service ao dattabi-service gera span de cliente HTTP, todos exportados aos índices otel-v1-apm-span-*.
  • O shell (página index.html) carrega o OTel browser SDK e propaga traceparent nas chamadas fetch() de /api/config/workspaces e /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

  1. Abra SistemaWorkspaces.
  2. Clique em Novo workspace, no topo da lista à esquerda.
  3. 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

  1. Selecione o workspace e abra a aba Painéis.
  2. 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

  1. Acesse Sistema → Workspaces (/configurar/workspaces).
  2. Selecione o workspace desejado e abra a aba Painéis.
  3. Clique em Vincular existentes.
  4. 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.
  5. 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 setPanelRefs ainda não reconhece dashboards do DATTABI como referência válida (rejeita os de outro workspace com 400 e descarta os do próprio). Até o backend aceitar kind == '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

  1. No menu lateral, expanda o grupo do workspace.
  2. Os dashboards cujo panelRef está efetivamente persistido aparecem como botões (com o título do dashboard) — ver a limitação acima.
  3. Clique para abrir o dashboard (/dattabi.html?dashId=<id>) em uma nova aba.

Usar os contextos do workspace para filtrar dados

  1. Defina os contextos do workspace na aba de contextos do gerenciador.
  2. No menu lateral, expanda o grupo do workspace e clique em uma funcionalidade.
  3. 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-service estar acessível pelo config-service na URL configurada em datta.dattabi-service.url (default http://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ível debug).
  • O JWT interno usado na chamada S2S exige datta.jwt.secret/JWT_SECRET populado no config-service; sem segredo válido, o cabeçalho Authorization não é enviado e o dattabi-service pode recusar a leitura.
  • A permissão DATTABI_VIEW deve estar mapeada nas roles que precisam ver os dashboards, e CONFIG_EDIT nas 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 dedicada chat-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 panelRef não persiste hoje: a UI do gerenciador permite marcar dashboards do DATTABI (inclusive do próprio workspace), mas o setPanelRefs atual 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ão Workspace.Panel persistidos, 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.

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 ?domain ocorre 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 contextoCartão nativoTela aberta
processo_judicialPainel de Processospainel de processos (view dashboard)
dossie_cadastralPainel de Onboarding PJlista de dossiês PJ por CNPJ (view kyb-onboarding)
demais tiposnenhum
  • O catálogo de painéis resolve, por workspace, quais painéis nativos se aplicam (campo workspaceIds de 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 (panelRefs com workspaceId == "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, o dattabi.js compõe um PNG fiel (pixels reais dos canvases ECharts + textos de KPIs/tabelas) e envia via PUT .../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 iframe nã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. O embed=1 esconde a toolbar (pb-embed) e implica viewOnly, 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=1 o 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 por datta-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). O dashId viaja 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 (dashboard para processo judicial, kyb-onboarding para 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ão panel-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 view dashboard permanece roteável (deeplink ?view=dashboard segue 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:

  1. 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.
  2. 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):

js
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/panelId nulos 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 por WorkspaceAccess.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ão Workspace.Panel): eles são aceitos pela existência do workspace de origem e resolvidos no menu/galeria via GET /api/config/panels.
  • Rejeita apenas quando a ref não é nativa, não é canvas e o workspace de origem não existe: 400 Bad Request com {"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 setPanelRefs atual é 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):

js
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) via window.parent.postMessage({type:'NAVIGATE_TO', payload: p.view || 'dashboard'}).
  • Dashboard BI (kind === 'dattabi-dashboard'): abre p.url (/dattabi.html?dashId=<id>) em nova aba, após passar pelo safePanelUrl.
  • Canvas (não aparece na home hoje, mas o roteamento existe): ?view=painel&ws=<ws>&panel=<panel>&mode=view em 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:

EndpointPermissã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:

  1. Acesse Sistema → Workspaces e selecione o workspace.
  2. Na aba Painéis, clique em Vincular existentes.
  3. Marque, com os checkboxes, todos os painéis desejados (dashboards BI e/ou painéis de outros workspaces) e confirme.
  4. Eles aparecem em Painéis vinculados com o badge de origem (Plataforma/Vinculado) e as ações Abrir e Remover vínculo.
  5. Remover o vínculo tira apenas a referência — o painel de origem permanece.

Descobrir e abrir painéis pela home:

  1. Na home (chat), clique no botão ao lado do enviar consulta.
  2. O popover Painéis lista os painéis acessíveis (nativos + dashboards BI), independentemente de workspace.
  3. Clique em um item para abrir o painel escolhido.