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