rovloom 0.5.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 (34) hide show
  1. package/README.md +8 -0
  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-rwpynx2j.js → chunk-0qgk7mhz.js} +12 -11
  5. package/dist/src/{chunk-erdq0rn8.js → chunk-3dvn33c4.js} +57 -58
  6. package/dist/src/chunk-5b8g62kw.js +438 -0
  7. package/dist/src/{chunk-64ftwe9b.js → chunk-8ykrqhxh.js} +8 -2
  8. package/dist/src/{chunk-9y36gntb.js → chunk-9zkq0kc8.js} +10 -5
  9. package/dist/src/{chunk-5n8csngz.js → chunk-d2kz25f2.js} +156 -11
  10. package/dist/src/{chunk-ympzv86c.js → chunk-ez7d5dac.js} +13 -4
  11. package/dist/src/{chunk-emcefrc1.js → chunk-g27b2gmx.js} +3 -4
  12. package/dist/src/{chunk-abd4envz.js → chunk-gyvqqnjb.js} +30 -57
  13. package/dist/src/{chunk-e7x51y0t.js → chunk-jf4r0r44.js} +2 -2
  14. package/dist/src/{chunk-4etdq0cp.js → chunk-jka7v3wb.js} +4 -7
  15. package/dist/src/{chunk-xhq21g24.js → chunk-m9c1pnv9.js} +3 -3
  16. package/dist/src/{chunk-7k9g0x5e.js → chunk-mg0fd6ph.js} +1 -1
  17. package/dist/src/{chunk-0ake7e4r.js → chunk-n6wtjyfb.js} +6 -14
  18. package/dist/src/{chunk-nyn75b8v.js → chunk-qjh9x872.js} +30 -1
  19. package/dist/src/{chunk-r0nfthsf.js → chunk-s3ydr65k.js} +2 -2
  20. package/dist/src/{chunk-ybte82d3.js → chunk-srfbjwtk.js} +4 -3
  21. package/dist/src/{chunk-sj24fpz3.js → chunk-ssp57p43.js} +24 -28
  22. package/dist/src/{chunk-ebm3kvnq.js → chunk-wxqhscsw.js} +26 -3
  23. package/dist/src/{chunk-7q96e161.js → chunk-x0td5wn4.js} +6 -12
  24. package/dist/src/{chunk-9yg6x3gt.js → chunk-ytpfmcp1.js} +4 -3
  25. package/dist/src/{chunk-fnakrk0a.js → chunk-yv9px0f5.js} +22 -25
  26. package/dist/src/{chunk-n073xnr1.js → chunk-zjkjjm81.js} +43 -46
  27. package/dist/src/cli/index.js +23 -15
  28. package/dist/src/cli/main.js +14 -9
  29. package/dist/src/mcp/index.js +6 -5
  30. package/dist/src/runtime/index.js +2 -2
  31. package/dist/src/service/index.js +3 -4
  32. package/dist/src/store.js +1 -1
  33. package/dist/src/webui/index.js +1095 -156
  34. package/package.json +1 -1
