@softize/opus 18.2.0 → 18.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -60,9 +60,24 @@ superfícies que não são listagens, como relatórios. O estado é controlado p
60
60
  />
61
61
  ```
62
62
 
63
- Seletores inline mantêm largura previsível (`w-40`; lookup e múltiplo usam `w-52`). Rótulos longos
64
- são truncados no controle, mas permanecem completos na lista. No modal de filtros avançados, o
65
- seletor ocupa toda a largura. Busca, filtros e período aparecem somente quando declarados.
63
+ Seletores inline de valor único abraçam o valor selecionado até `max-w-80`. Valores maiores são
64
+ truncados no controle, enquanto o dropdown pode crescer além dele para preservar a opção.
65
+ Multiseletores mantêm `w-52`; no modal de filtros avançados, o seletor ocupa toda a largura.
66
+ `filterIcons` associa um ícone leading ao nome do filtro quando a superfície precisa reforçar
67
+ dimensões recorrentes, sem substituir o rótulo. Busca, filtros e período aparecem somente quando
68
+ declarados.
69
+
70
+ Quando o valor de repouso depende da superfície, use `filterDefaults`. O valor padrão participa da
71
+ consulta, mas não exibe a ação de limpar. Depois de uma alteração, o `X` aparece e restaura esse
72
+ valor, inclusive quando ele é definido em runtime, como a pessoa da sessão. Sem declaração, filtros
73
+ simples usam o valor vazio como padrão; por isso, opções como “Todos” também não nascem com `X`.
74
+
75
+ Use `periodPosition` para manter o período antes ou depois dos filtros inline. Quando ele precisa
76
+ apenas aparecer depois de um filtro específico, use `{ afterFilter: 'dateBasis' }`. Se tipo de data
77
+ e período formam uma única decisão, declare `period={{ basisFilter: 'dateBasis' }}`: o Opus os reúne
78
+ num só controle, com ícone no início e uma única ação de limpar no fim. Quando a pessoa muda o tipo
79
+ ou o período padrão, essa ação aparece e restaura os dois valores. Se a âncora de `periodPosition`
80
+ não existir, o período permanece visível no fim dos filtros inline.
66
81
 
67
82
  ## Onde cada filtro aparece
68
83
 
@@ -70,7 +85,10 @@ Cada filtro declara `placement`. `inline`, o padrão, mantém o controle na barr
70
85
  ao modal “Filtros”; `external` mantém o filtro no estado navegável e no input, mas deixa a
71
86
  apresentação para o consumidor, como cards de status acima da lista. No modal, filtros com a mesma
72
87
  `section` formam um grupo, e `advancedFilters.columns` distribui os grupos em até três colunas.
73
- Campos de data avançados usam calendário. A antiga `advanced: true` continua aceita durante a
88
+ Use `advancedFilters.sectionIcons` quando os grupos representarem dimensões recorrentes; a chave é
89
+ o mesmo rótulo de `section`, e a legenda mantém o agrupamento leve com ícone e divisor.
90
+ Um filtro `select` pode declarar `searchable: true` quando a lista fechada for longa; a opção geral
91
+ continua disponível e participa da busca. Campos de data avançados usam calendário. A antiga `advanced: true` continua aceita durante a
74
92
  migração e equivale a `placement: 'advanced'`.
75
93
 
76
94
  ```tsx
