rovloom 0.5.0 → 0.6.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 (64) hide show
  1. package/README.md +9 -1
  2. package/dist/src/admin/index.js +1 -1
  3. package/dist/src/{chunk-emcefrc1.js → chunk-0e9gg4sg.js} +3 -4
  4. package/dist/src/chunk-1xer1ent.js +102 -0
  5. package/dist/src/{chunk-863tk45b.js → chunk-41fny9qm.js} +5 -5
  6. package/dist/src/{chunk-zjzrcc59.js → chunk-59h200cr.js} +139 -7
  7. package/dist/src/chunk-5w5srj3b.js +93 -0
  8. package/dist/src/{chunk-d4ejrtjq.js → chunk-6n6b1xpy.js} +1 -1
  9. package/dist/src/{chunk-ympzv86c.js → chunk-6w4g9fw8.js} +16 -4
  10. package/dist/src/{chunk-smpevv7n.js → chunk-73aq2qkz.js} +9 -16
  11. package/dist/src/{chunk-0ake7e4r.js → chunk-8c9yc0ge.js} +9 -17
  12. package/dist/src/{chunk-57ayxqnb.js → chunk-8fy8w7ds.js} +2 -23
  13. package/dist/src/{chunk-64ftwe9b.js → chunk-8ykrqhxh.js} +8 -2
  14. package/dist/src/{chunk-2tn0r64b.js → chunk-9h9x79bx.js} +5 -5
  15. package/dist/src/{chunk-nyn75b8v.js → chunk-av31bx1e.js} +31 -2
  16. package/dist/src/{chunk-0xgpxdgc.js → chunk-bbdtsg4b.js} +7 -5
  17. package/dist/src/{chunk-q5pgbwb3.js → chunk-e7jgpxkf.js} +2 -2
  18. package/dist/src/{chunk-zj48jvfe.js → chunk-efty77sb.js} +3 -40
  19. package/dist/src/{chunk-abd4envz.js → chunk-enj7s8cv.js} +71 -78
  20. package/dist/src/{chunk-4etdq0cp.js → chunk-ftpy8xx3.js} +313 -28
  21. package/dist/src/{chunk-zdxww7ms.js → chunk-h3xyq7zk.js} +1 -1
  22. package/dist/src/{chunk-ttsrctxg.js → chunk-hxmq28pg.js} +9 -7
  23. package/dist/src/{chunk-r0nfthsf.js → chunk-j59ac0xs.js} +2 -2
  24. package/dist/src/{chunk-5s2emvww.js → chunk-j6svdqr8.js} +10 -302
  25. package/dist/src/{chunk-9y36gntb.js → chunk-jkzfck98.js} +10 -5
  26. package/dist/src/{chunk-sj24fpz3.js → chunk-m76cmkfy.js} +28 -30
  27. package/dist/src/{chunk-5n8csngz.js → chunk-m96mbzcw.js} +816 -305
  28. package/dist/src/{chunk-xhq21g24.js → chunk-m9c1pnv9.js} +3 -3
  29. package/dist/src/{chunk-fnakrk0a.js → chunk-n3z3jaxy.js} +39 -36
  30. package/dist/src/{chunk-e7x51y0t.js → chunk-ndj0ay2p.js} +19 -18
  31. package/dist/src/chunk-pgs9jp6t.js +320 -0
  32. package/dist/src/chunk-pksaqt9a.js +25 -0
  33. package/dist/src/{chunk-ebm3kvnq.js → chunk-pnep7yhj.js} +31 -100
  34. package/dist/src/chunk-qn5xae7g.js +66 -0
  35. package/dist/src/{chunk-0pewfj89.js → chunk-qp8tgms2.js} +1 -1
  36. package/dist/src/{chunk-7k9g0x5e.js → chunk-rmgw8h27.js} +10 -8
  37. package/dist/src/{chunk-gbsbwssz.js → chunk-sf4ztxba.js} +3 -3
  38. package/dist/src/{chunk-7q96e161.js → chunk-shez7hkc.js} +6 -12
  39. package/dist/src/chunk-sqz614bh.js +30 -0
  40. package/dist/src/{chunk-09f9q26d.js → chunk-vdwzb63d.js} +2 -2
  41. package/dist/src/chunk-xtvgm6kf.js +48 -0
  42. package/dist/src/chunk-y8zm75jt.js +157 -0
  43. package/dist/src/chunk-yasqw2vj.js +756 -0
  44. package/dist/src/{chunk-ybte82d3.js → chunk-z0qmfqvd.js} +4 -3
  45. package/dist/src/chunk-zm705snh.js +53 -0
  46. package/dist/src/cli/index.js +39 -30
  47. package/dist/src/cli/main.js +14 -9
  48. package/dist/src/connector/herdr/installation.js +4 -4
  49. package/dist/src/connector/herdr/provider.js +9 -9
  50. package/dist/src/connector/herdr/runtime.js +7 -7
  51. package/dist/src/connector/luvus/index.js +7 -6
  52. package/dist/src/mcp/index.js +16 -14
  53. package/dist/src/runtime/index.js +16 -15
  54. package/dist/src/service/index.js +3 -4
  55. package/dist/src/store.js +4 -4
  56. package/dist/src/transport/index.js +9 -9
  57. package/dist/src/webui/index.js +975 -834
  58. package/package.json +1 -1
  59. package/dist/src/chunk-5j0gj6w2.js +0 -7
  60. package/dist/src/chunk-9yg6x3gt.js +0 -24
  61. package/dist/src/chunk-erdq0rn8.js +0 -1300
  62. package/dist/src/chunk-n073xnr1.js +0 -1117
  63. package/dist/src/chunk-rwpynx2j.js +0 -51
  64. package/dist/src/chunk-vwcntax3.js +0 -228
@@ -1,7 +1,4 @@
1
1
  // @bun
