@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.
- package/CHANGELOG.md +26 -0
- package/README.md +1 -0
- package/docs/adr/0022-execution-monitoring-is-a-projection.md +39 -0
- package/package.json +21 -1
- package/registry/skills/build-opus-ui/SKILL.md +3 -2
- package/registry/skills/build-opus-ui/references/ui-patterns.md +4 -3
- package/src/core/errors.ts +0 -1
- package/src/core/types.ts +4 -0
- package/src/executions/drivers/memory.ts +39 -0
- package/src/executions/drivers/pg-changes.ts +104 -0
- package/src/executions/drivers/pg.ts +74 -0
- package/src/executions/index.ts +129 -0
- package/src/executions/stream.ts +123 -0
- package/src/ui/components/patterns/data-state.tsx +1 -1
- package/src/ui/components/patterns/list.tsx +606 -251
- package/src/ui/components/patterns/page.tsx +16 -5
- package/src/ui/components/patterns/presentation.tsx +32 -6
- package/src/ui/components/primitives/field.tsx +23 -2
- package/src/ui/components/primitives/metric-card.tsx +64 -5
- package/src/ui/components/primitives/select.tsx +27 -23
- package/src/ui/components/primitives/table.tsx +4 -1
- package/src/ui/docs/content/action-list.md +84 -46
- package/src/ui/docs/content/executions.md +157 -0
- package/src/ui/docs/content/field.md +6 -1
- package/src/ui/docs/content/metric-card.md +43 -0
- package/src/ui/docs/content/select.md +4 -0
- package/src/ui/docs/registry.tsx +7 -0
- package/src/ui/meta.ts +1 -1
- package/src/ui/react.tsx +1 -0
|
@@ -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
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
331
|
-
|
|
|
332
|
-
| `action`
|
|
333
|
-
| `state`
|
|
334
|
-
| `onStateChange`
|
|
335
|
-
| `filterOptions`
|
|
336
|
-
| `
|
|
337
|
-
| `
|
|
338
|
-
| `
|
|
339
|
-
| `
|
|
340
|
-
| `
|
|
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.
|
|
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
|
|
363
|
-
| ----------------------- |
|
|
364
|
-
| `action` | `ListAction<TInput, TItem>`
|
|
365
|
-
| `input` | `TInput`
|
|
366
|
-
| `
|
|
367
|
-
| `
|
|
368
|
-
| `
|
|
369
|
-
| `
|
|
370
|
-
| `
|
|
371
|
-
| `
|
|
372
|
-
| `
|
|
373
|
-
| `
|
|
374
|
-
| `
|
|
375
|
-
| `
|
|
376
|
-
| `
|
|
377
|
-
| `
|
|
378
|
-
| `
|
|
379
|
-
| `
|
|
380
|
-
| `
|
|
381
|
-
| `
|
|
382
|
-
| `
|
|
383
|
-
| `
|
|
384
|
-
| `
|
|
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
|
|
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
|
package/src/ui/docs/registry.tsx
CHANGED
|
@@ -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
|
|
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