node-red-contrib-room-tools 1.8.4 → 1.8.9

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.
@@ -1,41 +1,107 @@
1
- # ControlPanel 前端页面编写规范
1
+ # ControlPanel 前端页面编写指南
2
2
 
3
- 本文适用于当前 `ui-control-panel` 的可视化绑定版本。给 AI 编写页面前先阅读本文:页面作者只负责普通 Dashboard 2 / Vue 页面和控件状态,不编写设备协议、驱动路径、绑定表或 ControlPanel 专用 API。
3
+ 适用基线:`node-red-contrib-room-tools@1.8.9` 的 `ui-control-panel`,Dashboard 2,Vue Options API。本文按当前代码审核,不表示支持任意 Vue、Vuetify 或第三方组件。
4
4
 
5
- ## 页面交付与绑定流程
5
+ 给 AI 编写页面前阅读本文。目标是:页面作者只写普通前端页面;部署人员在网页编辑模式选择设备与点位;ControlPanel 保存映射并负责设备命令、反馈和网页侧量程换算。
6
6
 
7
- 1. 在 `ui-control-panel` 的 Template 中粘贴 Vue 单文件组件式页面(`<template>`、可选的 `<script>` 和 `<style>`)。节点选择 Dashboard 页面,填写 `Room ID`、普通页面账号及独立的编辑账号,保存并 Deploy。
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
- 绑定时设备必须在线,且 Automator 目录能返回该设备的参数。已有绑定会保存在 Flow 中;Automator 启动时不靠设备在线才能显示已保存的页面。可绑定点位按目录中的参数类型、枚举、量程和读写权限筛选;不是任意设备命令都可绑定。
9
+ 页面作者使用普通 HTML、CSS 和 Vue 编写布局、样式、导航、控件及其本地状态。无需知道驱动协议、点位路径、内部 Control ID 或绑定表结构。标准 HTML `id`、Vue `v-model` 和 ARIA 属性不是 ControlPanel 专用标签。
13
10
 
14
- ## 页面代码的基本约定
11
+ 节点负责登录界面、普通与编辑权限、控件识别、映射保存、桥接生成、命令发送和反馈更新。页面不调用这些内部服务,不实现第二套设备通信。
15
12
 
