npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@benenutri/mitra-kanban

v1.9.0

Published

Motor de quadros (kanban) configurável para projetos da plataforma Mitra: tabelas, server functions e componentes React instaláveis por versão.

Downloads

1,249

Readme

@benenutri/mitra-kanban

Motor de quadros (kanban) configurável para projetos da plataforma Mitra: tabelas, Server Functions e componentes React que um projeto novo instala e atualiza por versão. O gestor da área monta fases, campos, condições e automações sozinho — a estrutura do banco não muda.

Só para sistemas novos. Legados (SGC, ITSM, CRM Ativa, Comercial 360) não recebem a lib.

O que vem no pacote

| Parte | Caminho | O que é | |---|---|---| | Backend | backend/ | schema.mjs (13 tabelas KB_*, UPGRADES), escritas.mjs (30 SQL internas), funcoes.mjs (27 SF públicas), install.mjs (instalar()), index.mjs | | Motor | motor/motor.mjs | regra pura: condições, visibilidade, validação, normalização — roda na SF, na tela e nos testes; e o modelo de e-mail (montarEmail, spec 016) | | Mapa | frontend/server-functions.ts | bloco canônico SERVER_FUNCTION_NAMES / SERVER_FUNCTIONS para copiar no consumidor | | Componentes | dist/ (de src/) | KanbanProvider, KanbanBoard, CardDrawer, BoardAdmin | | Importação | backend/pipefy/ | extrair.mjs (Pipefy → pacote), mapear.mjs (conversão pura), carregar.mjs (pacote → banco; --conferir, --simulador) — ver Importar quadros do Pipefy |

Spec, plan e tasks: docs/specs/001-motor-kanban/ (motor), 002-aparencia-quadro/ (cores, cara do card, gravação ao sair do campo), 003-card-aberto/ (card em três colunas, abas, passagens, vários responsáveis), 004-construtor-de-fases/ (administração numa tela só com prévia, observação por campo, capa do card por fase, os três tempos do cartão), 005-tarefas-do-card/, 006-chat-do-card/ e 007-anexos-do-card/ (as abas Tarefas, Chat e Anexos do card aberto), 010-etiquetas/ (etiquetas do quadro como selo, filtro por etiqueta, cadastro de opções em linhas, "+" para etiqueta e responsável no card), 011-importar-pipefy/ (quadros do Pipefy trazidos por TI em duas etapas — extrair para um pacote, conferir, carregar — com histórico e origem por card) e 013-pontos-de-extensao/ (abas, ações e regras do sistema consumidor no card aberto — ganchos de servidor para mover e excluir, nota no histórico, card por processo sem sessão, {{anexos}} no e-mail) e 016-email/ (o modelo de e-mail Benenutri: automação, menção, novo responsável e tarefa atribuída saem no mesmo modelo, e o sistema consumidor o usa nos e-mails próprios).

Instalar num projeto Mitra novo

  1. Nos dois lados do projeto: npm i @benenutri/mitra-kanban (em backend/ e em frontend/).
  2. Copie os dois blocos de node_modules/@benenutri/mitra-kanban/frontend/server-functions.ts para o frontend/src/lib/server-functions.ts do projeto (junto das funções próprias dele).
  3. Crie backend/add-001-kanban.mjs:
import 'dotenv/config';
import { pathToFileURL, fileURLToPath } from 'node:url';
import { instalar, montarDefinitions, lerIdsDoMapa } from '@benenutri/mitra-kanban/backend';

// Ids das escritas vêm do mapa do frontend: é por ele que o simulador resolve
// chamadas por id, antes e depois do sync-server-function-map.
const MAPA = fileURLToPath(new URL('../frontend/src/lib/server-functions.ts', import.meta.url));
export const definitions = montarDefinitions(lerIdsDoMapa(MAPA));

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
  const sdk = await import('mitra-sdk');
  sdk.configureSdkMitra({
    baseURL: process.env.MITRA_BASE_URL, token: process.env.MITRA_TOKEN, integrationURL: process.env.MITRA_BASE_URL_INTEGRATIONS,
  });
  await instalar({ sdk, projectId: Number(process.env.MITRA_PROJECT_ID), mapaFrontend: MAPA });
}
  1. No simulador do projeto (backend/simulator/config.mjs):
export const PUBLISH_ORDER = ['add-001-kanban.mjs', /* ...os add-* do projeto */];
export const DDL_SOURCES = [
  join(HERE, '..', 'setup-backend.mjs'),
  fileURLToPath(import.meta.resolve('@benenutri/mitra-kanban/backend/schema')),
];
  1. frontend/src/index.css, logo após os @import: @source "../node_modules/@benenutri/mitra-kanban"; — as telas da lib usam as classes de token do design system da Benenutri (bg-card, text-ink-secondary, bg-paper-sunken…), e o Tailwind precisa escanear o pacote para gerá-las. O projeto precisa ter o design system instalado (/benenutri:design), que é quem define os tokens.
  2. Rotas (ver seção Componentes).
  3. Perfil de acesso do projeto: libere as 27 SF kb* e as 30 kbEsc* para o perfil business.
  4. Anexos (spec 007): passe enviarArquivo ao criarKanbanApi (ver Componentes). Sem ele, a aba Anexos aceita só link.

Publicar: commit → no sandbox do projeto: cd backend && node add-001-kanban.mjs && node sync-server-function-map.mjs, depois cd frontend && npm run build e share. A plataforma gera as migrations.

Atualizar a versão num projeto

cd backend && npm i @benenutri/[email protected]
cd frontend && npm i @benenutri/[email protected]

Acrescente ao server-functions.ts os nomes novos (o instalar() avisa quais faltam). No sandbox: node add-001-kanban.mjs && node sync-server-function-map.mjs, build, share. instalar() é idempotente e só aditivo: CREATE IF NOT EXISTS, UPGRADES guardados por coluna, SFs upsertadas por nome, versão em KB_META.