@@ -79,6 +97,7 @@ filters: {
79
97
  status: {
80
98
  label: 'Status',
81
99
  type: 'select',
100
+ searchable: true,
82
101
  placement: 'inline',
83
102
  options: { kind: 'dictionary', ref: 'workspace.status' },
84
103
  },
@@ -99,7 +118,17 @@ filters: {
99
118
  }
100
119
 
101
120
  // Na tela:
102
- <ActionList action={workspaceList} input={{}} advancedFilters={{ columns: 2 }} />
121
+ <ActionList
122
+ action={workspaceList}
123
+ input={{}}
124
+ advancedFilters={{
125
+ columns: 2,
126
+ sectionIcons: {
127
+ Relacionamento: <UserRound aria-hidden="true" />,
128
+ Datas: <CalendarDays aria-hidden="true" />,
129
+ },
130
+ }}
131
+ />
103
132
  ```
104
133
 
105
134
  ## Colunas de dicionário
@@ -327,27 +356,31 @@ render(
327
356
 
328
357
  ## Propriedades de ActionFilterBar
329
358
 
330
- | Propriedade | Tipo | Padrão | Descrição |
331
- | --------------- | ---------------------------------------------------- | ------- | ---------------------------------------------------------------------------------- |
332
- | `action` | `Pick<ListAction, 'filters' \| 'text' \| 'periods'>` | | Declara busca, filtros e períodos disponíveis. |
333
- | `state` | `ActionFilterState` | | Estado atual da barra. |
334
- | `onStateChange` | `(next) => void` | | Recebe o estado completo depois de cada alteração. |
335
- | `filterOptions` | `Record<string, SelectOption[]>` | | Fornece opções de runtime para filtros select e lookup. |
336
- | `advancedFilters` | `{ columns?: 1 \| 2 \| 3 }` | `{ columns: 1 }` | Define as colunas do diálogo de filtros avançados; o Opus deriva a largura correspondente. |
337
- | `onRefresh` | `() => Promise<void> \| void` | | Exibe a ação de recarregar e executa a consulta do consumidor. |
338
- | `refreshing` | `boolean` | `false` | Desabilita e anima a ação de recarregar durante a consulta. |
339
- | `controls` | `ReactNode` | | Controles auxiliares agrupados com recarregar em um `ButtonGroup` espaçado. |
340
- | `actions` | `ReactNode` | | Ações do consumidor exibidas depois do grupo de controles, com um intervalo maior. |
359
+ | Propriedade | Tipo | Padrão | Descrição |
360
+ | ----------------- | ---------------------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------ |
361
+ | `action` | `Pick<ListAction, 'filters' \| 'text' \| 'periods'>` | | Declara busca, filtros e períodos disponíveis. |
362
+ | `state` | `ActionFilterState` | | Estado atual da barra. |
363
+ | `onStateChange` | `(next) => void` | | Recebe o estado completo depois de cada alteração. |
364
+ | `filterOptions` | `Record<string, SelectOption[]>` | | Fornece opções de runtime para filtros select e lookup. |
365
+ | `filterDefaults` | `Record<string, string>` | | Define o valor de repouso de filtros simples e o destino da ação de limpar. |
366
+ | `filterIcons` | `Record<string, ReactNode>` | | Associa um ícone leading ao controle de cada filtro. |
367
+ | `advancedFilters` | `{ columns?: 1 \| 2 \| 3; sectionIcons?: Record<string, ReactNode> }` | `{ columns: 1 }` | Define as colunas do diálogo e os ícones opcionais das seções; o Opus deriva a largura correspondente. |
368
+ | `periodPosition` | `'before' \| 'after' \| { afterFilter: string }` | `'before'` | Posiciona o período antes, depois ou imediatamente após um filtro inline. |
369
+ | `period` | `{ basisFilter: string }` | | Reúne o período e o filtro inline indicado em um único controle. |
370
+ | `onRefresh` | `() => Promise<void> \| void` | | Exibe a ação de recarregar e executa a consulta do consumidor. |
371
+ | `refreshing` | `boolean` | `false` | Desabilita e anima a ação de recarregar durante a consulta. |
372
+ | `controls` | `ReactNode` | | Controles auxiliares agrupados com recarregar em um `ButtonGroup` espaçado. |
373
+ | `actions` | `ReactNode` | | Ações do consumidor exibidas depois do grupo de controles, com um intervalo maior. |
341
374
 
342
375
  ## Declaração na ListAction
343
376
 
344
- | Chave | O que declara |
345
- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
346
- | `columns` | `{ key, label, type?, sortable?, fit?, hidden?, dateFormat?, dictionary?, empty? }` — a tabela. `hidden` nasce oculta e pode ser exibida pelo seletor de colunas; `dictionary` nomeia o dicionário do provider; `empty` dá o significado da ausência. |
347
- | `filters` | `{ [nome]: { label, type, options?, multiple?, placement?, section?, depends?, … } }` — a toolbar. `placement` classifica o controle como `inline`, `advanced` ou `external`; filtros avançados com `section` são agrupados no diálogo; `external` mantém estado e input, mas delega a apresentação ao consumidor. `depends` desabilita/cascateia; options `kind: 'lookup'` busca em uma action. Valor aplicado entra no input com o mesmo nome. |
348
- | `text` | `{ fields }` — liga a busca; convenção: param `q` no input. Com período/filtros no contrato ela fica à direita; sendo a ÚNICA forma de recorte, abre a linha. |
349
- | `sort` | `{ fields, default }` — ordenação inicial; header ordenável escreve `sort: 'chave:dir'`. |
350
- | `periods` | `{ value, label, default? }[]` — o controle de período (presets + Personalizado com calendário); `default: true` marca o preset inicial, senão vale o primeiro; materializa em `from`/`to` no input. |
377
+ | Chave | O que declara |
378
+ | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
379
+ | `columns` | `{ key, label, type?, sortable?, fit?, hidden?, dateFormat?, dictionary?, empty? }` — a tabela. `hidden` nasce oculta e pode ser exibida pelo seletor de colunas; `dictionary` nomeia o dicionário do provider; `empty` dá o significado da ausência. |
380
+ | `filters` | `{ [nome]: { label, type, options?, multiple?, searchable?, allowEmpty?, placement?, section?, depends?, … } }` — a toolbar. `placement` classifica o controle como `inline`, `advanced` ou `external`; filtros avançados com `section` são agrupados no diálogo; `external` mantém estado e input, mas delega a apresentação ao consumidor. `searchable` habilita busca local em `select`; `allowEmpty: false` omite a opção vazia. `depends` desabilita/cascateia; options `kind: 'lookup'` busca em uma action. Valor aplicado entra no input com o mesmo nome. |
381
+ | `text` | `{ fields }` — liga a busca; convenção: param `q` no input. Ela aparece depois dos filtros inline e imediatamente antes do acesso aos filtros avançados. A busca participa da contagem de controles inline: com mais de um controle, a barra exibe as labels; quando ela é o único recorte, sua label é omitida. |
382
+ | `sort` | `{ fields, default }` — ordenação inicial; header ordenável escreve `sort: 'chave:dir'`. |
383
+ | `periods` | `{ value, label, default? }[]` — o controle de período (presets + Personalizado com calendário); `default: true` marca o preset inicial, senão vale o primeiro; materializa em `from`/`to` no input. |
351
384
 
352
385
  ## useListAction
353
386
 
@@ -359,26 +392,31 @@ chamar a action.
359
392
 
360
393
  ## Propriedades de ActionList
361
394
 
362
- | Propriedade | Tipo | Padrão | Descrição |
363
- | ----------------------- | ------------------------------------- | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
364
- | `action` | `ListAction<TInput, TItem>` | | A ListAction do Opus (kind list, output = item). A paginação envia `limit`/`page` e usa o `total` devolvido. |
365
- | `input` | `TInput` | | O ESCOPO BASE (ex.: { workspaceId }) — a toolbar soma por cima, nunca sobrescreve. |
366
- | `cells` | `Record<string, (item) => ReactNode>` | | Células custom por cima das colunas do contrato (chave = column.key). |
367
- | `filterOptions` | `Record<string, SelectOption[]>` | | Opções de runtime para os filtros select/lookup (chave = nome do filtro). |
368
- | `advancedFilters` | `{ columns?: 1 \| 2 \| 3 }` | `{ columns: 1 }` | Define as colunas dos grupos de filtros avançados; o Opus deriva a largura correspondente do diálogo. |
369
- | `views` | `Record<string, ActionListView<TItem>>` | | Visualizações alternativas `{ label, icon?, render }`; o seletor alterna entre a tabela e estas. |
370
- | `beforeContent` | `ReactNode` | | Conteúdo entre a toolbar e o corpo, como um resumo que reage ao recorte atual. |
371
- | `columns` | `ActionListColumn<TItem>[]` | | Tabela EXPLÍCITA — sobrepõe as colunas do contrato (escape hatch). |
372
- | `children` | `(items, refetch) => ReactNode` | | Modo composição: layout livre; a toolbar segue. Tem precedência sobre columns. |
373
- | `batch` | `ActionListBatchAction[]` | | Ações em lote: `{ label, can?, run, confirm?: { title, message? }, destructive? }` — liga a multi-seleção. |
374
- | `rowId` | `(item) => string` | `item.id` | Identidade da linha para seleção. |
375
- | `pageSize` | `number` | `50` | Itens por página padrão (vira `limit`/`page` no input; handler devolve `total`). O usuário troca no popover de exibição. |
376
- | `state / onStateChange` | `ActionListState / (next) => void` | interno | Estado da toolbar controlado — para quem embala sincronizar com a URL. |
377
- | `empty` | `(items) => boolean` | `items.length === 0` | Sobrepõe o vazio derivado, por exemplo quando um formulário inline aberto conta como conteúdo. |
378
- | `loading` | `boolean` | | Carga extra agregada à da consulta, como uma query irmã de que os `children` dependem. |
379
- | `emptyMessage` | `string` | `'Nenhum resultado.'` | Frase do estado vazio (o `Empty` do `DataState`). |
380
- | `errorMessage` | `string` | `'Não foi possível carregar.'` | Título do aviso de erro; a ação de tentar de novo refaz a consulta. |
381
- | `retryLabel` | `string` | `'Tentar de novo'` | Nome acessível e tooltip da ação de recuperação. |
382
- | `onRowClick` | `(item: TItem) => void` | | Clique na linha (ex.: navegar para o detalhe) — só na tabela. |
383
- | `toolbarActions` | `ReactNode` | | Operações ligadas ao recorte atual, no fim da barra e separadas do grupo de controles. |
384
- | `rowActions` | `(item: TItem) => ReactNode` | | Ações por linha (coluna final, à direita). A partir de duas, entram num grupo com intervalo compacto; uma só fica solta, sem grupo. Cliques ali não disparam o `onRowClick`. |
395
+ | Propriedade | Tipo | Padrão | Descrição |
396
+ | ----------------------- | --------------------------------------- | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
397
+ | `action` | `ListAction<TInput, TItem>` | | A ListAction do Opus (kind list, output = item). A paginação envia `limit`/`page` e usa o `total` devolvido. |
398
+ | `input` | `TInput` | | O ESCOPO BASE (ex.: { workspaceId }) — a toolbar soma por cima, nunca sobrescreve. |
399
+ | `layout` | `'flow' \| 'fill'` | `'flow'` | Em `fill`, ocupa a altura disponível e mantém a rolagem dentro da região de resultados. |
400
+ | `cells` | `Record<string, (item) => ReactNode>` | | Células custom por cima das colunas do contrato (chave = column.key). |
401
+ | `filterOptions` | `Record<string, SelectOption[]>` | | Opções de runtime para os filtros select/lookup (chave = nome do filtro). |
402
+ | `filterDefaults` | `Record<string, string>` | | Valores de repouso aplicados à consulta; o `X` só aparece quando o filtro diverge deles e retorna a eles. |
403
+ | `filterIcons` | `Record<string, ReactNode>` | | Ícones leading dos filtros, indexados pelo nome declarado no contrato. |
404
+ | `advancedFilters` | `{ columns?: 1 \| 2 \| 3; sectionIcons?: Record<string, ReactNode> }` | `{ columns: 1 }` | Define as colunas e os ícones opcionais dos grupos de filtros avançados; o Opus deriva a largura correspondente do diálogo. |
405
+ | `periodPosition` | `'before' \| 'after' \| { afterFilter: string }` | `'before'` | Posiciona o período antes, depois ou imediatamente após um filtro inline. |
406
+ | `period` | `{ basisFilter: string }` | | Reúne o período e o filtro inline indicado em um único controle. |
407
+ | `views` | `Record<string, ActionListView<TItem>>` | | Visualizações alternativas `{ label, icon?, render }`; o seletor alterna entre a tabela e estas. |
408
+ | `beforeContent` | `ReactNode` | | Conteúdo entre a toolbar e o corpo, como um resumo que reage ao recorte atual. |
409
+ | `columns` | `ActionListColumn<TItem>[]` | | Tabela EXPLÍCITA — sobrepõe as colunas do contrato (escape hatch). |
410
+ | `children` | `(items, refetch) => ReactNode` | | Modo composição: layout livre; a toolbar segue. Tem precedência sobre columns. |
411
+ | `batch` | `ActionListBatchAction[]` | | Ações em lote: `{ label, can?, run, confirm?: { title, message? }, destructive? }` — liga a multi-seleção. |
412
+ | `rowId` | `(item) => string` | `item.id` | Identidade da linha para seleção. |
413
+ | `pageSize` | `number` | `50` | Itens por página padrão (vira `limit`/`page` no input; handler devolve `total`). O usuário troca no popover de exibição. |
414
+ | `state / onStateChange` | `ActionListState / (next) => void` | interno | Estado da toolbar controlado — para quem embala sincronizar com a URL. |
415
+ | `empty` | `(items) => boolean` | `items.length === 0` | Sobrepõe o vazio derivado, por exemplo quando um formulário inline aberto conta como conteúdo. |
416
+ | `loading` | `boolean` | | Carga extra agregada à da consulta, como uma query irmã de que os `children` dependem. |
417
+ | `emptyMessage` | `string` | `'Nenhum resultado.'` | Frase do estado vazio (o `Empty` do `DataState`). |
418
+ | `errorMessage` | `string` | `'Não foi possível carregar.'` | Título do aviso de erro; a ação de tentar de novo refaz a consulta. |
419
+ | `retryLabel` | `string` | `'Tentar de novo'` | Nome acessível e tooltip da ação de recuperação. |
420
+ | `onRowClick` | `(item: TItem) => void` | | Clique na linha (ex.: navegar para o detalhe) — só na tabela. |
421
+ | `toolbarActions` | `ReactNode` | | Operações ligadas ao recorte atual, no fim da barra e separadas do grupo de controles. |
422
+ | `rowActions` | `(item: TItem) => ReactNode` | | Ações por linha (coluna final, à direita). A partir de duas, entram num grupo com intervalo compacto; uma só fica solta, sem grupo. Cliques ali não disparam o `onRowClick`. |
@@ -0,0 +1,157 @@
1
+ ---
2
+ title: Acompanhamento de execuções
3
+ ---
4
+
5
+ # Acompanhamento de execuções
6
+
7
+ Use `executions` para reunir progresso e resultado de tarefas executadas por motores diferentes.
8
+ O módulo é experimental e opt-in: instalar o Opus não passa a descobrir crons, filas ou workers.
9
+ O executor continua dono do estado; este recurso mantém uma fotografia para consulta.
10
+
11
+ Para colocar trabalho em segundo plano, use [Filas](/docs/queue). Para acompanhar trabalho já
12
+ existente, adapte seu executor à projeção abaixo. Não usar a projeção para decidir reenvio ou retry.
13
+
14
+ ## Publicar uma fotografia
15
+
16
+ O executor precisa conservar um ID e uma revisão crescente, inclusive entre tentativas e
17
+ reinícios. Grave essa revisão com o estado autoritativo; não use `Date.now()` nem um contador
18
+ exclusivo da memória do processo. O exemplo usa o driver em memória somente para demonstrar o
19
+ protocolo:
20
+
21
+ ```ts
22
+ import { memoryExecutions } from '@softize/opus/executions/memory'
23
+
24
+ const executions = memoryExecutions()
25
+ await executions.observe({
26
+ id: 'export-42',
27
+ tenantId: 'organization-7',
28
+ operation: 'catalog.export',
29
+ revision: 3,
30
+ attempt: 1,
31
+ status: 'running',
32
+ trigger: 'schedule',
33
+ createdAt: '2026-09-21T10:00:00.000Z',
34
+ updatedAt: '2026-09-21T10:01:00.000Z',
35
+ heartbeatAt: '2026-09-21T10:01:00.000Z',
36
+ progress: { completed: 150, total: 500 },
37
+ })
38
+ ```
39
+
40
+ `observe` retorna `false` para uma revisão repetida ou obsoleta e para uma tentativa de trocar
41
+ tenant, operação, criação ou disparo do mesmo ID. O ID é único em todo o store, não por tenant.
42
+ Cron que enfileira trabalho continua com
43
+ `trigger: 'schedule'`; entrar na fila não cria outra execução. Retomada mantém o ID e avança
44
+ revisão/tentativa conforme o executor. A projeção não valida transições de negócio.
45
+
46
+ Uma falha de projeção lança erro para o adaptador tratar. Registrar essa falha e reconciliar
47
+ depois a partir do executor; não deixar o erro provocar retry de um efeito já realizado.
48
+ `observe` não contém proteção automática para o fluxo de negócio do consumidor.
49
+
50
+ ## Persistência e processos separados
51
+
52
+ Use `pgExecutions({ pool, table?, channel? })` para persistir. O pool é fornecido pelo consumidor,
53
+ sem dependência de conexão global. `executionTableSql(table?)` devolve o DDL para incluir na
54
+ migration; não executa a migration, grants ou retenção. Nomes de tabela/canal aceitam um
55
+ identificador SQL simples. Tabela padrão: `execution_snapshots`; canal: `opus_executions`.
56
+
57
+ O driver atualiza somente uma revisão maior e publica NOTIFY no mesmo statement. Em uma
58
+ transação externa, o aviso só sai no commit. Rollback não publica progresso. O snapshot é
59
+ durável; o aviso não é histórico nem fila de entrega.
60
+
61
+ `pgExecutionChanges({ connect, onError, channel?, reconnectMs? })` compartilha uma conexão
62
+ LISTEN entre seus assinantes. `connect` deve entregar uma conexão nova, já conectada, dedicada
63
+ e com timeout de conexão configurado. Não fornecer uma conexão emprestada por um pooler
64
+ transacional. O driver fecha a conexão quando sai o último assinante, registra falhas por
65
+ `onError` e tenta reconectar após `reconnectMs` (padrão: 1.000 ms; mínimo: 100 ms).
66
+
67
+ Após cada conexão, o assinante recebe `null`: refaça a fotografia. Uma conexão nova não recupera
68
+ avisos perdidos. O consumidor precisa reconciliar também snapshots que não foram gravados por
69
+ falha de banco; reconectar SSE não corrige uma projeção que nunca foi atualizada.
70
+
71
+ ## Atualizar a interface por SSE
72
+
73
+ `executionStream` devolve uma `Response` Web para montar em uma rota do servidor. O consumidor
74
+ fornece autenticação e autorização; nenhum token é recebido pela URL ou pelo protocolo do SDK.
75
+
76
+ ```ts
77
+ import { executionStream } from '@softize/opus/executions/stream'
78
+ import type { ExecutionChanges, ExecutionNotice } from '@softize/opus/executions'
79
+
80
+ async function openProgress(
81
+ request: Request,
82
+ changes: ExecutionChanges,
83
+ authorize: (notice: ExecutionNotice | null) => Promise<boolean>,
84
+ logError: (error: unknown) => void,
85
+ ) {
86
+ return executionStream({ changes, authorize, signal: request.signal, onError: logError })
87
+ }
88
+ ```
89
+
90
+ Para `authorize(null)`, verificar sessão e direito de acompanhar. Para um aviso, verificar
91
+ também tenant, operação e recurso exato. Essa verificação ocorre novamente durante o stream,
92
+ inclusive nos heartbeats e antes de emitir uma invalidação. Heartbeats revalidam a sessão
93
+ independentemente da fila de avisos: a função pode receber um aviso e `null` concorrentemente.
94
+ As actions de leitura devem aplicar a mesma política: filtrar a
95
+ consulta por tenant não substitui autorização.
96
+
97
+ O cliente recebe somente `event: invalidate` com `{}`. Ao recebê-lo, invalidar as queries das
98
+ actions autorizadas. Manter uma conexão compartilhada no provider da aplicação, não uma por
99
+ card. A primeira invalidação e a reconexão exigem nova fotografia; não há `Last-Event-ID` nem
100
+ replay. O navegador pode usar `EventSource` com cookies same-origin e sua reconexão nativa.
101
+
102
+ Heartbeat padrão: 15 segundos. Duração máxima padrão: cinco minutos, seguida de reconexão.
103
+ O stream encerra na revogação de acesso, abort, falha de autorização, perda de LISTEN ou cliente lento. Há limites
104
+ de 128 avisos pendentes e 16 frames na saída; excedê-los encerra a conexão para nova fotografia,
105
+ sem acumular um log em memória. Sessão é reavaliada no máximo a cada heartbeat enquanto a
106
+ autorização responde; configure timeout no serviço de autorização para não mantê-la pendente.
107
+
108
+ IDs, tenant e operação nos avisos são metadados operacionais internos. Não usar CPF, telefone,
109
+ e-mail ou segredo nesses identificadores. PostgreSQL NOTIFY não é uma fronteira de autorização:
110
+ restringir as credenciais e o acesso ao banco. Nada do aviso é copiado para o SSE.
111
+
112
+ ## Detectar ausência de sinal
113
+
114
+ `executionLiveness(snapshot, nowMs, staleAfterMs)` retorna `inactive` quando não está em
115
+ execução, `unknown` quando não há heartbeat confiável, `fresh` com sinal recente e `stale`
116
+ quando o limite foi atingido. Heartbeat futuro é `unknown`, para não esconder diferença de
117
+ relógio. O chamador define a tolerância de acordo com a frequência do worker.
118
+
119
+ `stale` não altera `status`, não confirma falha e não autoriza retomar. Pausar, cancelar e
120
+ repetir continuam comandos do domínio. Logs completos e notificações pessoais também
121
+ continuam fora desta projeção.
122
+
123
+ ## Referência de ExecutionSnapshot
124
+
125
+ | Campo | Contrato |
126
+ | --- | --- |
127
+ | `id`, `tenantId`, `operation` | ID operacional, tenant explícito ou `null` e código da operação; IDs textuais limitados a 200 caracteres |
128
+ | `revision`, `attempt` | Inteiros seguros; revisão positiva e tentativa não negativa |
129
+ | `status` | `queued`, `running`, `paused`, `succeeded`, `failed` ou `cancelled` |
130
+ | `trigger` | `manual`, `schedule`, `queue` ou `system`; imutável para o mesmo ID |
131
+ | `createdAt`, `updatedAt` | Datas UTC; atualização não pode anteceder criação |
132
+ | `startedAt`, `finishedAt`, `heartbeatAt` | Datas UTC opcionais |
133
+ | `progress` | `completed` não negativo e `total` opcional, não inferior ao concluído; ambos inteiros seguros |
134
+
135
+ `executionSnapshot` valida, normaliza datas e seleciona somente os campos acima.
136
+ `executionNotice` extrai ID, tenant, operação e revisão. `parseExecutionNotice` valida dados
137
+ recebidos pelo transporte e remove propriedades extras.
138
+
139
+ ## Referência de ExecutionStore e ExecutionChanges
140
+
141
+ | Método | Contrato |
142
+ | --- | --- |
143
+ | `observe(snapshot)` | Grava somente revisão nova; retorna se alterou a projeção |
144
+ | `get(id, tenantId)` | Busca escopada; devolve snapshot ou `null` |
145
+ | `list({ tenantId, operations, limit? })` | Lista por atualização decrescente e ID; operações obrigatórias, limite padrão 50 e máximo 200 |
146
+ | `subscribe(listener, onUnavailable?)` | Assina aviso ou `null` de reconexão; comunica perda do transporte ao segundo callback e devolve função de saída |
147
+
148
+ `tenantId: null` seleciona somente execuções sem tenant, não todos os tenants. Lista de operações
149
+ vazia não retorna registros. `executionQuery` valida esse recorte. O catálogo de operações,
150
+ autorização, paginação completa do histórico e retenção pertencem ao consumidor; esta leitura
151
+ limitada atende os snapshots recentes, não substitui uma action `kind: 'list'`.
152
+
153
+ ## Limites desta etapa
154
+
155
+ Não há adaptadores automáticos de runtime, BullMQ ou scheduler, heartbeat automático,
156
+ reconciliação agendada, UI pronta ou mecanismo genérico de pausa. Esses consumidores precisam
157
+ ser implementados e testados antes de afirmar que todos os processos estão acompanhados.
@@ -38,6 +38,10 @@ vincula ao controle, ligue-o ao controle por `aria-labelledby` para manter o nom
38
38
  Declare `data-invalid="true"` no `Field` para que o rótulo acompanhe o contexto de erro;
39
39
  `aria-invalid` marca apenas o controle. O `ActionForm` define os dois atributos sozinho.
40
40
 
41
+ Quando vários conjuntos compartilham a mesma superfície, use `variant="section"` para separar cada
42
+ grupo com uma linha leve. `icon` acrescenta uma referência visual antes do título sem transformar o
43
+ conjunto em outro card; escolha um ícone que represente a dimensão do grupo, não uma ação.
44
+
41
45
  ```tsx preview col md
42
46
  <FieldSet>
43
47
  <FieldLegend>Novo agente</FieldLegend>
@@ -70,7 +74,8 @@ Declare `data-invalid="true"` no `Field` para que o rótulo acompanhe o contexto
70
74
 
71
75
  | Propriedade | Tipo | Padrão | Descrição |
72
76
  |---|---|---|---|
73
- | `variant` | `'legend' \| 'label'` | `'legend'` | Define o porte de título de seção ou de rótulo compacto. |
77
+ | `variant` | `'legend' \| 'label' \| 'section'` | `'legend'` | Define o porte de título, de rótulo compacto ou de seção com divisor. |
78
+ | `icon` | `ReactNode` | | Exibe um ícone decorativo antes da legenda. Na variante `section`, o ícone recebe um suporte tonal. |
74
79
 
75
80
  ## Propriedades de FieldError
76
81
 
@@ -31,6 +31,40 @@ relacionada à medida. Um controle que explica o rótulo, como um tooltip, perte
31
31
  />
32
32
  ```
33
33
 
34
+ ## Com medida complementar
35
+
36
+ Use `secondaryMetric` quando a mesma dimensão precisar mostrar uma segunda leitura sem competir
37
+ com o valor principal. A medida complementar aparece como uma lâmina posterior, com valor, rótulo
38
+ e `labelAction` próprios. Não use essa composição para duas métricas sem relação direta; nesse caso,
39
+ apresente dois cards independentes.
40
+
41
+ ```tsx preview col
42
+ <MetricCard
43
+ label="Agendados"
44
+ value="18"
45
+ labelAction={
46
+ <Tooltip>
47
+ <TooltipTrigger asChild>
48
+ <Button size="icon-xs" variant="ghost" aria-label="Sobre Agendados"><Info /></Button>
49
+ </TooltipTrigger>
50
+ <TooltipContent>Eventos atualmente agendados.</TooltipContent>
51
+ </Tooltip>
52
+ }
53
+ secondaryMetric={{
54
+ label: 'agendadas no período',
55
+ value: '7',
56
+ labelAction: (
57
+ <Tooltip>
58
+ <TooltipTrigger asChild>
59
+ <Button size="icon-xs" variant="ghost" aria-label="Sobre o período"><Info /></Button>
60
+ </TooltipTrigger>
61
+ <TooltipContent>Agendamentos registrados dentro do período.</TooltipContent>
62
+ </Tooltip>
63
+ ),
64
+ }}
65
+ />
66
+ ```
67
+
34
68
  ## Carregamento
35
69
 
36
70
  `loading` preserva a geometria do card com placeholders e marca a superfície como ocupada para
@@ -49,6 +83,15 @@ tecnologias assistivas. O contêiner da coleção continua responsável pela men
49
83
  | `loading` | `boolean` | `false` | Substitui rótulo e valor por skeletons e marca `aria-busy`. |
50
84
  | `labelAction` | `ReactNode` | | Controle ao lado do rótulo, como uma explicação em tooltip. |
51
85
  | `description` | `ReactNode` | | Contexto que explica recorte, proporção ou significado do valor. |
86
+ | `secondaryMetric` | `MetricCardSecondaryMetric` | | Medida complementar exibida como uma lâmina posterior sob o card principal. |
52
87
  | `icon` | `ReactNode` | | Ícone decorativo que identifica a natureza da medida. |
53
88
  | `context` | `'neutral' \| 'info' \| 'success' \| 'warning' \| 'danger'` | `'neutral'` | Contexto semântico do ícone; não altera a superfície do card. |
54
89
  | `action` | `ReactNode` | | Ação relacionada diretamente à medida. |
90
+
91
+ ## Propriedades de MetricCardSecondaryMetric
92
+
93
+ | Propriedade | Tipo | Padrão | Descrição |
94
+ |---|---|---|---|
95
+ | `label` | `ReactNode` | | O nome da medida complementar. |
96
+ | `value` | `ReactNode` | | O valor complementar já formatado pelo consumidor. |
97
+ | `labelAction` | `ReactNode` | | Controle próprio ao lado do rótulo, como uma explicação em tooltip. |
@@ -24,6 +24,10 @@ render(
24
24
  )
25
25
  ```
26
26
 
27
+ O gatilho respeita a largura definida pelo contexto. Ao abrir, a lista usa essa largura como mínimo
28
+ e pode crescer até `32rem` para preservar rótulos maiores sem alterar o layout do formulário ou da
29
+ toolbar. Rótulos que ultrapassarem esse limite continuam truncados como proteção de layout.
30
+
27
31
  ## Lista pesquisável
28
32
 
29
33
  Use `searchable` quando a quantidade ou os rótulos dificultarem encontrar uma opção. O gatilho
@@ -33,6 +33,7 @@ import eventsMd from "./content/events.md?raw";
33
33
  import logMd from "./content/log.md?raw";
34
34
  import observabilityMd from "./content/observability.md?raw";
35
35
  import queueMd from "./content/queue.md?raw";
36
+ import executionsMd from "./content/executions.md?raw";
36
37
  import schedulerMd from "./content/scheduler.md?raw";
37
38
  import actionsMd from "./content/actions.md?raw";
38
39
  import dataMd from "./content/data.md?raw";
@@ -321,6 +322,12 @@ export const PROTOCOL_SECTIONS: DocSection[] = [
321
322
  badge: "queue",
322
323
  render: doc(queueMd),
323
324
  },
325
+ {
326
+ slug: "executions",
327
+ title: "Acompanhamento de execuções",
328
+ badge: "executions",
329
+ render: doc(executionsMd),
330
+ },
324
331
  {
325
332
  slug: "scheduler",
326
333
  title: "Agendador",
package/src/ui/meta.ts CHANGED
@@ -81,7 +81,7 @@ export const componentMeta = {
81
81
  name: "metric-card",
82
82
  ancestry: "opus",
83
83
  whenToUse:
84
- "Medida resumida com rótulo, valor em destaque e descrição opcional. `context` realça semanticamente somente o ícone; aceita ação relacionada e estado de carregamento. O consumidor calcula e formata o valor; para conteúdo geral ou estrutura livre, use Card.",
84
+ "Medida resumida com rótulo, valor em destaque e descrição opcional. Use `secondaryMetric` para uma segunda leitura diretamente relacionada, exibida como lâmina posterior com explicação própria. `context` realça semanticamente somente o ícone; aceita ação relacionada e estado de carregamento. O consumidor calcula e formata os valores; para conteúdo geral ou estrutura livre, use Card.",
85
85
  },
86
86
  chat: {
87
87
  name: "chat",
package/src/ui/react.tsx CHANGED
@@ -105,6 +105,7 @@ export { MetricCard } from "./components/primitives/metric-card.tsx";
105
105
  export type {
106
106
  MetricCardProps,
107
107
  MetricCardContext,
108
+ MetricCardSecondaryMetric,
108
109
  MetricCardTone,
109
110
  } from "./components/primitives/metric-card.tsx";
110
111