node-red-contrib-room-tools 1.8.4 → 1.8.8
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/CONTROL_PANEL_AI_GUIDE.md +156 -39
- package/README.md +10 -0
- package/control-catalog.js +7 -6
- package/control-panel.js +5 -5
- package/control-policy.js +36 -478
- package/control-type-model.js +30 -31
- package/gateway.cjs +13 -261
- package/managed-bridge-admin.js +5 -11
- package/native-binding-plan.js +1 -1
- package/npm-shrinkwrap.json +433 -0
- package/package.json +8 -5
- package/panel-binding-editor.js +25 -36
- package/panel-editor-auth.js +3 -39
- package/panel-http.js +69 -0
- package/panel-visual-apply.js +19 -13
|
@@ -1,41 +1,107 @@
|
|
|
1
|
-
# ControlPanel
|
|
1
|
+
# ControlPanel 前端页面编写指南
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
适用基线:`node-red-contrib-room-tools@1.8.8` 的 `ui-control-panel`,Dashboard 2,Vue Options API。本文按当前代码审核,不表示支持任意 Vue、Vuetify 或第三方组件。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
给 AI 编写页面前阅读本文。目标是:页面作者只写普通前端页面;部署人员在网页编辑模式选择设备与点位;ControlPanel 保存映射并负责设备命令、反馈和网页侧量程换算。
|
|
6
6
|
|
|
7
|
-
1.
|
|
8
|
-
2. 重新打开该节点,点击 `Open page binding editor`。用编辑账号登录;编辑页默认启用 Select,可点击或右键点选可绑定控件,再选择在线设备、读写点位与编号方式。控件的绑定会先保存为草稿。
|
|
9
|
-
3. 在编辑页点 Apply,节点校验并保存隐藏映射、备份 Flow、重新生成该面板的 native bridge 并部署。Apply 后仍留在编辑页;点 Exit 才返回普通页面的登录页。
|
|
10
|
-
4. 普通用户用普通账号登录后才能操作已绑定控件。修改 Template 可能使旧控件定位失效;重新 Deploy 后进入编辑页检查并重新 Apply。普通 Deploy 本身不会替代编辑页的 Apply。
|
|
7
|
+
## 1. 职责边界
|
|
11
8
|
|
|
12
|
-
|
|
9
|
+
页面作者使用普通 HTML、CSS 和 Vue 编写布局、样式、导航、控件及其本地状态。无需知道驱动协议、点位路径、内部 Control ID 或绑定表结构。标准 HTML `id`、Vue `v-model` 和 ARIA 属性不是 ControlPanel 专用标签。
|
|
13
10
|
|
|
14
|
-
|
|
11
|
+
节点负责登录界面、普通与编辑权限、控件识别、映射保存、桥接生成、命令发送和反馈更新。页面不调用这些内部服务,不实现第二套设备通信。
|
|
15
12
|
|
|
16
|
-
|
|
17
|
-
- Vue 绑定字段使用 `data()` 中的顶层简单字段,例如 `volume`、`muted`、`source`。不要用 `state.volume`、数组下标、计算属性或表达式作为待绑定模型。
|
|
18
|
-
- 给控件稳定且唯一的标准 `id` 或 `name`,尤其是重复出现或外观相同的控件。不要把待绑定控件放在 `v-for` 内;绑定时控件应实际显示在页面上。
|
|
19
|
-
- 不要手写 `data-cp-*` 标签、设备路径、HTTP/WebSocket 调用、驱动命令或反馈回写。`data-cp-*` 会被拒绝。不要给待绑定控件叠加 `@click`、`@input`、`@change`、`@update:model-value` 等自行处理设备状态的事件,也不要额外绑定 `:value`、`:checked` 或 `:model-value`(下述只读进度条例外)。
|
|
20
|
-
- Vuetify 标签一律成对闭合,包括只用于装饰的组件:写 `<v-icon></v-icon>`,不要写 `<v-icon/>`。Dashboard 模板会把自闭合自定义标签后的控件错误地吞进去;节点现会直接报错。
|
|
21
|
-
- 普通页面可以使用其他 Vuetify 组件作布局或纯页面交互,但只有下表列出的组件具备自动设备绑定适配器。
|
|
13
|
+
不得从旧页面复制以下内容:
|
|
22
14
|
|
|
23
|
-
|
|
15
|
+
- 手写 `data-cp-*` 属性。这些属性会被拒绝;运行时自动添加的内部标记不属于页面交付代码。
|
|
16
|
+
- `sendControl`、`postControl`、`setControlSession`、`deviceValue` 等旧页面接口。
|
|
17
|
+
- 写死设备路径、驱动命令、绑定表,或调用 ControlPanel 的 HTTP、WebSocket、SSE 接口。
|
|
18
|
+
- 为修复反馈而添加设备轮询、强制回写滑杆、重复发送监听器或设备状态定时器。
|
|
24
19
|
|
|
25
|
-
|
|
20
|
+
这些限制针对设备绑定,不禁止正常的页面导航、CSS 动画、图片明暗变化等纯前端功能。未绑定控件不会自动控制设备。待绑定控件的模型应独立;不要让两个不同设备控件共用一个 `v-model` 字段。
|
|
21
|
+
|
|
22
|
+
## 2. 页面格式
|
|
23
|
+
|
|
24
|
+
交付可直接粘贴到节点 Template 的组件片段:一个 `<template>`,可选的 `<script>`、`<style>`。不要交付带 `<html>`、`<head>`、`<body>` 的完整文档,也不要交付需要外部构建才能运行的项目。
|
|
25
|
+
|
|
26
|
+
脚本使用 `export default { data() { ... }, computed: { ... }, methods: { ... } }`。待绑定字段必须是 `data()` 中的顶层、可写、简单字段,例如 `volume`、`muted`、`source`、`status`。不支持将 `state.volume`、数组下标、计算属性、函数调用或表达式作为绑定模型;不使用 `<script setup>`、组件导入或自行创建 Vue 应用。
|
|
27
|
+
|
|
28
|
+
为待绑定控件设置稳定、唯一的普通 `id`,尤其是重复控件。也可使用标准 `name`,但优先使用 `id`。不要根据运行时随机值生成 ID。改动 ID、标签类型或绑定写法后,原映射可能失效,需要重新检查。
|
|
29
|
+
|
|
30
|
+
所有 Vuetify 标签都成对闭合,包括装饰组件:`<v-icon></v-icon>`,不要写 `<v-icon/>`。Dashboard 的 HTML 渲染可能错误地吞入后续元素,节点会拒绝这种自闭合写法。原生 HTML 的 void 元素,如 `<input>`、`<img>`,不受此限制。
|
|
31
|
+
|
|
32
|
+
## 3. 可绑定控件
|
|
33
|
+
|
|
34
|
+
以下表格是当前自动绑定适配器的范围,不是整个 Vuetify 的组件清单。
|
|
35
|
+
|
|
36
|
+
| 用途 | 原生 HTML | Vuetify | 数据与提交方式 |
|
|
26
37
|
| --- | --- | --- | --- |
|
|
27
|
-
|
|
|
28
|
-
|
|
|
29
|
-
| 单选枚举 |
|
|
30
|
-
|
|
|
31
|
-
|
|
|
32
|
-
|
|
|
38
|
+
| 连续数值 | `input[type=range]`、`input[type=number]` | `v-slider` | 数字;原生 input/change 发送,Vuetify 模型变化发送并在变化停止后提交最终值。 |
|
|
39
|
+
| 布尔开关 | `input[type=checkbox]`、语义开关按钮 | `v-switch`、`v-checkbox` | 布尔值;切换发送。语义按钮写法见下节。 |
|
|
40
|
+
| 单选枚举 | `select` | `v-select`、`v-radio-group` | 字符串或数字的单个值,必须与设备枚举值对应。 |
|
|
41
|
+
| 文本输入 | `input[type=text]`、`textarea` | `v-text-field`、`v-textarea` | 字符串;原生 change 提交,Vuetify 在 change 或失焦时提交,`v-text-field` 还支持 Enter。 |
|
|
42
|
+
| 只读数值 | `meter`、`progress` | `v-progress-linear`、`v-progress-circular` | 只绑定 Feedback point,不发送命令。 |
|
|
43
|
+
| 只读文本 | `span`、`strong`、`b`、`output`、`h1`、`h2`、`h3` | 暂无 | 使用简单字段插值,如 `{{ status }}`,或由节点替换的纯文本;只绑定读点位。 |
|
|
44
|
+
|
|
45
|
+
原生输入可以不写 Vue 模型,也支持简单顶层 `v-model`。Vuetify 交互控件必须写简单 `v-model`;进度组件可以使用简单 `:model-value="progress"`。初始化数值为数字、开关为布尔值、文本为字符串,不用数组、对象或空值占位。
|
|
46
|
+
|
|
47
|
+
### 3.1 通用限制
|
|
48
|
+
|
|
49
|
+
- 待绑定控件自身不能有 `@click`、`@input`、`@change`、`@update:model-value` 等任何事件指令。即使事件只做本地操作,也会被当前识别器判为已有逻辑。页面导航等事件可放在独立、不绑定设备的控件上。
|
|
50
|
+
- 不额外叠加 `:value`、`:checked`、`:model-value`。只读进度组件的 `:model-value` 是例外;语义开关按钮的简单 ARIA 布尔绑定也是例外。
|
|
51
|
+
- 只允许 range/select 的 `v-model.number`;不要用 `.lazy`、`.trim`、带参数的 `v-model` 或其他修饰符。
|
|
52
|
+
- 待绑定元素不能位于 `v-for` 中,即使写了 `:key`。相同模板会生成多个元素,当前不能按列表项分别绑定。
|
|
53
|
+
- 标签页、`v-if` 等正常布局可以保留。编辑该控件时先让它显示出来,并确保同一定位只出现一个元素。绑定提示会随元素重建恢复。
|
|
54
|
+
- 普通 Vuetify 布局组件可使用,但 `v-btn`、`v-range-slider`、`v-autocomplete`、`v-combobox`、原生 radio 等不在自动设备绑定清单中。不要假定外观相似就支持。
|
|
55
|
+
|
|
56
|
+
### 3.2 各类控件限制
|
|
33
57
|
|
|
34
|
-
|
|
58
|
+
- 数值:`min`、`max`、`step` 用固定数字,也可用 `:min="0"`、`:max="100"`、`:step="1"`。不能绑定到动态字段或表达式。`v-slider` 要求 min < max、step > 0,不支持 `step="any"`。
|
|
59
|
+
- 原生 range 默认 0–100;原生 meter/progress 默认 0–1。原生 number 没有缺省完整量程;需要自动缩放时明确写 min/max。不要把原生缺省量程误当成设备量程。
|
|
60
|
+
- Vuetify switch/checkbox 不支持 `value`、自定义 `true-value`/`false-value`、`multiple` 或数组模型。
|
|
61
|
+
- Vuetify select 不支持多选、`return-object`、`clearable` 或对象模型;选项可用标量数组,或普通 `{ title, value }` 选项但模型必须是标量。`v-radio-group` 使用标量模型,子项也不能自带事件处理器。
|
|
62
|
+
- Vuetify 文本控件只用字符串模型。`v-text-field` 使用文本类型,不用数字、密码等类型;不支持 `.number`。`v-textarea` 的 Enter 是换行,不是提交。
|
|
63
|
+
- `v-progress-linear` 从 0 到固定正数 max,默认 max=100;不支持 min、step、indeterminate、clickable。`v-progress-circular` 固定 0–100,不声明 min/max/step,也不使用 indeterminate。
|
|
64
|
+
- 文本展示推荐只放一个简单插值,如 `<span>{{ status }}</span>`,不使用 `status.toFixed()`、拼接表达式或多个插值。复杂展示可用另外的纯前端元素,不把它作为设备反馈绑定目标。
|
|
35
65
|
|
|
36
|
-
|
|
66
|
+
### 3.3 普通按钮与开关按钮
|
|
67
|
+
|
|
68
|
+
一个只有文字的普通 `<button>` 不会自动成为设备控件。需要布尔设备开关时使用标准语义:
|
|
69
|
+
|
|
70
|
+
```html
|
|
71
|
+
<button id="room-power" type="button" :aria-pressed="powered">电源</button>
|
|
72
|
+
<button id="room-mute" type="button" role="switch" :aria-checked="muted">静音</button>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
在 `data()` 中将 `powered`、`muted` 初始化为布尔值。也可使用静态 `aria-pressed="false"` 或 `role="switch" aria-checked="false"`,由节点更新按钮状态。不要叠加点击事件。`aria-pressed`/`aria-checked` 的动态表达式只写顶层字段,不写取反、条件或函数。
|
|
76
|
+
|
|
77
|
+
## 4. 示例
|
|
78
|
+
|
|
79
|
+
### 4.1 原生 H5 控件
|
|
80
|
+
|
|
81
|
+
```html
|
|
82
|
+
<template>
|
|
83
|
+
<section>
|
|
84
|
+
<label for="room-volume">音量</label>
|
|
85
|
+
<input id="room-volume" type="range" min="0" max="100" step="1" v-model.number="volume">
|
|
86
|
+
<label><input id="room-mute" type="checkbox" v-model="muted">静音</label>
|
|
87
|
+
<button id="room-power" type="button" :aria-pressed="powered">电源</button>
|
|
88
|
+
<select id="room-source" v-model="source">
|
|
89
|
+
<option value="A">输入 A</option>
|
|
90
|
+
<option value="B">输入 B</option>
|
|
91
|
+
</select>
|
|
92
|
+
<span id="room-status">{{ status }}</span>
|
|
93
|
+
</section>
|
|
94
|
+
</template>
|
|
95
|
+
<script>
|
|
96
|
+
export default {
|
|
97
|
+
data () {
|
|
98
|
+
return { volume: 0, muted: false, powered: false, source: 'A', status: '待机' }
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
</script>
|
|
102
|
+
```
|
|
37
103
|
|
|
38
|
-
###
|
|
104
|
+
### 4.2 Vuetify 控件
|
|
39
105
|
|
|
40
106
|
```html
|
|
41
107
|
<template>
|
|
@@ -43,6 +109,11 @@
|
|
|
43
109
|
<v-slider id="room-volume" label="音量" v-model="volume" :min="0" :max="100" :step="1"></v-slider>
|
|
44
110
|
<v-switch id="room-mute" label="静音" v-model="muted"></v-switch>
|
|
45
111
|
<v-select id="room-source" label="输入源" v-model="source" :items="sources"></v-select>
|
|
112
|
+
<v-radio-group id="room-mode" label="模式" v-model="mode">
|
|
113
|
+
<v-radio label="模式 A" value="A"></v-radio>
|
|
114
|
+
<v-radio label="模式 B" value="B"></v-radio>
|
|
115
|
+
</v-radio-group>
|
|
116
|
+
<v-text-field id="room-title" label="名称" v-model="title"></v-text-field>
|
|
46
117
|
<v-progress-linear id="room-level" :model-value="level" :max="100"></v-progress-linear>
|
|
47
118
|
<span id="room-status">{{ status }}</span>
|
|
48
119
|
</section>
|
|
@@ -51,24 +122,70 @@
|
|
|
51
122
|
export default {
|
|
52
123
|
data () {
|
|
53
124
|
return {
|
|
54
|
-
volume: 0,
|
|
55
|
-
|
|
56
|
-
source: 'A',
|
|
57
|
-
sources: ['A', 'B'],
|
|
58
|
-
level: 0,
|
|
59
|
-
status: '待机'
|
|
125
|
+
volume: 0, muted: false, source: 'A', sources: ['A', 'B'],
|
|
126
|
+
mode: 'A', title: '', level: 0, status: '待机'
|
|
60
127
|
}
|
|
61
128
|
}
|
|
62
129
|
}
|
|
63
130
|
</script>
|
|
64
131
|
```
|
|
65
132
|
|
|
66
|
-
|
|
133
|
+
示例选项 A/B 只是占位数据,不保证与实际设备相符。部署人员应确认驱动的枚举值,再决定前端选项。节点不会自动生成或改写下拉菜单选项。
|
|
134
|
+
|
|
135
|
+
图片明暗、数值旁显等效果可以正常依赖 `volume` 或 `light` 做 Vue/CSS 展示。这些展示元素不发送设备命令,也不要反过来用 watch、mounted 或定时器重置正在绑定的字段。
|
|
136
|
+
|
|
137
|
+
## 5. 部署与绑定
|
|
138
|
+
|
|
139
|
+
1. 将页面粘贴到 `ui-control-panel` Template,选择 Dashboard 页面,设置 Room ID、普通账号及独立的编辑账号。普通账号密码至少 12 个字符;账号密码由节点配置,不能写入 H5。保存并 Deploy。
|
|
140
|
+
2. 重新打开节点,点击 `Open page binding editor`,用编辑账号登录。Select 默认激活,可以点击、右键或触屏长按可绑定控件。
|
|
141
|
+
3. 选择在线设备、模式、Write point、Feedback point、编号方式,保存草稿。草稿和亮紫色边框不表示已部署;边框只在编辑模式显示。
|
|
142
|
+
4. 点 Apply。节点检查当前模板、目录参数和 Flow 版本,备份后替换该面板此前生成的桥接并部署。不会无限追加旧桥接,也不会清除其他面板的绑定。Apply 成功后仍在编辑页,无需再次登录。
|
|
143
|
+
5. 点 Exit,回到普通页面登录页,再输入普通账号密码。普通账号不能进入编辑模式;不登录不能操作设备。进程重启或凭据变化可能要求重新登录。
|
|
144
|
+
|
|
145
|
+
Bindings 列表可 Edit 或 Remove;这些操作先改草稿,最后统一 Apply。从列表进入 Edit,保存或取消后返回列表。刷新、离开或重启前先 Apply,避免草稿丢失。
|
|
146
|
+
|
|
147
|
+
普通 Deploy 不能替代网页 Apply。Template 改动可能使已保存控件失效;重新 Deploy 后检查绑定。Apply 提示 Flow/Template changed 时重新载入编辑页后再检查,不能靠重复点击覆盖他人的部署。
|
|
148
|
+
|
|
149
|
+
设备必须在线且当前运行环境的 AMX 目录可返回参数,才能查询或重新 Apply 相关绑定。已保存映射在 Flow 中,启动和显示页面不要求驱动在线;但断线时不能保证设备控制或新反馈。Mac 安装模块不等于 MUSE 已安装,Push 也不等于复制本地 node_modules。部署模块属于运行环境操作,不属于前端开发职责。
|
|
150
|
+
|
|
151
|
+
## 6. 匹配、编号与范围
|
|
152
|
+
|
|
153
|
+
点位筛选基于参数描述,而不是页面文字或驱动显式 `controlType`:
|
|
154
|
+
|
|
155
|
+
| 参数描述 | 可匹配控件 |
|
|
156
|
+
| --- | --- |
|
|
157
|
+
| boolean,或无枚举且范围恰为 0–1 的 integer | 布尔开关;整数 0/1 由节点转换为前端布尔值。 |
|
|
158
|
+
| integer/float 数值参数 | range、数值进度;有枚举或 0/1 布尔语义时优先对应相应类别。 |
|
|
159
|
+
| enum,或带枚举描述的 integer/string | 单选枚举。当前整数路径以 `/source`、`/preset` 结尾也会按选择类处理;仍需确认合法选项。 |
|
|
160
|
+
| string/dstring | 文本输入。 |
|
|
161
|
+
| 上述类型的可读参数 | 简单文本展示也可绑定。 |
|
|
162
|
+
|
|
163
|
+
只有 `kind=param` 且具备相应 ro/wo/rw 权限的点位才参与绑定。范围未知不会通过“读取一个当前值”猜出量程;普通命令、数组或未知描述不保证适配。页面无需自己判断这些类型。
|
|
164
|
+
|
|
165
|
+
Write point 改动时 Feedback point 跟随;单独改 Feedback point 不反向改 Write point。只读模式没有 Write point。编号选择默认 Zero-based;实际保存优先采用目录里的 `.onebased`,目录未提供时才采用用户选择。它不是对网页音量值加减 1,也不是驱动 `protocolChannel` 的编号。
|
|
166
|
+
|
|
167
|
+
量程适配只发生在网页侧:有效的页面 [uiMin, uiMax] 与驱动 [driverMin, driverMax] 之间线性换算,反馈反向换算,不改驱动代码或协议。两边都必须是有限数字且 max > min;缺少有效量程时原值传递,不能假定自动缩放。整数设备目标会取整;回到网页时按页面 step 对齐。Write/Feedback 使用不同量程时分别换算,部署人员应确认二者属于同一逻辑控制量。
|
|
168
|
+
|
|
169
|
+
## 7. 排查顺序
|
|
170
|
+
|
|
171
|
+
| 现象 | 先检查 |
|
|
172
|
+
| --- | --- |
|
|
173
|
+
| 控件不能选中 | 是否在支持清单、是否有事件指令/复杂模型/v-for、自闭合 Vuetify、重复 ID;阅读编辑页给出的拒绝原因。 |
|
|
174
|
+
| 有设备但点位为空 | 在线状态、参数目录、值类型与 ro/wo/rw 权限;不是所有设备命令都属于可绑定参数。 |
|
|
175
|
+
| 草稿有紫框但普通页无效 | Apply 是否成功、模式和点位是否正确、桥接是否同步、MUSE 是否加载预期模块版本。 |
|
|
176
|
+
| 滑杆动不了 | 是否仍在 Select 编辑模式、模型类型是否正确、是否存在手写回写逻辑;不要先改驱动。 |
|
|
177
|
+
| 页面变化但设备不变 | 写映射和驱动收发日志。前端变化不证明命令到达设备。 |
|
|
178
|
+
| 设备已变但页面旧值 | Feedback point、事件/读取是否有新反馈。节点不能凭写入成功制造真实设备反馈。 |
|
|
179
|
+
| 原生与 Vuetify 表现不同 | 是否符合各自模型、事件和量程约束;不要给绑定控件额外增加发送监听。 |
|
|
180
|
+
|
|
181
|
+
隔离测试只验证软件逻辑,不能证明真实设备闭环、MUSE 网络或驱动版本都正确。
|
|
67
182
|
|
|
68
|
-
##
|
|
183
|
+
## 8. 给 AI 的交付检查
|
|
69
184
|
|
|
70
|
-
-
|
|
71
|
-
-
|
|
72
|
-
-
|
|
73
|
-
-
|
|
74
|
-
-
|
|
185
|
+
- 输出可粘贴的 Template 组件片段,不输出 Node-RED Flow、设备路径、绑定 JSON 或账号密码。
|
|
186
|
+
- 所有待绑定控件位于支持清单,使用稳定唯一 ID;数据模型简单、独立且类型正确。
|
|
187
|
+
- 页面量程明确、数值常量有效;下拉菜单选项与实际设备值对应。
|
|
188
|
+
- 没有手写 `data-cp-*`、旧发送接口、设备通信、绑定控件事件或复杂模型。
|
|
189
|
+
- Vuetify 标签成对闭合;不把自动绑定目标放入 v-for。
|
|
190
|
+
- 保留正常的视觉与纯前端交互;不为了绑定重写驱动或添加第二套控制逻辑。
|
|
191
|
+
- 不声称任意普通页面都可无条件自动绑定;不支持的组件要明确说明,不能伪造兼容写法。
|
package/README.md
CHANGED
|
@@ -18,6 +18,16 @@ Use paired Vuetify tags in Dashboard templates, such as `<v-slider></v-slider>`;
|
|
|
18
18
|
|
|
19
19
|
After clicking Apply in the page binding editor, the package validates the mapping, backs up the active Flow, generates the native bridge for that panel, and deploys the updated Flow in one operation. The bridge revision must match the saved mapping before a command can reach a device. A Node-RED simulation verifies software behavior, while live device operation requires a separate check.
|
|
20
20
|
|
|
21
|
+
The current runtime has one panel-scoped control path. The retired topic-based region gateway, fixed demo scenes, hand-written control tags, and `sendControl` authoring API are not supported. The page editor and server use the same target compatibility rules.
|
|
22
|
+
|
|
21
23
|
## Installation
|
|
22
24
|
|
|
23
25
|
Install this package in the Node-RED user directory or declare it in the active project `package.json`. Installing on the Mac does not install the package on a MUSE controller.
|
|
26
|
+
|
|
27
|
+
The binding editor uses the running Node-RED server's loopback port and admin root to read the AMX device catalog, on both desktop Automator and MUSE. No desktop-only catalog environment variable is required. The AMX catalog routes must be available on that runtime.
|
|
28
|
+
|
|
29
|
+
Node.js 18.16.0 or newer is required. Dashboard 2 and the AMX Command/Status/Event nodes must be installed separately. The active Node-RED project must declare its Flow and credentials filenames; its user directory must be writable for private backups. Authenticated LAN users can Apply; Flow administration itself stays on loopback. Direct TLS or an authenticated Node-RED admin API needs an additional server-side transport adapter and is not configured automatically.
|
|
30
|
+
|
|
31
|
+
The release includes `npm-shrinkwrap.json` to install the tested dependency versions. Deploy/Push must reference the intended registry version or a controller-accessible tarball; a `file:` dependency pointing at a Mac path is not portable to MUSE. Restart the controller runtime after replacing module code.
|
|
32
|
+
|
|
33
|
+
For development, run `node --test test-*.cjs`. The isolated integration test uses the executing Node version; `CP_TEST_NODE` and `CP_TEST_PANEL_PACKAGE` can select an alternate runtime and unpacked release package.
|
package/control-catalog.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
const { schemaVersion } = require('./control-type-contract.json')
|
|
2
2
|
|
|
3
|
-
function catalogBase () {
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
function catalogBase (settings) {
|
|
4
|
+
const port = Number(settings?.uiPort)
|
|
5
|
+
const runtimeBase = settings?.httpAdminRoot !== false &&
|
|
6
|
+
Number.isInteger(port) && port > 0 && port <= 65535
|
|
7
|
+
? `http://127.0.0.1:${port}${settings?.httpAdminRoot || '/'}` : ''
|
|
8
|
+
const raw = process.env.CONTROL_PANEL_CATALOG_BASE_URL || runtimeBase
|
|
8
9
|
if (!raw) return null
|
|
9
10
|
const url = new URL(raw)
|
|
10
11
|
if (url.protocol !== 'http:' || !['127.0.0.1', 'localhost'].includes(url.hostname) || url.username || url.password) {
|
|
11
|
-
throw new Error('
|
|
12
|
+
throw new Error('Catalog URL must be an unauthenticated localhost HTTP URL')
|
|
12
13
|
}
|
|
13
14
|
return url.href.endsWith('/') ? url.href : `${url.href}/`
|
|
14
15
|
}
|
package/control-panel.js
CHANGED
|
@@ -66,7 +66,7 @@ function getStateStore (RED) {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
module.exports = function (RED) {
|
|
69
|
-
const base = catalogBase()
|
|
69
|
+
const base = catalogBase(RED.settings)
|
|
70
70
|
const activePanels = new Map()
|
|
71
71
|
const activeEditors = new Map()
|
|
72
72
|
RED.httpNode.use((req, res, next) => {
|
|
@@ -101,7 +101,7 @@ module.exports = function (RED) {
|
|
|
101
101
|
}
|
|
102
102
|
})
|
|
103
103
|
RED.httpAdmin.post('/ui-control-panel/catalog/targets', RED.auth.needsPermission('nodes.read'), async (req, res) => {
|
|
104
|
-
if (!base) return res.status(404).json({ error: '
|
|
104
|
+
if (!base) return res.status(404).json({ error: 'Device catalog is unavailable' })
|
|
105
105
|
try {
|
|
106
106
|
res.json({ targets: await listCatalogTargets(base, req.body?.device) })
|
|
107
107
|
} catch (error) {
|
|
@@ -154,7 +154,6 @@ module.exports = function (RED) {
|
|
|
154
154
|
password: node.credentials?.password,
|
|
155
155
|
room: boundRoom,
|
|
156
156
|
prefix: endpoint,
|
|
157
|
-
boundOnly: true,
|
|
158
157
|
secureCookie: config.secureCookie === true,
|
|
159
158
|
restoredSessions: takeSessions(gatewayHandoffs, node.id, gatewayAuthKey),
|
|
160
159
|
onBoundCommand: (message, identity) => {
|
|
@@ -185,7 +184,8 @@ module.exports = function (RED) {
|
|
|
185
184
|
password: node.credentials?.editorPassword,
|
|
186
185
|
secureCookie: config.secureCookie === true,
|
|
187
186
|
restoredSessions: takeSessions(editorHandoffs, node.id, editorAuthKey),
|
|
188
|
-
getConfig: async req => editorConfig(await getSnapshot(req.socket.localPort
|
|
187
|
+
getConfig: async req => editorConfig(await getSnapshot(req.socket.localPort, fetch,
|
|
188
|
+
RED.settings.httpAdminRoot ?? '/'), node.id),
|
|
189
189
|
getDevices: async () => {
|
|
190
190
|
if (!base) throw new Error('Device catalog unavailable')
|
|
191
191
|
return listCatalogDevices(base)
|
|
@@ -200,8 +200,8 @@ module.exports = function (RED) {
|
|
|
200
200
|
templateHash: body.templateHash,
|
|
201
201
|
entries: body.entries,
|
|
202
202
|
userDir: RED.settings.userDir,
|
|
203
|
-
remoteAddress: req.socket.remoteAddress,
|
|
204
203
|
localPort: req.socket.localPort,
|
|
204
|
+
adminRoot: RED.settings.httpAdminRoot ?? '/',
|
|
205
205
|
catalogBase: base
|
|
206
206
|
})
|
|
207
207
|
})
|