@gmag11/nodered-mcp-server 1.0.1

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.
Files changed (89) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +162 -0
  3. package/index.js +133 -0
  4. package/package.json +58 -0
  5. package/resources/skills/nodered-flow-builder/SKILL.md +659 -0
  6. package/resources/skills/nodered-flow-layout/SKILL.md +395 -0
  7. package/resources/skills/nodered-flowfuse-dashboard/SKILL.md +941 -0
  8. package/resources/skills/nodered-fundamentals/SKILL.md +323 -0
  9. package/resources/skills/nodered-jsonata/SKILL.md +1039 -0
  10. package/resources/skills/nodered-mustache/SKILL.md +588 -0
  11. package/resources/skills/nodered-node-reference/SKILL.md +1020 -0
  12. package/resources/skills/nodered-node-reference/examples/common.json +113 -0
  13. package/resources/skills/nodered-node-reference/examples/network.json +107 -0
  14. package/resources/skills/nodered-node-reference/examples/parser.json +147 -0
  15. package/resources/skills/nodered-node-reference/examples/sequence.json +141 -0
  16. package/resources/skills/nodered-node-reference/examples/storage.json +104 -0
  17. package/resources/skills/nodered-patterns/SKILL.md +414 -0
  18. package/resources/skills/nodered-patterns/examples/error-handler.json +72 -0
  19. package/resources/skills/nodered-patterns/examples/http-endpoint.json +42 -0
  20. package/resources/skills/nodered-patterns/examples/mqtt-subscriber.json +47 -0
  21. package/resources/skills/nodered-patterns/examples/timer-flow.json +50 -0
  22. package/resources/skills/nodered-subflows/SKILL.md +261 -0
  23. package/resources/skills/nodered-uibuilder/SKILL.md +500 -0
  24. package/src/auth/api-key-verifier.js +36 -0
  25. package/src/auth/composite-verifier.js +59 -0
  26. package/src/auth/config.js +106 -0
  27. package/src/auth/oauth-clients-store.js +107 -0
  28. package/src/auth/oauth-provider.js +149 -0
  29. package/src/auth/oauth-token-store.js +312 -0
  30. package/src/nodered/auth.js +158 -0
  31. package/src/nodered/client.js +199 -0
  32. package/src/nodered/comms-client.js +500 -0
  33. package/src/renderer/colors.js +161 -0
  34. package/src/renderer/geometry.js +115 -0
  35. package/src/renderer/html-builder.js +571 -0
  36. package/src/renderer/index.js +51 -0
  37. package/src/renderer/ir-builder.js +161 -0
  38. package/src/renderer/layout.js +126 -0
  39. package/src/renderer/mermaid-builder.js +109 -0
  40. package/src/renderer/svg-builder.js +228 -0
  41. package/src/schemas/responses.js +283 -0
  42. package/src/server.js +844 -0
  43. package/src/skills/loader.js +84 -0
  44. package/src/staging-store.js +258 -0
  45. package/src/tools/add-nodes-to-group.js +216 -0
  46. package/src/tools/connect-nodes.js +115 -0
  47. package/src/tools/constants.js +45 -0
  48. package/src/tools/create-flow.js +87 -0
  49. package/src/tools/create-node.js +126 -0
  50. package/src/tools/create-subflow-instance.js +123 -0
  51. package/src/tools/create-subflow.js +101 -0
  52. package/src/tools/delete-context.js +60 -0
  53. package/src/tools/delete-flow.js +81 -0
  54. package/src/tools/delete-group.js +116 -0
  55. package/src/tools/delete-node.js +73 -0
  56. package/src/tools/delete-subflow.js +103 -0
  57. package/src/tools/deploy.js +94 -0
  58. package/src/tools/disconnect-nodes.js +158 -0
  59. package/src/tools/export-flow.js +161 -0
  60. package/src/tools/export-subflow.js +78 -0
  61. package/src/tools/flow-utils.js +376 -0
  62. package/src/tools/get-config-nodes.js +86 -0
  63. package/src/tools/get-context.js +76 -0
  64. package/src/tools/get-flow-diagram.js +99 -0
  65. package/src/tools/get-flow-nodes.js +116 -0
  66. package/src/tools/get-flows.js +74 -0
  67. package/src/tools/get-node-detail.js +77 -0
  68. package/src/tools/get-node-type-detail.js +92 -0
  69. package/src/tools/get-palette-nodes.js +63 -0
  70. package/src/tools/get-staging-status.js +34 -0
  71. package/src/tools/get-subflow-detail.js +110 -0
  72. package/src/tools/get-subflows.js +105 -0
  73. package/src/tools/import-flow.js +310 -0
  74. package/src/tools/inject-message.js +117 -0
  75. package/src/tools/install-node.js +31 -0
  76. package/src/tools/read-debug-messages.js +155 -0
  77. package/src/tools/refresh-staging.js +62 -0
  78. package/src/tools/remove-nodes-from-group.js +162 -0
  79. package/src/tools/render-staging.js +69 -0
  80. package/src/tools/response-utils.js +42 -0
  81. package/src/tools/search-nodes.js +134 -0
  82. package/src/tools/uninstall-node.js +31 -0
  83. package/src/tools/update-flow.js +95 -0
  84. package/src/tools/update-group.js +77 -0
  85. package/src/tools/update-node.js +132 -0
  86. package/src/tools/update-subflow.js +84 -0
  87. package/src/transport/http.js +252 -0
  88. package/src/transport/stdio.js +16 -0
  89. package/src/transport/ws-server.js +223 -0
