@t4h.framework/vite-panel-plugin 0.0.0-experimental-20260905063543

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 ADDED
@@ -0,0 +1,7 @@
1
+ # @t4h.framework/vite-panel-plugin
2
+
3
+ ## 0.0.0-experimental-20260905063543
4
+
5
+ ### Minor Changes
6
+
7
+ - [#148](https://github.com/tech4humans-brasil/framework/pull/148) [`893edbc`](https://github.com/tech4humans-brasil/framework/commit/893edbc4a5eddb08fed59eae6c88816365dae71d) Thanks [@gusteycamargo](https://github.com/gusteycamargo)! - Initialize package
package/README.md ADDED
@@ -0,0 +1,193 @@
1
+ # @t4h.framework/vite-panel-plugin
2
+
3
+ The panel of the framework: the workflows of an app with a form to start runs
4
+ from their input schema, the runs with filters and a live execution trace
5
+ (activities as a waterfall, child runs nested under the activity that started
6
+ them, input, output, duration, logs, the activity that failed), and an inbox of
7
+ activities waiting for an external advance.
8
+
9
+ It ships with `@t4h.framework/dev`, which plugs it into the Vite instance it
10
+ runs in middleware mode, so there is nothing to install or configure: run
11
+ `framework dev` and open the printed base URL. API paths (`/projects/*`,
12
+ `/_dev/*`) keep going to the dev server; everything else is the panel.
13
+
14
+ ## Hosting the panel elsewhere
15
+
16
+ The package is a Vite plugin (`apply: 'serve'`) that serves the pre-built SPA
17
+ from `dist/client` and injects the host configuration in the page:
18
+
19
+ ```ts
20
+ import panel from '@t4h.framework/vite-panel-plugin'
21
+
22
+ panel({
23
+ config: {
24
+ basePath: '/panel',
25
+ apiUrl: '/tenants/acme/projects/billing/versions/v3',
26
+ api: { manifest: '/manifest.json' },
27
+ project: 'billing',
28
+ metadata: { tenant: 'acme', version: 'v3' },
29
+ fields: {
30
+ run: 'input,output,rejectedReason,activities(pendingStatus,childrenCount)',
31
+ runActivity: 'pendingStatus,childrenCount,input,output,pendings,children',
32
+ },
33
+ capabilities: {
34
+ trigger: false,
35
+ advance: false,
36
+ events: false,
37
+ rebuild: false,
38
+ },
39
+ routes: {
40
+ runs: {
41
+ method: 'POST',
42
+ path: '/tenants/:tenant/projects/:project/versions/:version/workflows/:workflow/runs',
43
+ },
44
+ advance: {
45
+ method: 'PATCH',
46
+ path: '/tenants/:tenant/projects/:project/versions/:version/workflows/:workflow/runs/:run/activities/:activity',
47
+ },
48
+ },
49
+ },
50
+ })
51
+ ```
52
+
53
+ | Option | Default (local dev server) | Meaning |
54
+ | ---------------------- | --------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
55
+ | `title` | `Framework Panel` | Name shown in the sidebar and the browser tab |
56
+ | `basePath` | `''` (root) | Path the panel is mounted at; the plugin only answers under it and injects `<base href>` so the relative asset URLs resolve |
57
+ | `apiUrl` | `/_dev` | Where the panel API described below lives, exactly as the panel should call it: a same-origin path or an absolute URL, with the route paths appended |
58
+ | `project` | the manifest id | Project (app) id, the one id the core defines; shown in the top bar and filled into `:project` of the mirror routes |
59
+ | `metadata` | `{}` | `{ [key]: string }` labels of the host about this panel (the runtime sends `tenant` and `version`); each one is a box in the top bar and fills the `:key` of the same name in the routes |
60
+ | `fields` | `{}` | The `q` the panel sends on `api.runs` (`runs`), `api.run` (`run`) and `api.runActivity` (`runActivity`), in the host's own field-selection grammar; omitted, no `q` is sent |
61
+ | `api` | the paths of the local dev server (`/manifest`, `/runs/:run`, `/runs/:run/activities/:activity`, …) | Where each read and write of the panel API lives under `apiUrl`, as path templates with `:workflow`, `:run` and `:activity`; the panel knows what it asks, not where a host mounts it |
62
+ | `capabilities.trigger` | `true` | The host exposes `POST {apiUrl}/workflows/:id/runs` without authorization; otherwise the Run button becomes "Copy as curl" of the mirror route |
63
+ | `capabilities.advance` | `true` | Same for `POST {apiUrl}/activities/:id/advance` and the inbox |
64
+ | `capabilities.events` | `true` | The host streams `GET {apiUrl}/events`; otherwise the panel never polls and the top bar gets a Refresh button |
65
+ | `capabilities.rebuild` | `true` | The host rebuilds the app and starts over, like the dev server: the top bar shows the build state and empty lists mention the last build; off for a host that keeps its runs |
66
+ | `routes` | `POST /projects/:project/workflows/:workflow/runs` and `PATCH .../runs/:run/activities/:activity` | `{ method, path, headers? }` of the authorized routes a client must call, shown as `curl` examples with those headers; `path` is expanded with `project` and `metadata` |
67
+
68
+ The configuration is read from `window.__T4H_PANEL__`, written by the plugin in
69
+ place of the `<!--panel-config-->` marker of `index.html`; `<!--panel-base-->`
70
+ receives the `<base href>`. The host forwards `basePath` requests to the Vite
71
+ middlewares; everything else stays with the host.
72
+
73
+ ## Hosting without Vite
74
+
75
+ A host that has no Vite (the production runtime) takes the same pieces from
76
+ `@t4h.framework/vite-panel-plugin/node`, which imports nothing from `vite`:
77
+
78
+ ```ts
79
+ import {
80
+ createPanelAssetsHandler,
81
+ createPanelHandler,
82
+ render,
83
+ resolvePanelConfig,
84
+ } from '@t4h.framework/vite-panel-plugin/node'
85
+ ```
86
+
87
+ | Export | Use |
88
+ | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
89
+ | `createPanelHandler` | The whole panel as one `node:http` middleware — the page for a navigation under `basePath`, the assets for everything else. What the plugin uses |
90
+ | `createPanelAssetsHandler` | The hashed assets, cached forever, for a host that mounts them at a shared public path; `index: false` so a miss falls through |
91
+ | `render` | `index.html` with the config and the `<base href>` in place; `{ baseHref }` overrides the default `` `${basePath}/` `` |
92
+ | `resolvePanelConfig` | Fills the local defaults into a partial config, what `render` expects; `createPanelHandler` does it for you |
93
+ | `PANEL_CLIENT_DIST` | `dist/client`, the built SPA |
94
+ | `PANEL_CLIENT_ASSETS_DIR` | `dist/client/assets`, safe to serve publicly and cache forever — the names are hashed and `index.html` is not in there |
95
+ | `assertPanelBuilt` | Throws when the package was not built |
96
+
97
+ The runtime serves one panel per project version, so `basePath` carries the
98
+ tenant and the version while the assets live at a single shared path. Splitting
99
+ the two is what `baseHref` is for:
100
+
101
+ ```ts
102
+ app.use('/panel/assets', createPanelAssetsHandler())
103
+
104
+ // inside the authenticated controller of `.../versions/:version/panel`
105
+ return render(
106
+ resolvePanelConfig({
107
+ basePath: `/tenants/${tenant}/projects/${project}/versions/${version}/panel`,
108
+ apiUrl: `/tenants/${tenant}/projects/${project}/versions/${version}/panel/api`,
109
+ project,
110
+ metadata: { tenant, version },
111
+ capabilities: { trigger: false, advance: false, events: false },
112
+ }),
113
+ { baseHref: '/panel/' },
114
+ )
115
+ ```
116
+
117
+ Every link the SPA renders is absolute (`basePath` is the router `basename`)
118
+ and `fetch` goes to `apiUrl` as given, so a `<base href>` that does not match
119
+ `basePath` only affects where the assets are looked up.
120
+
121
+ ## Panel API
122
+
123
+ A host implements these routes under `apiUrl`, at the paths below or wherever
124
+ `api` relocates them. Dates are ISO strings;
125
+ `input`, `output`, `result`, `payload` and `args` are framework tagged JSON.
126
+ Every listing is a page: the panel asks for `limit` items from `offset` (25 at
127
+ a time, never more than 50) and the host answers the `count` of everything that
128
+ matched alongside, so the panel never holds more than one page. Child runs are
129
+ the exception: "show more" under an activity fetches the next page of 15 and
130
+ keeps the ones already on screen.
131
+
132
+ | Method | Route | Answer |
133
+ | ------ | -------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
134
+ | `GET` | `/manifest` | `{ id, description, workflows: [{ id, description, schema, authorization? }], authorization? }`; a host may leave `authorization` out |
135
+ | `GET` | `/state` | `{ state: 'idle' \| 'starting' \| 'ready' \| 'exited' }`; only called with `capabilities.rebuild` |
136
+ | `GET` | `/runs?status&workflow&id&parentRunId&parentActivityId&rootOnly&createdAfter&createdBefore&limit&offset` | `{ count, runs: Run[] }` newest first; `id` is a prefix, `createdAfter`/`createdBefore` are epoch milliseconds |
137
+ | `GET` | `/runs/:id?q` | `Run & { input, output, rejectedReason }`; `q` is `fields.run` |
138
+ | `GET` | `/runs/:id/activities?limit&offset&q` | `{ count, activities: Activity[] }` newest first; `q` is `fields.runActivities` |
139
+ | `GET` | `/runs/:id/activities/:activityId?q` | `Activity & { input, output }`; `q` is `fields.runActivity` |
140
+ | `GET` | `/runs/:id/activities/:activityId/pendings?limit&offset&q` | `{ count, pendings: Pending[] }` newest first; `q` is `fields.runActivityPendings` |
141
+ | `GET` | `/runs/:id/logs?limit&offset` | `{ count, logs: [{ level, args, at }] }` newest first |
142
+ | `GET` | `/activities/waiting?limit&offset` | `{ count, activities: [{ id, pendingId, runId, workflowId, type, schema, timeoutAt }] }` newest first |
143
+ | `GET` | `/events` | SSE: `manifest`, `build:status`, `build:fail`, `app:exit`, `run:created`, `run:updated` (both with a `Run`), `ping`; only opened with `capabilities.events` |
144
+ | `POST` | `/workflows/:workflowId/runs` | Only with `capabilities.trigger`; body is the input, answers `201 { id }` |
145
+ | `POST` | `/activities/:activityId/advance` | Only with `capabilities.advance`; body `{ payload }`, answers `{ id, type, status }` |
146
+
147
+ `Run` is `{ id, workflow, status, rejectedActivityId, parentWorkflowRunId,
148
+ parentWorkflowRunActivityId, createdAt, updatedAt }` with `status` in
149
+ `running | fulfilled | rejected`. `Activity` is `{ id, type, kind, createdAt }`
150
+ plus, on request, `hasChildren`, `input` and `output`; an async one (`kind:
151
+ 'async'`) also carries `completedAt` and, on request, `pendingStatus` (status
152
+ of its last pending). `Pending` is `{ id, previousId, status, schema, timeoutAt,
153
+ result, payload, createdAt, updatedAt }`.
154
+
155
+ No answer embeds a list: a run does not carry its activities, an activity does
156
+ not carry its pendings or the runs it started. Each is its own paged route,
157
+ newest first, and the trace reads one page at a time when the reader gets
158
+ there: the activities of the run, the pendings of an open async activity, and
159
+ the runs an activity started as `GET /runs?parentActivityId=…`, 15 at a time
160
+ behind "show more", since a `startWorkflowMany` can fan out to hundreds of
161
+ thousands. A sync activity records no completion: the trace
162
+ takes the next activity starting, or the run ending, as its end. Every listing
163
+ is newest first, logs included.
164
+
165
+ `q` is opaque to the panel: it sends `fields.run` and `fields.runActivity`
166
+ from its configuration, verbatim, and a host that answers the full shape
167
+ without one (the local dev server) leaves them out. The panel reads a run's
168
+ activities as summaries and asks for one activity's input, output, pendings
169
+ and children only when the reader opens it, so a trace of hundreds of
170
+ activities costs one request until then.
171
+
172
+ ## Developing the panel
173
+
174
+ Inside the SPA every read of the API is a TanStack Query (`src/queries.ts`,
175
+ which also holds the mutations and the cache writes each server event turns
176
+ into) and the single `EventSource` lives in `src/events.tsx`: nothing
177
+ subscribes to events on its own — a component reads a query and the stream
178
+ keeps it current; nothing polls, and a host with `capabilities.events` off
179
+ gets a Refresh button in the top bar that invalidates every query. The filters of the runs page are
180
+ typed URL state through nuqs (`src/filters.ts`), which is where the query
181
+ string of a shared link is defined.
182
+
183
+ Every instant on screen is measured by the server; what is still going counts
184
+ against the browser clock through `useTimer` (`src/hooks/useTimer.ts`), one
185
+ timer per rate shared by every counter, read in the leaf that shows the number
186
+ so a tick never re-renders the page around it. `yarn test` covers the three.
187
+
188
+ `yarn dev` starts Vite on the panel sources with `/_dev` proxied to a dev
189
+ server on port 3000, so run `framework dev` on any project first. `yarn build`
190
+ produces `dist/client` (the SPA, validated by the Vite build) and the two entry
191
+ points `dist/index.js` (the Vite plugin) and `dist/node.js` (the Vite-free
192
+ handler), type-checked by `check-types`; the published package ships all of
193
+ them.