跳转到内容

自建面板(dashboard)

rha 没有内置 UI。它提供的是一块画布:你(或者你的 agent)用任意 HTML/CSS/JS 画一个 目录,传上去,daemon 原样端出来,再给页面一条通往 /api/* 的认证路径。仅此而已——不解析 HTML、不校验布局、不限制技术栈。

这是取舍不是欠账。任何一层「描述语言」都会给页面能长成什么样封顶,而封顶的位置就是那套 语言的表达力。所以这里没有那一层。

配套的 Claude skill 在 skills/rha-dashboard/:让 agent 现场画一个面板时,把它挂上,下面 这些坑它就都知道了。

面板名只能是 [a-z0-9_-],最长 64 字符(它是 URL 的一段,不是 TOML 键,所以允许 -; 不允许大写是为了躲开大小写不敏感文件系统上两个名字撞成一个目录)。

Terminal window
rha dashboard push bedroom ./dash # 上传/替换
rha dashboard list # 列出全部
rha dashboard rollback bedroom # 回到上一版
rha dashboard rm bedroom # 删掉

本地目录长这样,入口必须叫 index.html:

dash/
index.html
style.css
app.js
vendor/chart.umd.js ← 第三方库自己带进来

push 在本机就先查三件事,省得等服务端报错:有没有 index.html、有没有符号链接、打包后 超没超大小上限。它还会自动跳过 node_modules/ 和 .git/。

打包姿势:-C ./dash .,不是 ./dash

Section titled “打包姿势:-C ./dash .,不是 ./dash”

CLI 内部走的就是下面这个端点,不用 rha 的工具也能直接传:

Terminal window
tar cf - -C ./dash . | curl -X PUT --data-binary @- \
-H 'Content-Type: application/x-tar' -H "Authorization: Bearer $RHA_TOKEN" \
http://192.168.52.40:8420/api/dashboards/bedroom

注意是 -C ./dash .(进到目录内部打包),不是 tar cf - ./dash——后者会让归档里每个 路径都多一层 dash/,服务端就找不到 index.html 了。报错信息会点名这个坑,但最好一开始 就不踩。

服务端只收未压缩的 tar,tar czf 出来的 gzip 流会被直接拒。

没有本地磁盘目录时用 MCP 工具 upload_dashboard(name, files),files 是 [{path, text}],crate 内部会替你打成 tar。它只收文本——图片、字体、wasm 这类二进制 资源传不了,遇到就改用 rha dashboard push。见 MCP 那页。

限制 值 什么时候撞上
整个上传 8 MiB 误把整个项目目录而不是构建产物传了进去
条目数(文件 + 目录都算) 512 整目录复制某个前端库的 dist/
路径层数 8 基本撞不上
单个路径长度 255 字符 基本撞不上

路径段只能是 [A-Za-z0-9._-],不能以 . 开头、不能含 ..。目录里不能有符号链接 ——服务端只接受普通文件和普通目录,一个指向 /etc/rha/rha.toml 的链接就等于把 token 发 出去了。别把 sourcemap 一起传,node_modules/ 和 .git/ 走 CLI 会自动跳过,sourcemap 不会。

引一行就有 window.rha:

<script src="/_rha/sdk.js"></script>

五个方法,全部返回 Promise,出错抛异常:

await rha.devices() // [{name, label, manufacturer, model, entity_ids}]
await rha.entities() // 全部实体, 字段同 /api/entities
await rha.entity('fan.power')
await rha.set('plug_bedroom.switch_s2_on_p1', true)
await rha.history('thermo.temperature', {hourly: true, from: since})

它极薄,只做三件事:带上凭据、拆 {ok, data, error} 信封、错误变异常。不做轮询、状态 管理、DOM、渲染、缓存、重连。要多实时自己写:

setInterval(async () => render(await rha.entities()), 2000)

一个 setInterval 封装只有二十行,但把它放进 SDK 就意味着 SDK 开始对「页面该怎么组织」 有意见了。

别按用户的口头描述猜实体 id,先调 entities() / devices() 拿真东西。

  • class —— 这是什么。outlet / light / fan / switch 是主开关(值是 bool); speed / swing / lock 是依附在某台可控设备上的附属量(转速档位 / 摆头 / 童锁,同样 可写,speed 的值是整数或字符串档位不是 bool);temperature / humidity / battery / power / weight 是只读读数;button 是瞬时事件。
  • domain —— 数值的取值域。{"enum":[1,2,3,4]} 画四档,{"range":{"min":0,"max":100}} 画滑条。没有它就别画滑条:只看 value_type 是 int 会画出一个能拖到 87 的风扇档位 条,而设备只认 1/2/3/4。
  • kind —— switch 可写,sensor 只读,trigger 是瞬时事件。

history 的选项是 {limit, hourly, from, to},默认窗口是最近 24 小时(to 默认现在, from 默认 to - 24h),limit 默认 500、上限 5000。

1. limit 截的是窗口里最早的 N 条,不是最近的 N 条。 底层 SQL 是 ORDER BY ts ASC LIMIT。插座功率这种 15 秒一个点的实体,24 小时有五千多个点,用默认参数 拿到的是一天前的那 500 个——曲线停在十几个小时前,而页面看上去毫无异常。取近况一律用 from 划窗口,别靠 limit。

2. hourly: true 时 limit 完全无效。 那条查询走 GROUP BY,压根不接 limit。要控 制点数就调 from。长窗口(≥12 小时)一律用 hourly,它回的是 {bucket, avg, min, max, count},把 min/max/count 放进 tooltip 比画三条线清楚。hourly 只对数值实体有效,非数值的会报 entity ... is not numeric。

没开 [storage] 时 history 整个报错(503),不是返回空数组。

认证模型:cookie 能开关设备,但改不了规则

Section titled “认证模型:cookie 能开关设备,但改不了规则”

面板页面用一张签名 cookie 调读接口,写接口仍然只认 Bearer。边界是这么切的:

认 cookie 只认 Authorization: Bearer
/d/*(面板页面本身) /api/rules*(改自动化规则)
/_rha/sdk.js /api/reload
/api/entities*(含 set) /api/dashboards/*(传/删/回滚面板文件)
/api/devices

cookie 是无状态的(HMAC 签名里带过期时间,服务端不存任何东西),所以 daemon 重启后它 仍然有效——服务端 session 表会在每次升级重启时把全家人一起踢下线,那对一个天天用的面板很 烦。副作用要说清楚:改 RHA_TOKEN 会让所有已发的 cookie 立刻失效,得重新用带 token 的链接打开一次。换钥匙就该换锁。

有效期 30 天,期间打开过会自动续期(剩余不足四分之一时续一张新的)。大约一个月不打开会 过期,到期后再打开会看到一坨裸 JSON——那不是故障,用下面带 token 的链接重开一次就好。

手机浏览器没地方加 Authorization header,所以 /d/* 多认一条 URL 参数。带 token 打开一 次就换到 cookie,之后直接开不带 token 的地址即可(可以添加到主屏幕):

http://192.168.52.40:8420/d/bedroom/?token=<RHA_TOKEN>

通过后立刻种 cookie 并 302 掉 URL 里的 token(其它 query 参数原样保留——面板是纯静态 页面,会指望 location.search 里那些参数还在),所以 token 只在浏览器历史里留一次。

1. 只用相对路径引本地资源(<link href="style.css">),别写 /style.css。 /d/{名字}(不带尾斜杠)会 302 到 /d/{名字}/——浏览器解析相对引用的基准是「当前 URL 去 掉最后一段」,只有停在带尾斜杠的 /d/{名字}/ 上,style.css 才会解析成 /d/{名字}/style.css 而不是 /d/style.css。所以你告诉用户的链接要写带尾斜杠那种形式。

2. 第三方库自带进 vendor/,不引 CDN。 rha 装在局域网里,断网时 CDN 会白屏。

3. 入口必须叫 index.html,服务端强制,缺了直接 400。

还有一条不用你操心但值得知道:SDK 的 API 前缀是从自己的 src 反推的,索引页和那条 302 的 Location 也都用相对形式——所以整个 daemon 挂在反向代理的子路径下(比如 /rha/)时, 面板不用改一个字。

/d/(只有尾斜杠、没有名字)是 daemon 生成的极简索引页,列出当前所有面板。它是基础设施 不是内容,所以不走 agent。