@@ -13,6 +13,111 @@ import {
13
13
 
14
14
  // src/webui/i18n.ts
15
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",
16
121
  "provider.disable": "\u7981\u7528\u6B64 provider",
17
122
  "provider.target": "Provider \u76EE\u6807",
18
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",
@@ -21,8 +126,9 @@ var chinese = Object.freeze({
21
126
  "app.subtitle": "\u8BBE\u5907\u4E0E\u8FDE\u63A5\u7BA1\u7406",
22
127
  "app.console": "Gateway \u63A7\u5236\u53F0",
23
128
  "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",
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}",
26
132
  "language.label": "\u8BED\u8A00",
27
133
  "language.system": "\u8DDF\u968F\u6D4F\u89C8\u5668",
28
134
  "language.chinese": "\u7B80\u4F53\u4E2D\u6587",
@@ -60,7 +166,7 @@ var chinese = Object.freeze({
60
166
  "manage.nativeEffect": "\u6B64\u64CD\u4F5C\u4E0D\u4FDD\u8BC1\u505C\u6B62\u6216\u56DE\u6EDA\u539F\u751F\u5DE5\u4F5C\u3002",
61
167
  "manage.action": "\u64CD\u4F5C",
62
168
  "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",
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",
64
170
  "pairing.name": "\u65B0\u8BBE\u5907\u540D\u79F0",
65
171
  "pairing.policy": "\u6388\u6743\u653F\u7B56\u5F15\u7528",
66
172
  "pairing.create": "\u65B0\u5EFA\u914D\u5BF9\u7533\u8BF7",
@@ -70,7 +176,7 @@ var chinese = Object.freeze({
70
176
  "pairing.cancel": "\u53D6\u6D88",
71
177
  "pairing.created": "\u7533\u8BF7\u5DF2\u521B\u5EFA\u3002\u628A\u7533\u8BF7\u7F16\u53F7\u4EA4\u7ED9\u65B0\u8BBE\u5907\uFF1A",
72
178
  "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",
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",
74
180
  "pairing.userCode": "\u6838\u5BF9\u7801",
75
181
  "pairing.approve": "\u6279\u51C6\u7B7E\u53D1\u51ED\u636E",
76
182
  "pairing.approved": "\u5DF2\u6279\u51C6\uFF0C\u51ED\u636E\u5DF2\u6258\u7BA1\u4EA4\u4ED8\uFF0C\u7B49\u5F85\u8BBE\u5907\u53D6\u56DE\u2026",
@@ -102,7 +208,7 @@ var chinese = Object.freeze({
102
208
  "action.cancel": "\u53D6\u6D88\u8BFB\u53D6",
103
209
  "reader.pending": "\u6B63\u5728\u8BFB\u53D6\u2026",
104
210
  "reader.stale": "\u5FEB\u7167\u5DF2\u8FC7\u671F",
105
- "reader.snapshot": "\u53EA\u8BFB\u5FEB\u7167",
211
+ "reader.snapshot": "\u5DF2\u66F4\u65B0",
106
212
  "reader.empty": "\u65E0\u53EF\u7528\u5FEB\u7167",
107
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",
108
214
  "error.forbidden": "\u5F53\u524D\u7BA1\u7406\u5458\u6CA1\u6709\u6B64\u8303\u56F4\u7684\u67E5\u770B\u6743\u9650\u3002",
@@ -245,6 +351,111 @@ var chinese = Object.freeze({
245
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"
246
352
  });
247
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.",
248
459
  "provider.disable": "Disable this provider",
249
460
  "provider.target": "Provider target",
250
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.",
@@ -253,8 +464,9 @@ var english = Object.freeze({
253
464
  "app.subtitle": "Device and connection management",
254
465
  "app.console": "Gateway console",
255
466
  "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",
467
+ "app.managedFooter": "Rovloom \xB7 Your devices, agents and workflows.",
468
+ "app.footer": "Rovloom \xB7 Gateway console",
469
+ "preference.switch": "Switch to {value}",
258
470
  "language.label": "Language",
259
471
  "language.system": "Browser language",
260
472
  "language.chinese": "\u7B80\u4F53\u4E2D\u6587",
@@ -292,7 +504,7 @@ var english = Object.freeze({
292
504
  "manage.nativeEffect": "This operation does not guarantee stopping or rolling back native work.",
293
505
  "manage.action": "Action",
294
506
  "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.",
507
+ "pairing.intro": "Give your workstation a name. Create an application, then follow the steps on the right.",
296
508
  "pairing.name": "New device name",
297
509
  "pairing.policy": "Authorization policy reference",
298
510
  "pairing.create": "Create pairing application",
@@ -334,7 +546,7 @@ var english = Object.freeze({
334
546
  "action.cancel": "Cancel read",
335
547
  "reader.pending": "Reading\u2026",
336
548
  "reader.stale": "Snapshot expired",
337
- "reader.snapshot": "Read-only snapshot",
549
+ "reader.snapshot": "Up to date",
338
550
  "reader.empty": "No snapshot",
339
551
  "error.unauthenticated": "The management session is missing or expired. Sign in through the protected administrator entry.",
340
552
  "error.forbidden": "The current administrator cannot view this scope.",
@@ -508,6 +720,84 @@ function createLocalizer(language) {
508
720
  };
509
721
  }
510
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
+
511
801
  // src/webui/view.ts
512
802
  var connectionNames = {
513
803
  connecting: "connection.connecting",
@@ -579,6 +869,9 @@ function routeHref(route) {
579
869
  if (route.providerCursor !== null) {
580
870
  parameters.set("providers", route.providerCursor);
581
871
  }
872
+ if (route.machineId === null && route.section) {
873
+ parameters.set("view", route.section);
874
+ }
582
875
  return `#${parameters.toString()}`;
583
876
  }
584
877
  function parseRoute(hash) {
@@ -587,7 +880,7 @@ function parseRoute(hash) {
587
880
  return empty;
588
881
  }
589
882
  const parameters = new URLSearchParams(hash.replace(/^#/, ""));
590
- for (const key of ["machine", "cursor", "providers"]) {
883
+ for (const key of ["machine", "cursor", "providers", "view"]) {
591
884
  if (parameters.getAll(key).length > 1 || (parameters.get(key)?.length ?? 0) > 1024) {
592
885
  return empty;
593
886
  }
@@ -596,7 +889,8 @@ function parseRoute(hash) {
596
889
  return {
597
890
  machineId,
598
891
  cursor: machineId ? null : parameters.get("cursor") || null,
599
- 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") } : {}
600
894
  };
601
895
  }
602
896
  function current(observedAt, freshness) {
@@ -624,7 +918,7 @@ function listObservation(key, observedAt, freshness) {
624
918
  function currentDeviceLink(device, freshness) {
625
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";
626
920
  }
627
- function deviceBadges(device, freshness) {
921
+ function deviceBadges(device, freshness, compact = false) {
628
922
  const { i18n } = freshness;
629
923
  const { t } = i18n;
630
924
  const result = node("div", "", "badges");
@@ -661,7 +955,9 @@ function deviceBadges(device, freshness) {
661
955
  } else {
662
956
  result.append(badge(t("device.authUnknown", { reason: i18n.reason(authentication.reason_code) }), "warning"));
663
957
  }
664
- result.append(badge(t("device.readyUnknown")));
958
+ if (!compact) {
959
+ result.append(badge(t("device.readyUnknown")));
960
+ }
665
961
  return result;
666
962
  }
667
963
  function unavailableActions(kind, i18n) {
@@ -703,20 +999,154 @@ function deviceActions(page, freshness, connected) {
703
999
  }
704
1000
  return section;
705
1001
  }
706
- 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) {
707
1079
  const { i18n } = freshness;
708
1080
  const { t } = i18n;
709
1081
  const heading = node("header", "", "page-heading");
710
- 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);
711
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
+ });
712
1136
  if (page.devices.items.length === 0) {
713
1137
  const empty = node("div", "", "empty");
714
- 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);
715
1145
  section.append(empty);
716
1146
  }
717
1147
  const columns = node("div", "", "device-columns");
718
1148
  columns.setAttribute("aria-hidden", "true");
719
- 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"]) {
720
1150
  columns.append(node("span", t(key)));
721
1151
  }
722
1152
  if (page.devices.items.length) {
@@ -724,32 +1154,60 @@ function deviceList(page, freshness) {
724
1154
  }
725
1155
  const list = node("ul", "", "device-list");
726
1156
  list.setAttribute("aria-label", t("devices.heading"));
727
- for (const device of page.devices.items) {
1157
+ for (const device of items) {
728
1158
  const row = node("li", "", "device-card");
729
1159
  const identity = node("div", "", "device-identity");
730
1160
  const link = node("a", device.name, "device-name");
731
1161
  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"));
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"));
734
1170
  const health = node("div", "", "device-health");
735
- 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));
736
1172
  const heartbeat = device.connection.verification === "verified" ? device.connection.value.last_heartbeat_at : null;
737
1173
  const activity = node("div", "", "device-activity");
738
1174
  activity.append(node("span", t("devices.activity"), "cell-label"));
739
1175
  activity.append(node("strong", heartbeat === null ? t("time.noHeartbeat") : i18n.age(heartbeat, freshness.now), "activity-age"));
740
1176
  activity.append(node("p", t("devices.received", { time: i18n.timestamp(heartbeat) }), "muted"));
741
1177
  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));
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);
746
1191
  row.append(identity, health, activity, observations);
747
1192
  list.append(row);
748
1193
  }
749
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
+ }
750
1203
  if (page.devices.next_cursor !== undefined) {
751
1204
  const next = node("a", t("devices.next"), "button-link");
752
- 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
+ });
753
1211
  section.append(next);
754
1212
  }
755
1213
  return { heading, body: section };
@@ -894,11 +1352,15 @@ function syncChildren(parent, desired) {
894
1352
  parent.removeChild(parent.lastChild);
895
1353
  }
896
1354
  }
897
- 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" }) {
898
1356
  const { t } = i18n;
899
1357
  const focused = content.contains(document.activeElement) && document.activeElement instanceof HTMLAnchorElement ? document.activeElement.getAttribute("href") : null;
900
1358
  const metadataWasOpen = content.querySelector(".gateway-metadata")?.open ?? false;
901
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
+ ]));
902
1364
  const view = document.createDocumentFragment();
903
1365
  if (state.problem) {
904
1366
  const message = node("p", t(problemNames[state.problem]), "notice warning");
@@ -927,8 +1389,28 @@ function renderPage(content, state, route, i18n = createLocalizer("zh-CN"), mana
927
1389
  gatewayHeading.append(node("h2", t("gateway.heading")));
928
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"));
929
1391
  gatewayHeading.append(badge(state.stale ? t("reader.stale") : t("gateway.fresh"), state.stale ? "warning" : "neutral"));
930
- gateway.append(gatewayHeading);
931
- 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([
932
1414
  [t("gateway.listener"), t("gateway.listenerUnknown")],
933
1415
  [
934
1416
  t("gateway.authentication"),
@@ -947,22 +1429,28 @@ function renderPage(content, state, route, i18n = createLocalizer("zh-CN"), mana
947
1429
  })
948
1430
  ]
949
1431
  ], "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
1432
  gateway.append(metadata);
958
1433
  if (status.mode !== "active") {
959
1434
  gateway.append(node("p", t("gateway.degradedNote"), "notice warning"));
960
1435
  }
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);
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);
963
1448
  }
964
1449
  const identity = routeHref(route);
965
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
+ }
966
1454
  syncChildren(content, view);
967
1455
  } else {
968
1456
  content.replaceChildren(view);
@@ -1080,9 +1568,8 @@ function createUiPreferences(changed, host = browserHost) {
1080
1568
  }
1081
1569
 
1082
1570
  // src/webui/preference-control.ts
1083
- var paths = {
1571
+ var paths2 = {
1084
1572
  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
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",
1087
1574
  dark: "M20.9 13A9 9 0 0 1 11 3.1 9 9 0 1 0 20.9 13Z"
1088
1575
  };
@@ -1097,102 +1584,50 @@ function icon(name) {
1097
1584
  svg.setAttribute("stroke-linecap", "round");
1098
1585
  svg.setAttribute("stroke-linejoin", "round");
1099
1586
  const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
1100
- path.setAttribute("d", paths[name]);
1587
+ path.setAttribute("d", paths2[name]);
1101
1588
  svg.append(path);
1102
1589
  return svg;
1103
1590
  }
1104
- function createPreferenceControl(kind, choices, onChoose) {
1591
+ function createPreferenceControl(kind, onChoose) {
1105
1592
  const root = document.createElement("div");
1106
1593
  root.className = "preference";
1107
1594
  root.dataset.preference = kind;
1108
1595
  const trigger = document.createElement("button");
1109
1596
  trigger.type = "button";
1110
1597
  trigger.className = "preference-trigger";
1111
- trigger.setAttribute("aria-expanded", "false");
1112
1598
  const current = document.createElement("span");
1113
1599
  current.className = "visually-hidden";
1114
1600
  current.id = `rovloom-${kind}-current`;
1115
1601
  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();
1602
+ root.append(trigger, current);
1603
+ let next = "";
1604
+ function click() {
1605
+ if (next) {
1606
+ onChoose(next);
1166
1607
  }
1167
1608
  }
1168
1609
  function update(i18n, value) {
1169
- 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 })}`;
1170
1616
  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
1617
  trigger.title = `${label}: ${current.textContent}`;
1182
- 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"));
1183
1621
  }
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 };
1622
+ trigger.addEventListener("click", click);
1623
+ return {
1624
+ root,
1625
+ update,
1626
+ dispose() {
1627
+ next = "";
1628
+ trigger.removeEventListener("click", click);
1629
+ }
1630
+ };
1196
1631
  }
1197
1632
 
1198
1633
  // src/webui/request-id.ts
@@ -2552,6 +2987,18 @@ var errors2 = {
2552
2987
  outcome_unconfirmed: "manage.unknown",
2553
2988
  aborted: "manage.cancelled"
2554
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
+ }
2555
3002
  function node3(tag, className = "") {
2556
3003
  const element = document.createElement(tag);
2557
3004
  element.className = className;
@@ -2559,6 +3006,18 @@ function node3(tag, className = "") {
2559
3006
  }
2560
3007
  function createPairingActions(options) {
2561
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");
2562
3021
  const heading = node3("h2");
2563
3022
  const intro = node3("p", "muted");
2564
3023
  const form = node3("form", "pairing-form");
@@ -2570,7 +3029,10 @@ function createPairingActions(options) {
2570
3029
  nameInput.maxLength = 63;
2571
3030
  nameInput.pattern = "[a-z0-9][a-z0-9._-]{0,62}";
2572
3031
  nameInput.autocomplete = "off";
2573
- 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);
2574
3036
  const policyLabel = node3("label");
2575
3037
  const policyCaption = node3("span");
2576
3038
  const policyInput = node3("input");
@@ -2582,7 +3044,10 @@ function createPairingActions(options) {
2582
3044
  policyLabel.append(policyCaption, policyInput);
2583
3045
  const create = node3("button", "pairing-confirm");
2584
3046
  create.type = "submit";
2585
- 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);
2586
3051
  const reviewBox = node3("div", "pairing-review");
2587
3052
  reviewBox.hidden = true;
2588
3053
  const reviewText = node3("p", "pairing-status");
@@ -2600,7 +3065,21 @@ function createPairingActions(options) {
2600
3065
  message.setAttribute("role", "status");
2601
3066
  message.setAttribute("aria-live", "polite");
2602
3067
  const list = node3("ul", "pairing-entries");
2603
- 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);
2604
3083
  let i18n;
2605
3084
  let available = false;
2606
3085
  let disposed = false;
@@ -2705,6 +3184,11 @@ function createPairingActions(options) {
2705
3184
  if (!i18n) {
2706
3185
  return;
2707
3186
  }
3187
+ const signature = entryRenderSignature(i18n.language, entry);
3188
+ if (entry.rendered === signature) {
3189
+ return;
3190
+ }
3191
+ entry.rendered = signature;
2708
3192
  const t = i18n.t.bind(i18n);
2709
3193
  entry.item.replaceChildren();
2710
3194
  const head = node3("div", "pairing-entry-head");
@@ -2714,19 +3198,21 @@ function createPairingActions(options) {
2714
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");
2715
3199
  head.append(title, status);
2716
3200
  entry.item.append(head);
3201
+ status.classList.add("badge");
2717
3202
  const facts = node3("dl", "facts");
2718
3203
  const idTerm = node3("dt");
2719
3204
  idTerm.textContent = t("pairing.application");
2720
3205
  const idValue = node3("dd");
2721
3206
  const idCode = node3("code");
2722
3207
  idCode.textContent = entry.pairing_id;
2723
- idValue.append(idCode);
3208
+ idValue.append(idCode, copyControl(entry.pairing_id, t("copy.id")));
2724
3209
  const machineTerm = node3("dt");
2725
3210
  machineTerm.textContent = t("pairing.machine");
2726
3211
  const machineValue = node3("dd");
2727
3212
  machineValue.textContent = entry.machine_id;
2728
3213
  facts.append(idTerm, idValue, machineTerm, machineValue);
2729
3214
  entry.item.append(facts);
3215
+ updateSteps();
2730
3216
  if (entry.status === "pending" && entry.claim?.status === "pending") {
2731
3217
  const codeLabel = node3("p", "muted");
2732
3218
  codeLabel.textContent = t("pairing.userCode");
@@ -2748,6 +3234,19 @@ function createPairingActions(options) {
2748
3234
  entry.item.append(codeLabel, code, approve);
2749
3235
  }
2750
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
+ }
2751
3250
  function addEntry(entry) {
2752
3251
  const existing = entries.get(entry.pairing_id);
2753
3252
  if (existing) {
@@ -2756,6 +3255,7 @@ function createPairingActions(options) {
2756
3255
  return;
2757
3256
  }
2758
3257
  entries.set(entry.pairing_id, entry);
3258
+ options.committed?.();
2759
3259
  list.append(entry.item);
2760
3260
  renderEntry(entry);
2761
3261
  }
@@ -2837,7 +3337,19 @@ function createPairingActions(options) {
2837
3337
  available = isAvailable;
2838
3338
  region.hidden = !isAvailable;
2839
3339
  const t = localizer.t.bind(localizer);
2840
- 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");
2841
3353
  intro.textContent = t("pairing.intro");
2842
3354
  nameCaption.textContent = t("pairing.name");
2843
3355
  policyCaption.textContent = t("pairing.policy");
@@ -2856,6 +3368,17 @@ function createPairingActions(options) {
2856
3368
  suspend() {
2857
3369
  stopPolling();
2858
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
+ },
2859
3382
  clear() {
2860
3383
  stopPolling();
2861
3384
  flow?.dispose();
@@ -2874,8 +3397,305 @@ function createPairingActions(options) {
2874
3397
  };
2875
3398
  }
2876
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
+
2877
3697
  // src/webui/page.ts
2878
- function element(tag, text, className = "") {
3698
+ function element(tag, text = "", className = "") {
2879
3699
  const result = document.createElement(tag);
2880
3700
  result.textContent = text;
2881
3701
  result.className = className;
@@ -2884,21 +3704,51 @@ function element(tag, text, className = "") {
2884
3704
  async function unavailableRead() {
2885
3705
  throw new ReadError2("unavailable");
2886
3706
  }
2887
- function mountWebUi(root, read = unavailableRead, clock, management) {
2888
- const header = element("header", "", "topbar");
3707
+ function mountWebUi(root, read = unavailableRead, clock, management, resources) {
3708
+ const sidebar = element("aside", "", "sidebar");
2889
3709
  const brand = element("a", "", "brand");
2890
3710
  brand.href = "#";
2891
3711
  const logo = element("img", "", "brand-logo");
2892
- logo.src = "./rovloom-mark-transparent.png";
3712
+ logo.src = "./rovloom-logo.png";
2893
3713
  logo.alt = "Rovloom";
2894
- logo.width = 48;
2895
- logo.height = 48;
2896
- 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"));
2897
3747
  const title = element("span", "", "brand-context");
3748
+ headerLabel.append(title);
2898
3749
  const preferencesBar = element("div", "", "preferences");
2899
- const commandbar = element("nav", "", "commandbar");
2900
- const sectionLink = element("a", "", "section-nav");
2901
- sectionLink.href = "#";
3750
+ const commandbar = element("div", "", "commandbar");
3751
+ commandbar.setAttribute("role", "toolbar");
2902
3752
  const toolbar = element("div", "", "toolbar");
2903
3753
  const refresh = element("button", "");
2904
3754
  refresh.type = "button";
@@ -2912,25 +3762,24 @@ function mountWebUi(root, read = unavailableRead, clock, management) {
2912
3762
  content.tabIndex = -1;
2913
3763
  refresh.setAttribute("aria-controls", content.id);
2914
3764
  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));
3765
+ const language = createPreferenceControl("language", (value) => preferences.setLanguage(value));
3766
+ const theme = createPreferenceControl("theme", (value) => preferences.setTheme(value));
2925
3767
  preferencesBar.append(language.root, theme.root);
2926
3768
  toolbar.append(indicator, refresh, cancel);
2927
- commandbar.append(sectionLink, toolbar);
2928
- 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);
2929
3773
  const skip = element("a", "", "skip-link");
2930
3774
  skip.href = "#management-content";
2931
3775
  const footer = element("footer", "", "muted");
2932
- 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);
2933
3781
  let route = parseRoute(location.hash);
3782
+ let filter = { query: "", status: "all" };
2934
3783
  let reader;
2935
3784
  let disposed = false;
2936
3785
  let i18n = createLocalizer("en");
@@ -2956,6 +3805,9 @@ function mountWebUi(root, read = unavailableRead, clock, management) {
2956
3805
  }
2957
3806
  const pairing = management ? createPairingActions({
2958
3807
  transport: management,
3808
+ committed() {
3809
+ resourcePanel?.refresh();
3810
+ },
2959
3811
  authorizationLost(problem) {
2960
3812
  if (disposed) {
2961
3813
  return;
@@ -2969,20 +3821,62 @@ function mountWebUi(root, read = unavailableRead, clock, management) {
2969
3821
  if (pairing) {
2970
3822
  footer.before(pairing.region);
2971
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
+ }
2972
3843
  function paint(state, current) {
2973
3844
  refresh.disabled = state.pending || ["unauthenticated", "forbidden", "not_found"].includes(state.problem ?? "");
2974
3845
  cancel.disabled = !state.pending;
2975
3846
  indicator.textContent = i18n.t(state.pending ? "reader.pending" : state.snapshot ? state.stale ? "reader.stale" : "reader.snapshot" : "reader.empty");
2976
3847
  content.setAttribute("aria-busy", String(state.pending));
2977
3848
  indicator.dataset.state = state.pending ? "pending" : state.stale ? "stale" : state.snapshot ? "current" : "empty";
2978
- sectionLink.setAttribute("aria-current", current.machineId === null ? "page" : "location");
2979
3849
  if (["unauthenticated", "forbidden", "not_found"].includes(state.problem ?? "")) {
2980
3850
  actions?.clear();
2981
3851
  pairing?.clear();
2982
3852
  }
2983
3853
  pairing?.update(i18n, !!state.snapshot && !state.stale && state.snapshot.value.status.mode === "active");
2984
3854
  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);
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
+ }
2986
3880
  }
2987
3881
  function applyPreferences(state) {
2988
3882
  const languageChanged = i18n.language !== state.language;
@@ -2994,15 +3888,23 @@ function mountWebUi(root, read = unavailableRead, clock, management) {
2994
3888
  document.documentElement.dataset.theme = state.theme;
2995
3889
  document.title = t("app.title");
2996
3890
  brand.setAttribute("aria-label", t("app.navigation"));
2997
- title.textContent = t("app.console");
2998
- 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
+ }
2999
3901
  commandbar.setAttribute("aria-label", t("app.subtitle"));
3000
3902
  refresh.textContent = t("action.refresh");
3001
3903
  cancel.textContent = t("action.cancel");
3002
3904
  skip.textContent = t("app.skip");
3003
3905
  footer.textContent = t(actions ? "app.managedFooter" : "app.footer");
3004
- language.update(i18n, state.languageChoice);
3005
- theme.update(i18n, state.themeChoice);
3906
+ language.update(i18n, state.language);
3907
+ theme.update(i18n, state.theme);
3006
3908
  if (languageChanged && reader) {
3007
3909
  paint(reader.state(), route);
3008
3910
  }
@@ -3024,6 +3926,7 @@ function mountWebUi(root, read = unavailableRead, clock, management) {
3024
3926
  content.focus();
3025
3927
  }
3026
3928
  function refreshPage() {
3929
+ resourcePanel?.refresh();
3027
3930
  reader.refresh();
3028
3931
  }
3029
3932
  function cancelRead() {
@@ -3064,11 +3967,41 @@ function mountWebUi(root, read = unavailableRead, clock, management) {
3064
3967
  actions.open({ action, machine_id: device.machine_id }, button);
3065
3968
  }
3066
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
+ }
3067
4001
  function navigate() {
4002
+ filter = { query: "", status: "all" };
3068
4003
  actions?.suspend();
3069
4004
  pairing?.suspend();
3070
- language.close();
3071
- theme.close();
3072
4005
  if (location.hash === "#management-content") {
3073
4006
  content.focus();
3074
4007
  return;
@@ -3096,9 +4029,8 @@ function mountWebUi(root, read = unavailableRead, clock, management) {
3096
4029
  function hide() {
3097
4030
  actions?.suspend();
3098
4031
  pairing?.suspend();
3099
- language.close();
3100
- theme.close();
3101
4032
  reader.dispose();
4033
+ resourcePanel?.clear();
3102
4034
  content.replaceChildren();
3103
4035
  }
3104
4036
  function dispose() {
@@ -3107,9 +4039,13 @@ function mountWebUi(root, read = unavailableRead, clock, management) {
3107
4039
  }
3108
4040
  disposed = true;
3109
4041
  reader.dispose();
4042
+ resourcePanel?.dispose();
3110
4043
  actions?.dispose();
3111
4044
  pairing?.dispose();
3112
4045
  content.removeEventListener("click", deviceAction);
4046
+ content.removeEventListener("click", filterDevices);
4047
+ content.removeEventListener("input", searchDevices);
4048
+ root.removeEventListener("click", copy);
3113
4049
  skip.removeEventListener("click", skipContent);
3114
4050
  refresh.removeEventListener("click", refreshPage);
3115
4051
  cancel.removeEventListener("click", cancelRead);
@@ -3126,6 +4062,9 @@ function mountWebUi(root, read = unavailableRead, clock, management) {
3126
4062
  refresh.addEventListener("click", refreshPage);
3127
4063
  cancel.addEventListener("click", cancelRead);
3128
4064
  content.addEventListener("click", deviceAction);
4065
+ content.addEventListener("click", filterDevices);
4066
+ content.addEventListener("input", searchDevices);
4067
+ root.addEventListener("click", copy);
3129
4068
  window.addEventListener("hashchange", navigate);
3130
4069
  document.addEventListener("visibilitychange", visibility);
3131
4070
  window.addEventListener("pageshow", show);
@@ -3517,4 +4456,4 @@ export {
3517
4456
  unavailableRead
3518
4457
  };
3519
4458
 
3520
- //# debugId=D52975500C6ECA9664756E2164756E21
4459
+ //# debugId=9FAF58405B4DF80264756E2164756E21