A spec 004 não acrescenta server function nenhuma: kbFaseSalvar ganhou o parâmetro capaJson e a observação viaja no configJson que kbCampoSalvar já aceitava. Quem atualiza não mexe no server-functions.ts — só roda o add-001-kanban.mjs no sandbox para ganhar a coluna KB_FASE.CAPA_JSON.

A spec 010 (etiquetas) também não acrescenta SF, tabela, coluna nem token de cor — kbCardsListar ganhou o parâmetro etiqueta, e o selo sai de --data-N, --card e --foreground, que todo projeto com o design system já tem. Quem atualiza não cola nada: roda o add-001-kanban.mjs e pronto.

Nada muda de aparência para quem não ativar etiquetas: opção colorida de campo comum continua em ponto + rótulo, e só a lista de etiquetas vira selo.

A 1.1.0 (spec 011) acrescenta uma coluna, KB_CARD.ORIGEM_CHAVE (com índice), e nenhuma SF: quem atualiza só roda o add-001-kanban.mjs no sandbox. A mesma coluna serve à spec 008 (fonte:<id>:<chave>).

A 1.2.0 (specs 005, 006 e 007 — abas Tarefas, Chat e Anexos) acrescenta 4 tabelas (KB_TAREFA, KB_MENSAGEM, KB_MENSAGEM_LEITURA, KB_ANEXO, pelo CREATE IF NOT EXISTS, sem coluna nova em tabela existente), 10 SF públicas e 11 escritas. Quem atualiza:

  1. Copia os dois blocos novos do frontend/server-functions.ts da lib (ids locais 8017–8026 e 9020–9030) para o server-functions.ts do projeto — o instalar() avisa o que falta.
  2. Libera as 21 SF novas no perfil business.
  3. Passa enviarArquivo ao criarKanbanApi para a aba Anexos enviar arquivo (sem ele, só link).
  4. No simulador do projeto, para enviar arquivo localmente: copia de backend/simulator/server.mjs da lib a rota POST /interactions/uploadFilePublic e a GET /public/:nome. Sem elas o envio falha só no simulador.
  5. No sandbox: node add-001-kanban.mjs && node sync-server-function-map.mjs, build, share.

A 1.3.0 (spec 013 — pontos de extensão do sistema consumidor) não acrescenta tabela nem coluna: a nota é uma linha de KB_LOG e as regras ligadas são linhas de KB_META. Acrescenta uma SF pública, kbNotaRegistrar. Quem atualiza:

  1. Copia a linha kbNotaRegistrar nos dois blocos (SERVER_FUNCTION_NAMES e SERVER_FUNCTIONS) do frontend/server-functions.ts da lib para o server-functions.ts do projeto — o id fica o local até o sync-server-function-map.mjs trazer o real.
  2. Libera kbNotaRegistrar no perfil business.
  3. No sandbox: node add-001-kanban.mjs && node sync-server-function-map.mjs, build, share.
  4. Quer regras ligadas (mover/excluir por SQL do consumidor)? Passa ganchos ao instalar() no add-001-kanban.mjs — ver Regras ligadas (ganchos de servidor). Sem ganchos, nada muda.

Os pontos de tela (abasExtras, acoesExtras, podeMover, antesDeMover, podeExcluir, novoCard, atualizarACadaMs) são props de KanbanBoard/CardDrawer — não pedem nada no sandbox, só o npm i e o import (ver Pontos de extensão do sistema consumidor).

A 1.4.0 (spec 014 — movimento por processo) não acrescenta tabela, coluna nem SF: kbCardMover ganhou movidoPor e kbCardAtualizar ganhou atualizadoPor, os dois com origem, e só valem sem sessão. Quem atualiza não mexe no server-functions.ts nem no perfil — npm i nos dois lados e o node add-001-kanban.mjs de sempre no sandbox. Quem não chama as SFs sem sessão não vê diferença nenhuma.

A 1.6.0 (spec 016 — modelo de e-mail) não acrescenta tabela, coluna nem SF: os e-mails que já saíam (automação e menção no chat) passam a sair no modelo Benenutri, e dois avisos novos entram (novo responsável do card, tarefa atribuída). Quem atualiza roda npm i nos dois lados e o add-001-kanban.mjs de sempre; para o botão "Abrir card" e o nome do sistema no assunto, passa email: { sistema, urlDoCard } ao instalar() — ver E-mail (spec 016). Sem isso, os e-mails saem como [Benenutri] … e sem botão.

A 1.6.1 (correção de tela, spec 002 C-009) não acrescenta nada no servidor: só npm i no frontend. O quadro passa a preencher o main pelo layout (KanbanBoard em h-full, Panel e Board em flex-1) em vez de descontar 21rem da janela — era isso que deixava o main rolando junto com as colunas (duas barras) — e os cabeçalhos das fases dividem uma linha de grade (subgrid), então a descrição de uma fase não desalinha mais os cards das outras. A pilha de cada coluna ficou relative: os rótulos sr-only dos cards (absolutos) passam a ser contidos por ela, e não pela janela — era isso que criava uma barra de rolagem da página inteira que descia sem mostrar nada. Exige o layout do §10 (main com altura definida e overflow-y-auto); fora dele o quadro fica nos 30rem do padrão.

A 1.7.0 (spec 017 — anexo pelo chat) acrescenta uma coluna, KB_ANEXO.MENSAGEM_ID (com índice, pelo UPGRADES), e nenhuma SF: kbMensagemEnviar ganhou anexoNome, anexoUrl e anexoTamanho (vazios = como antes) e kbMensagensListar passa a trazer o anexo de cada mensagem. Quem atualiza roda npm i nos dois lados e o add-001-kanban.mjs no sandbox — sem ele a leitura do chat falha, porque o join usa a coluna nova. O clipe só aparece no chat de quem passa enviarArquivo ao criarKanbanApi (o mesmo da aba Anexos); sem ele nada muda.

