rovloom 0.4.0 → 0.6.0

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 (39) hide show
  1. package/README.md +21 -41
  2. package/dist/src/admin/index.js +1 -1
  3. package/dist/src/{chunk-smpevv7n.js → chunk-0dqx2mmz.js} +9 -16
  4. package/dist/src/{chunk-z1vhdr3c.js → chunk-0qgk7mhz.js} +16 -15
  5. package/dist/src/{chunk-qfyefed3.js → chunk-1bf0wmgk.js} +1 -1
  6. package/dist/src/{chunk-ymsx7hhd.js → chunk-3dvn33c4.js} +61 -62
  7. package/dist/src/chunk-5b8g62kw.js +438 -0
  8. package/dist/src/chunk-5j0gj6w2.js +7 -0
  9. package/dist/src/{chunk-v1httsp6.js → chunk-8ykrqhxh.js} +10 -4
  10. package/dist/src/{chunk-76wgt2g5.js → chunk-9zkq0kc8.js} +14 -8
  11. package/dist/src/{chunk-6hxy1fcg.js → chunk-bspzg8er.js} +2 -2
  12. package/dist/src/{chunk-dtj9y82v.js → chunk-d2kz25f2.js} +269 -39
  13. package/dist/src/{chunk-khmgp03q.js → chunk-ez7d5dac.js} +77 -16
  14. package/dist/src/{chunk-emcefrc1.js → chunk-g27b2gmx.js} +3 -4
  15. package/dist/src/{chunk-keg92d31.js → chunk-gyvqqnjb.js} +34 -59
  16. package/dist/src/{chunk-bjdqj43d.js → chunk-hpzr94hm.js} +23 -6
  17. package/dist/src/{chunk-t3pn1435.js → chunk-jf4r0r44.js} +4 -4
  18. package/dist/src/{chunk-4etdq0cp.js → chunk-jka7v3wb.js} +4 -7
  19. package/dist/src/{chunk-5vx1b9z8.js → chunk-m9c1pnv9.js} +5 -5
  20. package/dist/src/{chunk-7k9g0x5e.js → chunk-mg0fd6ph.js} +1 -1
  21. package/dist/src/{chunk-1fyfrgte.js → chunk-n6wtjyfb.js} +10 -18
  22. package/dist/src/{chunk-nyn75b8v.js → chunk-qjh9x872.js} +30 -1
  23. package/dist/src/{chunk-xmkgq3nh.js → chunk-s3ydr65k.js} +2 -2
  24. package/dist/src/{chunk-4af38z2b.js → chunk-srfbjwtk.js} +4 -3
  25. package/dist/src/{chunk-8x6ygvhh.js → chunk-ssp57p43.js} +27 -31
  26. package/dist/src/{chunk-qab3m6nh.js → chunk-wxqhscsw.js} +43 -7
  27. package/dist/src/{chunk-7q96e161.js → chunk-x0td5wn4.js} +6 -12
  28. package/dist/src/{chunk-ydmabg5t.js → chunk-ytpfmcp1.js} +7 -6
  29. package/dist/src/{chunk-wdr3sdsm.js → chunk-yv9px0f5.js} +60 -32
  30. package/dist/src/{chunk-se5daacy.js → chunk-z7kkfrgz.js} +1 -1
  31. package/dist/src/{chunk-fd9n51mc.js → chunk-zjkjjm81.js} +87 -61
  32. package/dist/src/cli/index.js +27 -19
  33. package/dist/src/cli/main.js +18 -13
  34. package/dist/src/mcp/index.js +6 -5
  35. package/dist/src/runtime/index.js +2 -2
  36. package/dist/src/service/index.js +3 -4
  37. package/dist/src/store.js +1 -1
  38. package/dist/src/webui/index.js +1107 -158
  39. package/package.json +1 -1
@@ -1,4 +1,7 @@
1
1
  // @bun