@@ -0,0 +1,941 @@
1
+ ---
2
+ name: nodered-flowfuse-dashboard
3
+ version: "1.30.2"
4
+ category: dashboard
5
+ description: >-
6
+ Reference for @flowfuse/node-red-dashboard (Dashboard 2.0) v1.30.2, the officially
7
+ recommended Node-RED dashboard. Covers widget catalog with properties,
8
+ config nodes (ui-base, ui-page, ui-group, ui-theme), wiring patterns, and common
9
+ dashboard recipes. Docs: https://dashboard.flowfuse.com/
10
+ tools:
11
+ - install-node
12
+ - create-node
13
+ - update-node
14
+ - get-node-type-detail
15
+ - get-palette-nodes
16
+ - get-node-detail
17
+ - connect-nodes
18
+ - inject-message
19
+ - get-config-nodes
20
+ - refresh-staging
21
+ - deploy
22
+ ---
23
+
24
+ # FlowFuse Dashboard 2.0 Reference
25
+
26
+ Comprehensive reference for building dashboards with **@flowfuse/node-red-dashboard** (Dashboard 2.0), the officially recommended successor to the obsolete `node-red-dashboard` v1. **Documented version: v1.30.2**.
27
+
28
+ > **⚠️ Staging reminder:** All create-node/connect-nodes calls stage changes locally. After building any dashboard, call `deploy()` to push to Node-RED. **NEVER skip deploy** — undeployed edits are not active.
29
+
30
+ > **⚠️ MANDATORY RULE:** Every Dashboard 2.0 flow MUST include a `ui-theme` config node. Every `ui-page` MUST reference that theme via its `theme` property. Dashboard 2.0 will not render correctly without a theme.
31
+ >
32
+ > **Prerequisites:** Read `nodered-fundamentals` first for core vocabulary. Use `nodered-flow-builder` for the step-by-step build workflow. See `nodered-flow-layout` for node positioning rules.
33
+
34
+ > **Widget discovery:** For any widget NOT listed in the deep-reference section below, or to verify current property values, use `get-palette-nodes` to list available Dashboard 2.0 types and `get-node-type-detail` to retrieve the full property schema.
35
+
36
+ ---
37
+
38
+ ## Concepts & Architecture
39
+
40
+ Dashboard 2.0 is a **widget-based** dashboard system. Each UI element (button, chart, gauge, form, etc.) is a standard Node-RED node that you create with `create-node` and wire with `connect-nodes` on the flow canvas. There is no separate UI editor — the dashboard layout is defined entirely by the node hierarchy.
41
+
42
+ **Key differences from obsolete v1:**
43
+ - Package name is `@flowfuse/node-red-dashboard` (NOT `node-red-dashboard`)
44
+ - Uses Vue 3 / Vuetify 3 under the hood (v1 used AngularJS)
45
+ - Config nodes are regular Node-RED config nodes (not a separate UI setup tab)
46
+ - Supports multi-tenancy via `msg._client` for per-user data (see Multi-Tenancy section in the official docs)
47
+ - The Dashboard 2.0 sidebar in the Node-RED editor lets you manage pages, groups, themes, and widget ordering visually
48
+
49
+ **Design patterns:**
50
+ - **Single Source of Truth:** All users see the same data (default). Wire a sensor to a gauge — everyone sees the same reading.
51
+ - **Multi-Tenancy:** Each user sees their own data. Toggle "Accept Client Data" in the Dashboard 2.0 sidebar for specific widget types. Use `msg._client` to target specific connections.
52
+
53
+ ---
54
+
55
+ ## Config Nodes
56
+
57
+ Dashboard 2.0 uses four config nodes that define the dashboard structure. **Creation order matters** because each references the previous one. **⚠️ `ui-theme` is MANDATORY — always create one before creating pages.**
58
+
59
+ ```
60
+ ui-base (app settings, URL path)
61
+ ├── ui-theme (colors, sizes) — ⚠️ MANDATORY, always create one
62
+ └── ui-page (navigation entry, layout type) — must reference ui-theme
63
+ └── ui-group (widget container, width)
64
+ └── widgets (ui-button, ui-chart, etc.)
65
+ ```
66
+
67
+ ### ui-base (`type: "ui-base"`)
68
+
69
+ Defines global dashboard settings: base URL path, app icon, navigation style, theme mode.
70
+
71
+ | Property | Type | Description |
72
+ |----------|------|-------------|
73
+ | `path` | string | Base URL endpoint (default: `"/dashboard"`) |
74
+ | `name` | string | Dashboard site name shown in browser tab |
75
+ | `appIcon` | string | URL to square icon image (192-512px), shown in browser tab and PWA |
76
+ | `sideNavigationStyle` | string | `"default"` (collapsing), `"fixed"`, `"icons"` (collapse to icons), `"over"` (appear over content), `"none"` (always hide) |
77
+ | `headerStyle` | string | `"default"` (scrolls away), `"fixed"` (always visible), `"hidden"` (no title bar) |
78
+ | `headerContent` | string | `"pageName"`, `"dashboardName"`, `"dashboardNamePageName"`, `"none"` |
79
+ | `includePagePathInLabel` | boolean | Show page path alongside name in sidebar |
80
+ | `allowInstall` | boolean | Enable PWA install prompt |
81
+ | `theme` | string | Theme config node ID for default theme |
82
+
83
+ **Creation:**
84
+ ```
85
+ create-node(type: "ui-base", name: "My Dashboard", properties: { path: "/dashboard", sideNavigationStyle: "default" })
86
+ ```
87
+
88
+ ### ui-page (`type: "ui-page"`)
89
+
90
+ Represents a navigable page in the dashboard sidebar. Must reference a `ui-base` node. **⚠️ The `theme` property is REQUIRED — every `ui-page` must reference a `ui-theme` config node.** Always create a `ui-theme` before creating pages.
91
+
92
+ | Property | Type | Required | Description |
93
+ |----------|------|----------|-------------|
94
+ | `ui` | string | ✅ | Config node ID of the parent `ui-base` |
95
+ | `path` | string | ✅ | URL path extending the base (e.g., `"/page1"`) |
96
+ | `name` | string | | Page display name in sidebar |
97
+ | `theme` | string | ✅ | Theme config node ID — **MANDATORY**, always create a `ui-theme` first |
98
+ | `icon` | string | | Material Design icon name (without `mdi-` prefix) |
99
+ | `layout` | string | | `"grid"` (default), `"fixed"`, `"notebook"`, `"tabs"` |
100
+ | `className` | string | | Custom CSS class(es) for advanced styling |
101
+ | `visible` | string | | `"true"` (default) or `"false"` — controls page visibility |
102
+ | `disabled` | string | | `"false"` (default) or `"true"` — controls page interactivity |
103
+ | `breakpoints` | array | | Responsive layout breakpoints: `[{ name, px, cols }]` — not available for fixed layout |
104
+
105
+ **Creation:**
106
+ ```
107
+ // Step 1: Create ui-theme first (REQUIRED)
108
+ create-node(type: "ui-theme", name: "Dashboard Theme", properties: {
109
+ colors: { surface: "#ffffff", primary: "#0094ce", bgPage: "#eeeeee", groupBg: "#ffffff", groupOutline: "#cccccc" },
110
+ sizes: { density: "default", pagePadding: "12px", groupGap: "12px", groupBorderRadius: "4px", widgetGap: "12px" }
111
+ })
112
+
113
+ // Step 2: Create ui-page with theme reference
114
+ create-node(type: "ui-page", name: "Home", properties: {
115
+ ui: "<uiBaseId>",
116
+ path: "/home",
117
+ icon: "home",
118
+ layout: "grid",
119
+ theme: "<uiThemeId>" // ⚠️ MANDATORY
120
+ })
121
+ ```
122
+
123
+ ### ui-group (`type: "ui-group"`)
124
+
125
+ A container for widgets on a page. Must reference a `ui-page` node. Widgets reference their parent `ui-group`.
126
+
127
+ | Property | Type | Description |
128
+ |----------|------|-------------|
129
+ | `page` | string | Config node ID of the parent `ui-page` |
130
+ | `name` | string | Group label shown in the dashboard |
131
+ | `width` | number | Width in grid columns (1-12, grid layout) or fixed px units |
132
+ | `height` | number | Minimum height in px |
133
+ | `groupType` | string | `"default"` (always visible) or `"dialog"` (triggered by `ui-control`) |
134
+ | `className` | string | Custom CSS class(es) for advanced styling |
135
+ | `showTitle` | boolean | Show the group name as a title bar above the widgets |
136
+ | `visible` | string | `"true"` (default) or `"false"` — controls group visibility |
137
+ | `disabled` | string | `"false"` (default) or `"true"` — controls group interactivity |
138
+
139
+ **Creation:**
140
+ ```
141
+ create-node(type: "ui-group", name: "Controls", properties: { page: "<uiPageId>", width: 6, height: 200 })
142
+ ```
143
+
144
+ **Note:** If you create a widget without specifying a `group`, Dashboard 2.0 auto-creates a default base, page, and group for you. However, for predictable layouts, always create config nodes explicitly.
145
+
146
+ ### ui-theme (`type: "ui-theme"`)
147
+
148
+ Defines the visual theme (colors and spacing) applied to dashboard pages. **⚠️ MANDATORY — every `ui-page` must reference a `ui-theme`. Always create one before creating any pages.**
149
+
150
+ | Property | Type | Description |
151
+ |----------|------|-------------|
152
+ | `name` | string | Theme display name in the Dashboard sidebar |
153
+ | `colors` | object | Color definitions: `{ surface, primary, bgPage, groupBg, groupOutline }` — all optional, hex colors |
154
+ | `sizes` | object | Sizing definitions: `{ density, pagePadding, groupGap, groupBorderRadius, widgetGap }` — all optional |
155
+
156
+ **Colors object:**
157
+ | Key | Description |
158
+ |-----|-------------|
159
+ | `surface` | Card/widget surface color (default: `"#ffffff"`) |
160
+ | `primary` | Primary accent color for headers, active elements (default: `"#0094ce"`) |
161
+ | `bgPage` | Page background color (default: `"#eeeeee"`) |
162
+ | `groupBg` | Group container background color (default: `"#ffffff"`) |
163
+ | `groupOutline` | Group container border color (default: `"#cccccc"`) |
164
+
165
+ **Sizes object:**
166
+ | Key | Description |
167
+ |-----|-------------|
168
+ | `density` | UI density: `"default"`, `"comfortable"`, `"compact"` |
169
+ | `pagePadding` | CSS value for page outer padding (default: `"12px"`) |
170
+ | `groupGap` | CSS value for gap between groups (default: `"12px"`) |
171
+ | `groupBorderRadius` | CSS value for group border radius (default: `"4px"`) |
172
+ | `widgetGap` | CSS value for gap between widgets within a group (default: `"12px"`) |
173
+
174
+ **Creation:**
175
+ ```
176
+ create-node(type: "ui-theme", name: "Dashboard Theme", properties: {
177
+ colors: {
178
+ surface: "#ffffff",
179
+ primary: "#0094ce",
180
+ bgPage: "#eeeeee",
181
+ groupBg: "#ffffff",
182
+ groupOutline: "#cccccc"
183
+ },
184
+ sizes: {
185
+ density: "default",
186
+ pagePadding: "12px",
187
+ groupGap: "12px",
188
+ groupBorderRadius: "4px",
189
+ widgetGap: "12px"
190
+ }
191
+ })
192
+ ```
193
+
194
+ ---
195
+
196
+ ## Widget Catalog — Complete List
197
+
198
+ Every available Dashboard 2.0 widget with its `type` string for `create-node`. The list is ordered by category.
199
+
200
+ ### Display Widgets
201
+
202
+ | Type | Description |
203
+ |------|-------------|
204
+ | `ui-text` | Displays text/HTML updated via `msg.payload`. Supports HTML formatting, JSONata value formatting |
205
+ | `ui-markdown` | Renders Markdown content sent via `msg.payload` |
206
+ | `ui-template` | Full Vue 3 component — custom HTML/CSS/JS, access to `msg` via `this.msg`. Use for custom charts, layouts, embedded content |
207
+ | `ui-table` | Tabular data display with sorting, pagination, search, cell types (text, link, button, color, progress, sparkline, image) |
208
+ | `ui-notification` | Toast notification popup with configurable position, timeout, color, dismiss/confirm buttons |
209
+ | `ui-audio` | Audio playback widget. Send URL in `msg.payload` to play |
210
+ | `ui-iframe` | Embeds an external URL in an iframe |
211
+ | `ui-led` | LED indicator light — sends boolean state from `msg.payload` |
212
+ | `ui-map` | Map display widget |
213
+
214
+ ### Input / Control Widgets
215
+
216
+ | Type | Description |
217
+ |------|-------------|
218
+ | `ui-button` | Clickable button. Emits `msg.payload` on click. Supports icon, color, pointer events |
219
+ | `ui-button-group` | Group of buttons that emit the clicked button's value |
220
+ | `ui-switch` | Toggle switch — emits on/off payload values |
221
+ | `ui-slider` | Numeric slider with min/max/step. Emits value as user slides |
222
+ | `ui-number-input` | Numeric text input field |
223
+ | `ui-text-input` | Free-form text input field |
224
+ | `ui-dropdown` | Dropdown select (single or multi-select). Options as array of `{label, value}` |
225
+ | `ui-radio-group` | Radio button group |
226
+ | `ui-date-picker` | Date selection widget |
227
+ | `ui-file-input` | File upload widget |
228
+ | `ui-form` | Multi-field form with submit/cancel buttons. Emits form data as object |
229
+ | `ui-control` | Page/group visibility control — show/hide/enable/disable groups or navigate pages |
230
+ | `ui-event` | Emits events on page load/view. Used for multi-tenancy patterns |
231
+
232
+ ### Data Visualization Widgets
233
+
234
+ | Type | Description |
235
+ |------|-------------|
236
+ | `ui-chart` | Multi-type chart (line, bar, scatter, pie/doughnut, histogram). Based on eCharts |
237
+ | `ui-gauge` | Gauge display (half, 3/4, tile, battery, water tank types with segments) |
238
+ | `ui-progress` | Progress bar — value 0-100 via `msg.payload` |
239
+
240
+ ### Layout Widgets
241
+
242
+ | Type | Description |
243
+ |------|-------------|
244
+ | `ui-spacer` | Adds vertical space between widgets in a group |
245
+
246
+ > **Discovery tip:** Widget catalogs grow over releases. Use `get-palette-nodes` filtered for `@flowfuse/node-red-dashboard` to get the current list. Use `get-node-type-detail` on any type to get its full property schema.
247
+
248
+ ---
249
+
250
+ ## Deep Reference — Top 10 Widgets
251
+
252
+ Full property tables and wiring examples for the most commonly used widgets.
253
+
254
+ ### ui-button
255
+
256
+ `type: "ui-button"` — Clickable button that emits `msg.payload` when clicked.
257
+
258
+ | Property | Type | Description |
259
+ |----------|------|-------------|
260
+ | `group` | string | Parent `ui-group` config node ID |
261
+ | `label` | string | Button text |
262
+ | `icon` | string | Material Design icon name (without `mdi-` prefix) |
263
+ | `iconPosition` | string | `"left"` or `"right"` |
264
+ | `color` | string | Button background color (CSS color or hex) |
265
+ | `textColor` | string | Button text color |
266
+ | `iconColor` | string | Icon color |
267
+ | `payload` | string | Value emitted as `msg.payload` when clicked |
268
+ | `payloadType` | string | `"str"`, `"num"`, `"json"`, `"bool"` |
269
+ | `topic` | string | `msg.topic` value on click |
270
+ | `emulateClick` | boolean | If true, incoming `msg` triggers a click emit |
271
+
272
+ **Wiring example — button triggers a flow:**
273
+ ```
274
+ // Create
275
+ create-node(type: "ui-button", name: "Start Process", properties: {
276
+ group: "<uiGroupId>",
277
+ label: "Start",
278
+ icon: "play",
279
+ color: "green",
280
+ payload: "start",
281
+ payloadType: "str"
282
+ })
283
+
284
+ // Wire: button → function (process)
285
+ connect-nodes(fromNodeId: "<buttonId>", outputPort: 0, toNodeId: "<functionId>")
286
+ ```
287
+
288
+ **Data OUT (button click):** The button outputs `msg.payload` = the configured payload value. Wire the button's output to a function/switch node.
289
+
290
+ **Data IN (emulate click):** Send any `msg` to the button's input to programmatically trigger a click (if `emulateClick: true`). The button will emit its configured payload on its output.
291
+
292
+ **Dynamic updates via msg.ui_update:**
293
+ ```javascript
294
+ msg.ui_update = {
295
+ label: "Stop",
296
+ icon: "stop",
297
+ color: "red"
298
+ }
299
+ // Send to button's input to change appearance at runtime
300
+ ```
301
+
302
+ ### ui-chart
303
+
304
+ `type: "ui-chart"` — Multi-type chart supporting line, bar, scatter, pie/doughnut, and histogram.
305
+
306
+ | Property | Type | Description |
307
+ |----------|------|-------------|
308
+ | `group` | string | Parent `ui-group` config node ID |
309
+ | `label` | string | Chart title |
310
+ | `chartType` | string | `"line"`, `"bar"`, `"scatter"`, `"pie"`, `"doughnut"` |
311
+ | `xAxisType` | string | `"timescale"`, `"linear"`, `"categorical"`, `"bins"` |
312
+ | `series` | string/array | Series grouping: `"msg.topic"`, `"key"` with key name, or JSON array of keys |
313
+ | `x` | string | Key in data for x-axis value. Leave blank for auto-timestamp on timescale |
314
+ | `y` | string | Key in data for y-axis value. Leave blank for `msg.payload` |
315
+ | `action` | string | `"append"` (add to existing) or `"replace"` (clear then add) |
316
+ | `showLegend` | boolean | Show chart legend |
317
+ | `pointShape` | string | `"circle"`, `"triangle"`, `"rect"`, `"diamond"`, etc. (line/scatter) |
318
+ | `pointRadius` | number | Point radius in px |
319
+ | `xAxisFormat` | string | Time format string (e.g., `"{HH}:{mm}:{ss}"`, `"{yyyy}-{M}-{d}"`) |
320
+ | `xAxisLimit` | number | Max data points or time limit before pruning old data (0 = unlimited) |
321
+ | `interpolation` | string | Line interpolation: `"linear"`, `"step"`, `"bezier"`, `"cubic"`, `"cubic-mono"` |
322
+
323
+ **Wiring example — live sensor chart:**
324
+ ```
325
+ // Create chart
326
+ create-node(type: "ui-chart", name: "Temperature", properties: {
327
+ group: "<uiGroupId>",
328
+ label: "Temperature Over Time",
329
+ chartType: "line",
330
+ xAxisType: "timescale",
331
+ series: "msg.topic",
332
+ action: "append",
333
+ xAxisLimit: 300 // keep last 300 data points
334
+ })
335
+
336
+ // Wire: inject (sensor) → chart
337
+ connect-nodes(fromNodeId: "<injectId>", outputPort: 0, toNodeId: "<chartId>")
338
+ ```
339
+
340
+ **Data format for line chart:**
341
+ ```javascript
342
+ // Simple value — auto-timestamp
343
+ msg.payload = 23.5
344
+ msg.topic = "Sensor A"
345
+
346
+ // Object with explicit timestamp and value
347
+ msg.payload = { time: Date.now(), value: 23.5 }
348
+ // With chart config: x → key "time", y → key "value"
349
+ ```
350
+
351
+ **Data format for bar chart:**
352
+ ```javascript
353
+ // Array of objects
354
+ msg.payload = [
355
+ { category: "A", value: 10 },
356
+ { category: "B", value: 25 },
357
+ { category: "C", value: 15 }
358
+ ]
359
+ // Config: x → "category", y → "value", xAxisType: "categorical"
360
+ ```
361
+
362
+ **Clearing chart data:** Send `msg.payload = []` to clear all data.
363
+
364
+ **Dynamic chart options via msg.ui_update:**
365
+ ```javascript
366
+ msg.ui_update = {
367
+ chartOptions: {
368
+ title: { textStyle: { fontSize: 20 } },
369
+ yAxis: { position: "right" }
370
+ }
371
+ }
372
+ // Full eCharts option object supported — changes are additive
373
+ ```
374
+
375
+ ### ui-gauge
376
+
377
+ `type: "ui-gauge"` — Gauge display with multiple visual types and color-coded segments.
378
+
379
+ | Property | Type | Description |
380
+ |----------|------|-------------|
381
+ | `group` | string | Parent `ui-group` config node ID |
382
+ | `label` | string | Label above the gauge |
383
+ | `gtype` | string | `"gauge-tile"`, `"gauge-battery"`, `"gauge-tank"`, `"gauge-half"`, `"gauge-34"` |
384
+ | `gstyle` | string | `"needle"` or `"rounded"` (for half/34 gauges) |
385
+ | `value` | string/expression | Value source: `msg.payload`, `msg.<property>`, JSONata expression, or static |
386
+ | `min` | number | Minimum value |
387
+ | `max` | number | Maximum value |
388
+ | `segments` | array | `[{ color: "#hex", from: number }]` — color bands on the arc |
389
+ | `prefix` | string | Text before the value (half/34 only) |
390
+ | `suffix` | string | Text after the value (half/34 only) |
391
+ | `units` | string | Small units text below the value (half/34 only) |
392
+ | `icon` | string | Material Design icon below the value (half/34 only) |
393
+ | `alwaysShowLabel` | boolean | Always show label on tile gauges |
394
+ | `floatingLabelPosition` | string | `"top-left"`, `"top-right"`, `"bottom-left"`, `"bottom-right"` (tile only) |
395
+
396
+ **Wiring example — real-time value display:**
397
+ ```
398
+ // Create gauge
399
+ create-node(type: "ui-gauge", name: "CPU Usage", properties: {
400
+ group: "<uiGroupId>",
401
+ label: "CPU",
402
+ gtype: "gauge-half",
403
+ gstyle: "rounded",
404
+ min: 0,
405
+ max: 100,
406
+ units: "%",
407
+ segments: [
408
+ { color: "#4caf50", from: 0 },
409
+ { color: "#ff9800", from: 60 },
410
+ { color: "#f44336", from: 80 }
411
+ ]
412
+ })
413
+
414
+ // Wire: inject (sensor) → gauge
415
+ connect-nodes(fromNodeId: "<injectId>", outputPort: 0, toNodeId: "<gaugeId>")
416
+ ```
417
+
418
+ **Data IN:** Send numeric `msg.payload` to update the gauge value. Use JSONata on the `value` property to format (e.g., `$round(payload, 1)` for 1 decimal).
419
+
420
+ **Dynamic updates via msg.ui_update:**
421
+ ```javascript
422
+ msg.ui_update = {
423
+ label: "Memory",
424
+ min: 0,
425
+ max: 64,
426
+ units: "GB",
427
+ segments: [{ color: "#2196f3", from: 0 }]
428
+ }
429
+ ```
430
+
431
+ ### ui-slider
432
+
433
+ `type: "ui-slider"` — Numeric slider with configurable range, ticks, and icons.
434
+
435
+ | Property | Type | Description |
436
+ |----------|------|-------------|
437
+ | `group` | string | Parent `ui-group` config node ID |
438
+ | `label` | string | Label to the left of the slider (HTML allowed) |
439
+ | `min` | number | Minimum value |
440
+ | `max` | number | Maximum value |
441
+ | `step` | number | Step increment |
442
+ | `thumbLabel` | string | `"true"`, `"false"`, `"always"` — when to show the thumb value |
443
+ | `showTicks` | string | `"true"`, `"false"`, `"always"` — when to show ticks |
444
+ | `color` | string | Main slider/thumb color |
445
+ | `colorTrack` | string | Track color |
446
+ | `colorThumb` | string | Thumb/handle color |
447
+ | `iconPrepend` | string | Material icon before the slider |
448
+ | `iconAppend` | string | Material icon after the slider |
449
+ | `output` | string | `"onChange"` (emit while sliding) or `"onRelease"` (emit on release) |
450
+ | `showTextField` | boolean | Show a text input alongside for direct value entry |
451
+
452
+ **Wiring example — slider controls a chart:**
453
+ ```
454
+ // Wire: slider → chart
455
+ connect-nodes(fromNodeId: "<sliderId>", outputPort: 0, toNodeId: "<chartId>")
456
+ // Moving the slider sends its value as msg.payload to the chart
457
+ ```
458
+
459
+ **Data IN (set value):** Send numeric `msg.payload` to programmatically set the slider position.
460
+
461
+ **Data OUT (user interaction):** Slider emits `msg.payload` = current numeric value whenever the user moves it.
462
+
463
+ ### ui-table
464
+
465
+ `type: "ui-table"` — Data table with sorting, pagination, search, and rich cell types.
466
+
467
+ | Property | Type | Description |
468
+ |----------|------|-------------|
469
+ | `group` | string | Parent `ui-group` config node ID |
470
+ | `label` | string | Table title |
471
+ | `maxRows` | number | Max rows per page (0 = no pagination) |
472
+ | `selection` | string | `"none"`, `"click"` (select row), `"checkbox"` (multi-select) |
473
+ | `showSearch` | boolean | Show search/filter bar |
474
+ | `autoColumns` | boolean | Auto-detect columns from data keys |
475
+ | `columns` | array | Manual column definitions with `{ value, label, width, align, type }` |
476
+ | `breakpoint` | string/number | `"xs"`, `"sm"`, `"md"`, `"lg"`, px value, or `"none"` for responsive card mode |
477
+ | `deselect` | boolean | Auto-deselect when data is replaced |
478
+
479
+ **Cell types:** `text`, `html`, `link`, `color`, `tick-cross`, `progress`, `sparkline-trend`, `sparkline-bar`, `button`, `row-number`, `image`.
480
+
481
+ **Wiring example — display API data:**
482
+ ```
483
+ // Create table
484
+ create-node(type: "ui-table", name: "Device List", properties: {
485
+ group: "<uiGroupId>",
486
+ label: "Devices",
487
+ maxRows: 10,
488
+ showSearch: true,
489
+ autoColumns: true
490
+ })
491
+
492
+ // Wire: http request → table
493
+ connect-nodes(fromNodeId: "<httpReqId>", outputPort: 0, toNodeId: "<tableId>")
494
+ ```
495
+
496
+ **Data IN format:**
497
+ ```javascript
498
+ // Array of objects — keys become columns
499
+ msg.payload = [
500
+ { id: 1, name: "Sensor A", status: "active", temp: 23.5 },
501
+ { id: 2, name: "Sensor B", status: "inactive", temp: 0 }
502
+ ]
503
+
504
+ // Single object — appended to existing data
505
+ msg.payload = { id: 3, name: "Sensor C", status: "active", temp: 19.2 }
506
+
507
+ // Empty array — clear all data
508
+ msg.payload = []
509
+ ```
510
+
511
+ **Data OUT (row interaction):**
512
+ ```javascript
513
+ // On row click (selection: "click")
514
+ msg.payload = { id: 1, name: "Sensor A", ... } // full row object
515
+ msg.action = "row_click"
516
+
517
+ // On checkbox selection (selection: "checkbox")
518
+ msg.payload = [{ id: 1, ... }, { id: 2, ... }] // array of selected rows
519
+ msg.action = "multiselect"
520
+
521
+ // On button cell click
522
+ msg.payload = { id: 1, ... } // full row object
523
+ msg.column = "columnKey"
524
+ msg.action = "button_click"
525
+ ```
526
+
527
+ ### ui-form
528
+
529
+ `type: "ui-form"` — Multi-field form that collects user input and emits it as an object on submit.
530
+
531
+ | Property | Type | Description |
532
+ |----------|------|-------------|
533
+ | `group` | string | Parent `ui-group` config node ID |
534
+ | `label` | string | Form title |
535
+ | `options` | array | Form field definitions: `[{ label, name, type, required }]` |
536
+ | `buttons` | object | `{ submit: "Submit", cancel: "Cancel" }` — omit cancel text to hide cancel button |
537
+ | `twoColumns` | boolean | Render form in two-column layout |
538
+ | `resetOnSubmit` | boolean | Clear form after submission |
539
+ | `topic` | string | `msg.topic` value on submit |
540
+
541
+ **Field types:** `text`, `multiline`, `password`, `email`, `number`, `checkbox`, `switch`, `date`, `time`, `dropdown`.
542
+
543
+ **Wiring example — form submission processing:**
544
+ ```
545
+ // Create form
546
+ create-node(type: "ui-form", name: "User Registration", properties: {
547
+ group: "<uiGroupId>",
548
+ label: "Register",
549
+ options: [
550
+ { label: "Name", name: "name", type: "text", required: true },
551
+ { label: "Email", name: "email", type: "email", required: true },
552
+ { label: "Age", name: "age", type: "number" },
553
+ { label: "Subscribe", name: "newsletter", type: "checkbox" }
554
+ ],
555
+ buttons: { submit: "Register", cancel: "Cancel" },
556
+ resetOnSubmit: true
557
+ })
558
+
559
+ // Wire: form → function (process) → debug
560
+ connect-nodes(fromNodeId: "<formId>", outputPort: 0, toNodeId: "<functionId>")
561
+ ```
562
+
563
+ **Data OUT:** On submit, `msg.payload` = `{ name: "John", email: "john@example.com", age: 30, newsletter: true }`.
564
+
565
+ **Data IN (pre-fill):** Send an object to the form's input to pre-fill fields:
566
+ ```javascript
567
+ msg.payload = { name: "John", email: "john@example.com" }
568
+ ```
569
+
570
+ ### ui-dropdown
571
+
572
+ `type: "ui-dropdown"` — Dropdown select with single or multi-select support.
573
+
574
+ | Property | Type | Description |
575
+ |----------|------|-------------|
576
+ | `group` | string | Parent `ui-group` config node ID |
577
+ | `label` | string | Label text (HTML allowed) |
578
+ | `options` | array | `[{ label: "Display", value: "val" }]` — can also be array of strings |
579
+ | `multiple` | boolean | Allow multi-selection |
580
+ | `chips` | boolean | Show selected items as chips (multi-select) |
581
+ | `clearable` | boolean | Show clear button |
582
+ | `allowSearch` | boolean | Enable text search/filter |
583
+ | `msgTrigger` | string | `"onChange"` or `"onClose"` — when to emit the value |
584
+
585
+ **Wiring example:**
586
+ ```
587
+ create-node(type: "ui-dropdown", name: "Device Selector", properties: {
588
+ group: "<uiGroupId>",
589
+ label: "Select Device",
590
+ options: [
591
+ { label: "Sensor A", value: "sensor_a" },
592
+ { label: "Sensor B", value: "sensor_b" },
593
+ { label: "Sensor C", value: "sensor_c" }
594
+ ]
595
+ })
596
+ ```
597
+
598
+ **Data OUT:** On selection, `msg.payload` = the selected option's `value` (or array of values for multi-select).
599
+
600
+ **Data IN (programmatic selection):**
601
+ ```javascript
602
+ // Single select
603
+ msg.payload = "sensor_a"
604
+
605
+ // Multi-select
606
+ msg.payload = ["sensor_a", "sensor_b"]
607
+
608
+ // Clear selection
609
+ msg.payload = []
610
+ ```
611
+
612
+ ### ui-text
613
+
614
+ `type: "ui-text"` — Displays a text value that updates with each received `msg.payload`. Supports HTML and JSONata formatting.
615
+
616
+ | Property | Type | Description |
617
+ |----------|------|-------------|
618
+ | `group` | string | Parent `ui-group` config node ID |
619
+ | `label` | string | Optional label above/beside the text |
620
+ | `layout` | string | `"row-left"`, `"row-center"`, `"row-right"`, `"row-spread"`, `"col-center"` |
621
+ | `value` | string/expression | Value source: `msg.payload`, `msg.<property>`, JSONata expression, or static |
622
+ | `font` | string | Font family (if custom style enabled) |
623
+ | `fontSize` | string | Font size (if custom style enabled) |
624
+ | `color` | string | Text color (if custom style enabled) |
625
+
626
+ **Wiring example:**
627
+ ```
628
+ create-node(type: "ui-text", name: "Status Display", properties: {
629
+ group: "<uiGroupId>",
630
+ label: "System Status",
631
+ layout: "row-left"
632
+ })
633
+
634
+ // Wire any source → ui-text
635
+ connect-nodes(fromNodeId: "<sourceId>", outputPort: 0, toNodeId: "<textId>")
636
+ ```
637
+
638
+ **Data IN:** Send `msg.payload` to update the displayed text. HTML is rendered if the payload contains HTML tags. Use JSONata on the `value` property to format (e.g., `$round(payload, 1)`).
639
+
640
+ **Dynamic update:**
641
+ ```javascript
642
+ msg.ui_update = { label: "Temperature", color: "#e53935", fontSize: "24px" }
643
+ ```
644
+
645
+ ### ui-switch
646
+
647
+ `type: "ui-switch"` — Toggle switch that emits configurable on/off payload values.
648
+
649
+ | Property | Type | Description |
650
+ |----------|------|-------------|
651
+ | `group` | string | Parent `ui-group` config node ID |
652
+ | `label` | string | Label text |
653
+ | `layout` | string | `"row-left"`, `"row-left-reverse"`, `"row-spread"`, `"row-spread-reverse"` |
654
+ | `clickableArea` | string | `"switch"` (only the switch) or `"full"` (label + switch) |
655
+ | `onIcon` | string | Material icon when on |
656
+ | `offIcon` | string | Material icon when off |
657
+ | `onColor` | string | Icon color when on |
658
+ | `offColor` | string | Icon color when off |
659
+ | `onPayload` | string/number/boolean | Value emitted when switched on |
660
+ | `offPayload` | string/number/boolean | Value emitted when switched off |
661
+ | `onPayloadType` | string | `"str"`, `"num"`, `"json"`, `"bool"` |
662
+ | `offPayloadType` | string | `"str"`, `"num"`, `"json"`, `"bool"` |
663
+ | `passthru` | boolean | If true, passes input msg through to output |
664
+ | `decouple` | boolean | If true (non-passthrough), switch shows output state, not input state |
665
+
666
+ **Wiring example — switch controls an output:**
667
+ ```
668
+ create-node(type: "ui-switch", name: "Enable Pump", properties: {
669
+ group: "<uiGroupId>",
670
+ label: "Pump",
671
+ onPayload: true,
672
+ onPayloadType: "bool",
673
+ offPayload: false,
674
+ offPayloadType: "bool"
675
+ })
676
+
677
+ // Wire: switch → function (control logic)
678
+ connect-nodes(fromNodeId: "<switchId>", outputPort: 0, toNodeId: "<functionId>")
679
+ ```
680
+
681
+ **Data OUT:** On toggle, `msg.payload` = onPayload or offPayload.
682
+
683
+ **Data IN (set state):** Send the matching payload value to programmatically toggle:
684
+ ```javascript
685
+ msg.payload = true // switch turns on
686
+ msg.payload = false // switch turns off
687
+ ```
688
+
689
+ ### ui-notification
690
+
691
+ `type: "ui-notification"` — Toast notification popup. **Unique:** attached to `ui-base` (via `ui` property), not `ui-group`.
692
+
693
+ | Property | Type | Description |
694
+ |----------|------|-------------|
695
+ | `ui` | string | Parent `ui-base` config node ID (NOT a group) |
696
+ | `position` | string | `"top right"`, `"top center"`, `"top left"`, `"bottom right"`, `"bottom center"`, `"bottom left"`, `"center center"` |
697
+ | `color` | string | Notification border/header color |
698
+ | `displayTime` | number | Auto-close timeout in seconds (0 = indefinite) |
699
+ | `showCountdown` | boolean | Show countdown progress bar |
700
+ | `allowDismiss` | boolean | Show dismiss button |
701
+ | `dismissText` | string | Dismiss button label |
702
+ | `allowConfirm` | boolean | Show confirm button |
703
+ | `confirmText` | string | Confirm button label |
704
+ | `raw` | boolean | If true, `msg.payload` is treated as raw HTML |
705
+
706
+ **Wiring example — alert on threshold:**
707
+ ```
708
+ create-node(type: "ui-notification", name: "Alerts", properties: {
709
+ ui: "<uiBaseId>",
710
+ position: "top right",
711
+ color: "#f44336",
712
+ displayTime: 5,
713
+ showCountdown: true,
714
+ allowDismiss: true
715
+ })
716
+
717
+ // Wire: function (threshold check) → notification
718
+ connect-nodes(fromNodeId: "<functionId>", outputPort: 0, toNodeId: "<notifId>")
719
+ ```
720
+
721
+ **Data IN:** Send `msg.payload` as the notification message text (or HTML if `raw: true`).
722
+
723
+ **Sending to all clients:** By default, notifications target specific clients via `msg._client`. To broadcast to all connected clients, delete `msg._client` before sending:
724
+ ```javascript
725
+ delete msg._client
726
+ msg.payload = "⚠️ System Alert: Temperature exceeds threshold!"
727
+ return msg
728
+ ```
729
+
730
+ **Data OUT:** When user clicks confirm or dismiss, the notification emits `msg.payload` with the action taken.
731
+
732
+ ---
733
+
734
+ ## Wiring Patterns
735
+
736
+ ### Data INTO Widgets (Display/Update)
737
+
738
+ Most display widgets accept `msg.payload` on their input to update their content:
739
+
740
+ ```
741
+ sensor/inject → [function/transform] → widget
742
+ ```
743
+
744
+ - **Gauge/Text/Progress:** Wire any source directly. `msg.payload` numeric or string updates the display.
745
+ - **Chart:** Wire data source. `msg.payload` can be a single value (auto-timestamped) or an object/array with x/y keys.
746
+ - **Table:** Wire data source. `msg.payload` as array of objects or single object to append.
747
+ - **Notification:** Wire event source. `msg.payload` is the message text/HTML.
748
+
749
+ **msg.topic for targeted updates:** When multiple widgets share a data bus, use `msg.topic` to differentiate. For charts, `msg.topic` controls series grouping. For text/gauge, `msg.topic` can be used in switch nodes upstream to route data.
750
+
751
+ ### Data OUT of Widgets (User Interaction)
752
+
753
+ Interactive widgets emit `msg.payload` from their output when the user interacts:
754
+
755
+ ```
756
+ widget → [function/switch/process] → ...
757
+ ```
758
+
759
+ - **Button:** Emits configured payload on click.
760
+ - **Switch:** Emits `onPayload`/`offPayload` on toggle.
761
+ - **Slider:** Emits numeric value as user slides.
762
+ - **Dropdown:** Emits selected `value` (or array for multi-select).
763
+ - **Form:** Emits `{ fieldName: value, ... }` on submit.
764
+ - **Table:** Emits row object on click/selection, with `msg.action` and `msg.column`.
765
+
766
+ ### Chaining Widgets
767
+
768
+ Widgets can be chained together to create interactive dashboards:
769
+
770
+ ```
771
+ slider → chart // slider value drives chart
772
+ button → control // button click shows/hides groups
773
+ dropdown → function // selection filters data
774
+ form → http request // form data posted to API
775
+ ```
776
+
777
+ ### Multi-Tenancy (Per-User Data)
778
+
779
+ Dashboard 2.0 supports per-user data via `msg._client`. Use the Dashboard 2.0 sidebar in Node-RED to enable "Accept Client Data" on specific widget types. When enabled:
780
+ - Every `msg` from that widget contains `msg._client` with the user's `socketId`
781
+ - Sending a `msg` with `_client` back targets only that user
782
+ - Delete `msg._client` to broadcast to all users
783
+
784
+ ---
785
+
786
+ ## Recipes
787
+
788
+ ### Recipe: Live Chart from a Data Source
789
+
790
+ **Goal:** Display a real-time line chart from a sensor or periodic data source.
791
+
792
+ **Nodes:** `inject` (sensor simulator) → `ui-chart`
793
+
794
+ **Steps:**
795
+ 1. Create config hierarchy (theme is REQUIRED):
796
+ ```
797
+ baseId = create-node(type: "ui-base", name: "Dashboard", properties: { path: "/dashboard" })
798
+ themeId = create-node(type: "ui-theme", name: "Dashboard Theme", properties: {
799
+ colors: { surface: "#ffffff", primary: "#0094ce", bgPage: "#eeeeee", groupBg: "#ffffff", groupOutline: "#cccccc" },
800
+ sizes: { density: "default", pagePadding: "12px", groupGap: "12px", groupBorderRadius: "4px", widgetGap: "12px" }
801
+ })
802
+ pageId = create-node(type: "ui-page", name: "Monitoring", properties: {
803
+ ui: baseId, path: "/monitor", layout: "grid", theme: themeId
804
+ })
805
+ groupId = create-node(type: "ui-group", name: "Charts", properties: { page: pageId, width: 12 })
806
+ ```
807
+ 2. Create chart:
808
+ ```
809
+ chartId = create-node(type: "ui-chart", name: "Live Readings", properties: {
810
+ group: groupId,
811
+ label: "Sensor Values",
812
+ chartType: "line",
813
+ xAxisType: "timescale",
814
+ series: "msg.topic",
815
+ action: "append",
816
+ xAxisLimit: 300
817
+ })
818
+ ```
819
+ 3. Create inject (or wire existing sensor):
820
+ ```
821
+ sourceId = create-node(type: "inject", name: "Sensor Sim", properties: {
822
+ payload: "23.5",
823
+ payloadType: "num",
824
+ topic: "Temperature",
825
+ repeat: "5"
826
+ })
827
+ ```
828
+ 4. Wire and deploy:
829
+ ```
830
+ connect-nodes(fromNodeId: sourceId, outputPort: 0, toNodeId: chartId)
831
+ deploy()
832
+ ```
833
+ 5. Open `http://<nodered-host>:1880/dashboard/monitor` to view.
834
+
835
+ ### Recipe: Button-Triggered Action
836
+
837
+ **Goal:** A button that triggers a function node to perform an action (API call, MQTT publish, etc.).
838
+
839
+ **Nodes:** `ui-button` → `function` → `debug` (or `http request`)
840
+
841
+ **Steps:**
842
+ 1. Ensure config hierarchy exists (base → page → group). Create if needed.
843
+ 2. Create button:
844
+ ```
845
+ buttonId = create-node(type: "ui-button", name: "Refresh Data", properties: {
846
+ group: "<groupId>",
847
+ label: "Refresh",
848
+ icon: "refresh",
849
+ color: "blue",
850
+ payload: "refresh",
851
+ payloadType: "str"
852
+ })
853
+ ```
854
+ 3. Create processing function:
855
+ ```
856
+ fnId = create-node(type: "function", name: "Fetch Data", properties: {
857
+ func: "// Fetch data from API\n// msg.payload already = 'refresh' from button\nreturn msg;",
858
+ outputs: 1
859
+ })
860
+ ```
861
+ 4. Wire:
862
+ ```
863
+ connect-nodes(fromNodeId: buttonId, outputPort: 0, toNodeId: fnId)
864
+ ```
865
+ 5. Deploy and test — clicking the button triggers the function.
866
+
867
+ ### Recipe: Form Submission Processing
868
+
869
+ **Goal:** Collect user input via a form and process it in Node-RED.
870
+
871
+ **Nodes:** `ui-form` → `function` (validate/process) → `debug` + `http request`
872
+
873
+ **Steps:**
874
+ 1. Create form:
875
+ ```
876
+ formId = create-node(type: "ui-form", name: "Contact Form", properties: {
877
+ group: "<groupId>",
878
+ label: "Contact Us",
879
+ options: [
880
+ { label: "Name", name: "name", type: "text", required: true },
881
+ { label: "Message", name: "message", type: "multiline", required: true }
882
+ ],
883
+ buttons: { submit: "Send", cancel: "Clear" },
884
+ resetOnSubmit: true
885
+ })
886
+ ```
887
+ 2. Create processing function:
888
+ ```
889
+ fnId = create-node(type: "function", name: "Process Form", properties: {
890
+ func: "// msg.payload = { name: '...', message: '...' }\nif (!msg.payload.name || !msg.payload.message) {\n return null; // drop invalid\n}\nmsg.topic = 'contact-form';\nreturn msg;",
891
+ outputs: 1
892
+ })
893
+ ```
894
+ 3. Wire:
895
+ ```
896
+ connect-nodes(fromNodeId: formId, outputPort: 0, toNodeId: fnId)
897
+ ```
898
+ 4. Deploy. On form submit, `msg.payload` contains `{ name: "User", message: "Hello" }`.
899
+
900
+ ### Recipe: Gauge Monitoring a Real-Time Value
901
+
902
+ **Goal:** Display a live gauge that updates from a sensor or data source.
903
+
904
+ **Nodes:** `inject` (or data source) → `ui-gauge`
905
+
906
+ **Steps:**
907
+ 1. Create gauge:
908
+ ```
909
+ gaugeId = create-node(type: "ui-gauge", name: "CPU Load", properties: {
910
+ group: "<groupId>",
911
+ label: "CPU",
912
+ gtype: "gauge-half",
913
+ gstyle: "needle",
914
+ min: 0,
915
+ max: 100,
916
+ units: "%",
917
+ segments: [
918
+ { color: "#4caf50", from: 0 },
919
+ { color: "#ff9800", from: 60 },
920
+ { color: "#f44336", from: 80 }
921
+ ]
922
+ })
923
+ ```
924
+ 2. Wire data source to gauge:
925
+ ```
926
+ connect-nodes(fromNodeId: "<sourceId>", outputPort: 0, toNodeId: gaugeId)
927
+ ```
928
+ 3. Deploy. Send numeric `msg.payload` values to the gauge to update the display.
929
+
930
+ ---
931
+
932
+ ## References
933
+
934
+ - **Official Documentation:** https://dashboard.flowfuse.com/
935
+ - **Getting Started Guide:** https://dashboard.flowfuse.com/getting-started.html
936
+ - **Widget Catalog:** https://dashboard.flowfuse.com/nodes/widgets.html
937
+ - **Config Nodes:** https://dashboard.flowfuse.com/nodes/config/ui-base.html
938
+ - **GitHub Repository:** https://github.com/FlowFuse/node-red-dashboard
939
+ - **npm Package:** `@flowfuse/node-red-dashboard` (documented at v1.30.2)
940
+
941
+ > **Version note:** This skill documents `@flowfuse/node-red-dashboard` v1.30.2. If the installed version differs significantly, consult the official docs for the latest widget properties and features. Use `get-node-type-detail` for runtime property discovery on any widget type.