A 1.8.0 (spec 018 — cabeçalho do quadro) não acrescenta nada no servidor: só npm i no frontend (o backend pode acompanhar a versão por consistência; o add-001-kanban.mjs não traz nada novo). Na tela do quadro o cabeçalho fica numa linha (nome do quadro, contagem e ações, sem o rótulo "Quadro", sem grudar no topo), a descrição do quadro vai para um balão atrás do botão de informação ao lado da contagem, e o piso de altura do quadro cai de 30rem para 22rem. Num notebook 1366×768 a pilha visível passa de 270px para 325px e a página deixa de rolar por baixo do cabeçalho. Lista de quadros e administração não mudam. PageHeader do vocabulário ganhou compacto (sem module nem description), e Popover, PopoverContent e PopoverTrigger passaram a ser exportados pelo vocabulário.

A 1.9.0 (spec 019 — níveis de acesso) acrescenta uma coluna, KB_BOARD.ACESSO_JSON (pelo UPGRADES), e nenhuma SF: quem atualiza roda npm i nos dois lados e o add-001-kanban.mjs no sandbox, sem mexer no server-functions.ts nem no perfil. Os quadros que já existem não mudam: a coluna nasce vazia, que é lida como "todos membros". Quadro criado pela 1.9.0 nasce com padrão restrito — ver Níveis de acesso (spec 019). Quadro importado do Pipefy (carga direta, sem kbBoardSalvar) também nasce com a coluna vazia, ou seja, membro.

Importar quadros do Pipefy

Ferramenta de linha de comando para TI (spec 011): um quadro do Pipefy vira um quadro do motor com fases, campos, etiquetas, cards (valores, responsáveis, prazo, datas, conclusão), histórico de passagem por fase e um registro de origem por card, a conversa (comentários) e os anexos (arquivos enviados). Duas etapas com um pacote em disco no meio — o pacote é o seguro contra desligar o Pipefy e a fonte de tudo: carregar de novo acrescenta o que faltou, sem voltar ao Pipefy.

  1. PIPEFY_TOKEN=... no .env do backend/ do projeto (token pessoal ou de serviço do Pipefy). Nunca vai para o git nem para o pacote.
  2. Congele o quadro no Pipefy (somente leitura): o que for editado lá depois da extração não entra.
cd backend
node node_modules/@benenutri/mitra-kanban/backend/pipefy/extrair.mjs --listar              # id, cards e nome de cada quadro
node node_modules/@benenutri/mitra-kanban/backend/pipefy/extrair.mjs --pipe 301234567      # pacote em pipefy/301234567/
node node_modules/@benenutri/mitra-kanban/backend/pipefy/carregar.mjs --pacote pipefy/301234567 --conferir
node node_modules/@benenutri/mitra-kanban/backend/pipefy/carregar.mjs --pacote pipefy/301234567 --simulador
node node_modules/@benenutri/mitra-kanban/backend/pipefy/carregar.mjs --pacote pipefy/301234567 --usuario [email protected]
  • Extrair lê o quadro pela API do Pipefy (≤ 5 requisições/s; espera e repete quando o Pipefy avisa excesso), grava os cards brutos em cards.jsonl, baixa os anexos na mesma passagem (o link vale 15 min) e confere a quantidade extraída contra a que cada fase declara. Termina "parcial", dizendo por quê, quando faltou card ou arquivo; --so-arquivos --saida <dir> refaz só os anexos perdidos; --sem-arquivos pula os anexos; --pagina muda o tamanho da página (50).
  • Conferir escreve relatorio.md no pacote sem gravar nada: fases e tipos finais, cada campo com a conversão de tipo, pessoas sem par no INT_USER, opções recriadas, históricos aproximados, valores que não couberam no tipo, e quantos comentários e arquivos entram na conversa e nos anexos.
  • Carregar grava por lotes com o token de desenvolvedor do .env — só INSERT/SELECT, nenhuma estrutura (o motor precisa estar na 1.1.0; senão a carga para antes de gravar). Idempotente: rodar de novo não duplica nem sobrescreve; card que ficou pela metade é completado. --usuario é quem importa (vira administrador do quadro, junto dos admins do Pipefy com par por e-mail); --lote muda o tamanho do lote (200). Os comentários entram na conversa do card com a data e o autor do Pipefy (pelo cadastro quando há par; pelo nome quando não há), e não contam como mensagens novas. --arquivos sobe os arquivos para o PUBLIC do projeto, guarda a URL em arquivos.json e, na mesma carga, cria os anexos dos cards; sem --arquivos o relatório diz quantos aguardam. Motor anterior à 1.2.0 carrega o resto e deixa conversa e anexos aguardando.
  • --simulador carrega o mesmo pacote no SQLite local (simulator/data/mitra-sim.db ou --banco) para o gestor ver o quadro antes de tocar em produção.

O que muda de forma (tabela completa no plan.md da 011): data e hora vira data (a hora fica no registro de origem); hora, CPF, CNPJ, identificador e fórmula viram texto com observação; conexão entre cards vira texto "Título (#id)"; responsável de formulário com várias pessoas fica com a primeira; etiquetas do pipe viram o campo etiquetas (spec 010) com a cor mais próxima da paleta; fase concluída do Pipefy é final — cancelado quando o nome tem cancel/recus/reprov/perd/arquiv/desist, sucesso nos demais (ajuste na administração). Opção que sumiu da lista do Pipefy mas ainda está em card é recriada. Histórico: o Pipefy só informa a primeira e a última entrada em cada fase, sem quem moveu — entra uma passagem por fase visitada, sem autor; card que voltou de fase fica "aproximado" e as datas brutas vão para o registro de origem, que também guarda o número e o link do card no Pipefy (a busca do quadro acha o card por esse número). Automações e condicionais do Pipefy não saem pela API: o relatório conta as condicionais e TI as refaz no construtor de fases.

Componentes

A lib fala com o Mitra pelo transporte do projeto (linhas/acao de mitra-api.ts): ela chama as server functions pelo nome e o projeto resolve o id. Nenhum SDK, fetch ou id dentro da lib.