16
- - 使用普通 HTML、CSS 和 Vue 编写布局、导航、场景展示和页面内部交互。需要设备绑定的控件只维护普通的控件值;发送、反馈、登录、范围换算由节点处理。
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
- | 用途 | 原生 HTML | Vuetify | 模型与限制 |
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
- | 连续数值控制 | `<input type="range">`、`<input type="number">` | `v-slider` | 数字;滑杆用简单 `v-model`,允许 `.number`。`min`、`max`、`step` 只写数字常量(包括 `:max="100"`),不要绑定动态字段。 |
28
- | 布尔控制 | `<input type="checkbox">`、`<button aria-pressed="false">`、`<button role="switch" aria-checked="false">` | `v-switch`、`v-checkbox` | Vuetify 使用布尔 `v-model`;不支持自定义 `true-value` / `false-value`、`multiple` 或值数组。语义按钮由节点更新状态并处理点击。 |
29
- | 单选枚举 | `<select>` | `v-select`、`v-radio-group` | Vuetify 使用字符串或数字的简单 `v-model`,选项值与驱动枚举值一致;可用 `.number`。不支持多选、`return-object`、`clearable`;单选项不要自带发送事件。 |
30
- | 文本控制 | `<input type="text">`、`<textarea>` | `v-text-field`、`v-textarea` | Vuetify 使用字符串 `v-model`;在 change 或失焦时提交,`v-text-field` 也支持 Enter。不支持数字型 `v-text-field` 或 `.number`。 |
31
- | 只读数值反馈 | `<meter>`、`<progress>` | `v-progress-linear`、`v-progress-circular` | 只能绑定读点位。Vuetify 用数字 `:model-value="progress"` 或 `v-model="progress"`;linear 可用固定正数 `max`,circular 固定 0–100;不支持 indeterminate/clickable。 |
32
- | 只读文本反馈 | `<span>`、`<strong>`、`<b>`、`<output>`、`<h1>`、`<h2>`、`<h3>` | 暂无自动适配组件 | 元素内使用单个顶层字段插值,如 `{{ status }}`;也可用纯文本。只可绑定读点位,不支持拼接或格式化表达式。 |
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
- 原生控件的简单顶层 `v-model` 也可用。Vuetify 交互控件必须有简单 `v-model`;其模型的初始类型要与表格一致。原生 `meter`/`progress` 默认量程为 0–1;原生 range 默认 0–100。为可读性和准确换算,数值控件尽量显式声明页面量程。
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
- 原生滑杆可按常规 Vue 写法使用,例如 `<input id="room-light" type="range" min="0" max="100" step="1" v-model.number="light">`,并在 `data()` 中将 `light` 初始化为数字。拖动产生的发送和设备反馈由 ControlPanel 处理;不要为了绑定再添加手写的 `input`/`change` 发送监听、定时器或强制回写滑杆值。若滑杆能拖动但设备无变化,或设备已变化而页面显示旧值,应分别检查绑定、设备收发和反馈的新鲜度,不要在页面里补第二套设备逻辑。
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
- muted: false,
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
- 其中 `sources` 的值仅是示例,实际枚举值要与所选驱动点位一致。控件可以先写好页面,设备与点位留到可视化绑定编辑页选择。
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 binding editor`,用编辑账号登录。Select 默认激活,可以点击、右键或触屏长按可绑定控件。`Open page` 打开普通控制页面,使用普通用户账号登录。
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
- - Write point 改动时,Feedback point 默认跟随;之后可单独指定反馈点位。新绑定默认 Zero-based;必须按实际设备协议确认。只读控件没有 Write point。
72
- - 编辑页只展示当前控件类型、值类型和读写权限兼容的参数。设备列表为空时,先检查设备在线和 Automator 参数目录,不能靠前端 HTML 使离线设备的点位出现。
73
- - 一个页面元素只能绑定一个映射。`v-for`、无法唯一定位的重复元素、复杂模型、动态量程、既有发送处理器及不在上表中的组件不会被自动接管。
74
- - Apply 成功才算映射及桥接部署完成。桥接版本与保存的映射不一致时命令会被拒绝。先验证登录、绑定、页面反馈,再检查实际设备收发;本地模拟测试不等于真实设备闭环。
185
+ - 输出可粘贴的 Template 组件片段,不输出 Node-RED Flow、设备路径、绑定 JSON 或账号密码。
186
+ - 所有待绑定控件位于支持清单,使用稳定唯一 ID;数据模型简单、独立且类型正确。
187
+ - 页面量程明确、数值常量有效;下拉菜单选项与实际设备值对应。
188
+ - 没有手写 `data-cp-*`、旧发送接口、设备通信、绑定控件事件或复杂模型。
189
+ - Vuetify 标签成对闭合;不把自动绑定目标放入 v-for。
190
+ - 保留正常的视觉与纯前端交互;不为了绑定重写驱动或添加第二套控制逻辑。
191
+ - 不声称任意普通页面都可无条件自动绑定;不支持的组件要明确说明,不能伪造兼容写法。
package/README.md CHANGED
@@ -4,7 +4,7 @@ This package provides `ui-control-panel` for Node-RED and MUSE Automator.
4
4
 
5
5
  ## ControlPanel
6
6
 
7
- The page author writes ordinary HTML/CSS/Vue and pastes it into the `ui-control-panel` Template. After configuring a Dashboard page, room ID, control login, and separate editor login, save and Deploy. Reopen the node and select **Open page binding editor**. Sign in with the editor account, right-click a supported control, choose its device points, and Apply.
7
+ The page author writes ordinary HTML/CSS/Vue and pastes it into the `ui-control-panel` Template. After configuring a Dashboard page, room ID, control login, and separate editor login, save and Deploy. Reopen the node and select **Open binding editor**. Sign in with the editor account, right-click a supported control, choose its device points, and Apply. **Open page** opens the normal control page with its separate user login.
8
8
 
9
9
  Both logins work over HTTP on a trusted local network. HTTPS is recommended for other networks because HTTP transmits credentials without encryption. Session cookies are marked `Secure` when the connection uses HTTPS.
10
10
 
@@ -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.
@@ -1,14 +1,15 @@
1
1
  const { schemaVersion } = require('./control-type-contract.json')
2
2
 
3
- function catalogBase () {
4
- const localAutomator = process.platform === 'darwin' &&
5
- process.execPath.includes('/MUSE Automator.app/Contents/MacOS/MUSE Automator')
6
- const raw = process.env.CONTROL_PANEL_POC_CATALOG_BASE_URL ||
7
- (localAutomator ? 'http://127.0.0.1:48080/red/' : '')
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('PoC catalog URL must be an unauthenticated localhost HTTP URL')
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
  }
@@ -229,9 +229,11 @@
229
229
  })
230
230
 
231
231
  $('#control-binding-auth').show()
232
- $('#control-visual-open').on('click', function () {
232
+ const openDashboardPage = (editMode) => {
233
+ const scope = $templateScope.val() || that.templateScope || 'local'
233
234
  const group = RED.nodes.node($('#node-input-group').val() || that.group)
234
- const page = RED.nodes.node($('#node-input-page').val() || that.page || group?.page)
235
+ const pageId = scope === 'local' ? group?.page : ($('#node-input-page').val() || that.page)
236
+ const page = RED.nodes.node(pageId)
235
237
  const base = RED.nodes.node(page?.ui)
236
238
  if (!page?.path) {
237
239
  RED.notify('Choose a Dashboard page, then save and deploy this node.', { type: 'warning' })
@@ -239,9 +241,11 @@
239
241
  }
240
242
  const url = new URL((base?.path || '/dashboard').replace(/\/$/, '') + '/' +
241
243
  page.path.replace(/^\//, ''), window.location.origin)
242
- url.searchParams.set('cp-edit', that.id)
244
+ if (editMode) url.searchParams.set('cp-edit', that.id)
243
245
  window.open(url.href, '_blank', 'noopener')
244
- })
246
+ }
247
+ $('#control-page-open').on('click', () => openDashboardPage(false))
248
+ $('#control-visual-open').on('click', () => openDashboardPage(true))
245
249
  RED.library.create({
246
250
  url: 'uitemplates', // where to get the data from
247
251
  type: 'ui-template', // the type of object the library is for
@@ -350,7 +354,7 @@
350
354
  <p>普通 HTML/Vue 控件由页面绑定编辑器自动识别,并通过面板的 authenticated session 发送控制。开发者不需要添加标签、调用自定义节点 API,或在 Template 中实现 throttling、latest-wins、队列和设备协议。</p>
351
355
  <p>页面只需按普通 H5/Vue 方式编写控件;编辑模式下在控件上打开绑定菜单,选择设备参数即可。登录、发送、反馈和状态同步由节点负责,Template 不应主动查询设备或把反馈回发成命令。</p>
352
356
  <p>See <code>CONTROL_PANEL_AI_GUIDE.md</code> in the node package for the complete AI authoring contract and examples.</p>
353
- <p>Save and Deploy the panel, then use Open page binding editor. Apply in that page validates and saves bindings, backs up the Flow, regenerates the native bridge, and deploys it. Ordinary Deploy alone does not apply bindings. Check targets and numbering before using real hardware.</p>
357
+ <p>Save and Deploy the panel, then use Open binding editor. Apply in that page validates and saves bindings, backs up the Flow, regenerates the native bridge, and deploys it. Ordinary Deploy alone does not apply bindings. Check targets and numbering before using real hardware.</p>
354
358
  </script>
355
359
 
356
360
  <script type="text/html" data-template-name="ui-control-panel">
@@ -428,10 +432,10 @@
428
432
  <div style="height:250px; min-height:100px" class="node-text-editor" id="node-input-format-editor" ></div>
429
433
  </div>
430
434
  <div class="form-row">
431
- <label>Page binding</label>
435
+ <label>Page</label>
432
436
  <input type="hidden" id="node-input-visualBindings">
433
- <button type="button" id="control-visual-open" class="red-ui-button">Open page binding editor</button>
434
- <span class="control-binding-hint">Use ordinary HTML/Vue controls. Binding is configured from the page.</span>
437
+ <button type="button" id="control-page-open" class="red-ui-button">Open page</button>
438
+ <button type="button" id="control-visual-open" class="red-ui-button">Open binding editor</button>
435
439
  </div>
436
440
  <div class="form-row" style="display:none">
437
441
  <input type="hidden" id="node-input-bridgeRevisionApplied">
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: 'Local PoC catalog is disabled' })
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), node.id),
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
  })