2
- import {
3
- DEFAULT_PAIRING_POLICY_REF
4
- } from "../chunk-5j0gj6w2.js";
5
2
  import {
6
3
  ReadError2,
7
4
  createSnapshotReader2,
@@ -13,6 +10,100 @@ import {
13
10
 
14
11
  // src/webui/i18n.ts
15
12
  var chinese = Object.freeze({
13
+ "resources.tokens": "MCP Token",
14
+ "resources.pairingCodes": "\u914D\u5BF9\u7801",
15
+ "resources.createCode": "\u751F\u6210\u914D\u5BF9\u7801",
16
+ "resources.codesNote": "\u4E00\u673A\u4E00\u7801\uFF1A\u751F\u6210\u540E\u628A\u52A0\u5165\u547D\u4EE4\u590D\u5236\u5230\u76EE\u6807\u673A\u5668\u6267\u884C\uFF0C\u5373\u5B8C\u6210\u914D\u5BF9\u3002\u672A\u4F7F\u7528\u7684\u7801\u53EF\u968F\u65F6\u4F5C\u5E9F\u3002",
17
+ "resources.codeCreatedNote": "\u5DF2\u751F\u6210\u3002\u590D\u5236\u4E0B\u9762\u7684\u547D\u4EE4\u5230 Connector \u673A\u5668\u8FD0\u884C\u5373\u53EF\uFF08\u7801\u53EA\u663E\u793A\u4E00\u6B21\u547D\u4EE4\u5F62\u6001\uFF0C\u5217\u8868\u4E2D\u53EF\u518D\u6B21\u590D\u5236\uFF09\u3002",
18
+ "resources.code_unused": "\u672A\u4F7F\u7528",
19
+ "resources.code_used": "\u5DF2\u4F7F\u7528",
20
+ "resources.code_revoked": "\u5DF2\u4F5C\u5E9F",
21
+ "resources.code_failed": "\u5931\u8D25",
22
+ "resources.revokeCode": "\u4F5C\u5E9F",
23
+ "resources.confirmRevokeCode": "\u4F5C\u5E9F\u8FD9\u5F20\u914D\u5BF9\u7801\uFF1F\u76EE\u6807\u8BBE\u5907\u9700\u751F\u6210\u65B0\u7801\u3002",
24
+ "resources.name": "\u540D\u79F0",
25
+ "resources.expiry": "\u6709\u6548\u671F",
26
+ "resources.days": "{count} \u5929",
27
+ "resources.create": "\u521B\u5EFA Token",
28
+ "resources.tokenNote": "\u4E3A AI \u5BA2\u6237\u7AEF\u521B\u5EFA Token\uFF0C\u53EF\u968F\u65F6\u67E5\u770B\u3001\u590D\u5236\u548C\u5220\u9664\u3002\u652F\u6301\u5F53\u524D\u548C\u4E4B\u540E\u914D\u5BF9\u7684\u8BBE\u5907\u3002",
29
+ "resources.endpoint": "MCP \u5730\u5740",
30
+ "resources.endpointNote": "\u8FDC\u7A0B\u8FDE\u63A5\u65F6\uFF0C\u5C06 127.0.0.1 \u6362\u6210 Gateway \u7684\u5730\u5740\u3002\u5728 AI \u5BA2\u6237\u7AEF\u7684 Authorization \u4E2D\u586B\u5199 Bearer \u52A0 Token\u3002",
31
+ "resources.expiresAt": "\u6709\u6548\u81F3 {time}",
32
+ "resources.active": "\u6709\u6548",
33
+ "resources.expired": "\u5DF2\u8FC7\u671F",
34
+ "resources.show": "\u663E\u793A",
35
+ "resources.hide": "\u9690\u85CF",
36
+ "resources.legacy": "\u65E7 Token \u4EC5\u4FDD\u5B58\u6458\u8981\uFF0C\u65E0\u6CD5\u8FD8\u539F\u3002",
37
+ "resources.reissue": "\u91CD\u65B0\u7B7E\u53D1",
38
+ "resources.confirmReissue": "\u91CD\u65B0\u7B7E\u53D1\u4F1A\u7ACB\u5373\u4F7F\u65E7 Token \u5931\u6548\uFF0C\u662F\u5426\u7EE7\u7EED\uFF1F",
39
+ "resources.delete": "\u5220\u9664",
40
+ "resources.deleteDevice": "\u5220\u9664\u8BBE\u5907",
41
+ "resources.deleteDeviceNote": "\u5220\u9664\u8BBE\u5907\u53CA\u5176\u8FD0\u884C\u914D\u7F6E\u3001\u914D\u5BF9\u8BB0\u5F55\u548C\u8C03\u7528\u5185\u5BB9\uFF0C\u5F53\u524D\u8FDE\u63A5\u4F1A\u65AD\u5F00\u3002",
42
+ "resources.confirmDevice": "\u5220\u9664\u8BE5\u8BBE\u5907\u53CA\u5176\u914D\u5BF9\u3001\u8C03\u7528\u8BB0\u5F55\uFF1F\u5982\u9700\u518D\u6B21\u8FDE\u63A5\uFF0C\u8BF7\u91CD\u65B0\u914D\u5BF9\u3002",
43
+ "resources.confirmToken": "\u5220\u9664 Token\u300C{name}\u300D\uFF1F\u4F7F\u7528\u5B83\u7684\u5BA2\u6237\u7AEF\u5C06\u65E0\u6CD5\u7EE7\u7EED\u8BBF\u95EE\u3002",
44
+ "resources.emptyTokens": "\u8FD8\u6CA1\u6709 MCP Token\u3002\u5728\u4E0A\u65B9\u521B\u5EFA\u4E00\u4E2A\uFF0C\u5373\u53EF\u8FDE\u63A5\u4F60\u7684 AI \u5BA2\u6237\u7AEF\u3002",
45
+ "resources.loading": "\u6B63\u5728\u52A0\u8F7D\u2026",
46
+ "resources.saving": "\u6B63\u5728\u4FDD\u5B58\u2026",
47
+ "resources.saved": "\u5DF2\u4FDD\u5B58",
48
+ "resources.failed": "\u64CD\u4F5C\u672A\u5B8C\u6210\uFF0C\u8BF7\u5237\u65B0\u540E\u91CD\u8BD5\u3002",
49
+ "resources.invalid": "\u8BF7\u4F7F\u7528\u4E0D\u91CD\u590D\u7684\u540D\u79F0\uFF08\u5B57\u6BCD\u3001\u6570\u5B57\u3001- \u6216 _\uFF09\uFF0C\u5E76\u9009\u62E9\u6709\u6548\u671F\u3002",
50
+ "nav.workspace": "\u5DE5\u4F5C\u7A7A\u95F4",
51
+ "nav.overview": "\u6982\u89C8",
52
+ "nav.devices": "\u8BBE\u5907\u7BA1\u7406",
53
+ "nav.pairing": "\u914D\u5BF9\u8BBE\u5907",
54
+ "nav.connect": "\u8FDE\u63A5 MCP",
55
+ "nav.resources": "\u5E2E\u52A9\u4E0E\u8D44\u6E90",
56
+ "nav.docs": "\u5FEB\u901F\u5F00\u59CB",
57
+ "nav.console": "\u7BA1\u7406\u63A7\u5236\u53F0",
58
+ "overview.eyebrow": "\u4F60\u7684\u5DE5\u4F5C\u7A7A\u95F4\uFF0C\u5C3D\u5728\u638C\u63E1",
59
+ "overview.title": "\u5DE5\u4F5C\u53F0",
60
+ "overview.description": "\u67E5\u770B\u7F51\u5173\u4E0E\u8BBE\u5907\u72B6\u6001\uFF0C\u8BA9\u8FDC\u7A0B\u5DE5\u4F5C\u6D41\u987A\u7545\u8FDE\u63A5\u3002",
61
+ "overview.visible": "\u672C\u9875\u8BBE\u5907",
62
+ "overview.visibleNote": "\u5F53\u524D\u6388\u6743\u8303\u56F4\u4E2D\u7684\u5217\u8868\u9875",
63
+ "overview.online": "\u8FDE\u63A5\u5DF2\u9A8C\u8BC1",
64
+ "overview.onlineNote": "\u8FDE\u63A5\u4E0E\u8EAB\u4EFD\u8BA4\u8BC1\u5747\u6709\u6548",
65
+ "overview.attention": "\u9700\u8981\u5173\u6CE8",
66
+ "overview.attentionNote": "\u672C\u9875\u5C1A\u672A\u9A8C\u8BC1\u8FDE\u63A5\u7684\u8BBE\u5907",
67
+ "overview.scope": "\u8BBE\u5907\u7EDF\u8BA1\u4EC5\u8986\u76D6\u5F53\u524D\u9875\uFF1B\u8FC7\u671F\u89C2\u6D4B\u4E0D\u4EE3\u8868\u5F53\u524D\u72B6\u6001\u3002",
68
+ "overview.store": "\u6570\u636E\u5B58\u50A8",
69
+ "overview.transport": "\u8BBE\u5907\u901A\u9053",
70
+ "overview.native": "\u539F\u751F\u8FD0\u884C\u73AF\u5883",
71
+ "overview.manage": "\u7BA1\u7406\u8BBE\u5907",
72
+ "overview.deviceSection": "\u5DF2\u8FDE\u63A5\u7684\u5DE5\u4F5C\u7A7A\u95F4",
73
+ "overview.deviceSubtitle": "\u4ECE\u4E00\u53F0\u8BBE\u5907\u5F00\u59CB\uFF0C\u5C06\u4F60\u7684\u5DE5\u5177\u548C\u5DE5\u4F5C\u533A\u8FDE\u63A5\u8D77\u6765\u3002",
74
+ "devices.search": "\u641C\u7D22\u5F53\u524D\u9875\u8BBE\u5907",
75
+ "devices.searchPlaceholder": "\u641C\u7D22\u8BBE\u5907\u540D\u79F0\u3001ID \u6216\u6807\u7B7E\u2026",
76
+ "devices.filterAll": "\u5168\u90E8",
77
+ "devices.filterOnline": "\u5DF2\u9A8C\u8BC1\u8FDE\u63A5",
78
+ "devices.filterAttention": "\u9700\u8981\u5173\u6CE8",
79
+ "devices.results": "\u5F53\u524D\u9875\u663E\u793A {shown} / {count} \u53F0\u8BBE\u5907",
80
+ "devices.noMatches": "\u6CA1\u6709\u5339\u914D\u7684\u8BBE\u5907",
81
+ "devices.noMatchesHint": "\u8BD5\u8BD5\u5176\u4ED6\u540D\u79F0\u3001ID \u6216\u6807\u7B7E\uFF0C\u6216\u5207\u6362\u72B6\u6001\u7B5B\u9009\u3002",
82
+ "devices.record": "\u89C2\u6D4B\u8BE6\u60C5",
83
+ "devices.open": "\u67E5\u770B {name} \u7684\u8BE6\u60C5",
84
+ "overview.available": "\u53EF\u7528",
85
+ "overview.degraded": "\u964D\u7EA7",
86
+ "overview.unavailable": "\u4E0D\u53EF\u7528",
87
+ "overview.unverified": "\u672A\u6838\u9A8C",
88
+ "pairing.pageTitle": "\u8FDE\u63A5\u4E00\u53F0\u65B0\u8BBE\u5907",
89
+ "pairing.pageDescription": "\u751F\u6210\u4E00\u673A\u4E00\u7801\uFF0C\u628A\u52A0\u5165\u547D\u4EE4\u590D\u5236\u5230\u76EE\u6807\u673A\u5668\u8FD0\u884C\u5373\u53EF\uFF1B\u6CA1\u6709\u7533\u8BF7\u3001\u6279\u51C6\u6216\u6838\u5BF9\u7801\u3002",
90
+ "pairing.loginRequired": "\u914D\u5BF9\u9700\u8981\u7BA1\u7406\u4F1A\u8BDD",
91
+ "pairing.loginHint": "\u4ECE Gateway \u7684\u7BA1\u7406\u5165\u53E3\u767B\u5F55\u540E\uFF0C\u5373\u53EF\u751F\u6210\u914D\u5BF9\u7801\u3002",
92
+ "connect.title": "\u628A\u5DE5\u4F5C\u7A7A\u95F4\u8FDE\u63A5\u5230 AI",
93
+ "connect.description": "\u8BBE\u5907\u5B8C\u6210\u914D\u5BF9\u540E\uFF0C\u4E3A AI \u5BA2\u6237\u7AEF\u7B7E\u53D1\u4E13\u7528 MCP Bearer\u3002",
94
+ "connect.stepOne": "\u627E\u5230\u5DF2\u6709\u8C03\u7528\u65B9",
95
+ "connect.stepOneNote": "\u5728 Gateway \u673A\u5668\u4E0A\u8FD0\u884C\uFF0C\u8BB0\u4E0B\u5217\u8868\u4E2D\u7684\u5B9E\u9645 caller ID\u3002",
96
+ "connect.stepTwo": "\u4E3A\u5BA2\u6237\u7AEF\u521B\u5EFA\u4EE4\u724C",
97
+ "connect.stepTwoNote": "\u628A EXISTING_CALLER_ID \u66FF\u6362\u4E3A\u4E0A\u4E00\u6B65\u7684 ID\u3002\u65B0\u4EE4\u724C\u4E5F\u53EF\u5728\u7BA1\u7406\u9762\u677F\u4E2D\u518D\u6B21\u67E5\u770B\u3002",
98
+ "connect.stepThree": "\u914D\u7F6E\u4F60\u7684 AI \u5BA2\u6237\u7AEF",
99
+ "connect.stepThreeNote": "\u9009\u62E9 Streamable HTTP\uFF0C\u586B\u5165 Gateway \u5B9E\u9645 MCP URL\uFF0C\u5E76\u7528\u7B7E\u53D1\u7684\u4EE4\u724C\u4F5C\u4E3A Bearer\u3002\u7BA1\u7406\u5458\u5BC6\u7801\u4EC5\u7528\u4E8E\u7BA1\u7406\u9875\u3002",
100
+ "connect.docker": "Docker \u90E8\u7F72\u547D\u4EE4",
101
+ "connect.docs": "\u67E5\u770B\u5B8C\u6574\u8FDE\u63A5\u6307\u5357",
102
+ "copy.action": "\u590D\u5236\u547D\u4EE4",
103
+ "copy.done": "\u5DF2\u590D\u5236",
104
+ "copy.failed": "\u672A\u80FD\u590D\u5236\uFF0C\u8BF7\u624B\u52A8\u9009\u62E9\u6587\u672C",
105
+ "empty.title": "\u4F60\u7684\u7B2C\u4E00\u53F0\u8BBE\u5907\uFF0C\u4ECE\u8FD9\u91CC\u5F00\u59CB",
106
+ "empty.description": "\u914D\u5BF9\u4E00\u53F0\u5B89\u88C5\u4E86 Luvus \u6216 Herdr \u7684\u5DE5\u4F5C\u7AD9\uFF0C\u5373\u53EF\u5728\u8FD9\u91CC\u67E5\u770B\u8FDE\u63A5\u3001\u8FD0\u884C\u5B9E\u4F8B\u548C\u8BBF\u95EE\u72B6\u6001\u3002",
16
107
  "provider.disable": "\u7981\u7528\u6B64 provider",
17
108
  "provider.target": "Provider \u76EE\u6807",
18
109
  "provider.effect": "\u4EC5\u7981\u7528\u6B64\u5B9E\u4F8B\u5E76\u9012\u589E binding revision\uFF1B\u8BBE\u5907\u6388\u6743\u53CA\u5176\u5B83\u5B9E\u4F8B\u4E0D\u53D8\u3002\u65E7\u6D41\u3001\u53E5\u67C4\u548C attempt \u4E0D\u590D\u6D3B\uFF0C\u5728\u9014\u539F\u751F\u5DE5\u4F5C\u53EF\u80FD\u7EE7\u7EED\u3002",
@@ -21,14 +112,13 @@ var chinese = Object.freeze({
21
112
  "app.subtitle": "\u8BBE\u5907\u4E0E\u8FDE\u63A5\u7BA1\u7406",
22
113
  "app.console": "Gateway \u63A7\u5236\u53F0",
23
114
  "app.skip": "\u8DF3\u5230\u7BA1\u7406\u5185\u5BB9",
24
- "app.managedFooter": "\u7BA1\u7406\u64CD\u4F5C\u987B\u660E\u786E\u786E\u8BA4\uFF1B\u6BCF\u6B21\u8BF7\u6C42\u4ECD\u7531\u540E\u53F0\u9A8C\u8BC1\u5F53\u524D\u4F1A\u8BDD\u4E0E\u6743\u9650\u3002",
25
- "app.footer": "\u53EA\u8BFB\u7BA1\u7406\u5207\u7247 \xB7 \u6240\u6709\u7BA1\u7406\u53D8\u66F4\u5C1A\u672A\u63A5\u5165 \xB7 \u6BCF\u6B21\u8BFB\u53D6\u987B\u7531\u540E\u7AEF\u9A8C\u8BC1\u5F53\u524D\u7BA1\u7406\u5458\u4E0E\u76EE\u6807\u6743\u9650",
115
+ "app.managedFooter": "Rovloom \xB7 \u4F60\u7684\u8BBE\u5907\u3001Agent \u4E0E\u5DE5\u4F5C\u6D41\u3002",
116
+ "app.footer": "Rovloom \xB7 Gateway \u63A7\u5236\u53F0",
117
+ "preference.switch": "\u5207\u6362\u4E3A {value}",
26
118
  "language.label": "\u8BED\u8A00",
27
- "language.system": "\u8DDF\u968F\u6D4F\u89C8\u5668",
28
119
  "language.chinese": "\u7B80\u4F53\u4E2D\u6587",
29
120
  "language.english": "English",
30
121
  "theme.label": "\u4E3B\u9898",
31
- "theme.system": "\u8DDF\u968F\u7CFB\u7EDF",
32
122
  "theme.light": "\u6D45\u8272",
33
123
  "theme.dark": "\u6DF1\u8272",
34
124
  "manage.heading": "\u8BBE\u5907\u7BA1\u7406\u64CD\u4F5C",
@@ -59,28 +149,6 @@ var chinese = Object.freeze({
59
149
  "manage.revokeEffect": "\u6301\u4E45\u64A4\u9500\u6210\u529F\u540E\uFF0C\u65E7\u8EAB\u4EFD\u5C06\u4E0D\u80FD\u91CD\u65B0\u63A5\u5165\uFF1B\u7ED3\u679C\u4EE5\u5F53\u524D\u6301\u4E45\u56DE\u6267\u4E3A\u51C6\uFF0C\u539F\u751F\u5DE5\u4F5C\u53EF\u80FD\u7EE7\u7EED\u3002",
60
150
  "manage.nativeEffect": "\u6B64\u64CD\u4F5C\u4E0D\u4FDD\u8BC1\u505C\u6B62\u6216\u56DE\u6EDA\u539F\u751F\u5DE5\u4F5C\u3002",
61
151
  "manage.action": "\u64CD\u4F5C",
62
- "pairing.title": "\u5728\u7EBF\u6DFB\u52A0\u8BBE\u5907",
63
- "pairing.intro": "\u65B0\u5EFA\u914D\u5BF9\u7533\u8BF7\u540E\uFF0C\u628A\u7533\u8BF7\u7F16\u53F7\u4EA4\u7ED9\u65B0\u8BBE\u5907\u8FD0\u884C rovloom connector --claim\uFF1B\u8BBE\u5907\u63D0\u4EA4\u8BA4\u9886\u540E\uFF0C\u5728\u6B64\u6838\u5BF9\u6838\u5BF9\u7801\u5E76\u6279\u51C6\u7B7E\u53D1\u51ED\u636E\u3002",
64
- "pairing.name": "\u65B0\u8BBE\u5907\u540D\u79F0",
65
- "pairing.policy": "\u6388\u6743\u653F\u7B56\u5F15\u7528",
66
- "pairing.create": "\u65B0\u5EFA\u914D\u5BF9\u7533\u8BF7",
67
- "pairing.preparing": "\u6B63\u5728\u51C6\u5907\u540E\u53F0\u786E\u8BA4\u2026",
68
- "pairing.review": "\u8BF7\u6838\u5BF9\u4EE5\u4E0B\u540E\u53F0\u6458\u8981\uFF0C\u786E\u8BA4\u540E\u624D\u4F1A\u63D0\u4EA4\u3002",
69
- "pairing.confirm": "\u786E\u8BA4\u6267\u884C",
70
- "pairing.cancel": "\u53D6\u6D88",
71
- "pairing.created": "\u7533\u8BF7\u5DF2\u521B\u5EFA\u3002\u628A\u7533\u8BF7\u7F16\u53F7\u4EA4\u7ED9\u65B0\u8BBE\u5907\uFF1A",
72
- "pairing.waitingClaim": "\u7B49\u5F85\u8BBE\u5907\u63D0\u4EA4\u8BA4\u9886\u2026",
73
- "pairing.claimArrived": "\u8BBE\u5907\u5DF2\u63D0\u4EA4\u8BA4\u9886\u3002\u6838\u5BF9\u4E0B\u65B9\u6838\u5BF9\u7801\u4E0E\u8BBE\u5907\u5C4F\u5E55\u4E00\u81F4\u540E\u6279\u51C6\u7B7E\u53D1\uFF1A",
74
- "pairing.userCode": "\u6838\u5BF9\u7801",
75
- "pairing.approve": "\u6279\u51C6\u7B7E\u53D1\u51ED\u636E",
76
- "pairing.approved": "\u5DF2\u6279\u51C6\uFF0C\u51ED\u636E\u5DF2\u6258\u7BA1\u4EA4\u4ED8\uFF0C\u7B49\u5F85\u8BBE\u5907\u53D6\u56DE\u2026",
77
- "pairing.paired": "\u8BBE\u5907\u5DF2\u53D6\u56DE\u51ED\u636E\uFF0C\u914D\u5BF9\u5B8C\u6210\u3002",
78
- "pairing.activated": "\u8BBE\u5907\u5DF2\u53D6\u56DE\u51ED\u636E\u5E76\u5B8C\u6210\u539F\u751F\u6FC0\u6D3B\uFF0C\u53EF\u6B63\u5F0F\u4F7F\u7528\u3002",
79
- "pairing.statusPending": "\u7533\u8BF7\u5F85\u8BA4\u9886",
80
- "pairing.expired": "\u7533\u8BF7\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u65B0\u5EFA\u3002",
81
- "pairing.failed": "\u64CD\u4F5C\u672A\u5B8C\u6210\uFF0C\u8BF7\u91CD\u65B0\u51C6\u5907\u3002",
82
- "pairing.application": "\u7533\u8BF7\u7F16\u53F7",
83
- "pairing.machine": "\u8BBE\u5907",
84
152
  "manage.machine": "\u76EE\u6807\u8BBE\u5907",
85
153
  "manage.epoch": "\u8FDE\u63A5\u4EE3\u6B21",
86
154
  "manage.revision": "\u8D44\u6E90\u7248\u672C",
@@ -102,7 +170,7 @@ var chinese = Object.freeze({
102
170
  "action.cancel": "\u53D6\u6D88\u8BFB\u53D6",
103
171
  "reader.pending": "\u6B63\u5728\u8BFB\u53D6\u2026",
104
172
  "reader.stale": "\u5FEB\u7167\u5DF2\u8FC7\u671F",
105
- "reader.snapshot": "\u53EA\u8BFB\u5FEB\u7167",
173
+ "reader.snapshot": "\u5DF2\u66F4\u65B0",
106
174
  "reader.empty": "\u65E0\u53EF\u7528\u5FEB\u7167",
107
175
  "error.unauthenticated": "\u7BA1\u7406\u4F1A\u8BDD\u672A\u767B\u5F55\u6216\u5DF2\u8FC7\u671F\u3002\u8BF7\u901A\u8FC7\u53D7\u4FDD\u62A4\u7684\u7BA1\u7406\u5458\u5165\u53E3\u91CD\u65B0\u767B\u5F55\u3002",
108
176
  "error.forbidden": "\u5F53\u524D\u7BA1\u7406\u5458\u6CA1\u6709\u6B64\u8303\u56F4\u7684\u67E5\u770B\u6743\u9650\u3002",
@@ -113,6 +181,15 @@ var chinese = Object.freeze({
113
181
  "error.timeout": "\u8BFB\u53D6\u8D85\u8FC7 5 \u79D2\u3002\u5F53\u524D\u663E\u793A\u4E0D\u662F\u5B9E\u65F6\u72B6\u6001\u3002",
114
182
  "error.cancelled": "\u5DF2\u53D6\u6D88\u672C\u6B21\u8BFB\u53D6\u3002",
115
183
  "time.noObservation": "\u5C1A\u65E0\u53EF\u4FE1\u89C2\u6D4B",
184
+ "reason.auth_failed": "\u8BA4\u8BC1\u5931\u8D25",
185
+ "reason.backend_unavailable": "\u540E\u7AEF\u4E0D\u53EF\u7528",
186
+ "reason.binding_mismatch": "\u7ED1\u5B9A\u4E0D\u4E00\u81F4",
187
+ "reason.connection_closed": "\u8FDE\u63A5\u5DF2\u5173\u95ED",
188
+ "reason.credential_expired": "\u51ED\u636E\u5DF2\u8FC7\u671F",
189
+ "reason.heartbeat_timeout": "\u5FC3\u8DF3\u8D85\u65F6",
190
+ "reason.not_recorded": "\u672A\u8BB0\u5F55",
191
+ "reason.protocol_error": "\u534F\u8BAE\u9519\u8BEF",
192
+ "reason.stale": "\u72B6\u6001\u8FC7\u671F",
116
193
  "time.noHeartbeat": "\u5C1A\u65E0\u53EF\u4FE1\u5FC3\u8DF3",
117
194
  "time.observed": "{time} \xB7 {age}{expired}",
118
195
  "time.expiredSuffix": " \xB7 \u5DF2\u8FC7\u671F",
@@ -144,15 +221,6 @@ var chinese = Object.freeze({
144
221
  "persisted.offline": "\u79BB\u7EBF\u767B\u8BB0 (offline)",
145
222
  "persisted.online": "\u5728\u7EBF\u767B\u8BB0 (online)",
146
223
  "persisted.revoked": "\u8EAB\u4EFD\u5DF2\u64A4\u9500 (revoked)",
147
- "reason.backend_unavailable": "\u540E\u7AEF\u4E0D\u53EF\u7528",
148
- "reason.not_recorded": "\u672A\u8BB0\u5F55",
149
- "reason.stale": "\u89C2\u6D4B\u5DF2\u8FC7\u671F",
150
- "reason.auth_failed": "\u8BA4\u8BC1\u5931\u8D25",
151
- "reason.credential_expired": "\u51ED\u636E\u8FC7\u671F",
152
- "reason.heartbeat_timeout": "\u5FC3\u8DF3\u8D85\u65F6",
153
- "reason.protocol_error": "\u534F\u8BAE\u9519\u8BEF",
154
- "reason.connection_closed": "\u8FDE\u63A5\u5DF2\u5173\u95ED",
155
- "reason.binding_mismatch": "\u914D\u7F6E\u4E0D\u4E00\u81F4",
156
224
  "device.expired": "\u8BBE\u5907\u89C2\u6D4B\u5DF2\u8FC7\u671F",
157
225
  "device.revokedHistory": "\u4E0A\u6B21\u6301\u4E45\u767B\u8BB0\uFF1A\u8EAB\u4EFD\u5DF2\u64A4\u9500\uFF08\u5DF2\u8FC7\u671F\uFF09",
158
226
  "device.oldRecords": "\u65E7\u8FDE\u63A5\u4E0E\u8BA4\u8BC1\u8BB0\u5F55\u4E0D\u80FD\u6062\u590D\u6388\u6743",
@@ -181,10 +249,6 @@ var chinese = Object.freeze({
181
249
  "devices.health": "\u8FDE\u63A5\u4E0E\u8BA4\u8BC1",
182
250
  "devices.activity": "\u6700\u8FD1\u5FC3\u8DF3",
183
251
  "devices.received": "Gateway \u63A5\u6536 {time}",
184
- "devices.observations": "\u89C2\u6D4B\u65F6\u95F4",
185
- "devices.emptyHeading": "\u5C1A\u65E0\u53EF\u5C55\u793A\u7684\u8BBE\u5907",
186
- "devices.empty": "\u6B64\u8303\u56F4\u5C1A\u65E0\u8BBE\u5907\u3002\u7531\u53D7\u4FDD\u62A4\u7684\u672C\u673A\u7BA1\u7406\u6E20\u9053\u5EFA\u7ACB\u914D\u5BF9\u7533\u8BF7\u540E\u518D\u5237\u65B0\u3002",
187
- "devices.heartbeat": "\u6700\u8FD1\u5FC3\u8DF3\uFF1A{age} \xB7 Gateway \u63A5\u6536 {time}",
188
252
  "devices.observed": "\u8BBE\u5907\u72B6\u6001\u89C2\u6D4B\uFF1A{time}",
189
253
  "devices.connectionObserved": "\u8FDE\u63A5\u89C2\u6D4B\uFF1A{time}",
190
254
  "devices.authObserved": "\u8BA4\u8BC1\u89C2\u6D4B\uFF1A{time}",
@@ -245,6 +309,100 @@ var chinese = Object.freeze({
245
309
  "gateway.degradedNote": "\u5F53\u524D\u7981\u6B62\u7BA1\u7406\u5199\u5165\uFF1B\u6301\u4E45\u64A4\u9500\u7B49\u64CD\u4F5C\u53EF\u80FD\u5C1A\u672A\u751F\u6548\uFF0C\u8BF7\u4F7F\u7528\u53D7\u4FDD\u62A4\u672C\u673A\u8BCA\u65AD\u3002"
246
310
  });
247
311
  var english = Object.freeze({
312
+ "resources.tokens": "MCP tokens",
313
+ "resources.pairingCodes": "Pairing codes",
314
+ "resources.createCode": "Generate code",
315
+ "resources.codesNote": "One code per device: run the displayed join command on the target machine to pair. Unused codes can be revoked anytime.",
316
+ "resources.codeCreatedNote": "Code created. Copy the command below and run it on the Connector machine.",
317
+ "resources.code_unused": "Unused",
318
+ "resources.code_used": "Used",
319
+ "resources.code_revoked": "Revoked",
320
+ "resources.code_failed": "Failed",
321
+ "resources.revokeCode": "Revoke",
322
+ "resources.confirmRevokeCode": "Revoke this pairing code? Generate a new one for the device.",
323
+ "resources.name": "Name",
324
+ "resources.expiry": "Valid for",
325
+ "resources.days": "{count} days",
326
+ "resources.create": "Create token",
327
+ "resources.tokenNote": "Create tokens for your AI clients. View, copy or delete them anytime. They work with current and future paired devices.",
328
+ "resources.endpoint": "MCP endpoint",
329
+ "resources.endpointNote": "For remote access, replace 127.0.0.1 with your Gateway address. Set Authorization to Bearer followed by the token in your AI client.",
330
+ "resources.expiresAt": "Expires {time}",
331
+ "resources.active": "Active",
332
+ "resources.expired": "Expired",
333
+ "resources.show": "Show",
334
+ "resources.hide": "Hide",
335
+ "resources.legacy": "This older token only has a hash and cannot be recovered.",
336
+ "resources.reissue": "Reissue",
337
+ "resources.confirmReissue": "Reissue this token? The previous token will stop working immediately.",
338
+ "resources.delete": "Delete",
339
+ "resources.deleteDevice": "Delete device",
340
+ "resources.deleteDeviceNote": "Remove this device, runtime configuration, pairing records and call content, and disconnect it.",
341
+ "resources.confirmDevice": "Delete this device and its pairing and call records? Pair it again to reconnect.",
342
+ "resources.confirmToken": "Delete token \u201C{name}\u201D? Clients using it will lose access.",
343
+ "resources.emptyTokens": "No MCP tokens yet. Create one above to connect your AI client.",
344
+ "resources.loading": "Loading\u2026",
345
+ "resources.saving": "Saving\u2026",
346
+ "resources.saved": "Saved",
347
+ "resources.failed": "The operation did not complete. Refresh and try again.",
348
+ "resources.invalid": "Use a unique name (letters, numbers, - or _) and a valid expiry.",
349
+ "nav.workspace": "WORKSPACE",
350
+ "nav.overview": "Overview",
351
+ "nav.devices": "Devices",
352
+ "nav.pairing": "Pair a device",
353
+ "nav.connect": "Connect MCP",
354
+ "nav.resources": "RESOURCES",
355
+ "nav.docs": "Quick start",
356
+ "nav.console": "Management console",
357
+ "overview.eyebrow": "YOUR WORKSPACE, CONNECTED",
358
+ "overview.title": "Workspace overview",
359
+ "overview.description": "Keep your gateway and devices in view. Bring your remote workflow together.",
360
+ "overview.visible": "Devices on this page",
361
+ "overview.visibleNote": "In the current authorized list page",
362
+ "overview.online": "Verified connections",
363
+ "overview.onlineNote": "Connection and identity are current",
364
+ "overview.attention": "Needs attention",
365
+ "overview.attentionNote": "Devices without a verified connection",
366
+ "overview.scope": "Device counts cover this page only. Expired observations do not establish current state.",
367
+ "overview.store": "Storage",
368
+ "overview.transport": "Device transport",
369
+ "overview.native": "Native runtime",
370
+ "overview.manage": "Manage devices",
371
+ "overview.deviceSection": "Your connected workspaces",
372
+ "overview.deviceSubtitle": "Connect your tools and workspaces, one device at a time.",
373
+ "devices.search": "Search devices on this page",
374
+ "devices.searchPlaceholder": "Search by device name, ID or label\u2026",
375
+ "devices.filterAll": "All devices",
376
+ "devices.filterOnline": "Verified",
377
+ "devices.filterAttention": "Needs attention",
378
+ "devices.results": "Showing {shown} of {count} devices on this page",
379
+ "devices.noMatches": "No matching devices",
380
+ "devices.noMatchesHint": "Try another name, ID or label, or change the status filter.",
381
+ "devices.record": "Observation details",
382
+ "devices.open": "View details for {name}",
383
+ "overview.available": "Available",
384
+ "overview.degraded": "Degraded",
385
+ "overview.unavailable": "Unavailable",
386
+ "overview.unverified": "Unverified",
387
+ "pairing.pageTitle": "Bring a new device on board",
388
+ "pairing.pageDescription": "Generate a per-device code and run the join command on the target machine. No applications, approvals or verification codes.",
389
+ "pairing.loginRequired": "A management session is required",
390
+ "pairing.loginHint": "Sign in through the Gateway management entry to generate pairing codes.",
391
+ "connect.title": "Connect your workspace to AI",
392
+ "connect.description": "Once a device is paired, issue a dedicated MCP Bearer for your AI client.",
393
+ "connect.stepOne": "Find an existing caller",
394
+ "connect.stepOneNote": "Run this on the Gateway machine and note the actual caller ID in the list.",
395
+ "connect.stepTwo": "Issue a client token",
396
+ "connect.stepTwoNote": "Replace EXISTING_CALLER_ID with that ID. New tokens can also be viewed later in the Dashboard.",
397
+ "connect.stepThree": "Configure your AI client",
398
+ "connect.stepThreeNote": "Choose Streamable HTTP, enter the actual Gateway MCP URL, and use the issued token as Bearer. The administrator password is for this console only.",
399
+ "connect.docker": "Docker deployment commands",
400
+ "connect.docs": "Read the connection guide",
401
+ "copy.action": "Copy command",
402
+ "copy.done": "Copied",
403
+ "copy.failed": "Copy unavailable; select the text manually",
404
+ "empty.title": "Start with your first device",
405
+ "empty.description": "Pair a workstation running Luvus or Herdr. Its connection, runtime instances and access status will appear here.",
248
406
  "provider.disable": "Disable this provider",
249
407
  "provider.target": "Provider target",
250
408
  "provider.effect": "Disables only this instance and advances its binding revision. Device authorization and other instances stay independent. Old streams, handles and attempts do not revive; in-flight native work may continue.",
@@ -253,14 +411,13 @@ var english = Object.freeze({
253
411
  "app.subtitle": "Device and connection management",
254
412
  "app.console": "Gateway console",
255
413
  "app.skip": "Skip to management content",
256
- "app.managedFooter": "Management actions require explicit confirmation; the server verifies the current session and permissions on every request.",
257
- "app.footer": "Read-only management \xB7 Management changes are unavailable \xB7 Each read requires current administrator and target authorization",
414
+ "app.managedFooter": "Rovloom \xB7 Your devices, agents and workflows.",
415
+ "app.footer": "Rovloom \xB7 Gateway console",
416
+ "preference.switch": "Switch to {value}",
258
417
  "language.label": "Language",
259
- "language.system": "Browser language",
260
418
  "language.chinese": "\u7B80\u4F53\u4E2D\u6587",
261
419
  "language.english": "English",
262
420
  "theme.label": "Theme",
263
- "theme.system": "System theme",
264
421
  "theme.light": "Light",
265
422
  "theme.dark": "Dark",
266
423
  "manage.heading": "Device management",
@@ -291,28 +448,6 @@ var english = Object.freeze({
291
448
  "manage.revokeEffect": "After durable revocation, that identity cannot reconnect. The current durable receipt determines the outcome; native work may continue.",
292
449
  "manage.nativeEffect": "This operation does not guarantee stopping or rolling back native work.",
293
450
  "manage.action": "Action",
294
- "pairing.title": "Add a device online",
295
- "pairing.intro": "Create a pairing application, then hand its ID to the new device running rovloom connector --claim. When the device submits its claim, compare the check code here and approve credential issuance.",
296
- "pairing.name": "New device name",
297
- "pairing.policy": "Authorization policy reference",
298
- "pairing.create": "Create pairing application",
299
- "pairing.preparing": "Preparing the background confirmation\u2026",
300
- "pairing.review": "Review the background summary below; nothing is submitted until you confirm.",
301
- "pairing.confirm": "Confirm",
302
- "pairing.cancel": "Cancel",
303
- "pairing.created": "Application created. Give this pairing ID to the new device:",
304
- "pairing.waitingClaim": "Waiting for the device to submit its claim\u2026",
305
- "pairing.claimArrived": "The device submitted a claim. Approve issuance only if the check code matches the device screen:",
306
- "pairing.userCode": "Check code",
307
- "pairing.approve": "Approve credential issuance",
308
- "pairing.approved": "Approved. The credential is held for delivery; waiting for the device to collect it\u2026",
309
- "pairing.paired": "The device collected its credential. Pairing complete.",
310
- "pairing.activated": "Credential collected and native activation complete. Ready to use.",
311
- "pairing.statusPending": "Awaiting claim",
312
- "pairing.expired": "This application expired; create a new one.",
313
- "pairing.failed": "The operation did not complete; prepare it again.",
314
- "pairing.application": "Pairing ID",
315
- "pairing.machine": "Device",
316
451
  "manage.machine": "Target device",
317
452
  "manage.epoch": "Connection epoch",
318
453
  "manage.revision": "Resource revision",
@@ -334,7 +469,7 @@ var english = Object.freeze({
334
469
  "action.cancel": "Cancel read",
335
470
  "reader.pending": "Reading\u2026",
336
471
  "reader.stale": "Snapshot expired",
337
- "reader.snapshot": "Read-only snapshot",
472
+ "reader.snapshot": "Up to date",
338
473
  "reader.empty": "No snapshot",
339
474
  "error.unauthenticated": "The management session is missing or expired. Sign in through the protected administrator entry.",
340
475
  "error.forbidden": "The current administrator cannot view this scope.",
@@ -345,6 +480,15 @@ var english = Object.freeze({
345
480
  "error.timeout": "The read exceeded 5 seconds. The displayed state is not live.",
346
481
  "error.cancelled": "This read was cancelled.",
347
482
  "time.noObservation": "No trusted observation",
483
+ "reason.auth_failed": "Authentication failed",
484
+ "reason.backend_unavailable": "Backend unavailable",
485
+ "reason.binding_mismatch": "Binding mismatch",
486
+ "reason.connection_closed": "Connection closed",
487
+ "reason.credential_expired": "Credential expired",
488
+ "reason.heartbeat_timeout": "Heartbeat timed out",
489
+ "reason.not_recorded": "Not recorded",
490
+ "reason.protocol_error": "Protocol error",
491
+ "reason.stale": "Stale",
348
492
  "time.noHeartbeat": "No trusted heartbeat",
349
493
  "time.observed": "{time} \xB7 {age}{expired}",
350
494
  "time.expiredSuffix": " \xB7 expired",
@@ -376,15 +520,6 @@ var english = Object.freeze({
376
520
  "persisted.offline": "Registered offline (offline)",
377
521
  "persisted.online": "Registered online (online)",
378
522
  "persisted.revoked": "Identity revoked (revoked)",
379
- "reason.backend_unavailable": "Backend unavailable",
380
- "reason.not_recorded": "Not recorded",
381
- "reason.stale": "Observation expired",
382
- "reason.auth_failed": "Authentication failed",
383
- "reason.credential_expired": "Credential expired",
384
- "reason.heartbeat_timeout": "Heartbeat timed out",
385
- "reason.protocol_error": "Protocol error",
386
- "reason.connection_closed": "Connection closed",
387
- "reason.binding_mismatch": "Binding mismatch",
388
523
  "device.expired": "Device observation expired",
389
524
  "device.revokedHistory": "Last stored registration: identity revoked (expired)",
390
525
  "device.oldRecords": "Old connection and authentication records cannot restore authorization",
@@ -413,10 +548,6 @@ var english = Object.freeze({
413
548
  "devices.health": "Connection & authentication",
414
549
  "devices.activity": "Last heartbeat",
415
550
  "devices.received": "Gateway received {time}",
416
- "devices.observations": "Observations",
417
- "devices.emptyHeading": "No devices to display",
418
- "devices.empty": "There are no devices in this scope. Create a pairing request through the protected local management channel, then refresh.",
419
- "devices.heartbeat": "Last heartbeat: {age} \xB7 Gateway received {time}",
420
551
  "devices.observed": "Device state observed: {time}",
421
552
  "devices.connectionObserved": "Connection observed: {time}",
422
553
  "devices.authObserved": "Authentication observed: {time}",
@@ -508,6 +639,84 @@ function createLocalizer(language) {
508
639
  };
509
640
  }
510
641
 
642
+ // src/webui/icons.ts
643
+ var paths = {
644
+ overview: "M3 3h7v7H3zM14 3h7v7h-7zM3 14h7v7H3zM14 14h7v7h-7z",
645
+ devices: "M3 4h18v12H3zM8 21h8M12 16v5",
646
+ pairing: "M8 3H5a2 2 0 0 0-2 2v3M16 3h3a2 2 0 0 1 2 2v3M3 16v3a2 2 0 0 0 2 2h3M21 16v3a2 2 0 0 1-2 2h-3M12 8v8M8 12h8",
647
+ connect: "M9 8 6 12l3 4M15 8l3 4-3 4M13 5l-2 14",
648
+ arrow: "M5 12h14M14 7l5 5-5 5",
649
+ plus: "M12 5v14M5 12h14",
650
+ search: "M21 21l-5-5M18 10a8 8 0 1 1-16 0 8 8 0 0 1 16 0",
651
+ activity: "M3 12h4l3-8 4 16 3-8h4",
652
+ shield: "M12 3 4 6v6c0 5 8 9 8 9s8-4 8-9V6zM8 12l3 3 5-6",
653
+ attention: "M12 3 2 21h20zM12 9v4M12 17h.01",
654
+ book: "M12 5c-3-2-6-2-10-1v15c4-1 7-1 10 1 3-2 6-2 10-1V4c-4-1-7-1-10 1zM12 5v15",
655
+ refresh: "M20 7v5h-5M4 17v-5h5M6 7a7 7 0 0 1 12-1l2 3M4 15l2 3a7 7 0 0 0 12-1",
656
+ copy: "M9 9h12v12H9zM15 9V3H3v12h6",
657
+ check: "M5 12l4 4L19 6",
658
+ chevron: "M9 5l7 7-7 7",
659
+ gateway: "M4 3h16v7H4zM4 14h16v7H4zM8 6.5h.01M8 17.5h.01M16 6.5h1M16 17.5h1"
660
+ };
661
+ function uiIcon(name) {
662
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
663
+ svg.classList.add("ui-icon");
664
+ for (const [key, value] of Object.entries({
665
+ viewBox: "0 0 24 24",
666
+ fill: "none",
667
+ stroke: "currentColor",
668
+ "stroke-width": "1.65",
669
+ "stroke-linecap": "round",
670
+ "stroke-linejoin": "round",
671
+ "aria-hidden": "true",
672
+ focusable: "false"
673
+ })) {
674
+ svg.setAttribute(key, value);
675
+ }
676
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
677
+ path.setAttribute("d", paths[name]);
678
+ svg.append(path);
679
+ return svg;
680
+ }
681
+
682
+ // src/webui/clipboard.ts
683
+ function copyControl(value, label) {
684
+ const button = document.createElement("button");
685
+ button.type = "button";
686
+ button.className = "copy-button";
687
+ button.dataset.copy = value;
688
+ button.setAttribute("aria-label", label);
689
+ button.title = label;
690
+ button.append(uiIcon("copy"));
691
+ return button;
692
+ }
693
+ async function copyText(value) {
694
+ try {
695
+ if (navigator.clipboard?.writeText) {
696
+ await navigator.clipboard.writeText(value);
697
+ return true;
698
+ }
699
+ } catch {}
700
+ const active = document.activeElement;
701
+ const input = document.createElement("textarea");
702
+ input.className = "copy-buffer";
703
+ input.value = value;
704
+ input.readOnly = true;
705
+ input.setAttribute("aria-label", "Copy");
706
+ document.body.append(input);
707
+ try {
708
+ input.select();
709
+ return document.execCommand("copy");
710
+ } catch {
711
+ return false;
712
+ } finally {
713
+ input.remove();
714
+ if (active instanceof HTMLElement && active.isConnected) {
715
+ active.focus({ preventScroll: true });
716
+ }
717
+ }
718
+ }
719
+
511
720
  // src/webui/view.ts
512
721
  var connectionNames = {
513
722
  connecting: "connection.connecting",
@@ -579,6 +788,9 @@ function routeHref(route) {
579
788
  if (route.providerCursor !== null) {
580
789
  parameters.set("providers", route.providerCursor);
581
790
  }
791
+ if (route.machineId === null && route.section) {
792
+ parameters.set("view", route.section);
793
+ }
582
794
  return `#${parameters.toString()}`;
583
795
  }
584
796
  function parseRoute(hash) {
@@ -587,7 +799,7 @@ function parseRoute(hash) {
587
799
  return empty;
588
800
  }
589
801
  const parameters = new URLSearchParams(hash.replace(/^#/, ""));
590
- for (const key of ["machine", "cursor", "providers"]) {
802
+ for (const key of ["machine", "cursor", "providers", "view"]) {
591
803
  if (parameters.getAll(key).length > 1 || (parameters.get(key)?.length ?? 0) > 1024) {
592
804
  return empty;
593
805
  }
@@ -596,7 +808,8 @@ function parseRoute(hash) {
596
808
  return {
597
809
  machineId,
598
810
  cursor: machineId ? null : parameters.get("cursor") || null,
599
- providerCursor: machineId ? parameters.get("providers") || null : null
811
+ providerCursor: machineId ? parameters.get("providers") || null : null,
812
+ ...!machineId && ["devices", "pairing", "connect"].includes(parameters.get("view") ?? "") ? { section: parameters.get("view") } : {}
600
813
  };
601
814
  }
602
815
  function current(observedAt, freshness) {
@@ -624,7 +837,7 @@ function listObservation(key, observedAt, freshness) {
624
837
  function currentDeviceLink(device, freshness) {
625
838
  return device.persisted_state === "online" && current(device.observed_at, freshness) && device.connection.verification === "verified" && current(device.connection.observed_at, freshness) && device.connection.value.state === "online" && device.authentication.verification === "verified" && current(device.authentication.observed_at, freshness) && device.authentication.value.state === "authenticated";
626
839
  }
627
- function deviceBadges(device, freshness) {
840
+ function deviceBadges(device, freshness, compact = false) {
628
841
  const { i18n } = freshness;
629
842
  const { t } = i18n;
630
843
  const result = node("div", "", "badges");
@@ -661,7 +874,9 @@ function deviceBadges(device, freshness) {
661
874
  } else {
662
875
  result.append(badge(t("device.authUnknown", { reason: i18n.reason(authentication.reason_code) }), "warning"));
663
876
  }
664
- result.append(badge(t("device.readyUnknown")));
877
+ if (!compact) {
878
+ result.append(badge(t("device.readyUnknown")));
879
+ }
665
880
  return result;
666
881
  }
667
882
  function unavailableActions(kind, i18n) {
@@ -703,20 +918,154 @@ function deviceActions(page, freshness, connected) {
703
918
  }
704
919
  return section;
705
920
  }
706
- function deviceList(page, freshness) {
921
+ function overviewMetrics(page, freshness) {
922
+ const t = freshness.i18n.t;
923
+ const online = page.devices.items.filter((device) => currentDeviceLink(device, freshness)).length;
924
+ const section = node("section", "", "overview-metrics");
925
+ section.setAttribute("aria-label", t("nav.overview"));
926
+ const rows = [
927
+ ["overview.visible", "overview.visibleNote", String(page.devices.items.length), "devices"],
928
+ ["overview.online", "overview.onlineNote", freshness.pageStale ? "\u2014" : String(online), "shield"],
929
+ [
930
+ "overview.attention",
931
+ "overview.attentionNote",
932
+ freshness.pageStale ? "\u2014" : String(page.devices.items.length - online),
933
+ "attention"
934
+ ]
935
+ ];
936
+ for (const [label, description, value, icon] of rows) {
937
+ const card = node("article", "", "metric-card");
938
+ const top = node("div", "", "metric-heading");
939
+ top.append(node("span", t(label)), uiIcon(icon));
940
+ card.append(top, node("strong", value, "metric-value"), node("p", t(description), "muted"));
941
+ section.append(card);
942
+ }
943
+ section.append(node("p", t("overview.scope"), "metric-scope"));
944
+ return section;
945
+ }
946
+ function guidePage(section, i18n, management) {
947
+ const { t } = i18n;
948
+ const heading = node("header", "", "page-heading");
949
+ const copy = node("div", "", "heading-copy");
950
+ copy.append(node("p", t("nav.workspace"), "eyebrow"));
951
+ copy.append(node("h1", t(section === "pairing" ? "pairing.pageTitle" : "connect.title")));
952
+ copy.append(node("p", t(section === "pairing" ? "pairing.pageDescription" : "connect.description"), "muted"));
953
+ heading.append(copy);
954
+ const body = node("section", "", "connection-guide");
955
+ if (section === "pairing") {
956
+ if (!management) {
957
+ const empty = node("div", "", "empty");
958
+ empty.append(uiIcon("shield"), node("h2", t("pairing.loginRequired")), node("p", t("pairing.loginHint"), "muted"));
959
+ body.append(empty);
960
+ }
961
+ return { heading, body };
962
+ }
963
+ const commands = ["rovloom token list", "rovloom token create --principal my_client --like EXISTING_CALLER_ID"];
964
+ for (const [index, title, description] of [
965
+ [0, "connect.stepOne", "connect.stepOneNote"],
966
+ [1, "connect.stepTwo", "connect.stepTwoNote"],
967
+ [2, "connect.stepThree", "connect.stepThreeNote"]
968
+ ]) {
969
+ const card = node("article", "", "connection-step");
970
+ const number = node("span", String(index + 1).padStart(2, "0"), "step-number");
971
+ const details = node("div", "", "connection-step-body");
972
+ details.append(node("h2", t(title)), node("p", t(description), "muted"));
973
+ if (index < commands.length) {
974
+ const command = commands[index];
975
+ const block = node("div", "", "command-snippet");
976
+ block.append(node("code", command), copyControl(command, t("copy.action")));
977
+ const docker = node("details", "", "docker-command");
978
+ docker.dataset.disclosure = "docker-command-" + index;
979
+ docker.append(node("summary", t("connect.docker")));
980
+ const dockerCommand = command.replace(/^rovloom /u, "docker compose run --rm gateway ");
981
+ const snippet = node("div", "", "command-snippet");
982
+ snippet.append(node("code", dockerCommand), copyControl(dockerCommand, t("copy.action")));
983
+ docker.append(snippet);
984
+ details.append(block, docker);
985
+ } else {
986
+ const link = node("a", t("connect.docs"), "text-link");
987
+ link.href = `https://github.com/ArnoChenFx/rovloom/blob/main/docs/QUICK-START${i18n.language === "zh-CN" ? "-zh" : ""}.md`;
988
+ link.target = "_blank";
989
+ link.rel = "noreferrer noopener";
990
+ details.append(link);
991
+ }
992
+ card.append(number, details);
993
+ body.append(card);
994
+ }
995
+ return { heading, body };
996
+ }
997
+ function deviceList(page, freshness, route, filter) {
707
998
  const { i18n } = freshness;
708
999
  const { t } = i18n;
709
1000
  const heading = node("header", "", "page-heading");
710
- heading.append(node("h1", t("devices.heading")), node("p", t("devices.description"), "muted"));
1001
+ const overview = route.section !== "devices";
1002
+ const copy = node("div", "", "heading-copy");
1003
+ copy.append(node("p", t("overview.eyebrow"), "eyebrow"));
1004
+ copy.append(node("h1", t(overview ? "overview.title" : "devices.heading")), node("p", t(overview ? "overview.description" : "devices.description"), "muted"));
1005
+ heading.append(copy);
1006
+ const add = node("a", "", "primary-button");
1007
+ add.href = "#view=pairing";
1008
+ add.append(uiIcon("plus"), node("span", t("nav.pairing")));
1009
+ heading.append(add);
711
1010
  const section = node("section", "", "devices-section");
1011
+ const sectionHeader = node("div", "", "section-heading");
1012
+ const sectionCopy = node("div");
1013
+ sectionCopy.append(node("h2", t(overview ? "overview.deviceSection" : "devices.heading")), node("p", t("overview.deviceSubtitle"), "muted"));
1014
+ sectionHeader.append(sectionCopy);
1015
+ if (overview) {
1016
+ const all = node("a", "", "text-link");
1017
+ all.href = "#view=devices";
1018
+ all.append(node("span", t("overview.manage")), uiIcon("arrow"));
1019
+ sectionHeader.append(all);
1020
+ }
1021
+ section.append(sectionHeader);
1022
+ if (page.devices.items.length) {
1023
+ const tools = node("div", "", "device-tools");
1024
+ const search = node("label", "", "search-field");
1025
+ const input = node("input");
1026
+ input.type = "search";
1027
+ input.name = "device-search";
1028
+ input.value = filter.query;
1029
+ input.placeholder = t("devices.searchPlaceholder");
1030
+ input.setAttribute("aria-label", t("devices.search"));
1031
+ search.append(uiIcon("search"), input);
1032
+ const filters = node("div", "", "device-filters");
1033
+ filters.setAttribute("role", "group");
1034
+ filters.setAttribute("aria-label", t("devices.health"));
1035
+ for (const [value, key] of [
1036
+ ["all", "devices.filterAll"],
1037
+ ["online", "devices.filterOnline"],
1038
+ ["attention", "devices.filterAttention"]
1039
+ ]) {
1040
+ const button = node("button", t(key));
1041
+ button.type = "button";
1042
+ button.dataset.deviceFilter = value;
1043
+ button.setAttribute("aria-pressed", String(value === filter.status));
1044
+ filters.append(button);
1045
+ }
1046
+ tools.append(search, filters);
1047
+ section.append(tools);
1048
+ }
1049
+ const items = page.devices.items.filter((device) => {
1050
+ const query = filter.query.trim().toLocaleLowerCase(i18n.language);
1051
+ const matches = !query || [device.name, device.machine_id, ...device.labels ?? []].some((value) => value.toLocaleLowerCase(i18n.language).includes(query));
1052
+ const online = currentDeviceLink(device, freshness);
1053
+ return matches && (filter.status === "all" || (filter.status === "online" ? online : !online));
1054
+ });
712
1055
  if (page.devices.items.length === 0) {
713
1056
  const empty = node("div", "", "empty");
714
- empty.append(node("h2", t("devices.emptyHeading")), node("p", t("devices.empty"), "muted"));
1057
+ const symbol = node("span", "", "empty-illustration");
1058
+ symbol.append(uiIcon("devices"));
1059
+ empty.append(symbol, node("h2", t("empty.title")), node("p", t("empty.description"), "muted"));
1060
+ const pair = node("a", "", "primary-button");
1061
+ pair.href = "#view=pairing";
1062
+ pair.append(uiIcon("plus"), node("span", t("nav.pairing")));
1063
+ empty.append(pair);
715
1064
  section.append(empty);
716
1065
  }
717
1066
  const columns = node("div", "", "device-columns");
718
1067
  columns.setAttribute("aria-hidden", "true");
719
- for (const key of ["devices.identity", "devices.health", "devices.activity", "devices.observations"]) {
1068
+ for (const key of ["devices.identity", "devices.health", "devices.activity", "devices.record"]) {
720
1069
  columns.append(node("span", t(key)));
721
1070
  }
722
1071
  if (page.devices.items.length) {
@@ -724,32 +1073,60 @@ function deviceList(page, freshness) {
724
1073
  }
725
1074
  const list = node("ul", "", "device-list");
726
1075
  list.setAttribute("aria-label", t("devices.heading"));
727
- for (const device of page.devices.items) {
1076
+ for (const device of items) {
728
1077
  const row = node("li", "", "device-card");
729
1078
  const identity = node("div", "", "device-identity");
730
1079
  const link = node("a", device.name, "device-name");
731
1080
  link.href = routeHref({ machineId: device.machine_id, cursor: null, providerCursor: null });
732
- identity.append(link, node("code", device.machine_id));
733
- identity.append(node("p", device.labels === null ? t("value.noLabelRecord") : device.labels.length ? device.labels.join(" \xB7 ") : t("value.noLabels"), "muted"));
1081
+ const deviceIcon = node("span", "", "device-icon");
1082
+ deviceIcon.append(uiIcon("devices"));
1083
+ const deviceCopy = node("div", "", "device-copy");
1084
+ const identifier = node("code", device.machine_id);
1085
+ identifier.title = device.machine_id;
1086
+ deviceCopy.append(link, identifier);
1087
+ identity.append(deviceIcon, deviceCopy);
1088
+ deviceCopy.append(node("p", device.labels === null ? t("value.noLabelRecord") : device.labels.length ? device.labels.join(" \xB7 ") : t("value.noLabels"), "muted"));
734
1089
  const health = node("div", "", "device-health");
735
- health.append(node("span", t("devices.health"), "cell-label"), deviceBadges(device, freshness));
1090
+ health.append(node("span", t("devices.health"), "cell-label"), deviceBadges(device, freshness, true));
736
1091
  const heartbeat = device.connection.verification === "verified" ? device.connection.value.last_heartbeat_at : null;
737
1092
  const activity = node("div", "", "device-activity");
738
1093
  activity.append(node("span", t("devices.activity"), "cell-label"));
739
1094
  activity.append(node("strong", heartbeat === null ? t("time.noHeartbeat") : i18n.age(heartbeat, freshness.now), "activity-age"));
740
1095
  activity.append(node("p", t("devices.received", { time: i18n.timestamp(heartbeat) }), "muted"));
741
1096
  const observations = node("div", "", "device-observations");
742
- observations.append(node("span", t("devices.observations"), "cell-label"));
743
- observations.append(listObservation("devices.observed", device.observed_at, freshness));
744
- observations.append(listObservation("devices.connectionObserved", device.connection.verification === "verified" ? device.connection.observed_at : null, freshness));
745
- observations.append(listObservation("devices.authObserved", device.authentication.verification === "verified" ? device.authentication.observed_at : null, freshness));
1097
+ const record = node("details", "", "device-record");
1098
+ record.dataset.disclosure = "device-record-" + device.machine_id;
1099
+ const summary = node("summary", t("devices.record"));
1100
+ record.append(summary);
1101
+ record.append(listObservation("devices.observed", device.observed_at, freshness));
1102
+ record.append(listObservation("devices.connectionObserved", device.connection.verification === "verified" ? device.connection.observed_at : null, freshness));
1103
+ record.append(listObservation("devices.authObserved", device.authentication.verification === "verified" ? device.authentication.observed_at : null, freshness));
1104
+ record.append(node("p", t("device.readyUnknown"), "muted"));
1105
+ const inspect = node("a", "", "inspect-device");
1106
+ inspect.href = link.href;
1107
+ inspect.setAttribute("aria-label", t("devices.open", { name: device.name }));
1108
+ inspect.append(uiIcon("arrow"));
1109
+ observations.append(record, inspect);
746
1110
  row.append(identity, health, activity, observations);
747
1111
  list.append(row);
748
1112
  }
749
1113
  section.append(list);
1114
+ if (page.devices.items.length && !items.length) {
1115
+ const empty = node("div", "", "empty search-empty");
1116
+ empty.append(uiIcon("search"), node("h3", t("devices.noMatches")), node("p", t("devices.noMatchesHint"), "muted"));
1117
+ section.append(empty);
1118
+ }
1119
+ if (page.devices.items.length) {
1120
+ section.append(node("p", t("devices.results", { shown: items.length, count: page.devices.items.length }), "list-count"));
1121
+ }
750
1122
  if (page.devices.next_cursor !== undefined) {
751
1123
  const next = node("a", t("devices.next"), "button-link");
752
- next.href = routeHref({ machineId: null, cursor: page.devices.next_cursor, providerCursor: null });
1124
+ next.href = routeHref({
1125
+ machineId: null,
1126
+ cursor: page.devices.next_cursor,
1127
+ providerCursor: null,
1128
+ section: route.section
1129
+ });
753
1130
  section.append(next);
754
1131
  }
755
1132
  return { heading, body: section };
@@ -894,11 +1271,15 @@ function syncChildren(parent, desired) {
894
1271
  parent.removeChild(parent.lastChild);
895
1272
  }
896
1273
  }
897
- function renderPage(content, state, route, i18n = createLocalizer("zh-CN"), management = false) {
1274
+ function renderPage(content, state, route, i18n = createLocalizer("zh-CN"), management = false, filter = { query: "", status: "all" }) {
898
1275
  const { t } = i18n;
899
1276
  const focused = content.contains(document.activeElement) && document.activeElement instanceof HTMLAnchorElement ? document.activeElement.getAttribute("href") : null;
900
1277
  const metadataWasOpen = content.querySelector(".gateway-metadata")?.open ?? false;
901
1278
  const metadataHadFocus = content.contains(document.activeElement) && document.activeElement?.matches(".gateway-metadata > summary");
1279
+ const disclosures = new Map([...content.querySelectorAll("details[data-disclosure]")].map((details) => [
1280
+ details.dataset.disclosure,
1281
+ details.open
1282
+ ]));
902
1283
  const view = document.createDocumentFragment();
903
1284
  if (state.problem) {
904
1285
  const message = node("p", t(problemNames[state.problem]), "notice warning");
@@ -927,8 +1308,28 @@ function renderPage(content, state, route, i18n = createLocalizer("zh-CN"), mana
927
1308
  gatewayHeading.append(node("h2", t("gateway.heading")));
928
1309
  gatewayHeading.append(badge(status.mode === "active" ? state.stale ? t("gateway.activeHistory") : t("gateway.active") : t("gateway.degraded"), status.mode === "active" && !state.stale ? "good" : "warning"));
929
1310
  gatewayHeading.append(badge(state.stale ? t("reader.stale") : t("gateway.fresh"), state.stale ? "warning" : "neutral"));
930
- gateway.append(gatewayHeading);
931
- gateway.append(facts([
1311
+ const mark = node("span", "", "gateway-symbol");
1312
+ mark.append(uiIcon("gateway"));
1313
+ gatewayHeading.prepend(mark);
1314
+ const components = node("div", "", "gateway-components");
1315
+ for (const [name, key] of [
1316
+ ["store", "overview.store"],
1317
+ ["transport", "overview.transport"],
1318
+ ["native", "overview.native"]
1319
+ ]) {
1320
+ const item = node("div", "", "component-item");
1321
+ item.append(node("span", t(key), "muted"), badge(t(`overview.${status.components[name]}`), status.components[name] === "available" && !state.stale ? "good" : "neutral"));
1322
+ components.append(item);
1323
+ }
1324
+ gateway.append(gatewayHeading, components);
1325
+ const metadata = node("details", "", "gateway-metadata");
1326
+ metadata.open = metadataWasOpen;
1327
+ metadata.append(node("summary", t("gateway.metadata")));
1328
+ metadata.append(facts([
1329
+ [t("gateway.generation"), status.recovery_generation],
1330
+ [t("gateway.policy"), status.policy_revision]
1331
+ ]));
1332
+ metadata.append(facts([
932
1333
  [t("gateway.listener"), t("gateway.listenerUnknown")],
933
1334
  [
934
1335
  t("gateway.authentication"),
@@ -947,22 +1348,28 @@ function renderPage(content, state, route, i18n = createLocalizer("zh-CN"), mana
947
1348
  })
948
1349
  ]
949
1350
  ], "gateway-facts"));
950
- const metadata = node("details", "", "gateway-metadata");
951
- metadata.open = metadataWasOpen;
952
- metadata.append(node("summary", t("gateway.metadata")));
953
- metadata.append(facts([
954
- [t("gateway.generation"), status.recovery_generation],
955
- [t("gateway.policy"), status.policy_revision]
956
- ]));
957
1351
  gateway.append(metadata);
958
1352
  if (status.mode !== "active") {
959
1353
  gateway.append(node("p", t("gateway.degradedNote"), "notice warning"));
960
1354
  }
961
- const section = route.machineId === null ? deviceList(page, freshness) : deviceDetail(page, freshness, management && status.mode === "active" && !state.stale);
962
- view.append(section.heading, gateway, section.body);
1355
+ const section = route.machineId !== null ? deviceDetail(page, freshness, management && status.mode === "active" && !state.stale) : route.section === "pairing" || route.section === "connect" ? guidePage(route.section, i18n, management) : deviceList(page, freshness, route, filter);
1356
+ view.append(section.heading);
1357
+ if (route.section === "pairing" && (state.stale || status.mode !== "active")) {
1358
+ view.append(node("p", t(state.stale ? "reader.stale" : "gateway.degradedNote"), "notice warning"));
1359
+ }
1360
+ if (route.machineId !== null || !["pairing", "connect"].includes(route.section ?? "")) {
1361
+ if (route.machineId === null) {
1362
+ view.append(overviewMetrics(page, freshness));
1363
+ }
1364
+ view.append(gateway);
1365
+ }
1366
+ view.append(section.body);
963
1367
  }
964
1368
  const identity = routeHref(route);
965
1369
  if (content.dataset.route === identity) {
1370
+ for (const details of view.querySelectorAll("details[data-disclosure]")) {
1371
+ details.open = disclosures.get(details.dataset.disclosure) ?? false;
1372
+ }
966
1373
  syncChildren(content, view);
967
1374
  } else {
968
1375
  content.replaceChildren(view);
@@ -1080,9 +1487,8 @@ function createUiPreferences(changed, host = browserHost) {
1080
1487
  }
1081
1488
 
1082
1489
  // src/webui/preference-control.ts
1083
- var paths = {
1490
+ var paths2 = {
1084
1491
  language: "M2 5h12M7 2h1M12 5c-1 6-4 9-8 11M5 8c2 4 4 6 7 8M13 22l5-11 5 11M15 18h6",
1085
- system: "M3 4h18v12H3zM8 21h8M12 16v5",
1086
1492
  light: "M16 12a4 4 0 1 0-8 0 4 4 0 0 0 8 0M12 2v2M12 20v2M2 12h2M20 12h2M5 5l1.5 1.5M17.5 17.5 19 19M5 19l1.5-1.5M17.5 6.5 19 5",
1087
1493
  dark: "M20.9 13A9 9 0 0 1 11 3.1 9 9 0 1 0 20.9 13Z"
1088
1494
  };
@@ -1097,102 +1503,50 @@ function icon(name) {
1097
1503
  svg.setAttribute("stroke-linecap", "round");
1098
1504
  svg.setAttribute("stroke-linejoin", "round");
1099
1505
  const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
1100
- path.setAttribute("d", paths[name]);
1506
+ path.setAttribute("d", paths2[name]);
1101
1507
  svg.append(path);
1102
1508
  return svg;
1103
1509
  }
1104
- function createPreferenceControl(kind, choices, onChoose) {
1510
+ function createPreferenceControl(kind, onChoose) {
1105
1511
  const root = document.createElement("div");
1106
1512
  root.className = "preference";
1107
1513
  root.dataset.preference = kind;
1108
1514
  const trigger = document.createElement("button");
1109
1515
  trigger.type = "button";
1110
1516
  trigger.className = "preference-trigger";
1111
- trigger.setAttribute("aria-expanded", "false");
1112
1517
  const current = document.createElement("span");
1113
1518
  current.className = "visually-hidden";
1114
1519
  current.id = `rovloom-${kind}-current`;
1115
1520
  trigger.setAttribute("aria-describedby", current.id);
1116
- const panel = document.createElement("div");
1117
- panel.className = "preference-panel";
1118
- panel.id = `rovloom-${kind}-choices`;
1119
- panel.hidden = true;
1120
- panel.setAttribute("role", "group");
1121
- trigger.setAttribute("aria-controls", panel.id);
1122
- const buttons = choices.map(([value]) => {
1123
- const button = document.createElement("button");
1124
- button.type = "button";
1125
- button.dataset.choice = value;
1126
- panel.append(button);
1127
- return button;
1128
- });
1129
- root.append(trigger, current, panel);
1130
- function close(restoreFocus = false) {
1131
- panel.hidden = true;
1132
- trigger.setAttribute("aria-expanded", "false");
1133
- if (restoreFocus) {
1134
- trigger.focus();
1135
- }
1136
- }
1137
- function click(event) {
1138
- const target = event.target instanceof Element ? event.target.closest("button") : null;
1139
- if (target === trigger) {
1140
- panel.hidden = !panel.hidden;
1141
- trigger.setAttribute("aria-expanded", String(!panel.hidden));
1142
- } else if (target instanceof HTMLButtonElement && buttons.includes(target)) {
1143
- onChoose(target.dataset.choice);
1144
- close(true);
1145
- }
1146
- }
1147
- function keydown(event) {
1148
- if (event.key === "Escape" && !panel.hidden) {
1149
- event.preventDefault();
1150
- close(true);
1151
- return;
1152
- }
1153
- if (!["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) {
1154
- return;
1155
- }
1156
- event.preventDefault();
1157
- const index = buttons.indexOf(document.activeElement);
1158
- const next = event.key === "Home" ? 0 : event.key === "End" || index < 0 && event.key === "ArrowUp" ? buttons.length - 1 : (index + (event.key === "ArrowUp" ? -1 : 1) + buttons.length) % buttons.length;
1159
- panel.hidden = false;
1160
- trigger.setAttribute("aria-expanded", "true");
1161
- buttons[next].focus();
1162
- }
1163
- function outside(event) {
1164
- if (event.target instanceof Node && !root.contains(event.target)) {
1165
- close();
1521
+ root.append(trigger, current);
1522
+ let next = "";
1523
+ function click() {
1524
+ if (next) {
1525
+ onChoose(next);
1166
1526
  }
1167
1527
  }
1168
1528
  function update(i18n, value) {
1169
- const label = i18n.t(kind === "language" ? "language.label" : "theme.label");
1529
+ const language = kind === "language";
1530
+ const label = i18n.t(language ? "language.label" : "theme.label");
1531
+ next = language ? value === "zh-CN" ? "en" : "zh-CN" : value === "dark" ? "light" : "dark";
1532
+ const name = i18n.t(language ? value === "zh-CN" ? "language.chinese" : "language.english" : value === "dark" ? "theme.dark" : "theme.light");
1533
+ const target = i18n.t(language ? next === "zh-CN" ? "language.chinese" : "language.english" : next === "dark" ? "theme.dark" : "theme.light");
1534
+ current.textContent = `${name}. ${i18n.t("preference.switch", { value: target })}`;
1170
1535
  trigger.setAttribute("aria-label", label);
1171
- panel.setAttribute("aria-label", label);
1172
- for (let index = 0;index < buttons.length; index++) {
1173
- const [choice, key] = choices[index];
1174
- const button = buttons[index];
1175
- button.textContent = i18n.t(key);
1176
- button.setAttribute("aria-pressed", String(choice === value));
1177
- if (choice === value) {
1178
- current.textContent = i18n.t(key);
1179
- }
1180
- }
1181
1536
  trigger.title = `${label}: ${current.textContent}`;
1182
- trigger.replaceChildren(icon(kind === "language" ? "language" : value === "dark" ? "dark" : value === "light" ? "light" : "system"));
1537
+ trigger.dataset.value = value;
1538
+ trigger.dataset.nextValue = next;
1539
+ trigger.replaceChildren(icon(language ? "language" : value === "dark" ? "dark" : "light"));
1183
1540
  }
1184
- function dispose() {
1185
- root.removeEventListener("click", click);
1186
- root.removeEventListener("keydown", keydown);
1187
- document.removeEventListener("pointerdown", outside);
1188
- document.removeEventListener("focusin", outside);
1189
- close();
1190
- }
1191
- root.addEventListener("click", click);
1192
- root.addEventListener("keydown", keydown);
1193
- document.addEventListener("pointerdown", outside);
1194
- document.addEventListener("focusin", outside);
1195
- return { root, update, close, dispose };
1541
+ trigger.addEventListener("click", click);
1542
+ return {
1543
+ root,
1544
+ update,
1545
+ dispose() {
1546
+ next = "";
1547
+ trigger.removeEventListener("click", click);
1548
+ }
1549
+ };
1196
1550
  }
1197
1551
 
1198
1552
  // src/webui/request-id.ts
@@ -1293,49 +1647,6 @@ function checkDeviceIntent(value) {
1293
1647
  summary({ ...operation, recovery_generation: "validation", expected_revision: 0, reason: "administrator_request" }, "invalid_request");
1294
1648
  return JSON.parse(JSON.stringify(value));
1295
1649
  }
1296
- function checkPairingIntent(value) {
1297
- record(value, ["action"], ["machine_id", "name", "policy_ref", "labels", "pairing_id", "claim_id", "user_code"], "invalid_request");
1298
- const action = value.action;
1299
- if (action === "pairing_create") {
1300
- const hasName = Object.hasOwn(value, "name");
1301
- const keys = hasName ? ["action", "name", "policy_ref", "labels"] : ["action", "machine_id", "policy_ref"];
1302
- record(value, keys, [], "invalid_request");
1303
- const intent = value;
1304
- const operation = intent.action === "pairing_create" && "name" in intent ? {
1305
- action: "pairing_create",
1306
- target: { name: intent.name, policy_ref: intent.policy_ref, labels: intent.labels }
1307
- } : {
1308
- action: "pairing_create",
1309
- target: {
1310
- machine_id: intent.machine_id,
1311
- policy_ref: intent.policy_ref
1312
- }
1313
- };
1314
- summary({ ...operation, recovery_generation: "validation", expected_revision: 0, reason: "administrator_request" }, "invalid_request");
1315
- return JSON.parse(JSON.stringify(value));
1316
- }
1317
- if (action === "pairing_claim_approve") {
1318
- record(value, ["action", "machine_id", "pairing_id", "claim_id", "user_code"], [], "invalid_request");
1319
- const intent = value;
1320
- if (intent.action !== "pairing_claim_approve") {
1321
- invalid("invalid_request");
1322
- }
1323
- summary({
1324
- action: "pairing_claim_approve",
1325
- target: {
1326
- machine_id: intent.machine_id,
1327
- pairing_id: intent.pairing_id,
1328
- claim_id: intent.claim_id,
1329
- user_code: intent.user_code
1330
- },
1331
- recovery_generation: "validation",
1332
- expected_revision: 0,
1333
- reason: "administrator_request"
1334
- }, "invalid_request");
1335
- return JSON.parse(JSON.stringify(value));
1336
- }
1337
- invalid("invalid_request");
1338
- }
1339
1650
  function pairingSummary(v, problem) {
1340
1651
  const target = v.target;
1341
1652
  if (v.action === "pairing_create") {
@@ -2285,597 +2596,339 @@ function createDeviceActions(options) {
2285
2596
  };
2286
2597
  }
2287
2598
 
2288
- // src/webui/pairing-flow.ts
2289
- function snapshot2(value) {
2290
- const copy = JSON.parse(JSON.stringify(value));
2291
- function freeze(item) {
2292
- if (item && typeof item === "object") {
2293
- for (const child of Object.values(item)) {
2294
- freeze(child);
2295
- }
2296
- Object.freeze(item);
2297
- }
2599
+ // src/webui/resource-client.ts
2600
+ class ResourceError extends Error {
2601
+ code;
2602
+ constructor(code) {
2603
+ super(code);
2604
+ this.code = code;
2298
2605
  }
2299
- freeze(copy);
2300
- return copy;
2301
- }
2302
- function failure2(error) {
2303
- return error instanceof ManagementClientError ? error.problem : "unavailable";
2304
2606
  }
2305
- function operationOf(intent) {
2306
- if (intent.action === "pairing_claim_approve") {
2307
- return {
2308
- action: "pairing_claim_approve",
2309
- target: {
2310
- machine_id: intent.machine_id,
2311
- pairing_id: intent.pairing_id,
2312
- claim_id: intent.claim_id,
2313
- user_code: intent.user_code
2314
- }
2315
- };
2316
- }
2317
- return "name" in intent ? { action: "pairing_create", target: { name: intent.name, policy_ref: intent.policy_ref, labels: intent.labels } } : { action: "pairing_create", target: { machine_id: intent.machine_id, policy_ref: intent.policy_ref } };
2607
+
2608
+ // src/webui/resource-panel.ts
2609
+ function node3(tag, text = "", className = "") {
2610
+ const element = document.createElement(tag);
2611
+ element.textContent = text;
2612
+ element.className = className;
2613
+ return element;
2318
2614
  }
2319
- var operationName = {
2320
- pairing_create: "pairings_create",
2321
- pairing_claim_approve: "pairings_claim_approve"
2322
- };
2323
- function createPairingManagementFlow(options) {
2324
- let state = { phase: "idle" };
2325
- let intent;
2326
- let write;
2327
- let review;
2328
- let original;
2329
- let active;
2330
- let generation = 0;
2331
- let disposed = false;
2332
- const requestId = options.requestId ?? managementRequestId;
2333
- async function execute(operation, args, signal) {
2334
- if (signal.aborted) {
2335
- throw new ManagementClientError("aborted");
2336
- }
2337
- managementWriteBody(operation, args);
2338
- const reply = await options.transport.execute(operation, args, signal);
2339
- return checkManagementWriteReply(operation, reply, args);
2615
+ function createResourcePanel(options) {
2616
+ const region = node3("section", "", "resource-panel");
2617
+ const feedback = node3("p", "", "resource-feedback");
2618
+ feedback.setAttribute("role", "status");
2619
+ let i18n;
2620
+ let key = "";
2621
+ let active = false;
2622
+ let pending = false;
2623
+ let controller = new AbortController;
2624
+ let data;
2625
+ let tokenName = "";
2626
+ let tokenDays = "365";
2627
+ let codeName = "";
2628
+ let created;
2629
+ let version = 0;
2630
+ let route;
2631
+ let language = "";
2632
+ function button(label, action, danger = false) {
2633
+ const result = node3("button", label, danger ? "danger-button" : "");
2634
+ result.type = "button";
2635
+ result.disabled = pending;
2636
+ result.addEventListener("click", action);
2637
+ return result;
2340
2638
  }
2341
- function begin(phase, signal) {
2342
- if (disposed || active || state.phase !== phase || signal?.aborted) {
2343
- throw new ManagementClientError("invalid_request");
2344
- }
2345
- const controller = new AbortController;
2346
- active = controller;
2347
- const current = ++generation;
2348
- const abort = () => cancel();
2349
- signal?.addEventListener("abort", abort, { once: true });
2350
- return {
2351
- controller,
2352
- current: () => !disposed && generation === current && !controller.signal.aborted,
2353
- done() {
2354
- signal?.removeEventListener("abort", abort);
2355
- if (active === controller) {
2356
- active = undefined;
2357
- }
2358
- }
2359
- };
2639
+ async function request(operation, args) {
2640
+ return options.transport(operation, args, controller.signal);
2360
2641
  }
2361
- function unconfirmed(problem) {
2362
- if (!original) {
2363
- state = { phase: "failed", problem: problem ?? "unavailable" };
2642
+ function failure(error) {
2643
+ if (error instanceof ResourceError && (error.code === "forbidden" || error.code === "unauthenticated")) {
2644
+ clear();
2645
+ options.authorizationLost(error.code);
2364
2646
  return;
2365
2647
  }
2366
- state = { phase: "unconfirmed", operation: original, ...problem ? { problem } : {} };
2367
- }
2368
- function accept(receipt) {
2369
- if (!original || !intent || !review || receipt.request_id !== original.request_id || receipt.history_epoch !== original.history_epoch || receipt.recovery_generation !== original.recovery_generation || receipt.action !== intent.action || receipt.operation_digest !== review.operation_digest) {
2370
- throw new ManagementClientError("invalid_response", true);
2371
- }
2372
- if (intent.action === "pairing_claim_approve" && receipt.target.machine_id !== intent.machine_id) {
2373
- throw new ManagementClientError("invalid_response", true);
2374
- }
2375
- if (receipt.evidence !== "current_durable" || receipt.operation_state === "outcome_unknown") {
2376
- unconfirmed("outcome_unconfirmed");
2377
- } else {
2378
- state = receipt.operation_state === "committed" ? { phase: "committed", receipt } : { phase: "not_committed", receipt };
2379
- }
2648
+ feedback.textContent = i18n.t(error instanceof ResourceError && error.code === "invalid_arguments" ? "resources.invalid" : "resources.failed");
2380
2649
  }
2381
- function cancel() {
2382
- if (!active) {
2383
- if (state.phase === "idle" || state.phase === "awaiting_confirmation") {
2384
- generation++;
2385
- state = { phase: "cancelled" };
2386
- }
2387
- return;
2388
- }
2389
- generation++;
2390
- const controller = active;
2391
- active = undefined;
2392
- const uncertain = state.phase === "submitting" || state.phase === "checking";
2650
+ async function refresh() {
2651
+ const generation = ++version;
2393
2652
  controller.abort();
2394
- if (uncertain) {
2395
- unconfirmed("aborted");
2396
- } else {
2397
- state = { phase: "cancelled" };
2398
- }
2399
- }
2400
- return {
2401
- state() {
2402
- return snapshot2(state);
2403
- },
2404
- async prepare(value, signal) {
2405
- const pending = begin("idle", signal);
2406
- try {
2407
- intent = checkPairingIntent(value);
2408
- state = { phase: "preparing", intent };
2409
- const id = requestId();
2410
- const history = await execute("history_open", {}, pending.controller.signal);
2411
- if (!pending.current()) {
2412
- return snapshot2(state);
2413
- }
2414
- const resource = "machine_id" in intent ? await execute("write_state_get", { machine_id: intent.machine_id }, pending.controller.signal) : {
2415
- resource_revision: 0,
2416
- recovery_generation: history.recovery_generation,
2417
- connection_epoch: null,
2418
- binding_revision: null
2419
- };
2420
- if (!pending.current()) {
2421
- return snapshot2(state);
2422
- }
2423
- if (history.recovery_generation !== resource.recovery_generation) {
2424
- throw new ManagementClientError("conflict");
2425
- }
2426
- const draft = {
2427
- ...operationOf(intent),
2428
- request_id: id,
2429
- history_epoch: history.history_epoch,
2430
- recovery_generation: history.recovery_generation,
2431
- expected_revision: resource.resource_revision,
2432
- reason: "administrator_request"
2433
- };
2434
- const confirmation = await execute("confirmations_create", draft, pending.controller.signal);
2435
- if (!pending.current()) {
2436
- return snapshot2(state);
2437
- }
2438
- review = snapshot2(confirmation);
2439
- write = {
2440
- request_id: id,
2441
- history_epoch: history.history_epoch,
2442
- recovery_generation: history.recovery_generation,
2443
- expected_revision: resource.resource_revision,
2444
- reason: draft.reason,
2445
- confirmation_id: confirmation.confirmation_id
2446
- };
2447
- original = {
2448
- request_id: id,
2449
- history_epoch: history.history_epoch,
2450
- recovery_generation: history.recovery_generation
2451
- };
2452
- if (confirmation.state === "expired") {
2453
- throw new ManagementClientError("conflict");
2454
- }
2455
- if (confirmation.state === "consumed") {
2456
- unconfirmed("outcome_unconfirmed");
2457
- } else {
2458
- state = { phase: "awaiting_confirmation", review };
2459
- }
2460
- } catch (error) {
2461
- if (pending.current()) {
2462
- state = { phase: "failed", problem: failure2(error) };
2463
- }
2464
- } finally {
2465
- pending.done();
2653
+ controller = new AbortController;
2654
+ pending = true;
2655
+ feedback.textContent = i18n.t("resources.loading");
2656
+ try {
2657
+ const value = await request(key === "connect" ? "tokens_list" : "pairing_codes_list", key === "connect" ? {} : {});
2658
+ if (generation !== version || !active) {
2659
+ return;
2466
2660
  }
2467
- return snapshot2(state);
2468
- },
2469
- async confirm(signal) {
2470
- const pending = begin("awaiting_confirmation", signal);
2471
- try {
2472
- if (!write || !review || !intent) {
2473
- throw new ManagementClientError("invalid_request");
2474
- }
2475
- state = { phase: "confirming", review };
2476
- const confirmed = await execute("confirmations_confirm", { ...write, operation_digest: review.operation_digest }, pending.controller.signal);
2477
- if (!pending.current()) {
2478
- return snapshot2(state);
2479
- }
2480
- if (managementConfirmationKey(confirmed) !== managementConfirmationKey(review)) {
2481
- throw new ManagementClientError("invalid_response");
2482
- }
2483
- if (confirmed.state === "consumed") {
2484
- unconfirmed("outcome_unconfirmed");
2485
- return snapshot2(state);
2486
- }
2487
- if (confirmed.state !== "confirmed") {
2488
- throw new ManagementClientError("conflict");
2489
- }
2490
- const operation = operationName[intent.action];
2491
- const target = intent.action === "pairing_claim_approve" ? {
2492
- machine_id: intent.machine_id,
2493
- pairing_id: intent.pairing_id,
2494
- claim_id: intent.claim_id,
2495
- user_code: intent.user_code
2496
- } : ("name" in intent) ? { name: intent.name, policy_ref: intent.policy_ref, labels: intent.labels } : { machine_id: intent.machine_id, policy_ref: intent.policy_ref };
2497
- state = { phase: "submitting", review };
2498
- const receipt = await execute(operation, { ...write, ...target }, pending.controller.signal);
2499
- if (pending.current()) {
2500
- accept(receipt);
2501
- }
2502
- } catch (error) {
2503
- if (pending.current()) {
2504
- if (state.phase === "submitting") {
2505
- unconfirmed(failure2(error));
2506
- } else {
2507
- state = { phase: "failed", problem: failure2(error) };
2508
- }
2509
- }
2510
- } finally {
2511
- pending.done();
2661
+ if (!value || !Array.isArray(value.items)) {
2662
+ throw new Error("invalid_reply");
2512
2663
  }
2513
- return snapshot2(state);
2514
- },
2515
- async checkReceipt(signal) {
2516
- const pending = begin("unconfirmed", signal);
2517
- try {
2518
- if (!original) {
2519
- throw new ManagementClientError("invalid_request");
2520
- }
2521
- state = { phase: "checking", operation: original };
2522
- const receipt = await execute("operations_get", original, pending.controller.signal);
2523
- if (pending.current()) {
2524
- accept(receipt);
2525
- }
2526
- } catch (error) {
2527
- if (pending.current()) {
2528
- unconfirmed(failure2(error));
2529
- }
2530
- } finally {
2531
- pending.done();
2664
+ data = value;
2665
+ feedback.textContent = "";
2666
+ } catch (error) {
2667
+ if (generation === version && active) {
2668
+ failure(error);
2669
+ }
2670
+ } finally {
2671
+ if (generation === version && active) {
2672
+ pending = false;
2673
+ render();
2532
2674
  }
2533
- return snapshot2(state);
2534
- },
2535
- cancel,
2536
- dispose() {
2537
- cancel();
2538
- disposed = true;
2539
2675
  }
2540
- };
2541
- }
2542
-
2543
- // src/webui/pairing-actions.ts
2544
- var errors2 = {
2545
- invalid_request: "manage.invalid",
2546
- invalid_response: "manage.invalid",
2547
- unauthenticated: "manage.unauthenticated",
2548
- forbidden: "manage.forbidden",
2549
- conflict: "manage.conflict",
2550
- busy: "manage.busy",
2551
- unavailable: "manage.unavailable",
2552
- outcome_unconfirmed: "manage.unknown",
2553
- aborted: "manage.cancelled"
2554
- };
2555
- function node3(tag, className = "") {
2556
- const element = document.createElement(tag);
2557
- element.className = className;
2558
- return element;
2559
- }
2560
- function createPairingActions(options) {
2561
- const region = node3("section", "pairing-section");
2562
- const heading = node3("h2");
2563
- const intro = node3("p", "muted");
2564
- const form = node3("form", "pairing-form");
2565
- const nameLabel = node3("label");
2566
- const nameCaption = node3("span");
2567
- const nameInput = node3("input");
2568
- nameInput.name = "pairing-name";
2569
- nameInput.required = true;
2570
- nameInput.maxLength = 63;
2571
- nameInput.pattern = "[a-z0-9][a-z0-9._-]{0,62}";
2572
- nameInput.autocomplete = "off";
2573
- nameLabel.append(nameCaption, nameInput);
2574
- const policyLabel = node3("label");
2575
- const policyCaption = node3("span");
2576
- const policyInput = node3("input");
2577
- policyInput.name = "pairing-policy";
2578
- policyInput.required = true;
2579
- policyInput.maxLength = 128;
2580
- policyInput.autocomplete = "off";
2581
- policyInput.value = DEFAULT_PAIRING_POLICY_REF;
2582
- policyLabel.append(policyCaption, policyInput);
2583
- const create = node3("button", "pairing-confirm");
2584
- create.type = "submit";
2585
- form.append(nameLabel, policyLabel, create);
2586
- const reviewBox = node3("div", "pairing-review");
2587
- reviewBox.hidden = true;
2588
- const reviewText = node3("p", "pairing-status");
2589
- const reviewFacts = node3("dl", "facts pairing-facts");
2590
- const controls = node3("div", "pairing-controls");
2591
- const confirm = node3("button", "pairing-confirm");
2592
- const check = node3("button");
2593
- const cancel = node3("button");
2594
- for (const button of [confirm, check, cancel]) {
2595
- button.type = "button";
2596
2676
  }
2597
- controls.append(confirm, check, cancel);
2598
- reviewBox.append(reviewText, reviewFacts, controls);
2599
- const message = node3("p", "pairing-status");
2600
- message.setAttribute("role", "status");
2601
- message.setAttribute("aria-live", "polite");
2602
- const list = node3("ul", "pairing-entries");
2603
- region.append(heading, intro, form, reviewBox, message, list);
2604
- let i18n;
2605
- let available = false;
2606
- let disposed = false;
2607
- let flow;
2608
- let flowIntent;
2609
- let timer;
2610
- const entries = new Map;
2611
- const pollMs = options.pollMs ?? 4000;
2612
- function fact(term, value) {
2613
- const dt = node3("dt");
2614
- dt.textContent = term;
2615
- const dd = node3("dd");
2616
- dd.textContent = value;
2617
- reviewFacts.append(dt, dd);
2618
- }
2619
- function paintFlow(state) {
2620
- if (!i18n) {
2677
+ async function mutate(operation, args) {
2678
+ if (pending || !active) {
2621
2679
  return;
2622
2680
  }
2623
- const t = i18n.t.bind(i18n);
2624
- check.hidden = true;
2625
- confirm.hidden = true;
2626
- if (state.phase === "idle" || state.phase === "cancelled") {
2627
- reviewBox.hidden = true;
2628
- return;
2629
- }
2630
- reviewBox.hidden = false;
2631
- if (state.phase === "preparing" || state.phase === "confirming" || state.phase === "submitting") {
2632
- reviewText.textContent = t("pairing.preparing");
2633
- return;
2634
- }
2635
- if (state.phase === "awaiting_confirmation") {
2636
- reviewText.textContent = t("pairing.review");
2637
- reviewFacts.replaceChildren();
2638
- const summary = state.review.summary;
2639
- fact(t("manage.action"), t(summary.action === "pairing_create" ? "pairing.create" : "pairing.approve"));
2640
- if (summary.action === "pairing_create") {
2641
- fact(t("pairing.name"), "name" in summary.target ? summary.target.name : summary.target.machine_id);
2642
- fact(t("pairing.policy"), summary.target.policy_ref);
2643
- } else if (summary.action === "pairing_claim_approve") {
2644
- fact(t("pairing.machine"), summary.target.machine_id);
2645
- fact(t("pairing.userCode"), summary.target.user_code);
2646
- }
2647
- fact(t("manage.expires"), new Date(state.review.expires_at).toISOString());
2648
- confirm.hidden = false;
2649
- confirm.textContent = t("pairing.confirm");
2650
- return;
2651
- }
2652
- if (state.phase === "committed") {
2653
- reviewBox.hidden = true;
2654
- const receipt = state.receipt;
2655
- if (state.receipt.action === "pairing_create" && receipt.pairing_id && receipt.machine_id) {
2656
- const name = flowIntent?.action === "pairing_create" && "name" in flowIntent ? flowIntent.name : entries.get(receipt.pairing_id)?.name ?? receipt.machine_id;
2657
- addEntry({
2658
- name,
2659
- machine_id: receipt.machine_id,
2660
- pairing_id: receipt.pairing_id,
2661
- status: "pending",
2662
- item: node3("li", "pairing-entry")
2663
- });
2664
- message.textContent = t("pairing.created");
2665
- } else if (state.receipt.action === "pairing_claim_approve") {
2666
- message.textContent = t("pairing.approved");
2681
+ const generation = version;
2682
+ pending = true;
2683
+ feedback.textContent = i18n.t("resources.saving");
2684
+ render();
2685
+ try {
2686
+ const reply = operation === "pairing_codes_create" ? await request(operation, args) : await request(operation, args);
2687
+ if (operation === "pairing_codes_create" && reply && typeof reply.code === "string") {
2688
+ created = reply;
2667
2689
  }
2668
- flow = undefined;
2669
- flowIntent = undefined;
2670
- ensurePolling();
2671
- return;
2672
- }
2673
- if (state.phase === "not_committed") {
2674
- reviewText.textContent = t("manage.notCommitted");
2675
- flow = undefined;
2676
- flowIntent = undefined;
2677
- return;
2678
- }
2679
- if (state.phase === "unconfirmed") {
2680
- reviewText.textContent = t("manage.unknown");
2681
- check.hidden = false;
2682
- check.textContent = t("manage.check");
2683
- return;
2684
- }
2685
- if (state.phase === "failed") {
2686
- reviewText.textContent = t(errors2[state.problem]);
2687
- if (state.problem === "unauthenticated" || state.problem === "forbidden") {
2688
- options.authorizationLost(state.problem);
2690
+ if (generation !== version || !active) {
2691
+ return;
2692
+ }
2693
+ feedback.textContent = i18n.t("resources.saved");
2694
+ if (operation === "tokens_create") {
2695
+ tokenName = "";
2696
+ tokenDays = "365";
2697
+ }
2698
+ if (operation === "pairing_codes_create") {
2699
+ codeName = "";
2700
+ }
2701
+ if (operation === "devices_delete") {
2702
+ options.committed(route.machineId ?? undefined);
2703
+ } else {
2704
+ await refresh();
2705
+ if (operation === "pairings_delete") {
2706
+ options.committed(undefined, args.pairing_id);
2707
+ }
2708
+ }
2709
+ } catch (error) {
2710
+ if (generation === version && active) {
2711
+ failure(error);
2712
+ }
2713
+ } finally {
2714
+ if (generation === version && active) {
2715
+ pending = false;
2716
+ render();
2689
2717
  }
2690
- flow = undefined;
2691
- flowIntent = undefined;
2692
- return;
2693
2718
  }
2694
- reviewBox.hidden = true;
2695
2719
  }
2696
- async function start(intent) {
2697
- if (disposed || !available || flow) {
2698
- return;
2720
+ function remove(operation, args, message) {
2721
+ if (window.confirm(message)) {
2722
+ mutate(operation, args);
2699
2723
  }
2700
- flowIntent = intent;
2701
- flow = createPairingManagementFlow({ transport: options.transport });
2702
- paintFlow(await flow.prepare(intent));
2703
2724
  }
2704
- function renderEntry(entry) {
2705
- if (!i18n) {
2725
+ function render() {
2726
+ if (!active) {
2706
2727
  return;
2707
2728
  }
2708
- const t = i18n.t.bind(i18n);
2709
- entry.item.replaceChildren();
2710
- const head = node3("div", "pairing-entry-head");
2711
- const title = node3("strong");
2712
- title.textContent = entry.name;
2713
- const status = node3("span", "muted");
2714
- status.textContent = entry.claim?.activation === "configured" ? t("pairing.activated") : entry.status === "paired" ? t("pairing.paired") : entry.status === "failed" ? t("pairing.failed") : entry.status === "expired" || entry.status === "cancelled" ? t("pairing.expired") : entry.claim?.status === "approved" ? t("pairing.approved") : entry.claim ? t("pairing.claimArrived") : t("pairing.waitingClaim");
2715
- head.append(title, status);
2716
- entry.item.append(head);
2717
- const facts = node3("dl", "facts");
2718
- const idTerm = node3("dt");
2719
- idTerm.textContent = t("pairing.application");
2720
- const idValue = node3("dd");
2721
- const idCode = node3("code");
2722
- idCode.textContent = entry.pairing_id;
2723
- idValue.append(idCode);
2724
- const machineTerm = node3("dt");
2725
- machineTerm.textContent = t("pairing.machine");
2726
- const machineValue = node3("dd");
2727
- machineValue.textContent = entry.machine_id;
2728
- facts.append(idTerm, idValue, machineTerm, machineValue);
2729
- entry.item.append(facts);
2730
- if (entry.status === "pending" && entry.claim?.status === "pending") {
2731
- const codeLabel = node3("p", "muted");
2732
- codeLabel.textContent = t("pairing.userCode");
2733
- const code = node3("div", "pairing-code");
2734
- code.textContent = entry.claim.user_code;
2735
- const approve = node3("button", "pairing-confirm");
2736
- approve.type = "button";
2737
- approve.textContent = t("pairing.approve");
2738
- const claim = entry.claim;
2739
- approve.addEventListener("click", () => {
2740
- start({
2741
- action: "pairing_claim_approve",
2742
- machine_id: entry.machine_id,
2743
- pairing_id: entry.pairing_id,
2744
- claim_id: claim.claim_id,
2745
- user_code: claim.user_code
2729
+ const { t } = i18n;
2730
+ const body = node3("div", "", "resource-body");
2731
+ if (key === "device") {
2732
+ const card = node3("article", "", "resource-delete-card");
2733
+ card.append(node3("div", t("resources.deleteDeviceNote"), "muted"), button(t("resources.deleteDevice"), () => remove("devices_delete", { machine_id: route.machineId }, t("resources.confirmDevice")), true));
2734
+ body.append(card);
2735
+ } else {
2736
+ const heading = node3("div", "", "section-heading");
2737
+ heading.append(node3("h2", t(key === "connect" ? "resources.tokens" : "resources.pairingCodes")), button(t("action.refresh"), () => {
2738
+ refresh();
2739
+ }));
2740
+ body.append(heading);
2741
+ if (key === "connect") {
2742
+ const form = node3("form", "", "token-create-form");
2743
+ const nameLabel = node3("label", t("resources.name"));
2744
+ const name = node3("input");
2745
+ name.name = "token-name";
2746
+ name.required = true;
2747
+ name.pattern = "[A-Za-z0-9_-]{1,128}";
2748
+ name.maxLength = 128;
2749
+ name.placeholder = "my-client";
2750
+ name.disabled = pending;
2751
+ name.value = tokenName;
2752
+ name.addEventListener("input", () => {
2753
+ tokenName = name.value;
2746
2754
  });
2747
- });
2748
- entry.item.append(codeLabel, code, approve);
2749
- }
2750
- }
2751
- function addEntry(entry) {
2752
- const existing = entries.get(entry.pairing_id);
2753
- if (existing) {
2754
- existing.status = entry.status;
2755
- renderEntry(existing);
2756
- return;
2757
- }
2758
- entries.set(entry.pairing_id, entry);
2759
- list.append(entry.item);
2760
- renderEntry(entry);
2761
- }
2762
- function unfinished(entry) {
2763
- return entry.status === "pending" || entry.status === "paired" && entry.claim?.status === "delivered";
2764
- }
2765
- async function poll() {
2766
- for (const entry of entries.values()) {
2767
- if (disposed || !unfinished(entry)) {
2768
- continue;
2769
- }
2770
- try {
2771
- const args = { machine_id: entry.machine_id, pairing_id: entry.pairing_id };
2772
- managementWriteBody("pairings_get", args);
2773
- const reply = await options.transport.execute("pairings_get", args, new AbortController().signal);
2774
- const status = checkManagementWriteReply("pairings_get", reply, args);
2775
- entry.status = status.status;
2776
- entry.claim = status.claim;
2777
- renderEntry(entry);
2778
- } catch (error) {
2779
- if (error instanceof ManagementClientError && (error.problem === "unauthenticated" || error.problem === "forbidden")) {
2780
- stopPolling();
2781
- options.authorizationLost(error.problem);
2782
- return;
2755
+ nameLabel.append(name);
2756
+ const daysLabel = node3("label", t("resources.expiry"));
2757
+ const days = node3("select");
2758
+ days.name = "token-days";
2759
+ days.disabled = pending;
2760
+ for (const value of [365, 90, 30, 7]) {
2761
+ const option = node3("option", t("resources.days", { count: value }));
2762
+ option.value = String(value);
2763
+ days.append(option);
2764
+ }
2765
+ days.value = tokenDays;
2766
+ days.addEventListener("change", () => {
2767
+ tokenDays = days.value;
2768
+ });
2769
+ daysLabel.append(days);
2770
+ const submit = node3("button", t("resources.create"), "primary-button");
2771
+ submit.type = "submit";
2772
+ submit.disabled = pending;
2773
+ form.append(nameLabel, daysLabel, submit);
2774
+ form.addEventListener("submit", (event) => {
2775
+ event.preventDefault();
2776
+ mutate("tokens_create", { principal: name.value.trim(), days: Number(days.value) });
2777
+ });
2778
+ body.append(node3("p", t("resources.tokenNote"), "muted"), form);
2779
+ if (data?.resource) {
2780
+ const endpoint = node3("div", "", "mcp-endpoint");
2781
+ endpoint.append(node3("span", t("resources.endpoint")), node3("code", data.resource), copyControl(data.resource, t("copy.action")));
2782
+ body.append(endpoint, node3("p", t("resources.endpointNote"), "muted"));
2783
+ }
2784
+ for (const item of data?.items ?? []) {
2785
+ const token = item;
2786
+ const card = node3("article", "", "token-card");
2787
+ card.dataset.tokenPrincipal = token.principal;
2788
+ const summary = node3("div", "", "token-summary");
2789
+ summary.append(node3("h3", token.principal), node3("span", t(token.expires_at > Date.now() ? "resources.active" : "resources.expired"), "badge"), node3("span", t("resources.expiresAt", { time: i18n.timestamp(token.expires_at) }), "muted"));
2790
+ const controls = node3("div", "", "token-controls");
2791
+ if (token.token) {
2792
+ const value = node3("input");
2793
+ value.type = "password";
2794
+ value.value = token.token;
2795
+ value.readOnly = true;
2796
+ value.setAttribute("aria-label", token.principal + " Token");
2797
+ value.autocomplete = "off";
2798
+ value.spellcheck = false;
2799
+ const toggle = button(t("resources.show"), () => {
2800
+ const hidden = value.type === "password";
2801
+ value.type = hidden ? "text" : "password";
2802
+ toggle.textContent = t(hidden ? "resources.hide" : "resources.show");
2803
+ });
2804
+ controls.append(value, toggle, copyControl(token.token, t("copy.action")));
2805
+ } else {
2806
+ controls.append(node3("span", t("resources.legacy"), "muted"), button(t("resources.reissue"), () => {
2807
+ if (window.confirm(t("resources.confirmReissue"))) {
2808
+ mutate("tokens_rotate", { principal: token.principal });
2809
+ }
2810
+ }));
2811
+ }
2812
+ controls.append(button(t("resources.delete"), () => remove("tokens_delete", { principal: token.principal }, t("resources.confirmToken", { name: token.principal })), true));
2813
+ card.append(summary, controls);
2814
+ body.append(card);
2815
+ }
2816
+ } else {
2817
+ const host = typeof location === "undefined" ? "127.0.0.1" : location.hostname;
2818
+ const form = node3("form", "", "token-create-form");
2819
+ const nameLabel = node3("label", t("resources.name"));
2820
+ const name = node3("input");
2821
+ name.name = "code-name";
2822
+ name.pattern = "[A-Za-z0-9._-]{0,63}";
2823
+ name.maxLength = 63;
2824
+ name.placeholder = "workstation";
2825
+ name.disabled = pending;
2826
+ name.value = codeName;
2827
+ name.addEventListener("input", () => {
2828
+ codeName = name.value;
2829
+ });
2830
+ nameLabel.append(name);
2831
+ const submit = node3("button", t("resources.createCode"), "primary-button");
2832
+ submit.type = "submit";
2833
+ submit.disabled = pending;
2834
+ form.append(nameLabel, submit);
2835
+ form.addEventListener("submit", (event) => {
2836
+ event.preventDefault();
2837
+ mutate("pairing_codes_create", name.value.trim() ? { name: name.value.trim() } : {});
2838
+ });
2839
+ body.append(node3("p", t("resources.codesNote"), "muted"), form);
2840
+ if (created) {
2841
+ const command = "rovloom connector --join ws://" + host + ":" + created.device_port + "#" + created.code;
2842
+ const card = node3("article", "", "pairing-record code-created");
2843
+ const details = node3("div");
2844
+ details.append(node3("h3", created.name), node3("p", t("resources.codeCreatedNote"), "muted"));
2845
+ const block = node3("div", "", "command-snippet");
2846
+ block.append(node3("code", command), copyControl(command, t("copy.action")));
2847
+ card.append(details, block);
2848
+ body.append(card);
2849
+ created = undefined;
2850
+ }
2851
+ const port = data?.device_port ?? 9443;
2852
+ for (const item of data?.items ?? []) {
2853
+ const entry = item;
2854
+ const card = node3("article", "", "pairing-record");
2855
+ const details = node3("div");
2856
+ const label = t("resources.code_" + entry.status);
2857
+ details.append(node3("h3", entry.name), node3("code", entry.machine_id), node3("p", label + " \xB7 " + i18n.timestamp(entry.created_at) + (entry.used_at ? " \xB7 " + i18n.timestamp(entry.used_at) : ""), "muted"));
2858
+ card.append(details);
2859
+ if (entry.status === "unused" && entry.code) {
2860
+ const command = "rovloom connector --join ws://" + host + ":" + port + "#" + entry.code;
2861
+ const block = node3("div", "", "command-snippet");
2862
+ block.append(node3("code", command), copyControl(command, t("copy.action")));
2863
+ card.append(block);
2864
+ card.append(button(t("resources.revokeCode"), () => remove("pairing_codes_revoke", { code_id: entry.code_id }, t("resources.confirmRevokeCode")), true));
2865
+ }
2866
+ body.append(card);
2783
2867
  }
2784
2868
  }
2869
+ if (data?.items.length === 0 && key === "connect") {
2870
+ body.append(node3("p", t("resources.emptyTokens"), "empty"));
2871
+ }
2785
2872
  }
2786
- if (![...entries.values()].some(unfinished)) {
2787
- stopPolling();
2788
- }
2789
- }
2790
- function ensurePolling() {
2791
- if (disposed || timer || ![...entries.values()].some(unfinished)) {
2792
- return;
2793
- }
2794
- timer = setInterval(() => {
2795
- poll();
2796
- }, pollMs);
2873
+ region.replaceChildren(body, feedback);
2797
2874
  }
2798
- function stopPolling() {
2799
- if (timer) {
2800
- clearInterval(timer);
2801
- timer = undefined;
2802
- }
2875
+ function clear() {
2876
+ active = false;
2877
+ key = "";
2878
+ data = undefined;
2879
+ tokenName = "";
2880
+ tokenDays = "365";
2881
+ codeName = "";
2882
+ created = undefined;
2883
+ version++;
2884
+ controller.abort();
2885
+ pending = false;
2886
+ region.replaceChildren();
2887
+ region.hidden = true;
2803
2888
  }
2804
- form.addEventListener("submit", (event) => {
2805
- event.preventDefault();
2806
- const name = nameInput.value.trim();
2807
- const policy = policyInput.value.trim();
2808
- if (!name || !policy) {
2809
- return;
2810
- }
2811
- start({ action: "pairing_create", name, policy_ref: policy, labels: [] });
2812
- });
2813
- confirm.addEventListener("click", () => {
2814
- if (flow) {
2815
- flow.confirm().then(paintFlow);
2816
- }
2817
- });
2818
- check.addEventListener("click", () => {
2819
- if (flow) {
2820
- flow.checkReceipt().then(paintFlow);
2821
- }
2822
- });
2823
- cancel.addEventListener("click", () => {
2824
- flow?.cancel();
2825
- if (flow) {
2826
- paintFlow(flow.state());
2827
- }
2828
- if (flow?.state().phase === "cancelled") {
2829
- flow = undefined;
2830
- flowIntent = undefined;
2831
- }
2832
- });
2833
2889
  return {
2834
2890
  region,
2835
- update(localizer, isAvailable) {
2836
- i18n = localizer;
2837
- available = isAvailable;
2838
- region.hidden = !isAvailable;
2839
- const t = localizer.t.bind(localizer);
2840
- heading.textContent = t("pairing.title");
2841
- intro.textContent = t("pairing.intro");
2842
- nameCaption.textContent = t("pairing.name");
2843
- policyCaption.textContent = t("pairing.policy");
2844
- create.textContent = t("pairing.create");
2845
- cancel.textContent = t("pairing.cancel");
2846
- for (const entry of entries.values()) {
2847
- renderEntry(entry);
2848
- }
2849
- if (flow) {
2850
- paintFlow(flow.state());
2851
- }
2852
- if (isAvailable) {
2853
- ensurePolling();
2891
+ clear,
2892
+ dispose: clear,
2893
+ refresh() {
2894
+ if (active && key !== "device") {
2895
+ refresh();
2854
2896
  }
2855
2897
  },
2856
- suspend() {
2857
- stopPolling();
2858
- },
2859
- clear() {
2860
- stopPolling();
2861
- flow?.dispose();
2862
- flow = undefined;
2863
- flowIntent = undefined;
2864
- entries.clear();
2865
- list.replaceChildren();
2866
- reviewBox.hidden = true;
2867
- message.textContent = "";
2868
- },
2869
- dispose() {
2870
- disposed = true;
2871
- stopPolling();
2872
- flow?.dispose();
2898
+ update(next, localizer, enabled) {
2899
+ const nextKey = next.machineId ? "device" : next.section === "connect" || next.section === "pairing" ? next.section : "";
2900
+ if (!enabled || !nextKey) {
2901
+ clear();
2902
+ return;
2903
+ }
2904
+ const changed = key !== nextKey || route?.machineId !== next.machineId;
2905
+ const translated = language !== localizer.language;
2906
+ route = next;
2907
+ i18n = localizer;
2908
+ language = localizer.language;
2909
+ active = true;
2910
+ region.hidden = false;
2911
+ if (changed) {
2912
+ key = nextKey;
2913
+ data = undefined;
2914
+ created = undefined;
2915
+ version++;
2916
+ controller.abort();
2917
+ controller = new AbortController;
2918
+ pending = false;
2919
+ render();
2920
+ if (key !== "device") {
2921
+ refresh();
2922
+ }
2923
+ } else if (translated) {
2924
+ render();
2925
+ }
2873
2926
  }
2874
2927
  };
2875
2928
  }
2876
2929
 
2877
2930
  // src/webui/page.ts
2878
- function element(tag, text, className = "") {
2931
+ function element(tag, text = "", className = "") {
2879
2932
  const result = document.createElement(tag);
2880
2933
  result.textContent = text;
2881
2934
  result.className = className;
@@ -2884,21 +2937,51 @@ function element(tag, text, className = "") {
2884
2937
  async function unavailableRead() {
2885
2938
  throw new ReadError2("unavailable");
2886
2939
  }
2887
- function mountWebUi(root, read = unavailableRead, clock, management) {
2888
- const header = element("header", "", "topbar");
2940
+ function mountWebUi(root, read = unavailableRead, clock, management, resources) {
2941
+ const sidebar = element("aside", "", "sidebar");
2889
2942
  const brand = element("a", "", "brand");
2890
2943
  brand.href = "#";
2891
2944
  const logo = element("img", "", "brand-logo");
2892
- logo.src = "./rovloom-mark-transparent.png";
2945
+ logo.src = "./rovloom-logo.png";
2893
2946
  logo.alt = "Rovloom";
2894
- logo.width = 48;
2895
- logo.height = 48;
2896
- brand.append(logo, element("span", "Rovloom", "brand-name"));
2947
+ logo.width = 180;
2948
+ logo.height = 60;
2949
+ brand.append(logo);
2950
+ const sidebarCaption = element("p", "", "sidebar-caption");
2951
+ const navHeading = element("p", "", "nav-heading");
2952
+ const navigation = element("nav", "", "sidebar-nav");
2953
+ const navLinks = ["overview", "devices", "pairing", "connect"].map((section) => {
2954
+ const link = element("a", "", "nav-link");
2955
+ link.href = section === "overview" ? "#" : "#view=" + section;
2956
+ const label = element("span");
2957
+ link.append(uiIcon(section), label);
2958
+ navigation.append(link);
2959
+ return { section, link, label };
2960
+ });
2961
+ const resourceHeading = element("p", "", "nav-heading resource-heading");
2962
+ const docs = element("a", "", "nav-link docs-link");
2963
+ docs.target = "_blank";
2964
+ docs.rel = "noopener noreferrer";
2965
+ const docsLabel = element("span");
2966
+ docs.append(uiIcon("book"), docsLabel);
2967
+ const sidebarStatus = element("div", "", "sidebar-status");
2968
+ const statusIcon = element("span", "", "sidebar-gateway-icon");
2969
+ statusIcon.append(uiIcon("gateway"));
2970
+ const statusCopy = element("div");
2971
+ const statusLabel = element("strong", "Gateway");
2972
+ const statusValue = element("span", "", "sidebar-status-value");
2973
+ statusCopy.append(statusLabel, statusValue);
2974
+ sidebarStatus.append(statusIcon, statusCopy);
2975
+ sidebar.append(brand, sidebarCaption, navHeading, navigation, resourceHeading, docs, sidebarStatus);
2976
+ const workspace = element("div", "", "workspace");
2977
+ const header = element("header", "", "topbar");
2978
+ const headerLabel = element("div", "", "header-label");
2979
+ headerLabel.append(uiIcon("overview"));
2897
2980
  const title = element("span", "", "brand-context");
2981
+ headerLabel.append(title);
2898
2982
  const preferencesBar = element("div", "", "preferences");
2899
- const commandbar = element("nav", "", "commandbar");
2900
- const sectionLink = element("a", "", "section-nav");
2901
- sectionLink.href = "#";
2983
+ const commandbar = element("div", "", "commandbar");
2984
+ commandbar.setAttribute("role", "toolbar");
2902
2985
  const toolbar = element("div", "", "toolbar");
2903
2986
  const refresh = element("button", "");
2904
2987
  refresh.type = "button";
@@ -2912,25 +2995,24 @@ function mountWebUi(root, read = unavailableRead, clock, management) {
2912
2995
  content.tabIndex = -1;
2913
2996
  refresh.setAttribute("aria-controls", content.id);
2914
2997
  cancel.setAttribute("aria-controls", content.id);
2915
- const language = createPreferenceControl("language", [
2916
- ["system", "language.system"],
2917
- ["zh-CN", "language.chinese"],
2918
- ["en", "language.english"]
2919
- ], (value) => preferences.setLanguage(value));
2920
- const theme = createPreferenceControl("theme", [
2921
- ["system", "theme.system"],
2922
- ["light", "theme.light"],
2923
- ["dark", "theme.dark"]
2924
- ], (value) => preferences.setTheme(value));
2998
+ const language = createPreferenceControl("language", (value) => preferences.setLanguage(value));
2999
+ const theme = createPreferenceControl("theme", (value) => preferences.setTheme(value));
2925
3000
  preferencesBar.append(language.root, theme.root);
2926
3001
  toolbar.append(indicator, refresh, cancel);
2927
- commandbar.append(sectionLink, toolbar);
2928
- header.append(brand, title, preferencesBar);
3002
+ commandbar.append(toolbar);
3003
+ const headerActions = element("div", "", "topbar-actions");
3004
+ headerActions.append(commandbar, preferencesBar);
3005
+ header.append(headerLabel, headerActions);
2929
3006
  const skip = element("a", "", "skip-link");
2930
3007
  skip.href = "#management-content";
2931
3008
  const footer = element("footer", "", "muted");
2932
- root.replaceChildren(skip, header, commandbar, content, footer);
3009
+ const copyFeedback = element("span", "", "visually-hidden");
3010
+ copyFeedback.setAttribute("role", "status");
3011
+ copyFeedback.setAttribute("aria-live", "polite");
3012
+ workspace.append(header, content, copyFeedback, footer);
3013
+ root.replaceChildren(skip, sidebar, workspace);
2933
3014
  let route = parseRoute(location.hash);
3015
+ let filter = { query: "", status: "all" };
2934
3016
  let reader;
2935
3017
  let disposed = false;
2936
3018
  let i18n = createLocalizer("en");
@@ -2954,20 +3036,21 @@ function mountWebUi(root, read = unavailableRead, clock, management) {
2954
3036
  if (actions) {
2955
3037
  footer.before(actions.region);
2956
3038
  }
2957
- const pairing = management ? createPairingActions({
2958
- transport: management,
2959
- authorizationLost(problem) {
2960
- if (disposed) {
2961
- return;
3039
+ const resourcePanel = resources ? createResourcePanel({
3040
+ transport: resources,
3041
+ committed(machine) {
3042
+ if (machine) {
3043
+ location.hash = "#view=devices";
2962
3044
  }
3045
+ },
3046
+ authorizationLost(problem) {
2963
3047
  reader.dispose();
2964
3048
  actions?.clear();
2965
- pairing?.clear();
2966
3049
  paint({ snapshot: null, pending: false, stale: true, ageMs: 0, problem }, route);
2967
3050
  }
2968
3051
  }) : undefined;
2969
- if (pairing) {
2970
- footer.before(pairing.region);
3052
+ if (resourcePanel) {
3053
+ footer.before(resourcePanel.region);
2971
3054
  }
2972
3055
  function paint(state, current) {
2973
3056
  refresh.disabled = state.pending || ["unauthenticated", "forbidden", "not_found"].includes(state.problem ?? "");
@@ -2975,14 +3058,32 @@ function mountWebUi(root, read = unavailableRead, clock, management) {
2975
3058
  indicator.textContent = i18n.t(state.pending ? "reader.pending" : state.snapshot ? state.stale ? "reader.stale" : "reader.snapshot" : "reader.empty");
2976
3059
  content.setAttribute("aria-busy", String(state.pending));
2977
3060
  indicator.dataset.state = state.pending ? "pending" : state.stale ? "stale" : state.snapshot ? "current" : "empty";
2978
- sectionLink.setAttribute("aria-current", current.machineId === null ? "page" : "location");
2979
3061
  if (["unauthenticated", "forbidden", "not_found"].includes(state.problem ?? "")) {
2980
3062
  actions?.clear();
2981
- pairing?.clear();
2982
3063
  }
2983
- pairing?.update(i18n, !!state.snapshot && !state.stale && state.snapshot.value.status.mode === "active");
2984
3064
  actions?.update(i18n, !!state.snapshot && !state.stale && state.snapshot.value.status.mode === "active", state.snapshot ? state.snapshot.observedAt + state.ageMs : undefined);
2985
- renderPage(content, state, current, i18n, !!actions);
3065
+ const section = current.machineId ? "devices" : current.section ?? "overview";
3066
+ content.dataset.section = section;
3067
+ for (const item of navLinks) {
3068
+ if (item.section === section) {
3069
+ item.link.setAttribute("aria-current", "page");
3070
+ } else {
3071
+ item.link.removeAttribute("aria-current");
3072
+ }
3073
+ }
3074
+ title.textContent = i18n.t("nav." + section);
3075
+ const active = !!state.snapshot && !state.stale && state.snapshot.value.status.mode === "active";
3076
+ sidebarStatus.dataset.state = active ? "current" : "unknown";
3077
+ statusValue.textContent = i18n.t(active ? "gateway.active" : state.snapshot ? state.stale ? "reader.stale" : "gateway.degraded" : "reader.empty");
3078
+ renderPage(content, state, current, i18n, !!actions, filter);
3079
+ const authorized = active && !["unauthenticated", "forbidden", "not_found"].includes(state.problem ?? "");
3080
+ resourcePanel?.update(current, i18n, authorized);
3081
+ if (resourcePanel && section === "connect" && authorized) {
3082
+ const guide = content.querySelector(".connection-guide");
3083
+ if (guide) {
3084
+ guide.hidden = true;
3085
+ }
3086
+ }
2986
3087
  }
2987
3088
  function applyPreferences(state) {
2988
3089
  const languageChanged = i18n.language !== state.language;
@@ -2994,15 +3095,23 @@ function mountWebUi(root, read = unavailableRead, clock, management) {
2994
3095
  document.documentElement.dataset.theme = state.theme;
2995
3096
  document.title = t("app.title");
2996
3097
  brand.setAttribute("aria-label", t("app.navigation"));
2997
- title.textContent = t("app.console");
2998
- sectionLink.textContent = t("devices.heading");
3098
+ title.textContent = t("nav.console");
3099
+ sidebarCaption.textContent = t("app.console");
3100
+ navHeading.textContent = t("nav.workspace");
3101
+ resourceHeading.textContent = t("nav.resources");
3102
+ navigation.setAttribute("aria-label", t("app.subtitle"));
3103
+ docsLabel.textContent = t("nav.docs");
3104
+ docs.href = `https://github.com/ArnoChenFx/rovloom/blob/main/docs/QUICK-START${state.language === "zh-CN" ? "-zh" : ""}.md`;
3105
+ for (const item of navLinks) {
3106
+ item.label.textContent = t("nav." + item.section);
3107
+ }
2999
3108
  commandbar.setAttribute("aria-label", t("app.subtitle"));
3000
3109
  refresh.textContent = t("action.refresh");
3001
3110
  cancel.textContent = t("action.cancel");
3002
3111
  skip.textContent = t("app.skip");
3003
3112
  footer.textContent = t(actions ? "app.managedFooter" : "app.footer");
3004
- language.update(i18n, state.languageChoice);
3005
- theme.update(i18n, state.themeChoice);
3113
+ language.update(i18n, state.language);
3114
+ theme.update(i18n, state.theme);
3006
3115
  if (languageChanged && reader) {
3007
3116
  paint(reader.state(), route);
3008
3117
  }
@@ -3024,6 +3133,7 @@ function mountWebUi(root, read = unavailableRead, clock, management) {
3024
3133
  content.focus();
3025
3134
  }
3026
3135
  function refreshPage() {
3136
+ resourcePanel?.refresh();
3027
3137
  reader.refresh();
3028
3138
  }
3029
3139
  function cancelRead() {
@@ -3064,11 +3174,40 @@ function mountWebUi(root, read = unavailableRead, clock, management) {
3064
3174
  actions.open({ action, machine_id: device.machine_id }, button);
3065
3175
  }
3066
3176
  }
3177
+ function searchDevices(event) {
3178
+ if (event.target instanceof HTMLInputElement && event.target.name === "device-search") {
3179
+ filter.query = event.target.value;
3180
+ paint(reader.state(), route);
3181
+ }
3182
+ }
3183
+ function filterDevices(event) {
3184
+ const button = event.target instanceof Element ? event.target.closest("[data-device-filter]") : null;
3185
+ const selected = button?.dataset.deviceFilter;
3186
+ if (selected === "all" || selected === "online" || selected === "attention") {
3187
+ filter.status = selected;
3188
+ paint(reader.state(), route);
3189
+ }
3190
+ }
3191
+ async function copy(event) {
3192
+ const button = event.target instanceof Element ? event.target.closest("button[data-copy]") : null;
3193
+ if (!button || button.disabled || !root.contains(button)) {
3194
+ return;
3195
+ }
3196
+ const value = button.dataset.copy;
3197
+ if (value === undefined) {
3198
+ return;
3199
+ }
3200
+ copyFeedback.textContent = "";
3201
+ const success = await copyText(value);
3202
+ if (!disposed) {
3203
+ copyFeedback.textContent = i18n.t(success ? "copy.done" : "copy.failed");
3204
+ button.title = copyFeedback.textContent;
3205
+ button.dataset.copied = String(success);
3206
+ }
3207
+ }
3067
3208
  function navigate() {
3209
+ filter = { query: "", status: "all" };
3068
3210
  actions?.suspend();
3069
- pairing?.suspend();
3070
- language.close();
3071
- theme.close();
3072
3211
  if (location.hash === "#management-content") {
3073
3212
  content.focus();
3074
3213
  return;
@@ -3081,7 +3220,6 @@ function mountWebUi(root, read = unavailableRead, clock, management) {
3081
3220
  function visibility() {
3082
3221
  if (document.visibilityState === "hidden") {
3083
3222
  actions?.suspend();
3084
- pairing?.suspend();
3085
3223
  }
3086
3224
  reader.visible(document.visibilityState !== "hidden");
3087
3225
  }
@@ -3095,10 +3233,8 @@ function mountWebUi(root, read = unavailableRead, clock, management) {
3095
3233
  }
3096
3234
  function hide() {
3097
3235
  actions?.suspend();
3098
- pairing?.suspend();
3099
- language.close();
3100
- theme.close();
3101
3236
  reader.dispose();
3237
+ resourcePanel?.clear();
3102
3238
  content.replaceChildren();
3103
3239
  }
3104
3240
  function dispose() {
@@ -3107,9 +3243,12 @@ function mountWebUi(root, read = unavailableRead, clock, management) {
3107
3243
  }
3108
3244
  disposed = true;
3109
3245
  reader.dispose();
3246
+ resourcePanel?.dispose();
3110
3247
  actions?.dispose();
3111
- pairing?.dispose();
3112
3248
  content.removeEventListener("click", deviceAction);
3249
+ content.removeEventListener("click", filterDevices);
3250
+ content.removeEventListener("input", searchDevices);
3251
+ root.removeEventListener("click", copy);
3113
3252
  skip.removeEventListener("click", skipContent);
3114
3253
  refresh.removeEventListener("click", refreshPage);
3115
3254
  cancel.removeEventListener("click", cancelRead);
@@ -3126,6 +3265,9 @@ function mountWebUi(root, read = unavailableRead, clock, management) {
3126
3265
  refresh.addEventListener("click", refreshPage);
3127
3266
  cancel.addEventListener("click", cancelRead);
3128
3267
  content.addEventListener("click", deviceAction);
3268
+ content.addEventListener("click", filterDevices);
3269
+ content.addEventListener("input", searchDevices);
3270
+ root.addEventListener("click", copy);
3129
3271
  window.addEventListener("hashchange", navigate);
3130
3272
  document.addEventListener("visibilitychange", visibility);
3131
3273
  window.addEventListener("pageshow", show);
@@ -3509,7 +3651,6 @@ export {
3509
3651
  createManagementPageCsrf,
3510
3652
  createManagementPageRead,
3511
3653
  createManagementWriteClient,
3512
- createPairingManagementFlow,
3513
3654
  createSnapshotReader2 as createSnapshotReader,
3514
3655
  mountWebUi,
3515
3656
  parseRoute,
@@ -3517,4 +3658,4 @@ export {
3517
3658
  unavailableRead
3518
3659
  };
3519
3660
 
3520
- //# debugId=D52975500C6ECA9664756E2164756E21
3661
+ //# debugId=57232E8032FB7D7964756E2164756E21