import { KanbanProvider, criarKanbanApi, QuadrosLista, KanbanBoard, BoardAdmin } from '@benenutri/mitra-kanban';
import { acao, linhas } from '@/lib/mitra-api';

const api = criarKanbanApi({
  acao: (nome, input) => acao(nome as Parameters<typeof acao>[0], input),
  linhas: (nome, input) => linhas(nome as Parameters<typeof linhas>[0], input),
  // Opcional (spec 007): sem ele, a aba Anexos aceita só link.
  enviarArquivo: async (arquivo) => {
    const r = await uploadFilePublicMitra({ file: arquivo }); // de 'mitra-interactions-sdk'
    if (!r?.result?.publicUrl) throw new Error(`O envio de ${arquivo.name} não devolveu o endereço.`);
    return r.result.publicUrl;
  },
});

<KanbanProvider api={api}>
  {/* /quadros */}
  <QuadrosLista onAbrir={(slug) => navigate(`/quadros/${slug}`)} onAdministrar={(slug) => navigate(`/quadros/${slug}/admin`)} />
  {/* /quadros/:slug — o card aberto vive na URL (?card=ID) */}
  <KanbanBoard slug={slug} cardId={cardId} onCardChange={(id) => setParams(id ? { card: String(id) } : {})}
    onAdministrar={() => navigate(`/quadros/${slug}/admin`)} onVoltar={() => navigate('/quadros')} />
  {/* /quadros/:slug/admin */}
  <BoardAdmin slug={slug} onVoltar={() => navigate(`/quadros/${slug}`)} />
</KanbanProvider>

| Componente | O que faz | |---|---| | QuadrosLista | lista com busca, "Novo quadro" (criador vira admin) e ações Abrir/Administrar | | KanbanBoard | colunas por fase numa faixa horizontal (sempre na ordem do fluxo; rola quando não cabem), altura fixa com rolagem por coluna, barra na cor da fase e descrição, busca, filtro por responsável (qualquer um da lista), concluídos, "Novo card" com campos visíveis ao vivo, filtro por etiqueta, card aberto. O cartão é só leitura (emenda de 2026-09-11): sem arrastar e sem seletor de fase na capa — mover é no card aberto, em "Mover para". Cara do card: etiquetas (opções com cor), os campos fixos do quadro e depois os que aquela fase acrescenta, responsáveis, prazo relativo, mensagens novas na conversa para quem olha (spec 006) e os três tempos (criação, fase — com a fração do SLA — e última alteração) | | CardDrawer | card aberto em três colunas (spec 003): à esquerda responsáveis (vários), prazo, formulário inicial como ficha ("clique para preencher") e histórico por passagem (campos da fase e eventos ao abrir); no meio as abas "Nesta fase" (chip, SLA, campos da fase, condições das próximas fases), "Chat" (spec 006), "Tarefas" (spec 005), "Anexos" (spec 007) e "Alterações" (prosa por dia com filtro); à direita "Mover para" em botões e Excluir. Título editável no lugar. Sem botão Salvar: grava ao sair do campo e ao fechar ("Salvo às HH:MM"); mover grava o formulário antes | | BoardAdmin | abas Geral, Fases e Automações. "Fases" é o construtor da spec 004: fluxo à esquerda, campos da etapa no meio (setas para a ordem, "+ Adicionar campo" com os tipos, ✎ para editar) e prévia à direita — o cartão do quadro (com "Editar capa") e o card aberto, com dados de exemplo. Campo tem observação e "Editável depois da fase"; CondicaoEditor e AcoesEditor seguem nos diálogos; sem permissão os formulários desabilitam | | CampoInput, CondicaoEditor, AcoesEditor | peças reutilizáveis para telas próprias do projeto | | useKanban(), criarKanbanApi | acesso tipado às 27 server functions públicas; enviarArquivo repassado do transporte |

Exemplo completo: mitra-projects/p-59277/frontend (App, AdminLayout e as três páginas).

Desenvolvimento local da lib com um consumidor: npm link aqui e npm link @benenutri/mitra-kanban --no-save nos dois lados do consumidor (backend/ — schema, server functions e seed — e frontend/), com npm run build:watch aqui. Linkar só o frontend deixa o simulador do consumidor no schema do registry. Mudou o schema? Regere o banco do simulador do consumidor (npm run sim:seed, com o simulador parado). O vite.config.ts do consumidor precisa de resolve.dedupe: ['react', 'react-dom', 'react/jsx-runtime', 'radix-ui', 'cmdk', 'lucide-react'] e server.fs.allow incluindo a pasta da lib (ver o do p-59277) — sem isso o React da lib é outra cópia.

Pontos de extensão do sistema consumidor (spec 013)

Um sistema com regra própria (o SGC 2 é o primeiro) acrescenta abas, ações e regras ao card aberto sem copiar o card nem a gravação do motor. KanbanBoard e CardDrawer aceitam estas props, todas opcionais — ausente, o comportamento é o de hoje:

  • abasExtras — abas do sistema depois das do motor (Nesta fase, Chat, Tarefas, Anexos, Alterações), na ordem informada, com rótulo, contagem opcional lida do card e o conteúdo desenhado pelo próprio sistema. Ausente, o card só mostra as abas do motor.
  • conteudoDaFase (1.5.0, spec 015) — conteúdo do sistema dentro da aba "Nesta fase", abaixo dos campos da fase e acima das condições de saída. O sistema decide pelo ctx.card.faseId o que cada fase mostra (itens na montagem, notas fiscais na expedição, B.O. quando falta produto): o card muda de assunto conforme anda, em vez de uma aba por assunto. Ausente, a aba só tem os campos do motor. Combina com abasExtras (um assunto que vale em toda fase pode continuar aba).
  • acoesExtras — botões do sistema na coluna de "Mover para", acima do Excluir. Ausente, a coluna mostra só o que o motor já mostra.
  • podeMover — síncrono, roda na renderização de cada destino: devolver um texto desabilita o botão com esse motivo (no title); null libera. Soma-se às condições de fase do motor, não as substitui. Ausente, todo destino que a fase permite aparece livre.
  • antesDeMover — assíncrono, roda no clique, antes de gravar: { ok: false, erro } recusa e mostra o erro na faixa de aviso, sem gravar nada; pode consultar o servidor (alertas, prazo). Ausente, só a checagem do motor vale.
  • podeExcluir — false esconde o botão Excluir do motor. É espelho de tela: quem decide de verdade é a regra ligada no servidor (ver Regras ligadas abaixo). Ausente, o Excluir segue a regra de hoje (criador do card ou administrador do quadro).
  • novoCard — troca o botão "Novo card" do motor (cabeçalho e estado vazio) pelo do sistema; false tira o botão do quadro. Ausente, o botão do motor continua.
  • atualizarACadaMs — o quadro recarrega a lista sozinho a cada N milissegundos, sem indicador de carregamento e pausado com a aba escondida. Ausente ou 0, sem atualização periódica.