2
+ import {
3
+ DEFAULT_PAIRING_POLICY_REF
4
+ } from "../chunk-5j0gj6w2.js";
2
5
  import {
3
6
  ReadError2,
4
7
  createSnapshotReader2,
@@ -10,6 +13,111 @@ import {
10
13
 
11
14
  // src/webui/i18n.ts
12
15
  var chinese = Object.freeze({
16
+ "resources.previous": "\u4E0A\u4E00\u9875",
17
+ "resources.next": "\u4E0B\u4E00\u9875",
18
+ "resources.tokens": "MCP Token",
19
+ "resources.pairings": "\u914D\u5BF9\u8BB0\u5F55",
20
+ "resources.name": "\u540D\u79F0",
21
+ "resources.expiry": "\u6709\u6548\u671F",
22
+ "resources.days": "{count} \u5929",
23
+ "resources.create": "\u521B\u5EFA Token",
24
+ "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",
25
+ "resources.endpoint": "MCP \u5730\u5740",
26
+ "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",
27
+ "resources.expiresAt": "\u6709\u6548\u81F3 {time}",
28
+ "resources.active": "\u6709\u6548",
29
+ "resources.expired": "\u5DF2\u8FC7\u671F",
30
+ "resources.show": "\u663E\u793A",
31
+ "resources.hide": "\u9690\u85CF",
32
+ "resources.legacy": "\u65E7 Token \u4EC5\u4FDD\u5B58\u6458\u8981\uFF0C\u65E0\u6CD5\u8FD8\u539F\u3002",
33
+ "resources.reissue": "\u91CD\u65B0\u7B7E\u53D1",
34
+ "resources.confirmReissue": "\u91CD\u65B0\u7B7E\u53D1\u4F1A\u7ACB\u5373\u4F7F\u65E7 Token \u5931\u6548\uFF0C\u662F\u5426\u7EE7\u7EED\uFF1F",
35
+ "resources.delete": "\u5220\u9664",
36
+ "resources.deleteDevice": "\u5220\u9664\u8BBE\u5907",
37
+ "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",
38
+ "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",
39
+ "resources.confirmToken": "\u5220\u9664 Token\u300C{name}\u300D\uFF1F\u4F7F\u7528\u5B83\u7684\u5BA2\u6237\u7AEF\u5C06\u65E0\u6CD5\u7EE7\u7EED\u8BBF\u95EE\u3002",
40
+ "resources.confirmPairing": "\u5220\u9664\u8FD9\u6761\u914D\u5BF9\u8BB0\u5F55\uFF1F\u672A\u5B8C\u6210\u7684\u914D\u5BF9\u4F1A\u7EC8\u6B62\uFF0C\u5DF2\u914D\u5BF9\u8BBE\u5907\u4ECD\u4FDD\u7559\u3002",
41
+ "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",
42
+ "resources.emptyPairings": "\u6682\u65E0\u914D\u5BF9\u8BB0\u5F55\u3002",
43
+ "resources.loading": "\u6B63\u5728\u52A0\u8F7D\u2026",
44
+ "resources.saving": "\u6B63\u5728\u4FDD\u5B58\u2026",
45
+ "resources.saved": "\u5DF2\u4FDD\u5B58",
46
+ "resources.failed": "\u64CD\u4F5C\u672A\u5B8C\u6210\uFF0C\u8BF7\u5237\u65B0\u540E\u91CD\u8BD5\u3002",
47
+ "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",
48
+ "resources.pending": "\u7B49\u5F85\u914D\u5BF9",
49
+ "resources.paired": "\u5DF2\u914D\u5BF9",
50
+ "resources.cancelled": "\u5DF2\u53D6\u6D88",
51
+ "nav.workspace": "\u5DE5\u4F5C\u7A7A\u95F4",
52
+ "nav.overview": "\u6982\u89C8",
53
+ "nav.devices": "\u8BBE\u5907\u7BA1\u7406",
54
+ "nav.pairing": "\u914D\u5BF9\u8BBE\u5907",
55
+ "nav.connect": "\u8FDE\u63A5 MCP",
56
+ "nav.resources": "\u5E2E\u52A9\u4E0E\u8D44\u6E90",
57
+ "nav.docs": "\u5FEB\u901F\u5F00\u59CB",
58
+ "nav.console": "\u7BA1\u7406\u63A7\u5236\u53F0",
59
+ "overview.eyebrow": "\u4F60\u7684\u5DE5\u4F5C\u7A7A\u95F4\uFF0C\u5C3D\u5728\u638C\u63E1",
60
+ "overview.title": "\u5DE5\u4F5C\u53F0",
61
+ "overview.description": "\u67E5\u770B\u7F51\u5173\u4E0E\u8BBE\u5907\u72B6\u6001\uFF0C\u8BA9\u8FDC\u7A0B\u5DE5\u4F5C\u6D41\u987A\u7545\u8FDE\u63A5\u3002",
62
+ "overview.visible": "\u672C\u9875\u8BBE\u5907",
63
+ "overview.visibleNote": "\u5F53\u524D\u6388\u6743\u8303\u56F4\u4E2D\u7684\u5217\u8868\u9875",
64
+ "overview.online": "\u8FDE\u63A5\u5DF2\u9A8C\u8BC1",
65
+ "overview.onlineNote": "\u8FDE\u63A5\u4E0E\u8EAB\u4EFD\u8BA4\u8BC1\u5747\u6709\u6548",
66
+ "overview.attention": "\u9700\u8981\u5173\u6CE8",
67
+ "overview.attentionNote": "\u672C\u9875\u5C1A\u672A\u9A8C\u8BC1\u8FDE\u63A5\u7684\u8BBE\u5907",
68
+ "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",
69
+ "overview.gateway": "\u7F51\u5173\u8FD0\u884C\u72B6\u6001",
70
+ "overview.available": "\u53EF\u7528",
71
+ "overview.degraded": "\u53D7\u9650",
72
+ "overview.unavailable": "\u4E0D\u53EF\u7528",
73
+ "overview.unverified": "\u672A\u6838\u5B9E",
74
+ "overview.store": "\u6570\u636E\u5B58\u50A8",
75
+ "overview.transport": "\u8BBE\u5907\u901A\u9053",
76
+ "overview.native": "\u539F\u751F\u8FD0\u884C\u73AF\u5883",
77
+ "overview.manage": "\u7BA1\u7406\u8BBE\u5907",
78
+ "overview.deviceSection": "\u5DF2\u8FDE\u63A5\u7684\u5DE5\u4F5C\u7A7A\u95F4",
79
+ "overview.deviceSubtitle": "\u4ECE\u4E00\u53F0\u8BBE\u5907\u5F00\u59CB\uFF0C\u5C06\u4F60\u7684\u5DE5\u5177\u548C\u5DE5\u4F5C\u533A\u8FDE\u63A5\u8D77\u6765\u3002",
80
+ "devices.search": "\u641C\u7D22\u5F53\u524D\u9875\u8BBE\u5907",
81
+ "devices.searchPlaceholder": "\u641C\u7D22\u8BBE\u5907\u540D\u79F0\u3001ID \u6216\u6807\u7B7E\u2026",
82
+ "devices.filterAll": "\u5168\u90E8",
83
+ "devices.filterOnline": "\u5DF2\u9A8C\u8BC1\u8FDE\u63A5",
84
+ "devices.filterAttention": "\u9700\u8981\u5173\u6CE8",
85
+ "devices.results": "\u5F53\u524D\u9875\u663E\u793A {shown} / {count} \u53F0\u8BBE\u5907",
86
+ "devices.noMatches": "\u6CA1\u6709\u5339\u914D\u7684\u8BBE\u5907",
87
+ "devices.noMatchesHint": "\u8BD5\u8BD5\u5176\u4ED6\u540D\u79F0\u3001ID \u6216\u6807\u7B7E\uFF0C\u6216\u5207\u6362\u72B6\u6001\u7B5B\u9009\u3002",
88
+ "devices.inspect": "\u67E5\u770B\u8BE6\u60C5",
89
+ "devices.record": "\u89C2\u6D4B\u8BE6\u60C5",
90
+ "devices.open": "\u67E5\u770B {name} \u7684\u8BE6\u60C5",
91
+ "pairing.pageTitle": "\u8FDE\u63A5\u4E00\u53F0\u65B0\u8BBE\u5907",
92
+ "pairing.pageDescription": "\u521B\u5EFA\u7533\u8BF7\u3001\u8FD0\u884C Connector\u3001\u6838\u5BF9\u4EE3\u7801\u3002\u4E09\u6B65\u628A\u5DE5\u4F5C\u7AD9\u52A0\u5165\u4F60\u7684\u5DE5\u4F5C\u7A7A\u95F4\u3002",
93
+ "pairing.stepCreate": "\u521B\u5EFA\u914D\u5BF9\u7533\u8BF7",
94
+ "pairing.stepRun": "\u8FD0\u884C Connector",
95
+ "pairing.stepApprove": "\u6838\u5BF9\u5E76\u6279\u51C6",
96
+ "pairing.formTitle": "\u6DFB\u52A0\u4F60\u7684\u5DE5\u4F5C\u7AD9",
97
+ "pairing.namePlaceholder": "\u4F8B\u5982 office-workstation",
98
+ "pairing.nameHint": "\u4F7F\u7528 1\u201363 \u4F4D\u5C0F\u5199\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u70B9\u3001\u4E0B\u5212\u7EBF\u6216\u8FDE\u5B57\u7B26\u3002",
99
+ "pairing.advanced": "\u9AD8\u7EA7\uFF1A\u914D\u5BF9\u7B56\u7565",
100
+ "pairing.guideTitle": "\u7136\u540E\uFF0C\u5728\u5DE5\u4F5C\u7AD9\u4E0A\u8FD0\u884C",
101
+ "pairing.guideDescription": "\u5728\u5B89\u88C5\u4E86 Luvus \u6216 Herdr \u7684\u8BBE\u5907\u7EC8\u7AEF\u4E2D\u8FD0\u884C\u4EE5\u4E0B\u547D\u4EE4\uFF0C\u6309\u63D0\u793A\u8F93\u5165 Gateway \u5730\u5740\u548C\u7533\u8BF7\u7F16\u53F7\u3002",
102
+ "pairing.guideNote": "\u8BA4\u9886\u540E\uFF0C\u6838\u5BF9\u7F51\u9875\u548C\u7EC8\u7AEF\u4E0A\u7684\u4EE3\u7801\uFF0C\u518D\u6279\u51C6\u672C\u673A\u539F\u751F\u5B89\u88C5\u8BBF\u95EE\u3002",
103
+ "pairing.loginRequired": "\u914D\u5BF9\u9700\u8981\u7BA1\u7406\u4F1A\u8BDD",
104
+ "pairing.loginHint": "\u4ECE Gateway \u7684\u7BA1\u7406\u5165\u53E3\u767B\u5F55\u540E\uFF0C\u5373\u53EF\u521B\u5EFA\u7533\u8BF7\u5E76\u6279\u51C6\u65B0\u8BBE\u5907\u3002",
105
+ "connect.title": "\u628A\u5DE5\u4F5C\u7A7A\u95F4\u8FDE\u63A5\u5230 AI",
106
+ "connect.description": "\u8BBE\u5907\u5B8C\u6210\u914D\u5BF9\u540E\uFF0C\u4E3A AI \u5BA2\u6237\u7AEF\u7B7E\u53D1\u4E13\u7528 MCP Bearer\u3002",
107
+ "connect.stepOne": "\u627E\u5230\u5DF2\u6709\u8C03\u7528\u65B9",
108
+ "connect.stepOneNote": "\u5728 Gateway \u673A\u5668\u4E0A\u8FD0\u884C\uFF0C\u8BB0\u4E0B\u5217\u8868\u4E2D\u7684\u5B9E\u9645 caller ID\u3002",
109
+ "connect.stepTwo": "\u4E3A\u5BA2\u6237\u7AEF\u521B\u5EFA\u4EE4\u724C",
110
+ "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",
111
+ "connect.stepThree": "\u914D\u7F6E\u4F60\u7684 AI \u5BA2\u6237\u7AEF",
112
+ "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",
113
+ "connect.docker": "Docker \u90E8\u7F72\u547D\u4EE4",
114
+ "connect.docs": "\u67E5\u770B\u5B8C\u6574\u8FDE\u63A5\u6307\u5357",
115
+ "copy.action": "\u590D\u5236\u547D\u4EE4",
116
+ "copy.id": "\u590D\u5236\u7533\u8BF7\u7F16\u53F7",
117
+ "copy.done": "\u5DF2\u590D\u5236",
118
+ "copy.failed": "\u672A\u80FD\u590D\u5236\uFF0C\u8BF7\u624B\u52A8\u9009\u62E9\u6587\u672C",
119
+ "empty.title": "\u4F60\u7684\u7B2C\u4E00\u53F0\u8BBE\u5907\uFF0C\u4ECE\u8FD9\u91CC\u5F00\u59CB",
120
+ "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",
13
121
  "provider.disable": "\u7981\u7528\u6B64 provider",
14
122
  "provider.target": "Provider \u76EE\u6807",
15
123
  "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",
@@ -18,8 +126,9 @@ var chinese = Object.freeze({
18
126
  "app.subtitle": "\u8BBE\u5907\u4E0E\u8FDE\u63A5\u7BA1\u7406",
19
127
  "app.console": "Gateway \u63A7\u5236\u53F0",
20
128
  "app.skip": "\u8DF3\u5230\u7BA1\u7406\u5185\u5BB9",
21
- "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",
22
- "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",
129
+ "app.managedFooter": "Rovloom \xB7 \u4F60\u7684\u8BBE\u5907\u3001Agent \u4E0E\u5DE5\u4F5C\u6D41\u3002",
130
+ "app.footer": "Rovloom \xB7 Gateway \u63A7\u5236\u53F0",
131
+ "preference.switch": "\u5207\u6362\u4E3A {value}",
23
132
  "language.label": "\u8BED\u8A00",
24
133
  "language.system": "\u8DDF\u968F\u6D4F\u89C8\u5668",
25
134
  "language.chinese": "\u7B80\u4F53\u4E2D\u6587",
@@ -57,7 +166,7 @@ var chinese = Object.freeze({
57
166
  "manage.nativeEffect": "\u6B64\u64CD\u4F5C\u4E0D\u4FDD\u8BC1\u505C\u6B62\u6216\u56DE\u6EDA\u539F\u751F\u5DE5\u4F5C\u3002",
58
167
  "manage.action": "\u64CD\u4F5C",
59
168
  "pairing.title": "\u5728\u7EBF\u6DFB\u52A0\u8BBE\u5907",
60
- "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",
169
+ "pairing.intro": "\u4E3A\u5DE5\u4F5C\u7AD9\u8D77\u4E00\u4E2A\u540D\u79F0\u3002\u521B\u5EFA\u914D\u5BF9\u7533\u8BF7\u540E\uFF0C\u6309\u53F3\u4FA7\u6307\u5F15\u5B8C\u6210\u8FDE\u63A5\u3002",
61
170
  "pairing.name": "\u65B0\u8BBE\u5907\u540D\u79F0",
62
171
  "pairing.policy": "\u6388\u6743\u653F\u7B56\u5F15\u7528",
63
172
  "pairing.create": "\u65B0\u5EFA\u914D\u5BF9\u7533\u8BF7",
@@ -67,7 +176,7 @@ var chinese = Object.freeze({
67
176
  "pairing.cancel": "\u53D6\u6D88",
68
177
  "pairing.created": "\u7533\u8BF7\u5DF2\u521B\u5EFA\u3002\u628A\u7533\u8BF7\u7F16\u53F7\u4EA4\u7ED9\u65B0\u8BBE\u5907\uFF1A",
69
178
  "pairing.waitingClaim": "\u7B49\u5F85\u8BBE\u5907\u63D0\u4EA4\u8BA4\u9886\u2026",
70
- "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",
179
+ "pairing.claimArrived": "\u8BBE\u5907\u5DF2\u53D1\u8D77\u8FDE\u63A5\u3002\u786E\u8BA4\u4E0B\u65B9\u4EE3\u7801\u4E0E\u5DE5\u4F5C\u7AD9\u7EC8\u7AEF\u4E00\u81F4\u540E\uFF0C\u518D\u6279\u51C6\u914D\u5BF9\uFF1A",
71
180
  "pairing.userCode": "\u6838\u5BF9\u7801",
72
181
  "pairing.approve": "\u6279\u51C6\u7B7E\u53D1\u51ED\u636E",
73
182
  "pairing.approved": "\u5DF2\u6279\u51C6\uFF0C\u51ED\u636E\u5DF2\u6258\u7BA1\u4EA4\u4ED8\uFF0C\u7B49\u5F85\u8BBE\u5907\u53D6\u56DE\u2026",
@@ -99,7 +208,7 @@ var chinese = Object.freeze({
99
208
  "action.cancel": "\u53D6\u6D88\u8BFB\u53D6",
100
209
  "reader.pending": "\u6B63\u5728\u8BFB\u53D6\u2026",
101
210
  "reader.stale": "\u5FEB\u7167\u5DF2\u8FC7\u671F",
102
- "reader.snapshot": "\u53EA\u8BFB\u5FEB\u7167",
211
+ "reader.snapshot": "\u5DF2\u66F4\u65B0",
103
212
  "reader.empty": "\u65E0\u53EF\u7528\u5FEB\u7167",
104
213
  "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",
105
214
  "error.forbidden": "\u5F53\u524D\u7BA1\u7406\u5458\u6CA1\u6709\u6B64\u8303\u56F4\u7684\u67E5\u770B\u6743\u9650\u3002",
@@ -242,6 +351,111 @@ var chinese = Object.freeze({
242
351
  "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"
243
352
  });
244
353
  var english = Object.freeze({
354
+ "resources.previous": "Previous",
355
+ "resources.next": "Next",
356
+ "resources.tokens": "MCP tokens",
357
+ "resources.pairings": "Pairing records",
358
+ "resources.name": "Name",
359
+ "resources.expiry": "Valid for",
360
+ "resources.days": "{count} days",
361
+ "resources.create": "Create token",
362
+ "resources.tokenNote": "Create tokens for your AI clients. View, copy or delete them anytime. They work with current and future paired devices.",
363
+ "resources.endpoint": "MCP endpoint",
364
+ "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.",
365
+ "resources.expiresAt": "Expires {time}",
366
+ "resources.active": "Active",
367
+ "resources.expired": "Expired",
368
+ "resources.show": "Show",
369
+ "resources.hide": "Hide",
370
+ "resources.legacy": "This older token only has a hash and cannot be recovered.",
371
+ "resources.reissue": "Reissue",
372
+ "resources.confirmReissue": "Reissue this token? The previous token will stop working immediately.",
373
+ "resources.delete": "Delete",
374
+ "resources.deleteDevice": "Delete device",
375
+ "resources.deleteDeviceNote": "Remove this device, runtime configuration, pairing records and call content, and disconnect it.",
376
+ "resources.confirmDevice": "Delete this device and its pairing and call records? Pair it again to reconnect.",
377
+ "resources.confirmToken": "Delete token \u201C{name}\u201D? Clients using it will lose access.",
378
+ "resources.confirmPairing": "Delete this pairing record? Unfinished pairing will stop. Paired devices are kept.",
379
+ "resources.emptyTokens": "No MCP tokens yet. Create one above to connect your AI client.",
380
+ "resources.emptyPairings": "No pairing records.",
381
+ "resources.loading": "Loading\u2026",
382
+ "resources.saving": "Saving\u2026",
383
+ "resources.saved": "Saved",
384
+ "resources.failed": "The operation did not complete. Refresh and try again.",
385
+ "resources.invalid": "Use a unique name (letters, numbers, - or _) and a valid expiry.",
386
+ "resources.pending": "Pending",
387
+ "resources.paired": "Paired",
388
+ "resources.cancelled": "Cancelled",
389
+ "nav.workspace": "WORKSPACE",
390
+ "nav.overview": "Overview",
391
+ "nav.devices": "Devices",
392
+ "nav.pairing": "Pair a device",
393
+ "nav.connect": "Connect MCP",
394
+ "nav.resources": "RESOURCES",
395
+ "nav.docs": "Quick start",
396
+ "nav.console": "Management console",
397
+ "overview.eyebrow": "YOUR WORKSPACE, CONNECTED",
398
+ "overview.title": "Workspace overview",
399
+ "overview.description": "Keep your gateway and devices in view. Bring your remote workflow together.",
400
+ "overview.visible": "Devices on this page",
401
+ "overview.visibleNote": "In the current authorized list page",
402
+ "overview.online": "Verified connections",
403
+ "overview.onlineNote": "Connection and identity are current",
404
+ "overview.attention": "Needs attention",
405
+ "overview.attentionNote": "Devices without a verified connection",
406
+ "overview.scope": "Device counts cover this page only. Expired observations do not establish current state.",
407
+ "overview.gateway": "Gateway health",
408
+ "overview.available": "Available",
409
+ "overview.degraded": "Degraded",
410
+ "overview.unavailable": "Unavailable",
411
+ "overview.unverified": "Unverified",
412
+ "overview.store": "Storage",
413
+ "overview.transport": "Device transport",
414
+ "overview.native": "Native runtime",
415
+ "overview.manage": "Manage devices",
416
+ "overview.deviceSection": "Your connected workspaces",
417
+ "overview.deviceSubtitle": "Connect your tools and workspaces, one device at a time.",
418
+ "devices.search": "Search devices on this page",
419
+ "devices.searchPlaceholder": "Search by device name, ID or label\u2026",
420
+ "devices.filterAll": "All devices",
421
+ "devices.filterOnline": "Verified",
422
+ "devices.filterAttention": "Needs attention",
423
+ "devices.results": "Showing {shown} of {count} devices on this page",
424
+ "devices.noMatches": "No matching devices",
425
+ "devices.noMatchesHint": "Try another name, ID or label, or change the status filter.",
426
+ "devices.inspect": "View details",
427
+ "devices.record": "Observation details",
428
+ "devices.open": "View details for {name}",
429
+ "pairing.pageTitle": "Bring a new device on board",
430
+ "pairing.pageDescription": "Create an application, run Connector, and verify the code. Your workspace is three steps away.",
431
+ "pairing.stepCreate": "Create an application",
432
+ "pairing.stepRun": "Run Connector",
433
+ "pairing.stepApprove": "Verify and approve",
434
+ "pairing.formTitle": "Add your workstation",
435
+ "pairing.namePlaceholder": "e.g. office-workstation",
436
+ "pairing.nameHint": "Use 1\u201363 lowercase letters, digits, dots, underscores or hyphens.",
437
+ "pairing.advanced": "Advanced: pairing policy",
438
+ "pairing.guideTitle": "Then, on your workstation",
439
+ "pairing.guideDescription": "Run this command on the device with Luvus or Herdr installed. Enter the Gateway address and application ID when prompted.",
440
+ "pairing.guideNote": "Match the code on this page with the terminal, then approve local native installation access.",
441
+ "pairing.loginRequired": "A management session is required",
442
+ "pairing.loginHint": "Sign in through the Gateway management entry to create applications and approve new devices.",
443
+ "connect.title": "Connect your workspace to AI",
444
+ "connect.description": "Once a device is paired, issue a dedicated MCP Bearer for your AI client.",
445
+ "connect.stepOne": "Find an existing caller",
446
+ "connect.stepOneNote": "Run this on the Gateway machine and note the actual caller ID in the list.",
447
+ "connect.stepTwo": "Issue a client token",
448
+ "connect.stepTwoNote": "Replace EXISTING_CALLER_ID with that ID. New tokens can also be viewed later in the Dashboard.",
449
+ "connect.stepThree": "Configure your AI client",
450
+ "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.",
451
+ "connect.docker": "Docker deployment commands",
452
+ "connect.docs": "Read the connection guide",
453
+ "copy.action": "Copy command",
454
+ "copy.id": "Copy application ID",
455
+ "copy.done": "Copied",
456
+ "copy.failed": "Copy unavailable; select the text manually",
457
+ "empty.title": "Start with your first device",
458
+ "empty.description": "Pair a workstation running Luvus or Herdr. Its connection, runtime instances and access status will appear here.",
245
459
  "provider.disable": "Disable this provider",
246
460
  "provider.target": "Provider target",
247
461
  "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.",
@@ -250,8 +464,9 @@ var english = Object.freeze({
250
464
  "app.subtitle": "Device and connection management",
251
465
  "app.console": "Gateway console",
252
466
  "app.skip": "Skip to management content",
253
- "app.managedFooter": "Management actions require explicit confirmation; the server verifies the current session and permissions on every request.",
254
- "app.footer": "Read-only management \xB7 Management changes are unavailable \xB7 Each read requires current administrator and target authorization",
467
+ "app.managedFooter": "Rovloom \xB7 Your devices, agents and workflows.",
468
+ "app.footer": "Rovloom \xB7 Gateway console",
469
+ "preference.switch": "Switch to {value}",
255
470
  "language.label": "Language",
256
471
  "language.system": "Browser language",
257
472
  "language.chinese": "\u7B80\u4F53\u4E2D\u6587",
@@ -289,7 +504,7 @@ var english = Object.freeze({
289
504
  "manage.nativeEffect": "This operation does not guarantee stopping or rolling back native work.",
290
505
  "manage.action": "Action",
291
506
  "pairing.title": "Add a device online",
292
- "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.",
507
+ "pairing.intro": "Give your workstation a name. Create an application, then follow the steps on the right.",
293
508
  "pairing.name": "New device name",
294
509
  "pairing.policy": "Authorization policy reference",
295
510
  "pairing.create": "Create pairing application",
@@ -331,7 +546,7 @@ var english = Object.freeze({
331
546
  "action.cancel": "Cancel read",
332
547
  "reader.pending": "Reading\u2026",
333
548
  "reader.stale": "Snapshot expired",
334
- "reader.snapshot": "Read-only snapshot",
549
+ "reader.snapshot": "Up to date",
335
550
  "reader.empty": "No snapshot",
336
551
  "error.unauthenticated": "The management session is missing or expired. Sign in through the protected administrator entry.",
337
552
  "error.forbidden": "The current administrator cannot view this scope.",
@@ -505,6 +720,84 @@ function createLocalizer(language) {
505
720
  };
506
721
  }
507
722
 
723
+ // src/webui/icons.ts
724
+ var paths = {
725
+ overview: "M3 3h7v7H3zM14 3h7v7h-7zM3 14h7v7H3zM14 14h7v7h-7z",
726
+ devices: "M3 4h18v12H3zM8 21h8M12 16v5",
727
+ 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",
728
+ connect: "M9 8 6 12l3 4M15 8l3 4-3 4M13 5l-2 14",
729
+ arrow: "M5 12h14M14 7l5 5-5 5",
730
+ plus: "M12 5v14M5 12h14",
731
+ search: "M21 21l-5-5M18 10a8 8 0 1 1-16 0 8 8 0 0 1 16 0",
732
+ activity: "M3 12h4l3-8 4 16 3-8h4",
733
+ shield: "M12 3 4 6v6c0 5 8 9 8 9s8-4 8-9V6zM8 12l3 3 5-6",
734
+ attention: "M12 3 2 21h20zM12 9v4M12 17h.01",
735
+ 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",
736
+ refresh: "M20 7v5h-5M4 17v-5h5M6 7a7 7 0 0 1 12-1l2 3M4 15l2 3a7 7 0 0 0 12-1",
737
+ copy: "M9 9h12v12H9zM15 9V3H3v12h6",
738
+ check: "M5 12l4 4L19 6",
739
+ chevron: "M9 5l7 7-7 7",
740
+ gateway: "M4 3h16v7H4zM4 14h16v7H4zM8 6.5h.01M8 17.5h.01M16 6.5h1M16 17.5h1"
741
+ };
742
+ function uiIcon(name) {
743
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
744
+ svg.classList.add("ui-icon");
745
+ for (const [key, value] of Object.entries({
746
+ viewBox: "0 0 24 24",
747
+ fill: "none",
748
+ stroke: "currentColor",
749
+ "stroke-width": "1.65",
750
+ "stroke-linecap": "round",
751
+ "stroke-linejoin": "round",
752
+ "aria-hidden": "true",
753
+ focusable: "false"
754
+ })) {
755
+ svg.setAttribute(key, value);
756
+ }
757
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
758
+ path.setAttribute("d", paths[name]);
759
+ svg.append(path);
760
+ return svg;
761
+ }
762
+
763
+ // src/webui/clipboard.ts
764
+ function copyControl(value, label) {
765
+ const button = document.createElement("button");
766
+ button.type = "button";
767
+ button.className = "copy-button";
768
+ button.dataset.copy = value;
769
+ button.setAttribute("aria-label", label);
770
+ button.title = label;
771
+ button.append(uiIcon("copy"));
772
+ return button;
773
+ }
774
+ async function copyText(value) {
775
+ try {
776
+ if (navigator.clipboard?.writeText) {
777
+ await navigator.clipboard.writeText(value);
778
+ return true;
779
+ }
780
+ } catch {}
781
+ const active = document.activeElement;
782
+ const input = document.createElement("textarea");
783
+ input.className = "copy-buffer";
784
+ input.value = value;
785
+ input.readOnly = true;
786
+ input.setAttribute("aria-label", "Copy");
787
+ document.body.append(input);
788
+ try {
789
+ input.select();
790
+ return document.execCommand("copy");
791
+ } catch {
792
+ return false;
793
+ } finally {
794
+ input.remove();
795
+ if (active instanceof HTMLElement && active.isConnected) {
796
+ active.focus({ preventScroll: true });
797
+ }
798
+ }
799
+ }
800
+
508
801
  // src/webui/view.ts
509
802
  var connectionNames = {
510
803
  connecting: "connection.connecting",
@@ -576,6 +869,9 @@ function routeHref(route) {
576
869
  if (route.providerCursor !== null) {
577
870
  parameters.set("providers", route.providerCursor);
578
871
  }
872
+ if (route.machineId === null && route.section) {
873
+ parameters.set("view", route.section);
874
+ }
579
875
  return `#${parameters.toString()}`;
580
876
  }
581
877
  function parseRoute(hash) {
@@ -584,7 +880,7 @@ function parseRoute(hash) {
584
880
  return empty;
585
881
  }
586
882
  const parameters = new URLSearchParams(hash.replace(/^#/, ""));
587
- for (const key of ["machine", "cursor", "providers"]) {
883
+ for (const key of ["machine", "cursor", "providers", "view"]) {
588
884
  if (parameters.getAll(key).length > 1 || (parameters.get(key)?.length ?? 0) > 1024) {
589
885
  return empty;
590
886
  }
@@ -593,7 +889,8 @@ function parseRoute(hash) {
593
889
  return {
594
890
  machineId,
595
891
  cursor: machineId ? null : parameters.get("cursor") || null,
596
- providerCursor: machineId ? parameters.get("providers") || null : null
892
+ providerCursor: machineId ? parameters.get("providers") || null : null,
893
+ ...!machineId && ["devices", "pairing", "connect"].includes(parameters.get("view") ?? "") ? { section: parameters.get("view") } : {}
597
894
  };
598
895
  }
599
896
  function current(observedAt, freshness) {
@@ -621,7 +918,7 @@ function listObservation(key, observedAt, freshness) {
621
918
  function currentDeviceLink(device, freshness) {
622
919
  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";
623
920
  }
624
- function deviceBadges(device, freshness) {
921
+ function deviceBadges(device, freshness, compact = false) {
625
922
  const { i18n } = freshness;
626
923
  const { t } = i18n;
627
924
  const result = node("div", "", "badges");
@@ -658,7 +955,9 @@ function deviceBadges(device, freshness) {
658
955
  } else {
659
956
  result.append(badge(t("device.authUnknown", { reason: i18n.reason(authentication.reason_code) }), "warning"));
660
957
  }
661
- result.append(badge(t("device.readyUnknown")));
958
+ if (!compact) {
959
+ result.append(badge(t("device.readyUnknown")));
960
+ }
662
961
  return result;
663
962
  }
664
963
  function unavailableActions(kind, i18n) {
@@ -700,20 +999,154 @@ function deviceActions(page, freshness, connected) {
700
999
  }
701
1000
  return section;
702
1001
  }
703
- function deviceList(page, freshness) {
1002
+ function overviewMetrics(page, freshness) {
1003
+ const t = freshness.i18n.t;
1004
+ const online = page.devices.items.filter((device) => currentDeviceLink(device, freshness)).length;
1005
+ const section = node("section", "", "overview-metrics");
1006
+ section.setAttribute("aria-label", t("nav.overview"));
1007
+ const rows = [
1008
+ ["overview.visible", "overview.visibleNote", String(page.devices.items.length), "devices"],
1009
+ ["overview.online", "overview.onlineNote", freshness.pageStale ? "\u2014" : String(online), "shield"],
1010
+ [
1011
+ "overview.attention",
1012
+ "overview.attentionNote",
1013
+ freshness.pageStale ? "\u2014" : String(page.devices.items.length - online),
1014
+ "attention"
1015
+ ]
1016
+ ];
1017
+ for (const [label, description, value, icon] of rows) {
1018
+ const card = node("article", "", "metric-card");
1019
+ const top = node("div", "", "metric-heading");
1020
+ top.append(node("span", t(label)), uiIcon(icon));
1021
+ card.append(top, node("strong", value, "metric-value"), node("p", t(description), "muted"));
1022
+ section.append(card);
1023
+ }
1024
+ section.append(node("p", t("overview.scope"), "metric-scope"));
1025
+ return section;
1026
+ }
1027
+ function guidePage(section, i18n, management) {
1028
+ const { t } = i18n;
1029
+ const heading = node("header", "", "page-heading");
1030
+ const copy = node("div", "", "heading-copy");
1031
+ copy.append(node("p", t("nav.workspace"), "eyebrow"));
1032
+ copy.append(node("h1", t(section === "pairing" ? "pairing.pageTitle" : "connect.title")));
1033
+ copy.append(node("p", t(section === "pairing" ? "pairing.pageDescription" : "connect.description"), "muted"));
1034
+ heading.append(copy);
1035
+ const body = node("section", "", "connection-guide");
1036
+ if (section === "pairing") {
1037
+ if (!management) {
1038
+ const empty = node("div", "", "empty");
1039
+ empty.append(uiIcon("shield"), node("h2", t("pairing.loginRequired")), node("p", t("pairing.loginHint"), "muted"));
1040
+ body.append(empty);
1041
+ }
1042
+ return { heading, body };
1043
+ }
1044
+ const commands = ["rovloom token list", "rovloom token create --principal my_client --like EXISTING_CALLER_ID"];
1045
+ for (const [index, title, description] of [
1046
+ [0, "connect.stepOne", "connect.stepOneNote"],
1047
+ [1, "connect.stepTwo", "connect.stepTwoNote"],
1048
+ [2, "connect.stepThree", "connect.stepThreeNote"]
1049
+ ]) {
1050
+ const card = node("article", "", "connection-step");
1051
+ const number = node("span", String(index + 1).padStart(2, "0"), "step-number");
1052
+ const details = node("div", "", "connection-step-body");
1053
+ details.append(node("h2", t(title)), node("p", t(description), "muted"));
1054
+ if (index < commands.length) {
1055
+ const command = commands[index];
1056
+ const block = node("div", "", "command-snippet");
1057
+ block.append(node("code", command), copyControl(command, t("copy.action")));
1058
+ const docker = node("details", "", "docker-command");
1059
+ docker.dataset.disclosure = "docker-command-" + index;
1060
+ docker.append(node("summary", t("connect.docker")));
1061
+ const dockerCommand = command.replace(/^rovloom /u, "docker compose run --rm gateway ");
1062
+ const snippet = node("div", "", "command-snippet");
1063
+ snippet.append(node("code", dockerCommand), copyControl(dockerCommand, t("copy.action")));
1064
+ docker.append(snippet);
1065
+ details.append(block, docker);
1066
+ } else {
1067
+ const link = node("a", t("connect.docs"), "text-link");
1068
+ link.href = `https://github.com/ArnoChenFx/rovloom/blob/main/docs/QUICK-START${i18n.language === "zh-CN" ? "-zh" : ""}.md`;
1069
+ link.target = "_blank";
1070
+ link.rel = "noreferrer noopener";
1071
+ details.append(link);
1072
+ }
1073
+ card.append(number, details);
1074
+ body.append(card);
1075
+ }
1076
+ return { heading, body };
1077
+ }
1078
+ function deviceList(page, freshness, route, filter) {
704
1079
  const { i18n } = freshness;
705
1080
  const { t } = i18n;
706
1081
  const heading = node("header", "", "page-heading");
707
- heading.append(node("h1", t("devices.heading")), node("p", t("devices.description"), "muted"));
1082
+ const overview = route.section !== "devices";
1083
+ const copy = node("div", "", "heading-copy");
1084
+ copy.append(node("p", t("overview.eyebrow"), "eyebrow"));
1085
+ copy.append(node("h1", t(overview ? "overview.title" : "devices.heading")), node("p", t(overview ? "overview.description" : "devices.description"), "muted"));
1086
+ heading.append(copy);
1087
+ const add = node("a", "", "primary-button");
1088
+ add.href = "#view=pairing";
1089
+ add.append(uiIcon("plus"), node("span", t("nav.pairing")));
1090
+ heading.append(add);
708
1091
  const section = node("section", "", "devices-section");
1092
+ const sectionHeader = node("div", "", "section-heading");
1093
+ const sectionCopy = node("div");
1094
+ sectionCopy.append(node("h2", t(overview ? "overview.deviceSection" : "devices.heading")), node("p", t("overview.deviceSubtitle"), "muted"));
1095
+ sectionHeader.append(sectionCopy);
1096
+ if (overview) {
1097
+ const all = node("a", "", "text-link");
1098
+ all.href = "#view=devices";
1099
+ all.append(node("span", t("overview.manage")), uiIcon("arrow"));
1100
+ sectionHeader.append(all);
1101
+ }
1102
+ section.append(sectionHeader);
1103
+ if (page.devices.items.length) {
1104
+ const tools = node("div", "", "device-tools");
1105
+ const search = node("label", "", "search-field");
1106
+ const input = node("input");
1107
+ input.type = "search";
1108
+ input.name = "device-search";
1109
+ input.value = filter.query;
1110
+ input.placeholder = t("devices.searchPlaceholder");
1111
+ input.setAttribute("aria-label", t("devices.search"));
1112
+ search.append(uiIcon("search"), input);
1113
+ const filters = node("div", "", "device-filters");
1114
+ filters.setAttribute("role", "group");
1115
+ filters.setAttribute("aria-label", t("devices.health"));
1116
+ for (const [value, key] of [
1117
+ ["all", "devices.filterAll"],
1118
+ ["online", "devices.filterOnline"],
1119
+ ["attention", "devices.filterAttention"]
1120
+ ]) {
1121
+ const button = node("button", t(key));
1122
+ button.type = "button";
1123
+ button.dataset.deviceFilter = value;
1124
+ button.setAttribute("aria-pressed", String(value === filter.status));
1125
+ filters.append(button);
1126
+ }
1127
+ tools.append(search, filters);
1128
+ section.append(tools);
1129
+ }
1130
+ const items = page.devices.items.filter((device) => {
1131
+ const query = filter.query.trim().toLocaleLowerCase(i18n.language);
1132
+ const matches = !query || [device.name, device.machine_id, ...device.labels ?? []].some((value) => value.toLocaleLowerCase(i18n.language).includes(query));
1133
+ const online = currentDeviceLink(device, freshness);
1134
+ return matches && (filter.status === "all" || (filter.status === "online" ? online : !online));
1135
+ });
709
1136
  if (page.devices.items.length === 0) {
710
1137
  const empty = node("div", "", "empty");
711
- empty.append(node("h2", t("devices.emptyHeading")), node("p", t("devices.empty"), "muted"));
1138
+ const symbol = node("span", "", "empty-illustration");
1139
+ symbol.append(uiIcon("devices"));
1140
+ empty.append(symbol, node("h2", t("empty.title")), node("p", t("empty.description"), "muted"));
1141
+ const pair = node("a", "", "primary-button");
1142
+ pair.href = "#view=pairing";
1143
+ pair.append(uiIcon("plus"), node("span", t("nav.pairing")));
1144
+ empty.append(pair);
712
1145
  section.append(empty);
713
1146
  }
714
1147
  const columns = node("div", "", "device-columns");
715
1148
  columns.setAttribute("aria-hidden", "true");
716
- for (const key of ["devices.identity", "devices.health", "devices.activity", "devices.observations"]) {
1149
+ for (const key of ["devices.identity", "devices.health", "devices.activity", "devices.record"]) {
717
1150
  columns.append(node("span", t(key)));
718
1151
  }
719
1152
  if (page.devices.items.length) {
@@ -721,32 +1154,60 @@ function deviceList(page, freshness) {
721
1154
  }
722
1155
  const list = node("ul", "", "device-list");
723
1156
  list.setAttribute("aria-label", t("devices.heading"));
724
- for (const device of page.devices.items) {
1157
+ for (const device of items) {
725
1158
  const row = node("li", "", "device-card");
726
1159
  const identity = node("div", "", "device-identity");
727
1160
  const link = node("a", device.name, "device-name");
728
1161
  link.href = routeHref({ machineId: device.machine_id, cursor: null, providerCursor: null });
729
- identity.append(link, node("code", device.machine_id));
730
- identity.append(node("p", device.labels === null ? t("value.noLabelRecord") : device.labels.length ? device.labels.join(" \xB7 ") : t("value.noLabels"), "muted"));
1162
+ const deviceIcon = node("span", "", "device-icon");
1163
+ deviceIcon.append(uiIcon("devices"));
1164
+ const deviceCopy = node("div", "", "device-copy");
1165
+ const identifier = node("code", device.machine_id);
1166
+ identifier.title = device.machine_id;
1167
+ deviceCopy.append(link, identifier);
1168
+ identity.append(deviceIcon, deviceCopy);
1169
+ deviceCopy.append(node("p", device.labels === null ? t("value.noLabelRecord") : device.labels.length ? device.labels.join(" \xB7 ") : t("value.noLabels"), "muted"));
731
1170
  const health = node("div", "", "device-health");
732
- health.append(node("span", t("devices.health"), "cell-label"), deviceBadges(device, freshness));
1171
+ health.append(node("span", t("devices.health"), "cell-label"), deviceBadges(device, freshness, true));
733
1172
  const heartbeat = device.connection.verification === "verified" ? device.connection.value.last_heartbeat_at : null;
734
1173
  const activity = node("div", "", "device-activity");
735
1174
  activity.append(node("span", t("devices.activity"), "cell-label"));
736
1175
  activity.append(node("strong", heartbeat === null ? t("time.noHeartbeat") : i18n.age(heartbeat, freshness.now), "activity-age"));
737
1176
  activity.append(node("p", t("devices.received", { time: i18n.timestamp(heartbeat) }), "muted"));
738
1177
  const observations = node("div", "", "device-observations");
739
- observations.append(node("span", t("devices.observations"), "cell-label"));
740
- observations.append(listObservation("devices.observed", device.observed_at, freshness));
741
- observations.append(listObservation("devices.connectionObserved", device.connection.verification === "verified" ? device.connection.observed_at : null, freshness));
742
- observations.append(listObservation("devices.authObserved", device.authentication.verification === "verified" ? device.authentication.observed_at : null, freshness));
1178
+ const record = node("details", "", "device-record");
1179
+ record.dataset.disclosure = "device-record-" + device.machine_id;
1180
+ const summary = node("summary", t("devices.record"));
1181
+ record.append(summary);
1182
+ record.append(listObservation("devices.observed", device.observed_at, freshness));
1183
+ record.append(listObservation("devices.connectionObserved", device.connection.verification === "verified" ? device.connection.observed_at : null, freshness));
1184
+ record.append(listObservation("devices.authObserved", device.authentication.verification === "verified" ? device.authentication.observed_at : null, freshness));
1185
+ record.append(node("p", t("device.readyUnknown"), "muted"));
1186
+ const inspect = node("a", "", "inspect-device");
1187
+ inspect.href = link.href;
1188
+ inspect.setAttribute("aria-label", t("devices.open", { name: device.name }));
1189
+ inspect.append(uiIcon("arrow"));
1190
+ observations.append(record, inspect);
743
1191
  row.append(identity, health, activity, observations);
744
1192
  list.append(row);
745
1193
  }
746
1194
  section.append(list);
1195
+ if (page.devices.items.length && !items.length) {
1196
+ const empty = node("div", "", "empty search-empty");
1197
+ empty.append(uiIcon("search"), node("h3", t("devices.noMatches")), node("p", t("devices.noMatchesHint"), "muted"));
1198
+ section.append(empty);
1199
+ }
1200
+ if (page.devices.items.length) {
1201
+ section.append(node("p", t("devices.results", { shown: items.length, count: page.devices.items.length }), "list-count"));
1202
+ }
747
1203
  if (page.devices.next_cursor !== undefined) {
748
1204
  const next = node("a", t("devices.next"), "button-link");
749
- next.href = routeHref({ machineId: null, cursor: page.devices.next_cursor, providerCursor: null });
1205
+ next.href = routeHref({
1206
+ machineId: null,
1207
+ cursor: page.devices.next_cursor,
1208
+ providerCursor: null,
1209
+ section: route.section
1210
+ });
750
1211
  section.append(next);
751
1212
  }
752
1213
  return { heading, body: section };
@@ -891,11 +1352,15 @@ function syncChildren(parent, desired) {
891
1352
  parent.removeChild(parent.lastChild);
892
1353
  }
893
1354
  }
894
- function renderPage(content, state, route, i18n = createLocalizer("zh-CN"), management = false) {
1355
+ function renderPage(content, state, route, i18n = createLocalizer("zh-CN"), management = false, filter = { query: "", status: "all" }) {
895
1356
  const { t } = i18n;
896
1357
  const focused = content.contains(document.activeElement) && document.activeElement instanceof HTMLAnchorElement ? document.activeElement.getAttribute("href") : null;
897
1358
  const metadataWasOpen = content.querySelector(".gateway-metadata")?.open ?? false;
898
1359
  const metadataHadFocus = content.contains(document.activeElement) && document.activeElement?.matches(".gateway-metadata > summary");
1360
+ const disclosures = new Map([...content.querySelectorAll("details[data-disclosure]")].map((details) => [
1361
+ details.dataset.disclosure,
1362
+ details.open
1363
+ ]));
899
1364
  const view = document.createDocumentFragment();
900
1365
  if (state.problem) {
901
1366
  const message = node("p", t(problemNames[state.problem]), "notice warning");
@@ -924,8 +1389,28 @@ function renderPage(content, state, route, i18n = createLocalizer("zh-CN"), mana
924
1389
  gatewayHeading.append(node("h2", t("gateway.heading")));
925
1390
  gatewayHeading.append(badge(status.mode === "active" ? state.stale ? t("gateway.activeHistory") : t("gateway.active") : t("gateway.degraded"), status.mode === "active" && !state.stale ? "good" : "warning"));
926
1391
  gatewayHeading.append(badge(state.stale ? t("reader.stale") : t("gateway.fresh"), state.stale ? "warning" : "neutral"));
927
- gateway.append(gatewayHeading);
928
- gateway.append(facts([
1392
+ const mark = node("span", "", "gateway-symbol");
1393
+ mark.append(uiIcon("gateway"));
1394
+ gatewayHeading.prepend(mark);
1395
+ const components = node("div", "", "gateway-components");
1396
+ for (const [name, key] of [
1397
+ ["store", "overview.store"],
1398
+ ["transport", "overview.transport"],
1399
+ ["native", "overview.native"]
1400
+ ]) {
1401
+ const item = node("div", "", "component-item");
1402
+ item.append(node("span", t(key), "muted"), badge(t(`overview.${status.components[name]}`), status.components[name] === "available" && !state.stale ? "good" : "neutral"));
1403
+ components.append(item);
1404
+ }
1405
+ gateway.append(gatewayHeading, components);
1406
+ const metadata = node("details", "", "gateway-metadata");
1407
+ metadata.open = metadataWasOpen;
1408
+ metadata.append(node("summary", t("gateway.metadata")));
1409
+ metadata.append(facts([
1410
+ [t("gateway.generation"), status.recovery_generation],
1411
+ [t("gateway.policy"), status.policy_revision]
1412
+ ]));
1413
+ metadata.append(facts([
929
1414
  [t("gateway.listener"), t("gateway.listenerUnknown")],
930
1415
  [
931
1416
  t("gateway.authentication"),
@@ -944,22 +1429,28 @@ function renderPage(content, state, route, i18n = createLocalizer("zh-CN"), mana
944
1429
  })
945
1430
  ]
946
1431
  ], "gateway-facts"));
947
- const metadata = node("details", "", "gateway-metadata");
948
- metadata.open = metadataWasOpen;
949
- metadata.append(node("summary", t("gateway.metadata")));
950
- metadata.append(facts([
951
- [t("gateway.generation"), status.recovery_generation],
952
- [t("gateway.policy"), status.policy_revision]
953
- ]));
954
1432
  gateway.append(metadata);
955
1433
  if (status.mode !== "active") {
956
1434
  gateway.append(node("p", t("gateway.degradedNote"), "notice warning"));
957
1435
  }
958
- const section = route.machineId === null ? deviceList(page, freshness) : deviceDetail(page, freshness, management && status.mode === "active" && !state.stale);
959
- view.append(section.heading, gateway, section.body);
1436
+ 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);
1437
+ view.append(section.heading);
1438
+ if (route.section === "pairing" && (state.stale || status.mode !== "active")) {
1439
+ view.append(node("p", t(state.stale ? "reader.stale" : "gateway.degradedNote"), "notice warning"));
1440
+ }
1441
+ if (route.machineId !== null || !["pairing", "connect"].includes(route.section ?? "")) {
1442
+ if (route.machineId === null) {
1443
+ view.append(overviewMetrics(page, freshness));
1444
+ }
1445
+ view.append(gateway);
1446
+ }
1447
+ view.append(section.body);
960
1448
  }
961
1449
  const identity = routeHref(route);
962
1450
  if (content.dataset.route === identity) {
1451
+ for (const details of view.querySelectorAll("details[data-disclosure]")) {
1452
+ details.open = disclosures.get(details.dataset.disclosure) ?? false;
1453
+ }
963
1454
  syncChildren(content, view);
964
1455
  } else {
965
1456
  content.replaceChildren(view);
@@ -1077,9 +1568,8 @@ function createUiPreferences(changed, host = browserHost) {
1077
1568
  }
1078
1569
 
1079
1570
  // src/webui/preference-control.ts
1080
- var paths = {
1571
+ var paths2 = {
1081
1572
  language: "M2 5h12M7 2h1M12 5c-1 6-4 9-8 11M5 8c2 4 4 6 7 8M13 22l5-11 5 11M15 18h6",
1082
- system: "M3 4h18v12H3zM8 21h8M12 16v5",
1083
1573
  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",
1084
1574
  dark: "M20.9 13A9 9 0 0 1 11 3.1 9 9 0 1 0 20.9 13Z"
1085
1575
  };
@@ -1094,102 +1584,56 @@ function icon(name) {
1094
1584
  svg.setAttribute("stroke-linecap", "round");
1095
1585
  svg.setAttribute("stroke-linejoin", "round");
1096
1586
  const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
1097
- path.setAttribute("d", paths[name]);
1587
+ path.setAttribute("d", paths2[name]);
1098
1588
  svg.append(path);
1099
1589
  return svg;
1100
1590
  }
1101
- function createPreferenceControl(kind, choices, onChoose) {
1591
+ function createPreferenceControl(kind, onChoose) {
1102
1592
  const root = document.createElement("div");
1103
1593
  root.className = "preference";
1104
1594
  root.dataset.preference = kind;
1105
1595
  const trigger = document.createElement("button");
1106
1596
  trigger.type = "button";
1107
1597
  trigger.className = "preference-trigger";
1108
- trigger.setAttribute("aria-expanded", "false");
1109
1598
  const current = document.createElement("span");
1110
1599
  current.className = "visually-hidden";
1111
1600
  current.id = `rovloom-${kind}-current`;
1112
1601
  trigger.setAttribute("aria-describedby", current.id);
1113
- const panel = document.createElement("div");
1114
- panel.className = "preference-panel";
1115
- panel.id = `rovloom-${kind}-choices`;
1116
- panel.hidden = true;
1117
- panel.setAttribute("role", "group");
1118
- trigger.setAttribute("aria-controls", panel.id);
1119
- const buttons = choices.map(([value]) => {
1120
- const button = document.createElement("button");
1121
- button.type = "button";
1122
- button.dataset.choice = value;
1123
- panel.append(button);
1124
- return button;
1125
- });
1126
- root.append(trigger, current, panel);
1127
- function close(restoreFocus = false) {
1128
- panel.hidden = true;
1129
- trigger.setAttribute("aria-expanded", "false");
1130
- if (restoreFocus) {
1131
- trigger.focus();
1132
- }
1133
- }
1134
- function click(event) {
1135
- const target = event.target instanceof Element ? event.target.closest("button") : null;
1136
- if (target === trigger) {
1137
- panel.hidden = !panel.hidden;
1138
- trigger.setAttribute("aria-expanded", String(!panel.hidden));
1139
- } else if (target instanceof HTMLButtonElement && buttons.includes(target)) {
1140
- onChoose(target.dataset.choice);
1141
- close(true);
1142
- }
1143
- }
1144
- function keydown(event) {
1145
- if (event.key === "Escape" && !panel.hidden) {
1146
- event.preventDefault();
1147
- close(true);
1148
- return;
1149
- }
1150
- if (!["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) {
1151
- return;
1152
- }
1153
- event.preventDefault();
1154
- const index = buttons.indexOf(document.activeElement);
1155
- 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;
1156
- panel.hidden = false;
1157
- trigger.setAttribute("aria-expanded", "true");
1158
- buttons[next].focus();
1159
- }
1160
- function outside(event) {
1161
- if (event.target instanceof Node && !root.contains(event.target)) {
1162
- close();
1602
+ root.append(trigger, current);
1603
+ let next = "";
1604
+ function click() {
1605
+ if (next) {
1606
+ onChoose(next);
1163
1607
  }
1164
1608
  }
1165
1609
  function update(i18n, value) {
1166
- const label = i18n.t(kind === "language" ? "language.label" : "theme.label");
1610
+ const language = kind === "language";
1611
+ const label = i18n.t(language ? "language.label" : "theme.label");
1612
+ next = language ? value === "zh-CN" ? "en" : "zh-CN" : value === "dark" ? "light" : "dark";
1613
+ const name = i18n.t(language ? value === "zh-CN" ? "language.chinese" : "language.english" : value === "dark" ? "theme.dark" : "theme.light");
1614
+ const target = i18n.t(language ? next === "zh-CN" ? "language.chinese" : "language.english" : next === "dark" ? "theme.dark" : "theme.light");
1615
+ current.textContent = `${name}. ${i18n.t("preference.switch", { value: target })}`;
1167
1616
  trigger.setAttribute("aria-label", label);
1168
- panel.setAttribute("aria-label", label);
1169
- for (let index = 0;index < buttons.length; index++) {
1170
- const [choice, key] = choices[index];
1171
- const button = buttons[index];
1172
- button.textContent = i18n.t(key);
1173
- button.setAttribute("aria-pressed", String(choice === value));
1174
- if (choice === value) {
1175
- current.textContent = i18n.t(key);
1176
- }
1177
- }
1178
1617
  trigger.title = `${label}: ${current.textContent}`;
1179
- trigger.replaceChildren(icon(kind === "language" ? "language" : value === "dark" ? "dark" : value === "light" ? "light" : "system"));
1618
+ trigger.dataset.value = value;
1619
+ trigger.dataset.nextValue = next;
1620
+ trigger.replaceChildren(icon(language ? "language" : value === "dark" ? "dark" : "light"));
1180
1621
  }
1181
- function dispose() {
1182
- root.removeEventListener("click", click);
1183
- root.removeEventListener("keydown", keydown);
1184
- document.removeEventListener("pointerdown", outside);
1185
- document.removeEventListener("focusin", outside);
1186
- close();
1187
- }
1188
- root.addEventListener("click", click);
1189
- root.addEventListener("keydown", keydown);
1190
- document.addEventListener("pointerdown", outside);
1191
- document.addEventListener("focusin", outside);
1192
- return { root, update, close, dispose };
1622
+ trigger.addEventListener("click", click);
1623
+ return {
1624
+ root,
1625
+ update,
1626
+ dispose() {
1627
+ next = "";
1628
+ trigger.removeEventListener("click", click);
1629
+ }
1630
+ };
1631
+ }
1632
+
1633
+ // src/webui/request-id.ts
1634
+ function managementRequestId() {
1635
+ const bytes = globalThis.crypto.getRandomValues(new Uint8Array(16));
1636
+ return "request_" + Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
1193
1637
  }
1194
1638
 
1195
1639
  // src/webui/write-contract.ts
@@ -1695,7 +2139,7 @@ function createDeviceManagementFlow(options) {
1695
2139
  let active;
1696
2140
  let generation = 0;
1697
2141
  let disposed = false;
1698
- const requestId = options.requestId ?? (() => "request_" + globalThis.crypto.randomUUID());
2142
+ const requestId = options.requestId ?? managementRequestId;
1699
2143
  async function execute(operation, args, signal) {
1700
2144
  if (signal.aborted) {
1701
2145
  throw new ManagementClientError("aborted");
@@ -2320,7 +2764,7 @@ function createPairingManagementFlow(options) {
2320
2764
  let active;
2321
2765
  let generation = 0;
2322
2766
  let disposed = false;
2323
- const requestId = options.requestId ?? (() => "request_" + globalThis.crypto.randomUUID());
2767
+ const requestId = options.requestId ?? managementRequestId;
2324
2768
  async function execute(operation, args, signal) {
2325
2769
  if (signal.aborted) {
2326
2770
  throw new ManagementClientError("aborted");
@@ -2543,6 +2987,18 @@ var errors2 = {
2543
2987
  outcome_unconfirmed: "manage.unknown",
2544
2988
  aborted: "manage.cancelled"
2545
2989
  };
2990
+ function entryRenderSignature(language, entry) {
2991
+ return [
2992
+ language,
2993
+ entry.name,
2994
+ entry.machine_id,
2995
+ entry.pairing_id,
2996
+ entry.status,
2997
+ entry.claim?.status ?? "",
2998
+ entry.claim?.user_code ?? "",
2999
+ entry.claim?.activation ?? ""
3000
+ ].join("|");
3001
+ }
2546
3002
  function node3(tag, className = "") {
2547
3003
  const element = document.createElement(tag);
2548
3004
  element.className = className;
@@ -2550,6 +3006,18 @@ function node3(tag, className = "") {
2550
3006
  }
2551
3007
  function createPairingActions(options) {
2552
3008
  const region = node3("section", "pairing-section");
3009
+ const steps = node3("ol", "pairing-steps");
3010
+ const stepItems = ["pairing.stepCreate", "pairing.stepRun", "pairing.stepApprove"].map((key, index) => {
3011
+ const item = node3("li");
3012
+ const number = node3("span", "pairing-step-number");
3013
+ number.textContent = String(index + 1).padStart(2, "0");
3014
+ const label = node3("span");
3015
+ item.append(number, label);
3016
+ steps.append(item);
3017
+ return { item, label, key };
3018
+ });
3019
+ const layout = node3("div", "pairing-layout");
3020
+ const setup = node3("div", "pairing-setup");
2553
3021
  const heading = node3("h2");
2554
3022
  const intro = node3("p", "muted");
2555
3023
  const form = node3("form", "pairing-form");
@@ -2561,7 +3029,10 @@ function createPairingActions(options) {
2561
3029
  nameInput.maxLength = 63;
2562
3030
  nameInput.pattern = "[a-z0-9][a-z0-9._-]{0,62}";
2563
3031
  nameInput.autocomplete = "off";
2564
- nameLabel.append(nameCaption, nameInput);
3032
+ const nameHint = node3("small", "field-hint");
3033
+ nameHint.id = "pairing-name-hint";
3034
+ nameInput.setAttribute("aria-describedby", nameHint.id);
3035
+ nameLabel.append(nameCaption, nameInput, nameHint);
2565
3036
  const policyLabel = node3("label");
2566
3037
  const policyCaption = node3("span");
2567
3038
  const policyInput = node3("input");
@@ -2569,10 +3040,14 @@ function createPairingActions(options) {
2569
3040
  policyInput.required = true;
2570
3041
  policyInput.maxLength = 128;
2571
3042
  policyInput.autocomplete = "off";
3043
+ policyInput.value = DEFAULT_PAIRING_POLICY_REF;
2572
3044
  policyLabel.append(policyCaption, policyInput);
2573
3045
  const create = node3("button", "pairing-confirm");
2574
3046
  create.type = "submit";
2575
- form.append(nameLabel, policyLabel, create);
3047
+ const advanced = node3("details", "pairing-advanced");
3048
+ const advancedSummary = node3("summary");
3049
+ advanced.append(advancedSummary, policyLabel);
3050
+ form.append(nameLabel, advanced, create);
2576
3051
  const reviewBox = node3("div", "pairing-review");
2577
3052
  reviewBox.hidden = true;
2578
3053
  const reviewText = node3("p", "pairing-status");
@@ -2590,7 +3065,21 @@ function createPairingActions(options) {
2590
3065
  message.setAttribute("role", "status");
2591
3066
  message.setAttribute("aria-live", "polite");
2592
3067
  const list = node3("ul", "pairing-entries");
2593
- region.append(heading, intro, form, reviewBox, message, list);
3068
+ setup.append(heading, intro, form, reviewBox, message);
3069
+ const guide = node3("aside", "pairing-guide");
3070
+ const guideIcon = node3("span", "pairing-guide-icon");
3071
+ guideIcon.append(uiIcon("connect"));
3072
+ const guideHeading = node3("h2");
3073
+ const guideDescription = node3("p", "muted");
3074
+ const command = node3("div", "command-snippet");
3075
+ const commandCode = node3("code");
3076
+ commandCode.textContent = "rovloom connector --claim";
3077
+ const commandCopy = copyControl("rovloom connector --claim", "");
3078
+ command.append(commandCode, commandCopy);
3079
+ const guideNote = node3("p", "pairing-guide-note");
3080
+ guide.append(guideIcon, guideHeading, guideDescription, command, guideNote);
3081
+ layout.append(setup, guide);
3082
+ region.append(steps, layout, list);
2594
3083
  let i18n;
2595
3084
  let available = false;
2596
3085
  let disposed = false;
@@ -2695,6 +3184,11 @@ function createPairingActions(options) {
2695
3184
  if (!i18n) {
2696
3185
  return;
2697
3186
  }
3187
+ const signature = entryRenderSignature(i18n.language, entry);
3188
+ if (entry.rendered === signature) {
3189
+ return;
3190
+ }
3191
+ entry.rendered = signature;
2698
3192
  const t = i18n.t.bind(i18n);
2699
3193
  entry.item.replaceChildren();
2700
3194
  const head = node3("div", "pairing-entry-head");
@@ -2704,19 +3198,21 @@ function createPairingActions(options) {
2704
3198
  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");
2705
3199
  head.append(title, status);
2706
3200
  entry.item.append(head);
3201
+ status.classList.add("badge");
2707
3202
  const facts = node3("dl", "facts");
2708
3203
  const idTerm = node3("dt");
2709
3204
  idTerm.textContent = t("pairing.application");
2710
3205
  const idValue = node3("dd");
2711
3206
  const idCode = node3("code");
2712
3207
  idCode.textContent = entry.pairing_id;
2713
- idValue.append(idCode);
3208
+ idValue.append(idCode, copyControl(entry.pairing_id, t("copy.id")));
2714
3209
  const machineTerm = node3("dt");
2715
3210
  machineTerm.textContent = t("pairing.machine");
2716
3211
  const machineValue = node3("dd");
2717
3212
  machineValue.textContent = entry.machine_id;
2718
3213
  facts.append(idTerm, idValue, machineTerm, machineValue);
2719
3214
  entry.item.append(facts);
3215
+ updateSteps();
2720
3216
  if (entry.status === "pending" && entry.claim?.status === "pending") {
2721
3217
  const codeLabel = node3("p", "muted");
2722
3218
  codeLabel.textContent = t("pairing.userCode");
@@ -2738,6 +3234,19 @@ function createPairingActions(options) {
2738
3234
  entry.item.append(codeLabel, code, approve);
2739
3235
  }
2740
3236
  }
3237
+ function updateSteps() {
3238
+ const entry = [...entries.values()].at(-1);
3239
+ const finished = entry?.claim?.activation === "configured" || entry?.status === "paired" && !entry.claim;
3240
+ const stage = !entry || ["expired", "failed", "cancelled"].includes(entry.status) ? 0 : finished ? 3 : entry.claim ? 2 : 1;
3241
+ for (const [index, step] of stepItems.entries()) {
3242
+ step.item.dataset.state = index < stage ? "complete" : index === stage ? "current" : "upcoming";
3243
+ if (index === stage) {
3244
+ step.item.setAttribute("aria-current", "step");
3245
+ } else {
3246
+ step.item.removeAttribute("aria-current");
3247
+ }
3248
+ }
3249
+ }
2741
3250
  function addEntry(entry) {
2742
3251
  const existing = entries.get(entry.pairing_id);
2743
3252
  if (existing) {
@@ -2746,6 +3255,7 @@ function createPairingActions(options) {
2746
3255
  return;
2747
3256
  }
2748
3257
  entries.set(entry.pairing_id, entry);
3258
+ options.committed?.();
2749
3259
  list.append(entry.item);
2750
3260
  renderEntry(entry);
2751
3261
  }
@@ -2827,7 +3337,19 @@ function createPairingActions(options) {
2827
3337
  available = isAvailable;
2828
3338
  region.hidden = !isAvailable;
2829
3339
  const t = localizer.t.bind(localizer);
2830
- heading.textContent = t("pairing.title");
3340
+ heading.textContent = t("pairing.formTitle");
3341
+ for (const step of stepItems) {
3342
+ step.label.textContent = t(step.key);
3343
+ }
3344
+ updateSteps();
3345
+ nameInput.placeholder = t("pairing.namePlaceholder");
3346
+ nameHint.textContent = t("pairing.nameHint");
3347
+ advancedSummary.textContent = t("pairing.advanced");
3348
+ guideHeading.textContent = t("pairing.guideTitle");
3349
+ guideDescription.textContent = t("pairing.guideDescription");
3350
+ guideNote.textContent = t("pairing.guideNote");
3351
+ commandCopy.setAttribute("aria-label", t("copy.action"));
3352
+ commandCopy.title = t("copy.action");
2831
3353
  intro.textContent = t("pairing.intro");
2832
3354
  nameCaption.textContent = t("pairing.name");
2833
3355
  policyCaption.textContent = t("pairing.policy");
@@ -2846,6 +3368,17 @@ function createPairingActions(options) {
2846
3368
  suspend() {
2847
3369
  stopPolling();
2848
3370
  },
3371
+ remove(pairingId) {
3372
+ entries.get(pairingId)?.item.remove();
3373
+ entries.delete(pairingId);
3374
+ if (flowIntent && "pairing_id" in flowIntent && flowIntent.pairing_id === pairingId) {
3375
+ flow?.dispose();
3376
+ flow = undefined;
3377
+ flowIntent = undefined;
3378
+ reviewBox.hidden = true;
3379
+ }
3380
+ updateSteps();
3381
+ },
2849
3382
  clear() {
2850
3383
  stopPolling();
2851
3384
  flow?.dispose();
@@ -2864,8 +3397,305 @@ function createPairingActions(options) {
2864
3397
  };
2865
3398
  }
2866
3399
 
3400
+ // src/webui/resource-client.ts
3401
+ class ResourceError extends Error {
3402
+ code;
3403
+ constructor(code) {
3404
+ super(code);
3405
+ this.code = code;
3406
+ }
3407
+ }
3408
+
3409
+ // src/webui/resource-panel.ts
3410
+ function node4(tag, text = "", className = "") {
3411
+ const element = document.createElement(tag);
3412
+ element.textContent = text;
3413
+ element.className = className;
3414
+ return element;
3415
+ }
3416
+ function createResourcePanel(options) {
3417
+ const region = node4("section", "", "resource-panel");
3418
+ const feedback = node4("p", "", "resource-feedback");
3419
+ feedback.setAttribute("role", "status");
3420
+ let i18n;
3421
+ let key = "";
3422
+ let active = false;
3423
+ let pending = false;
3424
+ let controller = new AbortController;
3425
+ let data;
3426
+ let cursor = 0;
3427
+ let tokenName = "";
3428
+ let tokenDays = "365";
3429
+ let version = 0;
3430
+ let route;
3431
+ let language = "";
3432
+ function button(label, action, danger = false) {
3433
+ const result = node4("button", label, danger ? "danger-button" : "");
3434
+ result.type = "button";
3435
+ result.disabled = pending;
3436
+ result.addEventListener("click", action);
3437
+ return result;
3438
+ }
3439
+ async function request(operation, args) {
3440
+ return options.transport(operation, args, controller.signal);
3441
+ }
3442
+ function failure(error) {
3443
+ if (error instanceof ResourceError && (error.code === "forbidden" || error.code === "unauthenticated")) {
3444
+ clear();
3445
+ options.authorizationLost(error.code);
3446
+ return;
3447
+ }
3448
+ feedback.textContent = i18n.t(error instanceof ResourceError && error.code === "invalid_arguments" ? "resources.invalid" : "resources.failed");
3449
+ }
3450
+ async function refresh() {
3451
+ const generation = ++version;
3452
+ controller.abort();
3453
+ controller = new AbortController;
3454
+ pending = true;
3455
+ feedback.textContent = i18n.t("resources.loading");
3456
+ try {
3457
+ const value = await request(key === "connect" ? "tokens_list" : "pairings_list", key === "connect" ? {} : { cursor });
3458
+ if (generation !== version || !active) {
3459
+ return;
3460
+ }
3461
+ if (!value || !Array.isArray(value.items)) {
3462
+ throw new Error("invalid_reply");
3463
+ }
3464
+ data = value;
3465
+ feedback.textContent = "";
3466
+ } catch (error) {
3467
+ if (generation === version && active) {
3468
+ failure(error);
3469
+ }
3470
+ } finally {
3471
+ if (generation === version && active) {
3472
+ pending = false;
3473
+ render();
3474
+ }
3475
+ }
3476
+ }
3477
+ async function mutate(operation, args) {
3478
+ if (pending || !active) {
3479
+ return;
3480
+ }
3481
+ const generation = version;
3482
+ pending = true;
3483
+ feedback.textContent = i18n.t("resources.saving");
3484
+ render();
3485
+ try {
3486
+ await request(operation, args);
3487
+ if (generation !== version || !active) {
3488
+ return;
3489
+ }
3490
+ feedback.textContent = i18n.t("resources.saved");
3491
+ if (operation === "tokens_create") {
3492
+ tokenName = "";
3493
+ tokenDays = "365";
3494
+ }
3495
+ if (operation === "devices_delete") {
3496
+ options.committed(route.machineId ?? undefined);
3497
+ } else {
3498
+ await refresh();
3499
+ if (operation === "pairings_delete") {
3500
+ options.committed(undefined, args.pairing_id);
3501
+ }
3502
+ }
3503
+ } catch (error) {
3504
+ if (generation === version && active) {
3505
+ failure(error);
3506
+ }
3507
+ } finally {
3508
+ if (generation === version && active) {
3509
+ pending = false;
3510
+ render();
3511
+ }
3512
+ }
3513
+ }
3514
+ function remove(operation, args, message) {
3515
+ if (window.confirm(message)) {
3516
+ mutate(operation, args);
3517
+ }
3518
+ }
3519
+ function render() {
3520
+ if (!active) {
3521
+ return;
3522
+ }
3523
+ const { t } = i18n;
3524
+ const body = node4("div", "", "resource-body");
3525
+ if (key === "device") {
3526
+ const card = node4("article", "", "resource-delete-card");
3527
+ card.append(node4("div", t("resources.deleteDeviceNote"), "muted"), button(t("resources.deleteDevice"), () => remove("devices_delete", { machine_id: route.machineId }, t("resources.confirmDevice")), true));
3528
+ body.append(card);
3529
+ } else {
3530
+ const heading = node4("div", "", "section-heading");
3531
+ heading.append(node4("h2", t(key === "connect" ? "resources.tokens" : "resources.pairings")), button(t("action.refresh"), () => {
3532
+ refresh();
3533
+ }));
3534
+ body.append(heading);
3535
+ if (key === "connect") {
3536
+ const form = node4("form", "", "token-create-form");
3537
+ const nameLabel = node4("label", t("resources.name"));
3538
+ const name = node4("input");
3539
+ name.name = "token-name";
3540
+ name.required = true;
3541
+ name.pattern = "[A-Za-z0-9_-]{1,128}";
3542
+ name.maxLength = 128;
3543
+ name.placeholder = "my-client";
3544
+ name.disabled = pending;
3545
+ name.value = tokenName;
3546
+ name.addEventListener("input", () => {
3547
+ tokenName = name.value;
3548
+ });
3549
+ nameLabel.append(name);
3550
+ const daysLabel = node4("label", t("resources.expiry"));
3551
+ const days = node4("select");
3552
+ days.name = "token-days";
3553
+ days.disabled = pending;
3554
+ for (const value of [365, 90, 30, 7]) {
3555
+ const option = node4("option", t("resources.days", { count: value }));
3556
+ option.value = String(value);
3557
+ days.append(option);
3558
+ }
3559
+ days.value = tokenDays;
3560
+ days.addEventListener("change", () => {
3561
+ tokenDays = days.value;
3562
+ });
3563
+ daysLabel.append(days);
3564
+ const submit = node4("button", t("resources.create"), "primary-button");
3565
+ submit.type = "submit";
3566
+ submit.disabled = pending;
3567
+ form.append(nameLabel, daysLabel, submit);
3568
+ form.addEventListener("submit", (event) => {
3569
+ event.preventDefault();
3570
+ mutate("tokens_create", { principal: name.value.trim(), days: Number(days.value) });
3571
+ });
3572
+ body.append(node4("p", t("resources.tokenNote"), "muted"), form);
3573
+ if (data?.resource) {
3574
+ const endpoint = node4("div", "", "mcp-endpoint");
3575
+ endpoint.append(node4("span", t("resources.endpoint")), node4("code", data.resource), copyControl(data.resource, t("copy.action")));
3576
+ body.append(endpoint, node4("p", t("resources.endpointNote"), "muted"));
3577
+ }
3578
+ for (const item of data?.items ?? []) {
3579
+ const token = item;
3580
+ const card = node4("article", "", "token-card");
3581
+ card.dataset.tokenPrincipal = token.principal;
3582
+ const summary = node4("div", "", "token-summary");
3583
+ summary.append(node4("h3", token.principal), node4("span", t(token.expires_at > Date.now() ? "resources.active" : "resources.expired"), "badge"), node4("span", t("resources.expiresAt", { time: i18n.timestamp(token.expires_at) }), "muted"));
3584
+ const controls = node4("div", "", "token-controls");
3585
+ if (token.token) {
3586
+ const value = node4("input");
3587
+ value.type = "password";
3588
+ value.value = token.token;
3589
+ value.readOnly = true;
3590
+ value.setAttribute("aria-label", token.principal + " Token");
3591
+ value.autocomplete = "off";
3592
+ value.spellcheck = false;
3593
+ const toggle = button(t("resources.show"), () => {
3594
+ const hidden = value.type === "password";
3595
+ value.type = hidden ? "text" : "password";
3596
+ toggle.textContent = t(hidden ? "resources.hide" : "resources.show");
3597
+ });
3598
+ controls.append(value, toggle, copyControl(token.token, t("copy.action")));
3599
+ } else {
3600
+ controls.append(node4("span", t("resources.legacy"), "muted"), button(t("resources.reissue"), () => {
3601
+ if (window.confirm(t("resources.confirmReissue"))) {
3602
+ mutate("tokens_rotate", { principal: token.principal });
3603
+ }
3604
+ }));
3605
+ }
3606
+ controls.append(button(t("resources.delete"), () => remove("tokens_delete", { principal: token.principal }, t("resources.confirmToken", { name: token.principal })), true));
3607
+ card.append(summary, controls);
3608
+ body.append(card);
3609
+ }
3610
+ } else {
3611
+ for (const item of data?.items ?? []) {
3612
+ const pairing = item;
3613
+ const card = node4("article", "", "pairing-record");
3614
+ const details = node4("div");
3615
+ const label = ["pending", "paired", "cancelled", "expired"].includes(pairing.status) ? t("resources." + pairing.status) : pairing.status;
3616
+ details.append(node4("h3", pairing.name), node4("code", pairing.pairing_id), node4("p", label + " \xB7 " + i18n.timestamp(pairing.expires), "muted"));
3617
+ card.append(details, button(t("resources.delete"), () => remove("pairings_delete", { pairing_id: pairing.pairing_id }, t("resources.confirmPairing")), true));
3618
+ body.append(card);
3619
+ }
3620
+ }
3621
+ if (key === "pairing" && (cursor > 0 || typeof data?.next_cursor === "number")) {
3622
+ const pages = node4("div", "", "token-controls");
3623
+ if (cursor > 0) {
3624
+ pages.append(button(t("resources.previous"), () => {
3625
+ cursor = Math.max(0, cursor - 50);
3626
+ refresh();
3627
+ }));
3628
+ }
3629
+ if (typeof data?.next_cursor === "number") {
3630
+ pages.append(button(t("resources.next"), () => {
3631
+ cursor = data.next_cursor;
3632
+ refresh();
3633
+ }));
3634
+ }
3635
+ body.append(pages);
3636
+ }
3637
+ if (data?.items.length === 0) {
3638
+ body.append(node4("p", t(key === "connect" ? "resources.emptyTokens" : "resources.emptyPairings"), "empty"));
3639
+ }
3640
+ }
3641
+ region.replaceChildren(body, feedback);
3642
+ }
3643
+ function clear() {
3644
+ active = false;
3645
+ key = "";
3646
+ data = undefined;
3647
+ cursor = 0;
3648
+ tokenName = "";
3649
+ tokenDays = "365";
3650
+ version++;
3651
+ controller.abort();
3652
+ pending = false;
3653
+ region.replaceChildren();
3654
+ region.hidden = true;
3655
+ }
3656
+ return {
3657
+ region,
3658
+ clear,
3659
+ dispose: clear,
3660
+ refresh() {
3661
+ if (active && key !== "device") {
3662
+ refresh();
3663
+ }
3664
+ },
3665
+ update(next, localizer, enabled) {
3666
+ const nextKey = next.machineId ? "device" : next.section === "connect" || next.section === "pairing" ? next.section : "";
3667
+ if (!enabled || !nextKey) {
3668
+ clear();
3669
+ return;
3670
+ }
3671
+ const changed = key !== nextKey || route?.machineId !== next.machineId;
3672
+ const translated = language !== localizer.language;
3673
+ route = next;
3674
+ i18n = localizer;
3675
+ language = localizer.language;
3676
+ active = true;
3677
+ region.hidden = false;
3678
+ if (changed) {
3679
+ key = nextKey;
3680
+ cursor = 0;
3681
+ data = undefined;
3682
+ version++;
3683
+ controller.abort();
3684
+ controller = new AbortController;
3685
+ pending = false;
3686
+ render();
3687
+ if (key !== "device") {
3688
+ refresh();
3689
+ }
3690
+ } else if (translated) {
3691
+ render();
3692
+ }
3693
+ }
3694
+ };
3695
+ }
3696
+
2867
3697
  // src/webui/page.ts
2868
- function element(tag, text, className = "") {
3698
+ function element(tag, text = "", className = "") {
2869
3699
  const result = document.createElement(tag);
2870
3700
  result.textContent = text;
2871
3701
  result.className = className;
@@ -2874,21 +3704,51 @@ function element(tag, text, className = "") {
2874
3704
  async function unavailableRead() {
2875
3705
  throw new ReadError2("unavailable");
2876
3706
  }
2877
- function mountWebUi(root, read = unavailableRead, clock, management) {
2878
- const header = element("header", "", "topbar");
3707
+ function mountWebUi(root, read = unavailableRead, clock, management, resources) {
3708
+ const sidebar = element("aside", "", "sidebar");
2879
3709
  const brand = element("a", "", "brand");
2880
3710
  brand.href = "#";
2881
3711
  const logo = element("img", "", "brand-logo");
2882
- logo.src = "./rovloom-mark-transparent.png";
3712
+ logo.src = "./rovloom-logo.png";
2883
3713
  logo.alt = "Rovloom";
2884
- logo.width = 48;
2885
- logo.height = 48;
2886
- brand.append(logo, element("span", "Rovloom", "brand-name"));
3714
+ logo.width = 180;
3715
+ logo.height = 60;
3716
+ brand.append(logo);
3717
+ const sidebarCaption = element("p", "", "sidebar-caption");
3718
+ const navHeading = element("p", "", "nav-heading");
3719
+ const navigation = element("nav", "", "sidebar-nav");
3720
+ const navLinks = ["overview", "devices", "pairing", "connect"].map((section) => {
3721
+ const link = element("a", "", "nav-link");
3722
+ link.href = section === "overview" ? "#" : "#view=" + section;
3723
+ const label = element("span");
3724
+ link.append(uiIcon(section), label);
3725
+ navigation.append(link);
3726
+ return { section, link, label };
3727
+ });
3728
+ const resourceHeading = element("p", "", "nav-heading resource-heading");
3729
+ const docs = element("a", "", "nav-link docs-link");
3730
+ docs.target = "_blank";
3731
+ docs.rel = "noopener noreferrer";
3732
+ const docsLabel = element("span");
3733
+ docs.append(uiIcon("book"), docsLabel);
3734
+ const sidebarStatus = element("div", "", "sidebar-status");
3735
+ const statusIcon = element("span", "", "sidebar-gateway-icon");
3736
+ statusIcon.append(uiIcon("gateway"));
3737
+ const statusCopy = element("div");
3738
+ const statusLabel = element("strong", "Gateway");
3739
+ const statusValue = element("span", "", "sidebar-status-value");
3740
+ statusCopy.append(statusLabel, statusValue);
3741
+ sidebarStatus.append(statusIcon, statusCopy);
3742
+ sidebar.append(brand, sidebarCaption, navHeading, navigation, resourceHeading, docs, sidebarStatus);
3743
+ const workspace = element("div", "", "workspace");
3744
+ const header = element("header", "", "topbar");
3745
+ const headerLabel = element("div", "", "header-label");
3746
+ headerLabel.append(uiIcon("overview"));
2887
3747
  const title = element("span", "", "brand-context");
3748
+ headerLabel.append(title);
2888
3749
  const preferencesBar = element("div", "", "preferences");
2889
- const commandbar = element("nav", "", "commandbar");
2890
- const sectionLink = element("a", "", "section-nav");
2891
- sectionLink.href = "#";
3750
+ const commandbar = element("div", "", "commandbar");
3751
+ commandbar.setAttribute("role", "toolbar");
2892
3752
  const toolbar = element("div", "", "toolbar");
2893
3753
  const refresh = element("button", "");
2894
3754
  refresh.type = "button";
@@ -2902,25 +3762,24 @@ function mountWebUi(root, read = unavailableRead, clock, management) {
2902
3762
  content.tabIndex = -1;
2903
3763
  refresh.setAttribute("aria-controls", content.id);
2904
3764
  cancel.setAttribute("aria-controls", content.id);
2905
- const language = createPreferenceControl("language", [
2906
- ["system", "language.system"],
2907
- ["zh-CN", "language.chinese"],
2908
- ["en", "language.english"]
2909
- ], (value) => preferences.setLanguage(value));
2910
- const theme = createPreferenceControl("theme", [
2911
- ["system", "theme.system"],
2912
- ["light", "theme.light"],
2913
- ["dark", "theme.dark"]
2914
- ], (value) => preferences.setTheme(value));
3765
+ const language = createPreferenceControl("language", (value) => preferences.setLanguage(value));
3766
+ const theme = createPreferenceControl("theme", (value) => preferences.setTheme(value));
2915
3767
  preferencesBar.append(language.root, theme.root);
2916
3768
  toolbar.append(indicator, refresh, cancel);
2917
- commandbar.append(sectionLink, toolbar);
2918
- header.append(brand, title, preferencesBar);
3769
+ commandbar.append(toolbar);
3770
+ const headerActions = element("div", "", "topbar-actions");
3771
+ headerActions.append(commandbar, preferencesBar);
3772
+ header.append(headerLabel, headerActions);
2919
3773
  const skip = element("a", "", "skip-link");
2920
3774
  skip.href = "#management-content";
2921
3775
  const footer = element("footer", "", "muted");
2922
- root.replaceChildren(skip, header, commandbar, content, footer);
3776
+ const copyFeedback = element("span", "", "visually-hidden");
3777
+ copyFeedback.setAttribute("role", "status");
3778
+ copyFeedback.setAttribute("aria-live", "polite");
3779
+ workspace.append(header, content, copyFeedback, footer);
3780
+ root.replaceChildren(skip, sidebar, workspace);
2923
3781
  let route = parseRoute(location.hash);
3782
+ let filter = { query: "", status: "all" };
2924
3783
  let reader;
2925
3784
  let disposed = false;
2926
3785
  let i18n = createLocalizer("en");
@@ -2946,6 +3805,9 @@ function mountWebUi(root, read = unavailableRead, clock, management) {
2946
3805
  }
2947
3806
  const pairing = management ? createPairingActions({
2948
3807
  transport: management,
3808
+ committed() {
3809
+ resourcePanel?.refresh();
3810
+ },
2949
3811
  authorizationLost(problem) {
2950
3812
  if (disposed) {
2951
3813
  return;
@@ -2959,20 +3821,62 @@ function mountWebUi(root, read = unavailableRead, clock, management) {
2959
3821
  if (pairing) {
2960
3822
  footer.before(pairing.region);
2961
3823
  }
3824
+ const resourcePanel = resources ? createResourcePanel({
3825
+ transport: resources,
3826
+ committed(machine, pairingId) {
3827
+ if (machine) {
3828
+ location.hash = "#view=devices";
3829
+ } else if (pairingId) {
3830
+ pairing?.remove(pairingId);
3831
+ }
3832
+ },
3833
+ authorizationLost(problem) {
3834
+ reader.dispose();
3835
+ actions?.clear();
3836
+ pairing?.clear();
3837
+ paint({ snapshot: null, pending: false, stale: true, ageMs: 0, problem }, route);
3838
+ }
3839
+ }) : undefined;
3840
+ if (resourcePanel) {
3841
+ footer.before(resourcePanel.region);
3842
+ }
2962
3843
  function paint(state, current) {
2963
3844
  refresh.disabled = state.pending || ["unauthenticated", "forbidden", "not_found"].includes(state.problem ?? "");
2964
3845
  cancel.disabled = !state.pending;
2965
3846
  indicator.textContent = i18n.t(state.pending ? "reader.pending" : state.snapshot ? state.stale ? "reader.stale" : "reader.snapshot" : "reader.empty");
2966
3847
  content.setAttribute("aria-busy", String(state.pending));
2967
3848
  indicator.dataset.state = state.pending ? "pending" : state.stale ? "stale" : state.snapshot ? "current" : "empty";
2968
- sectionLink.setAttribute("aria-current", current.machineId === null ? "page" : "location");
2969
3849
  if (["unauthenticated", "forbidden", "not_found"].includes(state.problem ?? "")) {
2970
3850
  actions?.clear();
2971
3851
  pairing?.clear();
2972
3852
  }
2973
3853
  pairing?.update(i18n, !!state.snapshot && !state.stale && state.snapshot.value.status.mode === "active");
2974
3854
  actions?.update(i18n, !!state.snapshot && !state.stale && state.snapshot.value.status.mode === "active", state.snapshot ? state.snapshot.observedAt + state.ageMs : undefined);
2975
- renderPage(content, state, current, i18n, !!actions);
3855
+ const section = current.machineId ? "devices" : current.section ?? "overview";
3856
+ content.dataset.section = section;
3857
+ for (const item of navLinks) {
3858
+ if (item.section === section) {
3859
+ item.link.setAttribute("aria-current", "page");
3860
+ } else {
3861
+ item.link.removeAttribute("aria-current");
3862
+ }
3863
+ }
3864
+ title.textContent = i18n.t("nav." + section);
3865
+ const active = !!state.snapshot && !state.stale && state.snapshot.value.status.mode === "active";
3866
+ sidebarStatus.dataset.state = active ? "current" : "unknown";
3867
+ statusValue.textContent = i18n.t(active ? "gateway.active" : state.snapshot ? state.stale ? "reader.stale" : "gateway.degraded" : "reader.empty");
3868
+ if (pairing) {
3869
+ pairing.region.hidden = section !== "pairing" || !active;
3870
+ }
3871
+ renderPage(content, state, current, i18n, !!actions, filter);
3872
+ const authorized = active && !["unauthenticated", "forbidden", "not_found"].includes(state.problem ?? "");
3873
+ resourcePanel?.update(current, i18n, authorized);
3874
+ if (resourcePanel && section === "connect" && authorized) {
3875
+ const guide = content.querySelector(".connection-guide");
3876
+ if (guide) {
3877
+ guide.hidden = true;
3878
+ }
3879
+ }
2976
3880
  }
2977
3881
  function applyPreferences(state) {
2978
3882
  const languageChanged = i18n.language !== state.language;
@@ -2984,15 +3888,23 @@ function mountWebUi(root, read = unavailableRead, clock, management) {
2984
3888
  document.documentElement.dataset.theme = state.theme;
2985
3889
  document.title = t("app.title");
2986
3890
  brand.setAttribute("aria-label", t("app.navigation"));
2987
- title.textContent = t("app.console");
2988
- sectionLink.textContent = t("devices.heading");
3891
+ title.textContent = t("nav.console");
3892
+ sidebarCaption.textContent = t("app.console");
3893
+ navHeading.textContent = t("nav.workspace");
3894
+ resourceHeading.textContent = t("nav.resources");
3895
+ navigation.setAttribute("aria-label", t("app.subtitle"));
3896
+ docsLabel.textContent = t("nav.docs");
3897
+ docs.href = `https://github.com/ArnoChenFx/rovloom/blob/main/docs/QUICK-START${state.language === "zh-CN" ? "-zh" : ""}.md`;
3898
+ for (const item of navLinks) {
3899
+ item.label.textContent = t("nav." + item.section);
3900
+ }
2989
3901
  commandbar.setAttribute("aria-label", t("app.subtitle"));
2990
3902
  refresh.textContent = t("action.refresh");
2991
3903
  cancel.textContent = t("action.cancel");
2992
3904
  skip.textContent = t("app.skip");
2993
3905
  footer.textContent = t(actions ? "app.managedFooter" : "app.footer");
2994
- language.update(i18n, state.languageChoice);
2995
- theme.update(i18n, state.themeChoice);
3906
+ language.update(i18n, state.language);
3907
+ theme.update(i18n, state.theme);
2996
3908
  if (languageChanged && reader) {
2997
3909
  paint(reader.state(), route);
2998
3910
  }
@@ -3014,6 +3926,7 @@ function mountWebUi(root, read = unavailableRead, clock, management) {
3014
3926
  content.focus();
3015
3927
  }
3016
3928
  function refreshPage() {
3929
+ resourcePanel?.refresh();
3017
3930
  reader.refresh();
3018
3931
  }
3019
3932
  function cancelRead() {
@@ -3054,11 +3967,41 @@ function mountWebUi(root, read = unavailableRead, clock, management) {
3054
3967
  actions.open({ action, machine_id: device.machine_id }, button);
3055
3968
  }
3056
3969
  }
3970
+ function searchDevices(event) {
3971
+ if (event.target instanceof HTMLInputElement && event.target.name === "device-search") {
3972
+ filter.query = event.target.value;
3973
+ paint(reader.state(), route);
3974
+ }
3975
+ }
3976
+ function filterDevices(event) {
3977
+ const button = event.target instanceof Element ? event.target.closest("[data-device-filter]") : null;
3978
+ const selected = button?.dataset.deviceFilter;
3979
+ if (selected === "all" || selected === "online" || selected === "attention") {
3980
+ filter.status = selected;
3981
+ paint(reader.state(), route);
3982
+ }
3983
+ }
3984
+ async function copy(event) {
3985
+ const button = event.target instanceof Element ? event.target.closest("button[data-copy]") : null;
3986
+ if (!button || button.disabled || !root.contains(button)) {
3987
+ return;
3988
+ }
3989
+ const value = button.dataset.copy;
3990
+ if (value === undefined) {
3991
+ return;
3992
+ }
3993
+ copyFeedback.textContent = "";
3994
+ const success = await copyText(value);
3995
+ if (!disposed) {
3996
+ copyFeedback.textContent = i18n.t(success ? "copy.done" : "copy.failed");
3997
+ button.title = copyFeedback.textContent;
3998
+ button.dataset.copied = String(success);
3999
+ }
4000
+ }
3057
4001
  function navigate() {
4002
+ filter = { query: "", status: "all" };
3058
4003
  actions?.suspend();
3059
4004
  pairing?.suspend();
3060
- language.close();
3061
- theme.close();
3062
4005
  if (location.hash === "#management-content") {
3063
4006
  content.focus();
3064
4007
  return;
@@ -3086,9 +4029,8 @@ function mountWebUi(root, read = unavailableRead, clock, management) {
3086
4029
  function hide() {
3087
4030
  actions?.suspend();
3088
4031
  pairing?.suspend();
3089
- language.close();
3090
- theme.close();
3091
4032
  reader.dispose();
4033
+ resourcePanel?.clear();
3092
4034
  content.replaceChildren();
3093
4035
  }
3094
4036
  function dispose() {
@@ -3097,9 +4039,13 @@ function mountWebUi(root, read = unavailableRead, clock, management) {
3097
4039
  }
3098
4040
  disposed = true;
3099
4041
  reader.dispose();
4042
+ resourcePanel?.dispose();
3100
4043
  actions?.dispose();
3101
4044
  pairing?.dispose();
3102
4045
  content.removeEventListener("click", deviceAction);
4046
+ content.removeEventListener("click", filterDevices);
4047
+ content.removeEventListener("input", searchDevices);
4048
+ root.removeEventListener("click", copy);
3103
4049
  skip.removeEventListener("click", skipContent);
3104
4050
  refresh.removeEventListener("click", refreshPage);
3105
4051
  cancel.removeEventListener("click", cancelRead);
@@ -3116,6 +4062,9 @@ function mountWebUi(root, read = unavailableRead, clock, management) {
3116
4062
  refresh.addEventListener("click", refreshPage);
3117
4063
  cancel.addEventListener("click", cancelRead);
3118
4064
  content.addEventListener("click", deviceAction);
4065
+ content.addEventListener("click", filterDevices);
4066
+ content.addEventListener("input", searchDevices);
4067
+ root.addEventListener("click", copy);
3119
4068
  window.addEventListener("hashchange", navigate);
3120
4069
  document.addEventListener("visibilitychange", visibility);
3121
4070
  window.addEventListener("pageshow", show);
@@ -3507,4 +4456,4 @@ export {
3507
4456
  unavailableRead
3508
4457
  };
3509
4458
 
3510
- //# debugId=8E3F9EB456F6148764756E2164756E21
4459
+ //# debugId=9FAF58405B4DF80264756E2164756E21