@softize/opus 19.0.0 → 20.1.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 +239 -0
- package/bin/cli.mjs +5 -0
- package/bin/lib/check.mjs +198 -10
- package/bin/lib/copy.mjs +29 -0
- package/bin/lib/mcp.mjs +1 -1
- package/docs/adr/0020-icon-tiles-are-explicitly-composed.md +8 -0
- package/docs/adr/0021-content-carries-structure-and-semantic-element.md +7 -0
- package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +19 -1
- package/docs/adr/0024-fill-height-propagates-by-context.md +118 -0
- package/docs/adr/0025-content-frame-treatment-does-not-change-semantics.md +134 -0
- package/docs/data-products.md +15 -9
- package/docs/protocol.md +3 -4
- package/docs/releasing.md +2 -2
- package/package.json +2 -2
- package/registry/skills/create-opus-action/scripts/scaffold.mjs +2 -1
- package/registry/templates/app/package.json +1 -1
- package/src/core/runtime.ts +96 -8
- package/src/core/types.ts +5 -2
- package/src/ui/components/patterns/action-form-card.tsx +11 -1
- package/src/ui/components/patterns/content-header.tsx +109 -18
- package/src/ui/components/patterns/fill-region.ts +150 -0
- package/src/ui/components/patterns/list.tsx +91 -92
- package/src/ui/components/patterns/page.tsx +20 -4
- package/src/ui/components/patterns/presentation.tsx +52 -37
- package/src/ui/components/patterns/split.tsx +44 -4
- package/src/ui/components/patterns/surface-anatomy.ts +35 -8
- package/src/ui/components/patterns/surface-header.tsx +58 -25
- package/src/ui/components/patterns/trigger.tsx +12 -2
- package/src/ui/components/primitives/alert.tsx +9 -1
- package/src/ui/components/primitives/avatar.tsx +7 -3
- package/src/ui/components/primitives/card.tsx +1 -1
- package/src/ui/components/primitives/carousel.tsx +34 -5
- package/src/ui/components/primitives/checkbox.tsx +11 -3
- package/src/ui/components/primitives/command.tsx +2 -1
- package/src/ui/components/primitives/copyable.tsx +120 -10
- package/src/ui/components/primitives/dialog.tsx +42 -11
- package/src/ui/components/primitives/drawer.tsx +10 -2
- package/src/ui/components/primitives/empty.tsx +1 -1
- package/src/ui/components/primitives/icon-picker.tsx +6 -2
- package/src/ui/components/primitives/icon-tile.tsx +7 -0
- package/src/ui/components/primitives/input-group.tsx +9 -2
- package/src/ui/components/primitives/input-otp.tsx +1 -1
- package/src/ui/components/primitives/kbd.tsx +1 -1
- package/src/ui/components/primitives/label.tsx +3 -0
- package/src/ui/components/primitives/menu.tsx +5 -0
- package/src/ui/components/primitives/popover.tsx +1 -1
- package/src/ui/components/primitives/progress.tsx +9 -1
- package/src/ui/components/primitives/slider.tsx +62 -11
- package/src/ui/components/primitives/truncate.tsx +10 -1
- package/src/ui/docs/content/accordion.md +74 -27
- package/src/ui/docs/content/action-form-card.md +24 -15
- package/src/ui/docs/content/action-form-dialog.md +17 -7
- package/src/ui/docs/content/action-form.md +92 -77
- package/src/ui/docs/content/action-list-dialog.md +75 -38
- package/src/ui/docs/content/action-list.md +141 -58
- package/src/ui/docs/content/action-trigger.md +27 -10
- package/src/ui/docs/content/action-view.md +14 -14
- package/src/ui/docs/content/actions.md +161 -66
- package/src/ui/docs/content/ai.md +43 -44
- package/src/ui/docs/content/alert.md +83 -29
- package/src/ui/docs/content/ask.md +51 -26
- package/src/ui/docs/content/aspect-ratio.md +22 -24
- package/src/ui/docs/content/audit.md +23 -19
- package/src/ui/docs/content/auth.md +38 -19
- package/src/ui/docs/content/avatar.md +54 -5
- package/src/ui/docs/content/badge.md +29 -9
- package/src/ui/docs/content/breadcrumb.md +21 -8
- package/src/ui/docs/content/button.md +73 -43
- package/src/ui/docs/content/calendar.md +134 -26
- package/src/ui/docs/content/card.md +41 -15
- package/src/ui/docs/content/carousel.md +37 -6
- package/src/ui/docs/content/chat.md +118 -37
- package/src/ui/docs/content/checkbox.md +73 -7
- package/src/ui/docs/content/cli.md +94 -55
- package/src/ui/docs/content/collapsible.md +43 -19
- package/src/ui/docs/content/command.md +30 -8
- package/src/ui/docs/content/composer.md +23 -8
- package/src/ui/docs/content/content.md +119 -27
- package/src/ui/docs/content/copyable.md +39 -8
- package/src/ui/docs/content/customization.md +55 -46
- package/src/ui/docs/content/cycle.md +33 -18
- package/src/ui/docs/content/data-products.md +115 -0
- package/src/ui/docs/content/data-state.md +20 -6
- package/src/ui/docs/content/data.md +20 -19
- package/src/ui/docs/content/detail.md +12 -5
- package/src/ui/docs/content/dialog.md +107 -24
- package/src/ui/docs/content/dictionary-value.md +4 -4
- package/src/ui/docs/content/dock.md +54 -19
- package/src/ui/docs/content/dot.md +35 -5
- package/src/ui/docs/content/drawer.md +63 -19
- package/src/ui/docs/content/empty-value.md +20 -6
- package/src/ui/docs/content/empty.md +24 -3
- package/src/ui/docs/content/events.md +76 -16
- package/src/ui/docs/content/executions.md +9 -9
- package/src/ui/docs/content/field.md +21 -6
- package/src/ui/docs/content/getting-started.md +44 -34
- package/src/ui/docs/content/icon-picker.md +19 -13
- package/src/ui/docs/content/icon-tile.md +38 -15
- package/src/ui/docs/content/input-otp.md +58 -7
- package/src/ui/docs/content/input.md +22 -3
- package/src/ui/docs/content/item.md +53 -6
- package/src/ui/docs/content/kbd.md +5 -1
- package/src/ui/docs/content/label.md +13 -4
- package/src/ui/docs/content/log.md +5 -5
- package/src/ui/docs/content/markdown.md +5 -4
- package/src/ui/docs/content/mcp.md +49 -23
- package/src/ui/docs/content/menu.md +95 -10
- package/src/ui/docs/content/metric-card.md +12 -4
- package/src/ui/docs/content/observability.md +10 -9
- package/src/ui/docs/content/page.md +118 -12
- package/src/ui/docs/content/pagination.md +13 -5
- package/src/ui/docs/content/pane.md +55 -5
- package/src/ui/docs/content/popover.md +42 -5
- package/src/ui/docs/content/presentation.md +91 -47
- package/src/ui/docs/content/progress.md +27 -14
- package/src/ui/docs/content/queue.md +39 -21
- package/src/ui/docs/content/radio-group.md +26 -4
- package/src/ui/docs/content/router.md +29 -10
- package/src/ui/docs/content/runtime.md +90 -54
- package/src/ui/docs/content/scheduler.md +53 -21
- package/src/ui/docs/content/scroll-area.md +42 -35
- package/src/ui/docs/content/select.md +78 -33
- package/src/ui/docs/content/semantic-context.md +21 -2
- package/src/ui/docs/content/separator.md +15 -9
- package/src/ui/docs/content/sidebar.md +3 -2
- package/src/ui/docs/content/skeleton.md +6 -1
- package/src/ui/docs/content/slider.md +37 -9
- package/src/ui/docs/content/spinner.md +4 -3
- package/src/ui/docs/content/split.md +19 -0
- package/src/ui/docs/content/storage.md +8 -9
- package/src/ui/docs/content/switch.md +10 -7
- package/src/ui/docs/content/table.md +46 -8
- package/src/ui/docs/content/tabs.md +14 -4
- package/src/ui/docs/content/testing.md +76 -31
- package/src/ui/docs/content/textarea.md +5 -4
- package/src/ui/docs/content/toast.md +48 -7
- package/src/ui/docs/content/toggle.md +3 -3
- package/src/ui/docs/content/tokens.md +15 -26
- package/src/ui/docs/content/tooltip.md +18 -2
- package/src/ui/docs/content/truncate.md +36 -6
- package/src/ui/docs/content/ui.md +74 -22
- package/src/ui/docs/content/upgrading.md +45 -36
- package/src/ui/docs/plugin.ts +36 -2
- package/src/ui/docs/registry.tsx +6 -0
- package/src/ui/drivers/react.tsx +16 -8
- package/src/ui/meta.ts +32 -32
- package/src/ui/react.tsx +1 -0
- package/src/vite/design.ts +16 -4
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
## Escolha única
|
|
2
2
|
|
|
3
|
-
Use `RadioGroup` quando todas as opções mutuamente exclusivas precisarem permanecer visíveis.
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Use `RadioGroup` quando todas as opções mutuamente exclusivas precisarem permanecer visíveis. Com
|
|
4
|
+
muitas opções, prefira [`Select`](/ui/select); para ligar ou desligar um único item, use
|
|
5
|
+
[`Checkbox`](/ui/checkbox) ou [`Switch`](/ui/switch). Cada `RadioGroupItem` declara um `value` e
|
|
6
|
+
deve estar associado a um `Label`, escrito logo depois do item. `defaultValue` define a opção
|
|
7
|
+
inicial no modo não controlado.
|
|
6
8
|
|
|
7
9
|
```tsx preview col-start
|
|
8
10
|
<RadioGroup defaultValue="balanced">
|
|
@@ -68,6 +70,25 @@ bloquear todas as opções, aplique `disabled` ao `RadioGroup`.
|
|
|
68
70
|
</RadioGroup>
|
|
69
71
|
```
|
|
70
72
|
|
|
73
|
+
## Estado inválido
|
|
74
|
+
|
|
75
|
+
Quando a escolha for obrigatória e nenhuma opção estiver marcada, declare `aria-invalid` em cada
|
|
76
|
+
`RadioGroupItem` e explique o que falta em um texto associado ao grupo por `aria-describedby`.
|
|
77
|
+
|
|
78
|
+
```tsx preview col-start
|
|
79
|
+
<RadioGroup aria-describedby="review-mode-error">
|
|
80
|
+
<div className="flex items-center gap-2">
|
|
81
|
+
<RadioGroupItem id="review-auto" value="auto" aria-invalid />
|
|
82
|
+
<Label htmlFor="review-auto">Revisar automaticamente</Label>
|
|
83
|
+
</div>
|
|
84
|
+
<div className="flex items-center gap-2">
|
|
85
|
+
<RadioGroupItem id="review-manual" value="manual" aria-invalid />
|
|
86
|
+
<Label htmlFor="review-manual">Revisar manualmente</Label>
|
|
87
|
+
</div>
|
|
88
|
+
</RadioGroup>
|
|
89
|
+
<FieldError id="review-mode-error">Escolha como a sessão será revisada.</FieldError>
|
|
90
|
+
```
|
|
91
|
+
|
|
71
92
|
## Propriedades de RadioGroup
|
|
72
93
|
|
|
73
94
|
| Propriedade | Tipo | Padrão | Descrição |
|
|
@@ -82,4 +103,5 @@ bloquear todas as opções, aplique `disabled` ao `RadioGroup`.
|
|
|
82
103
|
| Propriedade | Tipo | Padrão | Descrição |
|
|
83
104
|
|---|---|---|---|
|
|
84
105
|
| `value` | `string` | | Valor que o item atribui ao grupo quando selecionado. |
|
|
85
|
-
| `disabled` | `boolean` | `false` | Desabilita somente este item; o `Label`
|
|
106
|
+
| `disabled` | `boolean` | `false` | Desabilita somente este item; o `Label` escrito logo depois dele acompanha o estado. |
|
|
107
|
+
| `aria-invalid` | `boolean` | | Aplica a borda e o anel de erro ao item. |
|
|
@@ -19,11 +19,13 @@ function App() {
|
|
|
19
19
|
O router oferece leitura reativa da URL e `navigate`; o aplicativo decide o que renderizar. Ele não
|
|
20
20
|
inclui tabela de rotas, parâmetros tipados nem carregamento de dados.
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
Se a tela precisa casar padrões como `/users/:id/posts/:postId`, receber parâmetros tipados ou
|
|
23
|
+
carregar dados por rota, use uma biblioteca de rotas. Um app de back-office com uma dúzia de
|
|
24
|
+
destinos costuma dispensar esses recursos.
|
|
23
25
|
|
|
24
26
|
## As quatro peças
|
|
25
27
|
|
|
26
|
-
| | |
|
|
28
|
+
| API | Comportamento |
|
|
27
29
|
|---|---|
|
|
28
30
|
| `usePathname()` | O pathname como estado reativo. |
|
|
29
31
|
| `useSegments()` | Os segmentos, já sem os vazios: `/settings/users` → `['settings','users']`. |
|
|
@@ -32,15 +34,23 @@ A régua: se a sua tela precisa de casamento de padrão (`/users/:id/posts/:post
|
|
|
32
34
|
|
|
33
35
|
## Comportamentos preservados
|
|
34
36
|
|
|
35
|
-
**
|
|
37
|
+
**Navegar para o destino atual não cria entrada.** `navigate` resolve o destino com `new URL` e
|
|
38
|
+
compara o `href` inteiro; se a pessoa já está exatamente lá, nada acontece. Assim, clicar duas vezes
|
|
39
|
+
no mesmo item do menu não empilha entradas idênticas, e o botão Voltar continua saindo da página.
|
|
36
40
|
|
|
37
|
-
|
|
41
|
+
A comparação usa a URL resolvida, e não o texto cru, por dois motivos. O hash: estando em
|
|
42
|
+
`/a#secao`, `navigate('/a')` precisa remover a âncora, e não ser tratado como destino repetido. E a
|
|
43
|
+
codificação: `window.location` devolve `/relatórios` como `/relat%C3%B3rios`, então uma comparação
|
|
44
|
+
crua nunca casaria e cada clique empilharia uma entrada, o que afeta caminhos acentuados e
|
|
45
|
+
chamadas como ``navigate(`/reports?report=${arquivo}`)`` com nome de arquivo acentuado.
|
|
38
46
|
|
|
39
47
|
**A notificação usa um evento do navegador.** Como `pushState` não dispara `popstate`, `navigate`
|
|
40
48
|
emite o evento explicitamente. Isso mantém listeners existentes e cópias diferentes do pacote
|
|
41
49
|
sincronizados pela mesma janela.
|
|
42
50
|
|
|
43
|
-
|
|
51
|
+
**Todos os componentes leem a mesma URL.** Os hooks usam `useSyncExternalStore`. Ler
|
|
52
|
+
`window.location` dentro de `useState` ou `useEffect` permite que, na renderização concorrente, dois
|
|
53
|
+
componentes mostrem URLs diferentes no mesmo commit.
|
|
44
54
|
|
|
45
55
|
## `replace`: quando o passo não é história
|
|
46
56
|
|
|
@@ -48,14 +58,23 @@ sincronizados pela mesma janela.
|
|
|
48
58
|
navigate(`/reports?report=${file}`, { replace: true })
|
|
49
59
|
```
|
|
50
60
|
|
|
51
|
-
Use quando a mudança não
|
|
61
|
+
Use `replace` quando a mudança não deve virar um passo do botão Voltar: um rascunho que ganha id no
|
|
62
|
+
meio do fluxo, um filtro que a pessoa ajusta várias vezes seguidas. Para uma navegação entre
|
|
63
|
+
destinos, mantenha o padrão, `pushState`.
|
|
52
64
|
|
|
53
65
|
## SSR
|
|
54
66
|
|
|
55
|
-
|
|
67
|
+
No servidor, os hooks usam um valor fixo (`/` e busca vazia): renderizam sem `window` e passam a
|
|
68
|
+
refletir a URL real no primeiro render do cliente. Se a página precisa do caminho correto no HTML
|
|
69
|
+
servido, receba o caminho do framework por prop.
|
|
56
70
|
|
|
57
|
-
|
|
71
|
+
`navigate` acessa `window` diretamente e lança um erro quando chamado no servidor. Ler a URL fora
|
|
72
|
+
do navegador tem uma resposta razoável; navegar não tem, e ignorar a chamada em silêncio esconderia
|
|
73
|
+
o erro de uso.
|
|
58
74
|
|
|
59
|
-
##
|
|
75
|
+
## Validar destinos vindos de fora
|
|
60
76
|
|
|
61
|
-
`navigate` espera um caminho da própria origem
|
|
77
|
+
`navigate` espera um caminho da própria origem e não valida destinos. Quando o destino vem de fora,
|
|
78
|
+
como o `?redirect=` de um pós-login, valide-o antes de navegar: resolva com
|
|
79
|
+
`new URL(valor, origin)` e compare o `origin` do resultado, em vez de conferir apenas o prefixo do
|
|
80
|
+
texto.
|
|
@@ -4,94 +4,130 @@ title: Runtime & adapters
|
|
|
4
4
|
|
|
5
5
|
# Runtime & adapters
|
|
6
6
|
|
|
7
|
-
O
|
|
8
|
-
|
|
9
|
-
o runtime
|
|
7
|
+
O runtime executa as actions registradas e entrega a elas as capacidades que o app configurou:
|
|
8
|
+
servidor HTTP, banco, autenticação, auditoria e as demais. Cada capacidade é um adapter opcional.
|
|
9
|
+
O app monta o runtime com as peças de que precisa, e o Opus não assume o controle do restante da
|
|
10
|
+
aplicação.
|
|
10
11
|
|
|
11
12
|
## O modelo
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
Todo adapter declara `name` e `kind`. `init`, `dispose` e `healthCheck` são opcionais e acompanham o
|
|
15
|
+
ciclo de vida do runtime: `start()` inicializa os adapters, `dispose()` os desliga em ordem inversa
|
|
16
|
+
e `healthCheck()` reúne o estado de todos.
|
|
16
17
|
|
|
17
18
|
```ts
|
|
18
19
|
import { createRuntime } from '@softize/opus/core'
|
|
19
20
|
|
|
20
21
|
const runtime = createRuntime({
|
|
21
|
-
server: fastifyServer({ app }), //
|
|
22
|
+
server: fastifyServer({ app }), // expõe as actions como endpoints
|
|
22
23
|
data: kyselyData({ db }), // fornece ctx.db
|
|
23
|
-
auth:
|
|
24
|
-
audit: [pgAudit({ pool })], // trilha de auditoria
|
|
25
|
-
observability:
|
|
24
|
+
auth: jwtAuth({ secret, mapUser }), // resolve user, tenant e can
|
|
25
|
+
audit: [pgAudit({ pool })], // trilha de auditoria; aceita vários sinks
|
|
26
|
+
observability: openTelemetryObservability(), // span ativo e trace sem fornecedor fixo
|
|
26
27
|
storage: fsStorage({ root }), // fornece ctx.storage (experimental)
|
|
27
28
|
config: { env, i18n: { defaultLocale: 'pt-BR' } },
|
|
28
29
|
})
|
|
29
30
|
|
|
30
|
-
runtime.register([
|
|
31
|
+
runtime.register([workspaceDomain])
|
|
31
32
|
await runtime.start()
|
|
32
33
|
```
|
|
33
34
|
|
|
35
|
+
## Registrar domínios
|
|
36
|
+
|
|
37
|
+
Só o que foi registrado existe para o runtime: uma action fora do registro não é montada no servidor
|
|
38
|
+
nem pode ser executada. `runtime.register` aceita domínios criados com `defineDomain` e também
|
|
39
|
+
actions, reactions e schedules soltos, em uma mesma lista. Um domínio é achatado com seus
|
|
40
|
+
subdomínios; um nome repetido de action, reaction, schedule ou Produto de Dados lança erro, inclusive
|
|
41
|
+
entre domínios registrados separadamente.
|
|
42
|
+
|
|
43
|
+
O `start()` falha quando há reactions registradas sem `eventBus` (`runtime.eventbus_required`) ou
|
|
44
|
+
schedules sem `scheduler` (`runtime.scheduler_required`). A declaração de um domínio está em
|
|
45
|
+
[Actions & contratos](/docs/actions).
|
|
46
|
+
|
|
34
47
|
## As capacidades
|
|
35
48
|
|
|
36
|
-
|
|
37
|
-
|
|
49
|
+
Cada driver fica num subpath próprio do pacote. Drivers que dependem de bibliotecas maiores as
|
|
50
|
+
declaram como dependências opcionais: só quem usa o driver as instala.
|
|
38
51
|
|
|
39
52
|
| Capacidade | Fornece | Driver pronto |
|
|
40
53
|
|---|---|---|
|
|
41
|
-
| `server` |
|
|
42
|
-
| `data` | `ctx.db
|
|
43
|
-
| `auth` | `user
|
|
54
|
+
| `server` | expõe as actions por HTTP | `fastifyServer` — `@softize/opus/server/fastify` · `nodeServer` — `…/server/node` |
|
|
55
|
+
| `data` | `ctx.db`, verificação de drift e CRUD | `kyselyData`, `kyselyRepo`, `crudActions` — `@softize/opus/data/kysely` |
|
|
56
|
+
| `auth` | `user`, `tenantId` e `can` do contexto | `jwtAuth` — `…/auth/jwt` · `betterAuthSession` — `…/auth/better-auth` |
|
|
44
57
|
| `audit` | trilha por execução de action | `pgAudit` — `…/audit/pg` · `consoleAudit` — `…/audit/console` |
|
|
45
58
|
| `logger` | `ctx.log` estruturado | `pinoLogger` — `@softize/opus/log/pino` |
|
|
46
|
-
| `observability` | span ativo
|
|
47
|
-
| `eventBus` | `ctx.emit`
|
|
59
|
+
| `observability` | span ativo e propagação de trace | `openTelemetryObservability` — `@softize/opus/observability/opentelemetry` |
|
|
60
|
+
| `eventBus` | `ctx.emit` e reactions | `mittEvents` — `@softize/opus/events/mitt` |
|
|
48
61
|
| `queue` | jobs em background | `bullmqQueue` — `@softize/opus/queue/bullmq` |
|
|
49
|
-
| `scheduler` |
|
|
50
|
-
| `storage` | `ctx.storage`
|
|
51
|
-
| `cache` | `ctx.cache`
|
|
52
|
-
| `ai` | `ctx.ai`
|
|
53
|
-
| `client` | chamar actions de fora
|
|
62
|
+
| `scheduler` | schedules por cron ou intervalo | `nodeCronScheduler` — `@softize/opus/scheduler/node-cron` |
|
|
63
|
+
| `storage` | `ctx.storage` para arquivos | `fsStorage` — `…/storage/fs` · `s3Storage` — `…/storage/s3` |
|
|
64
|
+
| `cache` | `ctx.cache` de leitura (experimental) | `memoryCache` — `@softize/opus/cache/memory` |
|
|
65
|
+
| `ai` | `ctx.ai`: `complete`, `extract`, `run` e `runStream` | `anthropicAi` — `@softize/opus/ai/anthropic` |
|
|
66
|
+
| `client` | chamar actions de fora do servidor | `fetchClient` — `@softize/opus/client/fetch` |
|
|
54
67
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
68
|
+
`fastifyServer` devolve o adapter diretamente. `nodeServer` devolve `{ adapter, handler, hasRoute }`:
|
|
69
|
+
passe `adapter` ao runtime e ligue `handler` a um servidor `node:http`, ao Vite ou a um middleware no
|
|
70
|
+
estilo connect.
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
import { createServer } from 'node:http'
|
|
74
|
+
import { nodeServer } from '@softize/opus/server/node'
|
|
75
|
+
|
|
76
|
+
const node = nodeServer()
|
|
77
|
+
const runtime = createRuntime({ server: node.adapter })
|
|
78
|
+
createServer(node.handler).listen(3000)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Fora do runtime, o harness de teste (`runAction` e `testContext`, de `@softize/opus/testing`) executa
|
|
82
|
+
uma action em unidade com as etapas centrais do `execute`: valida a entrada, exige autenticação
|
|
83
|
+
quando a action não é pública, avalia o `authorize`, roda o handler e valida a saída. Ele não roda os
|
|
84
|
+
`loads` (os dados carregados entram pela opção `loaded`) nem grava audit. Veja
|
|
85
|
+
[Testes de action](/docs/testing).
|
|
58
86
|
|
|
59
87
|
## O contexto do handler
|
|
60
88
|
|
|
61
|
-
|
|
62
|
-
|
|
89
|
+
O `ctx` recebido pelo handler reúne o que os adapters fornecem. Sem o adapter correspondente, o
|
|
90
|
+
campo chega como `null`, ou como `undefined` no caso de `db`, e o handler decide como seguir.
|
|
91
|
+
|
|
92
|
+
- `user`, `tenantId` e `can`, do adapter `auth`.
|
|
93
|
+
- `db`, do adapter `data`.
|
|
94
|
+
- `log`, do `logger`; sem ele, o runtime usa um logger de console.
|
|
95
|
+
- `emit`, que publica no `eventBus`.
|
|
96
|
+
- `storage`, `cache` e `ai`, dos adapters homônimos.
|
|
97
|
+
- `provenance`, que identifica a origem da execução: HTTP, schedule, reaction, job, agente de IA ou
|
|
98
|
+
integração.
|
|
99
|
+
- `trace`, quando há contexto de trace.
|
|
100
|
+
- `meta`, com dados livres da chamada.
|
|
63
101
|
|
|
64
|
-
|
|
65
|
-
`storage` (storage) · `cache` (cache) · `ai` (ai) · `provenance` (quem disparou: http, schedule,
|
|
66
|
-
reaction…) · `trace` (quando configurado) · `meta`.
|
|
102
|
+
## Trace e observabilidade
|
|
67
103
|
|
|
68
|
-
O core não depende de OpenTelemetry. O `ObservabilityAdapter` envolve actions e reactions
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
104
|
+
O core não depende de OpenTelemetry. O `ObservabilityAdapter` envolve actions e reactions com
|
|
105
|
+
`runInSpan`, e o `TraceContext` efetivo segue para o resultado, o audit e os eventos. Um job recebe
|
|
106
|
+
o trace da execução que o enfileirou, e `executeJob()` cria a execução filha no worker sem
|
|
107
|
+
enfileirar a action de novo. Uma falha de instrumentação é tratada de forma tolerante e nunca
|
|
108
|
+
executa o handler outra vez.
|
|
73
109
|
|
|
74
|
-
O core não
|
|
75
|
-
`
|
|
76
|
-
|
|
77
|
-
`
|
|
110
|
+
O core não lê headers HTTP. Os drivers Fastify e Node preenchem `ContextBase.trace` com a opção
|
|
111
|
+
`traceContext: 'w3c'`, desativada por padrão; outros transportes continuam responsáveis pela própria
|
|
112
|
+
fronteira. O registro de audit traz dois campos: `traceContext`, com o contexto de trace (`traceId`, `spanId` e flags), e `trace`, com o
|
|
113
|
+
`requestId` e a action de origem (`parentActionId`). Os detalhes estão em [Observabilidade](/docs/observability).
|
|
78
114
|
|
|
79
|
-
Para actions,
|
|
80
|
-
|
|
81
|
-
representa span de erro. Reactions usam `resultKind: 'void'` e rejeitam em falha.
|
|
115
|
+
Para actions, a execução observada resolve com `ActionResult` e declara `resultKind:
|
|
116
|
+
'action-result'`: o driver inspeciona `result.ok`, e uma Promise resolvida com `ok: false` ainda
|
|
117
|
+
representa um span de erro. Reactions usam `resultKind: 'void'` e rejeitam em caso de falha.
|
|
82
118
|
|
|
83
|
-
##
|
|
119
|
+
## Execução sem requisição
|
|
84
120
|
|
|
85
|
-
|
|
86
|
-
|
|
121
|
+
Duas formas de código rodam sem uma requisição: um evento de domínio dispara uma
|
|
122
|
+
[reaction](/docs/events), e o tempo dispara um [schedule](/docs/scheduler), que executa uma action
|
|
123
|
+
com provenance própria. Ambos são registrados junto com as actions, e o manifest projeta os três.
|
|
87
124
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
125
|
+
`opus introspect` lista reactions, schedules e as ligações entre eles, mas só reconhece
|
|
126
|
+
declarações feitas com `defineAction`, `defineReaction` e `defineSchedule`. Actions do par
|
|
127
|
+
`defineContract` e `bindAction` e reactions do par `defineReactionContract` e `bindReaction` ficam de
|
|
128
|
+
fora do modelo que ele monta.
|
|
92
129
|
|
|
93
|
-
## Referência
|
|
130
|
+
## Referência completa
|
|
94
131
|
|
|
95
|
-
O contrato completo
|
|
96
|
-
|
|
97
|
-
que você instalou.
|
|
132
|
+
O contrato completo, com tipos, garantias, ordem de execução e erros, está em `docs/protocol.md`,
|
|
133
|
+
dentro do pacote instalado. Como o changelog, ele acompanha a versão que o projeto usa.
|
|
@@ -4,62 +4,94 @@ title: Agendador
|
|
|
4
4
|
|
|
5
5
|
# Agendador
|
|
6
6
|
|
|
7
|
-
Use um schedule para executar uma action por cron ou
|
|
8
|
-
agendamento e o runtime executa a action com provenance `schedule`,
|
|
7
|
+
Use um schedule para executar uma action por cron ou em intervalos, sem uma requisição. O
|
|
8
|
+
`SchedulerAdapter` registra o agendamento, e o runtime executa a action com provenance `schedule`,
|
|
9
|
+
que aparece no audit e no contexto do handler.
|
|
9
10
|
|
|
10
11
|
## O contrato
|
|
11
12
|
|
|
12
13
|
```ts
|
|
13
|
-
interface SchedulerAdapter {
|
|
14
|
+
interface SchedulerAdapter extends Adapter {
|
|
15
|
+
name: string // herdado de Adapter; identifica o driver
|
|
16
|
+
kind: 'scheduler'
|
|
14
17
|
register(spec: ScheduleDef, fire: () => Promise<void> | void): Unsubscribe
|
|
15
18
|
}
|
|
16
19
|
|
|
17
20
|
interface ScheduleDef {
|
|
18
21
|
name: string
|
|
19
|
-
action: string // action
|
|
20
|
-
cron?: string // '0 9 * * *' (
|
|
21
|
-
every?: string // '
|
|
22
|
-
timezone?: string // IANA,
|
|
23
|
-
input?: unknown | (() => unknown | Promise<unknown>) //
|
|
24
|
-
enabled?: boolean
|
|
22
|
+
action: string // action registrada, executada a cada disparo
|
|
23
|
+
cron?: string // '0 9 * * *' (todo dia às 9h)
|
|
24
|
+
every?: string // '15s', '5m', '1h', '1d': atalho de intervalo
|
|
25
|
+
timezone?: string // IANA, como 'America/Sao_Paulo'; sem ela, vale a hora local do processo
|
|
26
|
+
input?: unknown | (() => unknown | Promise<unknown>) // fixo ou calculado a cada disparo
|
|
27
|
+
enabled?: boolean // padrão true
|
|
28
|
+
description?: string
|
|
29
|
+
tags?: string[]
|
|
25
30
|
}
|
|
26
31
|
```
|
|
27
32
|
|
|
28
|
-
|
|
29
|
-
|
|
33
|
+
Declare `cron` ou `every`, não os dois. `every` aceita segundos e minutos até 59, horas até 23 e
|
|
34
|
+
somente `1d` para dias; intervalos maiores usam `cron`. Sem `input`, a action recebe `{}`.
|
|
30
35
|
|
|
31
36
|
## Driver
|
|
32
37
|
|
|
33
38
|
```ts
|
|
34
39
|
import { nodeCronScheduler } from '@softize/opus/scheduler/node-cron'
|
|
35
40
|
|
|
36
|
-
const scheduler = nodeCronScheduler()
|
|
41
|
+
const scheduler = nodeCronScheduler() // no mesmo processo do runtime
|
|
37
42
|
```
|
|
38
43
|
|
|
39
|
-
##
|
|
44
|
+
## Declarar no domínio
|
|
45
|
+
|
|
46
|
+
Declare o schedule com `defineSchedule` e registre-o no domínio, ao lado da action que ele executa:
|
|
40
47
|
|
|
41
48
|
```ts
|
|
42
|
-
import { defineSchedule } from '@softize/opus/core'
|
|
49
|
+
import { createRuntime, defineDomain, defineSchedule } from '@softize/opus/core'
|
|
50
|
+
import { caixaFechar } from './caixa-fechar.ts' // a action 'caixa.fechar', definida com bindAction
|
|
43
51
|
|
|
44
|
-
// Declarado no domínio, junto das actions:
|
|
45
52
|
const fecharDia = defineSchedule({
|
|
46
|
-
name: 'fechar-dia',
|
|
53
|
+
name: 'caixa.fechar-dia',
|
|
47
54
|
action: 'caixa.fechar',
|
|
48
55
|
cron: '0 23 * * *',
|
|
49
56
|
timezone: 'America/Sao_Paulo',
|
|
50
57
|
})
|
|
51
58
|
|
|
59
|
+
export const caixaDomain = defineDomain({
|
|
60
|
+
name: 'caixa',
|
|
61
|
+
actions: { caixaFechar },
|
|
62
|
+
schedules: [fecharDia],
|
|
63
|
+
})
|
|
64
|
+
|
|
52
65
|
const runtime = createRuntime({
|
|
53
66
|
// …server/data…
|
|
54
67
|
scheduler,
|
|
55
68
|
})
|
|
69
|
+
runtime.register([caixaDomain])
|
|
70
|
+
await runtime.start()
|
|
56
71
|
```
|
|
57
72
|
|
|
58
|
-
|
|
59
|
-
|
|
73
|
+
No `start()`, o runtime confere se cada schedule aponta para uma action registrada no mesmo runtime
|
|
74
|
+
e depois o registra no adapter. Um nome sem action registrada interrompe o `start()` com
|
|
75
|
+
`runtime.schedule_action_not_found`, antes de inicializar os adapters; a mensagem lista cada
|
|
76
|
+
schedule e a action que ele procurou. Com schedules registrados e sem `scheduler`, o `start()`
|
|
77
|
+
falha com `runtime.scheduler_required`. Um schedule com `enabled: false` não é registrado nem
|
|
78
|
+
conferido, então pode apontar para uma action que ainda não existe no ambiente.
|
|
79
|
+
|
|
80
|
+
## Preparar a action agendada
|
|
81
|
+
|
|
82
|
+
O schedule executa a action sem usuário: `ctx.user` é `null` e `ctx.can` nega tudo. Por isso, a
|
|
83
|
+
action alvo precisa declarar `public: true`; sem isso, cada disparo termina em
|
|
84
|
+
`auth.unauthenticated`. Para que ela não fique acessível por HTTP, declare também `internal: true`:
|
|
85
|
+
os servidores do Opus não expõem actions internas, e o schedule continua executando-a pelo runtime.
|
|
86
|
+
|
|
87
|
+
Quando um disparo não conclui a action, o runtime registra um erro no log com o schedule, a action,
|
|
88
|
+
o código e a mensagem do erro; o agendamento continua ativo e o próximo disparo executa a action de
|
|
89
|
+
novo. Uma falha dentro do pipeline da action, como `auth.unauthenticated` ou um erro do handler,
|
|
90
|
+
também fica registrada no audit. O callback `onError` do driver recebe somente erros lançados antes
|
|
91
|
+
da execução, como uma falha na função `input`.
|
|
60
92
|
|
|
61
93
|
## Limites atuais
|
|
62
94
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
95
|
+
O único driver é o `node-cron`, que roda no mesmo processo: o agendamento some se o processo cair e,
|
|
96
|
+
em um cluster, cada nó dispara o mesmo schedule. O modo design do Vite não registra schedules.
|
|
97
|
+
Backends duráveis ou distribuídos, como jobs repetidos do BullMQ ou Temporal, ainda não têm driver.
|
|
@@ -1,26 +1,28 @@
|
|
|
1
1
|
## Lista de altura fixa
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Use `ScrollArea` quando um bloco precisa manter a própria altura e rolar o conteúdo que passar dela.
|
|
4
|
+
Dê a altura ao `ScrollArea`, como `h-48`, e coloque o conteúdo dentro. A barra vertical já vem
|
|
5
|
+
incluída; não é preciso acrescentar `ScrollBar`.
|
|
4
6
|
|
|
5
7
|
```tsx preview col
|
|
6
|
-
const
|
|
7
|
-
{ id: '
|
|
8
|
-
{ id: '
|
|
9
|
-
{ id: '
|
|
10
|
-
{ id: '
|
|
11
|
-
{ id: '
|
|
12
|
-
{ id: '
|
|
13
|
-
{ id: '
|
|
14
|
-
{ id: '
|
|
8
|
+
const orders = [
|
|
9
|
+
{ id: 'OS-1842', title: 'Revisão dos 20.000 km', vehicle: 'ABC-1D23' },
|
|
10
|
+
{ id: 'OS-1839', title: 'Troca das pastilhas de freio', vehicle: 'BRA-2E19' },
|
|
11
|
+
{ id: 'OS-1835', title: 'Alinhamento e balanceamento', vehicle: 'RIO-4F56' },
|
|
12
|
+
{ id: 'OS-1830', title: 'Diagnóstico de ruído na suspensão', vehicle: 'SPO-7G88' },
|
|
13
|
+
{ id: 'OS-1827', title: 'Troca de óleo e filtro', vehicle: 'ABC-1D23' },
|
|
14
|
+
{ id: 'OS-1821', title: 'Substituição da bateria', vehicle: 'CWB-3H45' },
|
|
15
|
+
{ id: 'OS-1818', title: 'Higienização do ar-condicionado', vehicle: 'BRA-2E19' },
|
|
16
|
+
{ id: 'OS-1814', title: 'Revisão dos 40.000 km', vehicle: 'POA-9J12' },
|
|
15
17
|
]
|
|
16
18
|
|
|
17
19
|
render(
|
|
18
20
|
<ScrollArea className="h-48 w-full rounded-md border border-border">
|
|
19
21
|
<div className="p-3">
|
|
20
|
-
{
|
|
21
|
-
<div key={
|
|
22
|
-
<p className="font-medium">{
|
|
23
|
-
<p className="text-xs text-muted-foreground">{
|
|
22
|
+
{orders.map((order) => (
|
|
23
|
+
<div key={order.id} className="rounded-md px-2 py-1.5 text-sm hover:bg-muted">
|
|
24
|
+
<p className="font-medium">{order.title}</p>
|
|
25
|
+
<p className="text-xs text-muted-foreground">{order.id} · {order.vehicle}</p>
|
|
24
26
|
</div>
|
|
25
27
|
))}
|
|
26
28
|
</div>
|
|
@@ -30,17 +32,18 @@ render(
|
|
|
30
32
|
|
|
31
33
|
## Trilho horizontal
|
|
32
34
|
|
|
33
|
-
Para rolar na horizontal, acrescente
|
|
35
|
+
Para rolar na horizontal, acrescente `<ScrollBar orientation="horizontal" />` como filho e mantenha
|
|
36
|
+
o conteúdo em uma linha que não quebra, com `flex` e `w-max`.
|
|
34
37
|
|
|
35
38
|
```tsx preview col
|
|
36
|
-
const
|
|
39
|
+
const services = ['Troca de óleo', 'Filtro de ar', 'Alinhamento', 'Balanceamento', 'Pastilhas de freio', 'Velas de ignição']
|
|
37
40
|
|
|
38
41
|
render(
|
|
39
42
|
<ScrollArea className="w-full rounded-md border border-border whitespace-nowrap">
|
|
40
43
|
<div className="flex w-max gap-2 p-3">
|
|
41
|
-
{
|
|
42
|
-
<span key={
|
|
43
|
-
{
|
|
44
|
+
{services.map((service) => (
|
|
45
|
+
<span key={service} className="rounded-md border border-border bg-muted px-3 py-1.5 text-sm">
|
|
46
|
+
{service}
|
|
44
47
|
</span>
|
|
45
48
|
))}
|
|
46
49
|
</div>
|
|
@@ -49,28 +52,30 @@ render(
|
|
|
49
52
|
)
|
|
50
53
|
```
|
|
51
54
|
|
|
52
|
-
##
|
|
55
|
+
## Histórico com divisórias
|
|
53
56
|
|
|
54
|
-
O conteúdo é livre
|
|
57
|
+
O conteúdo interno é livre. Neste exemplo, o histórico de uma ordem de serviço usa `Separator`
|
|
58
|
+
entre as linhas. A barra só aparece quando o conteúdo passa da altura e o ponteiro está sobre a
|
|
59
|
+
área; para mantê-la sempre visível, passe `type="always"`.
|
|
55
60
|
|
|
56
61
|
```tsx preview col
|
|
57
|
-
const
|
|
58
|
-
'
|
|
59
|
-
'
|
|
60
|
-
'
|
|
61
|
-
'
|
|
62
|
-
'
|
|
63
|
-
'
|
|
64
|
-
'
|
|
65
|
-
'
|
|
66
|
-
'12:
|
|
62
|
+
const history = [
|
|
63
|
+
'08:02 Veículo recebido na oficina',
|
|
64
|
+
'08:15 Checklist de entrada concluído',
|
|
65
|
+
'08:40 Diagnóstico iniciado',
|
|
66
|
+
'09:10 Orçamento enviado ao cliente',
|
|
67
|
+
'09:32 Orçamento aprovado',
|
|
68
|
+
'10:05 Peças separadas no estoque',
|
|
69
|
+
'11:20 Serviço concluído',
|
|
70
|
+
'11:45 Teste de rodagem aprovado',
|
|
71
|
+
'12:00 Veículo liberado para retirada',
|
|
67
72
|
]
|
|
68
73
|
|
|
69
74
|
render(
|
|
70
75
|
<ScrollArea className="h-40 w-full rounded-md border border-border">
|
|
71
76
|
<div className="p-3 text-sm">
|
|
72
|
-
{
|
|
73
|
-
<div key={
|
|
77
|
+
{history.map((line, i) => (
|
|
78
|
+
<div key={line}>
|
|
74
79
|
{i > 0 && <Separator className="my-1.5" />}
|
|
75
80
|
<p className="text-muted-foreground">{line}</p>
|
|
76
81
|
</div>
|
|
@@ -84,11 +89,13 @@ render(
|
|
|
84
89
|
|
|
85
90
|
| Propriedade | Tipo | Padrão | Descrição |
|
|
86
91
|
|---|---|---|---|
|
|
87
|
-
| `className` | `string` | |
|
|
88
|
-
| `
|
|
92
|
+
| `className` | `string` | | Define as dimensões da janela rolável. Sem uma dimensão limitada, não há conteúdo a recortar. |
|
|
93
|
+
| `type` | `'hover' \| 'scroll' \| 'auto' \| 'always'` | `'hover'` | Quando a barra aparece: sob o ponteiro, durante a rolagem, sempre que houver excedente ou sempre. |
|
|
94
|
+
| `children` | `ReactNode` | | Conteúdo da janela, incluindo uma `ScrollBar` horizontal quando necessária. |
|
|
89
95
|
|
|
90
96
|
## Propriedades de ScrollBar
|
|
91
97
|
|
|
92
98
|
| Propriedade | Tipo | Padrão | Descrição |
|
|
93
99
|
|---|---|---|---|
|
|
94
100
|
| `orientation` | `'vertical' \| 'horizontal'` | `'vertical'` | Direção da barra. A barra vertical já faz parte de `ScrollArea`. |
|
|
101
|
+
| `className` | `string` | | Ajusta o trilho da barra. |
|