Toda aba e ação extra recebe o mesmo contexto (ExtensaoContexto): o card, a estrutura, o detalhe, os usuários, se está travado, e mover(faseId), recarregar(), avisar(tone, texto), fechar(). ctx.mover(faseId) é o mesmo mover do card aberto: salva o formulário → passa por antesDeMover → move pelo motor → mostra o aviso → recarrega o card e avisa o quadro atrás.

Como o SGC 2 liga:

<KanbanBoard
  slug="compras"
  novoCard={{ rotulo: 'Novo pedido', onClick: () => navigate('/pedidos/novo') }}
  atualizarACadaMs={60000}
  conteudoDaFase={(ctx) => <ConteudoDaFase {...ctx} />}   // itens e alertas na Análise, notas na expedição, B.O. na fase B.O. (spec 015)
  acoesExtras={(ctx) => <AcoesDoPedido {...ctx} />}
  podeMover={(card, fase, estrutura) => motivoDeDestino(persona, card, fase, estrutura)}   // mesma regra da SQL ligada, em TS puro
  antesDeMover={(card, fase) => verificarSaida(card, fase)}                                 // alertas, justificativas, prazo — consulta o servidor
  podeExcluir={() => permissoes.excluirPedido}
/>

A spec 013 também acrescenta, do lado do servidor:

  • kbNotaRegistrar (nova SF pública, JAVASCRIPT) — grava uma nota do sistema no histórico do card: cardId, texto (até 1.000 caracteres) e usuarioId (só vale sem sessão, para processo automático). Não toca a data de atualização (ATUALIZADO_EM) do card; recusa card excluído ou inexistente, ou texto vazio. Devolve { ok, id }.
  • kbCardCriar ganha criadoPor e origem: 'sistema' — só valem sem sessão (ex.: a agenda do consumidor criando pedido de madrugada); com sessão, os dois são ignorados e o criador é sempre quem está logado. Premissa de segurança: "sem sessão" é a situação que a plataforma só produz para o token de desenvolvedor (cron e scripts do sandbox); um usuário logado nunca chega aqui sem :VAR_USER. O mesmo vale para o usuarioId de kbNotaRegistrar e para o movidoPor de kbCardMover e o atualizadoPor de kbCardAtualizar (spec 014). Se o seu projeto tiver algum caminho em que a sessão do usuário não resolve um INT_USER, não libere essas quatro SFs no perfil business antes de fechar esse caminho.
  • kbCardMover ganha movidoPor e kbCardAtualizar ganha atualizadoPor, os dois com origem: 'sistema' (spec 014, 1.4.0) — pela mesma regra: só valem sem sessão (ex.: o cron que, com 100 % das notas no ERP, escreve o faturado no card e leva o pedido de Trânsito para Faturamento em nome do responsável); com sessão, quem move ou altera é quem está logado. A passagem fica com essa pessoa e com origem sistema, a linha de alteração do histórico fica com ela, e as automações de "card movido" e "campo alterado" rodam com ela. Obrigatórios da fase de origem, condição de entrada, conclusão e "só campo editável agora" valem igual; o que não vale é a regra ligada de podeMover — ela é sobre pessoas.
  • A ação enviar_email das automações entende {{anexos}} no assunto ou no corpo: com anexo, o marcador some do texto e a lista vira o bloco Anexos do modelo de e-mail (um link por arquivo ou link não removido da aba, na ordem dela); sem anexo, "Nenhum anexo" no lugar do marcador. Só consulta os anexos quando o marcador aparece de fato — a palavra "anexos" solta na prosa não custa nada (a spec 016 mudou a forma, não a regra).

Regras ligadas (ganchos de servidor)

Regra do sistema consumidor que o motor consulta no servidor antes de mover ou excluir um card — não é só a tela que decide, para não deixar passar quem chama por fora dela. Sem gancho ligado, vale a regra de hoje: mover segue as condições da fase, excluir é de quem criou o card ou do administrador do quadro.

O consumidor publica uma SF SQL por regra, com este contrato:

| Entrada ({{param}}) | Saída (uma linha) | |---|---| | usuarioId (quem age — o motor passa a sessão; nunca :VAR_USER, porque a chamada é aninhada), cardId, boardId, faseId (destino do movimento; 0 no excluir) | PODE (1 ou 0) e MOTIVO (texto; vazio quando pode) |

Exemplo (SGC 2, sgcPodeMover):

SELECT
  CASE WHEN P.PERSONA IN ('admin', 'analista', 'gestor') THEN 1
       WHEN P.PERSONA = 'diretoria' AND C.FASE_ID = F.FASE_ID THEN 1
       ELSE 0 END AS PODE,
  CASE WHEN P.PERSONA = 'diretoria' AND C.FASE_ID <> F.FASE_ID
       THEN 'Você só movimenta cards na fase Aprovação da Diretoria'
       ELSE '' END AS MOTIVO
FROM KB_CARD C
LEFT JOIN SGC_USUARIO_PERSONA P ON P.USER_ID = {{usuarioId}}
...
WHERE C.ID = {{cardId}}

Onde fica: KB_META, chaves gancho:podeMover e gancho:podeExcluir; o valor é o id da SF do consumidor.

Como ligar, no add-001-kanban.mjs do consumidor:

await instalar({
  sdk, projectId,
  ganchos: { podeMover: 'sgcPodeMover', podeExcluir: 'sgcPodeExcluir' },
});

instalar() resolve os nomes pelas server functions do projeto e upserta KB_META (idempotente). Sem ganchos, nada é gravado e nada muda.

Quando o motor consulta: só em movimento e exclusão feitos por um usuário. Automação (mover_fase) e criação ou movimento de card por processo sem sessão (origem: 'sistema') não passam pelo gancho — a regra ligada é sobre pessoas. Quem precisa barrar o processo o barra no próprio processo.

Quando a SQL falha ou não devolve linha: o motor recusa com "Regra do sistema indisponível." — fecha, em vez de abrir. Foi o consumidor que ligou a regra; deixar passar seria pular a checagem em silêncio.

A tela precisa espelhar a mesma regra em podeMover/podeExcluir (ver Pontos de extensão do sistema consumidor acima) — senão a pessoa vê um botão habilitado que o servidor recusa no clique. Há uma diferença de momento entre as duas recusas: a de antesDeMover (tela) acontece antes de qualquer gravação, inclusive do formulário; a do gancho de servidor chega depois que o card aberto já salvou o que estava no formulário (o motor grava o formulário e só então pede o movimento). Nada de fase, movimento ou histórico é gravado em nenhum dos dois casos — mas é mais um motivo para a tela não deixar chegar ao servidor o que ele vai recusar.

Ligar com a chave certa: instalar() só aceita podeMover e podeExcluir em ganchos; qualquer outra chave (ou um nome de SF que não existe no projeto) interrompe a instalação antes de criar tabela ou publicar função — uma regra "ligada" numa chave que o motor não lê seria uma regra desligada com mensagem de sucesso.

Níveis de acesso (spec 019)

Cada quadro tem quatro níveis. O administrador (quem criou e quem está em Administradores) vale mais que qualquer marca. Os outros três são escolhidos na aba Geral da administração, em Acesso: um padrão para quem não foi marcado e, pessoa a pessoa, as exceções.

| Nível | Vê | Altera | |---|---|---| | Administrador | todos os cards | tudo, inclusive a estrutura | | Membro | todos os cards | cards, tarefas, chat e anexos | | Restrito | só os cards que criou ou em que é responsável (principal ou na lista) | os mesmos; cria card; não reordena a coluna | | Somente leitura | todos os cards, com histórico, tarefas, chat e anexos | nada (só a marca de "mensagens lidas") |

  • Padrão. Quadro criado pela 1.9.0 nasce restrito. Quadro anterior, com ACESSO_JSON vazio, é membro para todos até o administrador trocar.
  • No servidor. As 13 SFs de escrita sobre card, kbCardsListar, kbCardDetalhe e kbMensagensListar conferem o nível. Card fora do alcance do restrito responde "Card não encontrado.", igual a um card que não existe. Leitor recebe "Você tem acesso somente leitura a este quadro.".
  • Antes dos ganchos. O nível é conferido antes das regras ligadas podeMover/podeExcluir (spec 013), que só são consultadas para quem o nível deixa agir.
  • Processo e automação não passam pelo nível. Chamada sem sessão "em nome de" (specs 013/014) e automações agem como hoje.
  • Na tela. kbBoardCarregar devolve papel (admin, membro, restrito ou leitor), e a estrutura leva papel. Para o leitor, o quadro tira o botão de criar (inclusive o novoCard do consumidor) e mostra o selo "Somente leitura", e o card fica no mesmo modo desabilitado do card excluído. O restrito vê o selo "Restrito: seus cards". As abas e ações do consumidor recebem ctx.travado como antes; quem desenha botão próprio fora do card usa papelNoQuadro(board, usuarioId), exportado pela lib, que é a mesma regra do servidor.
  • Gravar por código. kbBoardSalvar aceita acessoJson ({"padrao":"restrito","pessoas":{"3":"leitor"}}). Ausente ou vazio, mantém o que está gravado; nível desconhecido é recusado.

Responsáveis e campos de fase passada

Um card tem uma lista de responsáveis (KB_CARD_RESPONSAVEL); KB_CARD.RESPONSAVEL_ID continua existindo como espelho do primeiro, para quem lê a coluna. kbCardCriar/kbCardAtualizar recebem responsaveisIds ("3,4"); responsavelId sozinho ainda vale como lista de um. Filtro do quadro, condição $responsavel e e-mail de automação valem para qualquer um da lista; "Atribuir responsável" redefine a lista como só aquela pessoa.

Campo de fase é editável com o card na fase. Depois que o card sai, só se o admin marcar "Editável depois da fase" no campo (e só em fase já visitada). A regra é camposEditaveis no motor e vale na tela e no kbCardAtualizar (valor de campo não editável é ignorado; de fase não visitada, recusado).

Capa do card, observação e os três tempos

Capa por fase (spec 004): a cara do card tem duas partes, que se somam.

  1. Fixos do quadro — campos marcados "na capa de todas as fases" (NO_CARD): aparecem em qualquer fase, na ordem dos campos (campo de fase continua só na fase dele). É o que existia antes da 004.
  2. O que a fase acrescenta — a lista de chaves em KB_FASE.CAPA_JSON, na ordem dela, inclusive campo de outra fase (o que os fixos não fazem).

A regra é camposDoCard(campos, card, fase) no motor; chave morta, campo inativo, condição falsa e valor vazio não ocupam linha, e um campo citado nas duas partes aparece uma vez. Fixo não se tira de uma fase: para ele valer só em algumas, desmarque "todas as fases" e acrescente-o à lista de cada uma. A lista se edita na prévia da administração ("Editar capa"), que mostra as duas partes na ordem em que saem no cartão.

kbFaseSalvar grava o que recebe: quem chama sem capaJson devolve a fase ao padrão do quadro, do mesmo jeito que chamar sem descricao limpa a descrição. O BoardAdmin sempre manda a capa atual junto.

Observação do campo: uma linha guardada em CONFIG_JSON (config.observacao) e exposta como campo.observacao. Aparece sob o campo onde ele é editável (novo card, formulário inicial, fase atual e passagem "editável depois"); erro de validação ocupa o mesmo lugar e tem precedência. Não vai para a cara do card nem para o texto de busca.

Três tempos: todo cartão mostra criação, tempo na fase e última alteração, com ícone e ordem fixa (Tempos, no vocabulário). Vêm de CRIADO_EM, FASE_DESDE e ATUALIZADO_EM — nenhuma consulta nova. A unidade é a maior que couber (duracao: min → h → d) e, com SLA na fase, o relógio mostra a fração 21 h / 48 h, em tom de atenção quando passa (tempoNaFase). Não é configurável: vale em todo quadro.

Cores

Cor de fase e de opção de seleção vem de uma paleta fixa de 8 tokens da rampa de dados do design system (data-1 Azul … data-8 Vermelho — CORES no motor); o motor guarda o nome do token, nunca hex, e a tela renderiza var(--data-N), que existe nos dois temas. Na coluna a cor pinta a barra (o título continua ink-muted, porque três matizes da rampa não passam 4,5:1 como texto) e cada card leva uma faixa na borda esquerda. Na administração a cor se escolhe por bolinhas (nome no title e no hint). As colunas usam a altura tela do Board: a tela ocupa o main inteiro (h-full, layout do §10) e o quadro cresce até o rodapé dele, nunca abaixo de 30rem — quando a janela é mais baixa que isso, quem rola é o main, não uma segunda barra dentro do quadro. Os cabeçalhos das fases dividem uma linha de grade (subgrid): uma descrição de duas linhas numa fase não desalinha os cards das outras.

A etiqueta (e só ela) aparece como selo (Selo, exportado): o rótulo por dentro da cor. O selo contrasta com o tema — escuro com rótulo claro no tema claro, claro com rótulo escuro no escuro —, e a regra é simétrica, sem caso por tema: background: color-mix(in srgb, var(--data-N) 55%, var(--foreground)) e color: var(--background). 55% é o teto medido em que os oito matizes ainda passam de 4,5:1 nos dois temas (design.md §12) — nenhum token novo. O cartão mostra três e resume o resto em "+N".

Etiquetas (spec 010)

Etiqueta é campo, não tabela: cada quadro pode ter um campo reservado de chave etiquetas (seleção múltipla, sem fase, na capa), e a lista de etiquetas é o config.opcoes dele. Com isso ela herda validação, condição, automação, capa da fase, aba Alterações e busca — sem nenhuma tabela, coluna ou SF nova.

  • Ativar: administração › Fases › formulário inicial › "Ativar etiquetas" (cria o campo com a primeira).
  • Cadastrar: uma linha por etiqueta — rótulo, cor em bolinhas, selo de prévia. O valor nasce do rótulo na criação e não muda: é ele que está gravado nos cards, e é o que faz renomear valer para todos de uma vez.
  • Marcar: no card aberto, selos com × e um + que abre a lista e não fecha ao marcar.
  • Filtrar: um seletor na barra do quadro. O motor grava #chave# no TEXTO_BUSCA do card e o filtro procura essa forma — por isso ele acha pelo que está marcado, e não pela palavra no título.

Tarefas, chat e anexos (specs 005–007)

As três abas do meio do card aberto. Cada uma tem tabela própria, grava por server function e registra em Alterações o que importa; abrir o card continua sendo uma chamada (kbCardDetalhe traz tarefas, anexos e o total da conversa).

Tarefas (KB_TAREFA): título, responsável e prazo opcionais; concluir guarda quem e quando; setas para a ordem (subir grava só as duas que trocaram); excluir é de quem criou a tarefa ou do administrador. Condição de fase, de visibilidade e de automação ganham "Tarefas pendentes" ($tarefas): "Tarefas pendentes igual a 0" na entrada de uma fase exige tudo concluído. Tarefa não mexe no "alterado há" do cartão.

Chat (KB_MENSAGEM, KB_MENSAGEM_LEITURA): texto até 4.000 caracteres; remetente e hora sempre do servidor. Com a aba visível, um ciclo de kbMensagensListar a cada 5 s (20 s depois de 1 min sem mensagem nova), sempre incremental, só com a janela à mostra e uma consulta por vez — ciclo sem novidade não grava nada. Abre nas 50 mais recentes ("Carregar anteriores" traz o resto). "@" abre a lista de pessoas; o mencionado recebe e-mail (quem escreve não; editar não reenvia; desde a 1.6.0 o e-mail sai no modelo Benenutri, com a mensagem citada e o botão "Responder no card" quando email.urlDoCard foi configurado — ver E-mail (spec 016)). O autor edita e exclui nos primeiros 5 minutos; o administrador do quadro exclui qualquer uma, e a exclusão apaga o texto e fica em Alterações. O cartão do quadro mostra as mensagens novas para quem olha — de outras pessoas, depois da última que ela viu naquele card — e o número some quando ela abre a conversa. Desde a 1.7.0 (spec 017) a mensagem leva um arquivo: o clipe do composer (só com enviarArquivo) sobe o arquivo pelo projeto e a mesma kbMensagemEnviar grava mensagem, anexo e registro; o arquivo entra na aba Anexos com "pelo chat" e o chip da mensagem o abre. Só o arquivo, sem texto, vale. Excluir a mensagem não remove o arquivo (é documento do card); remover o arquivo pela aba Anexos deixa a mensagem dizendo "anexo removido".

Anexos (KB_ANEXO): o arquivo sobe pelo enviarArquivo do projeto e a lib só registra o endereço (kbAnexoRegistrar); link entra do mesmo jeito, com nome opcional (sem nome, o domínio). Só http(s); até 20 MB por arquivo. A aba avisa que arquivo enviado fica em endereço público — documento sensível vai como link. Remover é de quem anexou ou do administrador; some da lista, fica em Alterações e não apaga o arquivo do armazenamento.

E-mail (spec 016)

Todo e-mail que o motor manda sai num modelo só (design system Benenutri, §15): marca em texto, nome do sistema, título em frase, blocos opcionais (resumo, aviso, corpo, citação, dados, tabela, anexos), um botão e o rodapé com o motivo. Quatro envios usam o modelo:

  • a ação "Enviar e-mail" das automações — o texto do administrador é texto (linha em branco vira parágrafo, endereço vira link, **negrito**; HTML é escapado) e {{anexos}} vira o bloco de links;
  • a menção no chat — a mensagem como citação e "Responder no card";
  • novo responsável — "Você é responsável pelo card #N — título", só para quem entrou na lista (criação, edição ou ação "Atribuir responsável"), nunca para quem age nem para quem já estava;
  • tarefa atribuída — "Tarefa para você no card #N: título", na criação e na troca de responsável; a si mesmo, não.

Movimento, campo alterado e prazo continuam sendo assunto de automação do administrador. Falha no envio nunca desfaz a gravação: a tarefa fica, a mensagem fica (emailFalhou: true), e na automação a ação registra erro.

Configuração, uma vez, no add-001-kanban.mjs:

await instalar({
  sdk, projectId, mapaFrontend: MAPA,
  email: { sistema: 'SGC 2', urlDoCard: 'https://sgc.benenutri.com/#/quadro?card={id}' },
});

sistema é o nome curto (cabeçalho, assunto [SGC 2] … e rodapé; sem ele, Benenutri); urlDoCard é o endereço do card com {id} — sem ele o e-mail sai sem botão, nunca com link quebrado. Os dois ficam em KB_META (email:sistema, email:urlDoCard); sem email no instalar(), nada é tocado.

Os e-mails próprios do sistema (pedido à indústria, agenda, cobrança de NF) usam o mesmo modelo:

import { montarEmail } from '@benenutri/mitra-kanban/motor';

const { assunto, html } = montarEmail({
  sistema: 'SGC 2',
  rotulo: 'Pedido de compra · nº 482',
  titulo: 'Pedido de compra 482 — Benenutri',
  corpo: 'Prezados,\n\nSegue o pedido conforme os itens abaixo.',
  dados: [['Fornecedor', 'Nestlé Brasil'], ['Condição', '28 dias · CIF']],
  tabela: {
    titulo: 'Itens',
    colunas: [{ rotulo: 'Produto' }, { rotulo: 'Qtd', numerico: true }, { rotulo: 'Total', numerico: true }],
    linhas: [['Leite em pó 400 g', '120', 'R$ 18.450,00']],
    rodape: ['Total', '120', 'R$ 18.450,00'],
  },
  anexos: [{ nome: 'Pedido-482.pdf', url, tamanho: '184 KB' }],
  // destinatário de fora: sem `acao` (não abre o card) e rodapé que convida a responder
  rodape: 'Enviado pelo SGC 2 · Benenutri. Responda a este e-mail para falar com Compras.',
});
await sendEmailMitra({ projectId, to, subject: assunto, body: html });

Partes: sistema, rotulo, titulo (obrigatório), resumo, aviso: { tom: 'atencao' | 'alerta', texto }, corpo, citacao: { autor, quando, texto }, dados: [[rótulo, valor | { antes, depois }]], tabela, anexos, acao: { rotulo, url }, motivo, rodape, preheader, logoUrl. Tipos em motor/motor.d.mts (PartesDeEmail). Regras que valem para todo e-mail: título diz o que aconteceu; um botão só; texto é texto; dado ausente é travessão; o tom é uma faixa dentro do cartão e nunca recolore o e-mail; sempre claro; a marca é #45963d, nunca o verde da interface. O e-mail de referência está em assets/email.html da skill benenutri:design.

Desenvolver a lib

npm install
npm test            # sim:check + sim:smoke (57 SFs reais em SQLite) + test:motor
npm run sim         # simulador em http://localhost:3120 (senha mitra123; logins no seed)
npm run build       # tsc → dist/

Fluxo de uma versão nova: mudar → npm test verde → bump em package.json → coluna nova vai no CREATE e em UPGRADES → npm publish (o prepublishOnly roda os testes; publishConfig já é public) → atualizar os consumidores.

Versões: 0.1.0 = só backend (motor, schema, server functions, instalador); 1.0.0 = backend + componentes React (exports["."] → dist/). Peer dependencies do frontend: react, react-dom, radix-ui, cmdk, lucide-react, class-variance-authority, clsx, tailwind-merge — o kit do design system já as instala.

Para trabalhar com um consumidor local: npm link aqui e npm link @benenutri/mitra-kanban no consumidor; o package.json do consumidor continua apontando para a versão publicada (o sandbox não enxerga file:).

Fuso horário

O servidor grava instantes em UTC (AAAA-MM-DDTHH:MM:SS, sem Z). A tela converte para o fuso do navegador antes de mostrar hora e antes de decidir o dia (formatarDataHora, formatarData, dataLocal, hoje, relativo, agruparPorDia — todos em src/formato.ts). No servidor, a única decisão por dia — prazo vencido no cron — usa hojeNoFuso() (FUSO_MINUTOS = -180, Brasil sem horário de verão), nunca o dia UTC.

Regras que não se negocia

  • Nunca rode instalar()/install.mjs da máquina local: só no sandbox do projeto (migration).
  • schema.mjs só cresce: nada de DROP, RENAME ou mudança de tipo.
  • Nome de SF é contrato: renomear quebra todo consumidor.
  • motor/motor.mjs não importa nada e não contém {{ literal.
  • Commit sem trailer de agente (Co-Authored-By: Claude… é proibido).