@opendatalabs/personal-server-ts-server 0.2.0 → 1.0.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/dist/api-auth.d.ts +16 -0
- package/dist/api-auth.d.ts.map +1 -0
- package/dist/api-auth.js +97 -0
- package/dist/api-auth.js.map +1 -0
- package/dist/api.d.ts +4 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +3 -0
- package/dist/api.js.map +1 -1
- package/dist/app.d.ts +46 -1
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +102 -30
- package/dist/app.js.map +1 -1
- package/dist/bootstrap.d.ts +11 -3
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +266 -33
- package/dist/bootstrap.js.map +1 -1
- package/dist/client.d.ts +17 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +368 -0
- package/dist/client.js.map +1 -0
- package/dist/config/defaults.d.ts +12 -0
- package/dist/config/defaults.d.ts.map +1 -0
- package/dist/config/defaults.js +14 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/index.d.ts +4 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +4 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/loader.d.ts +8 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +74 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/paths.d.ts +9 -0
- package/dist/config/paths.d.ts.map +1 -0
- package/dist/config/paths.js +22 -0
- package/dist/config/paths.js.map +1 -0
- package/dist/hono-context.d.ts +14 -0
- package/dist/hono-context.d.ts.map +1 -0
- package/dist/hono-context.js +2 -0
- package/dist/hono-context.js.map +1 -0
- package/dist/index.js +64 -12
- package/dist/index.js.map +1 -1
- package/dist/keys/server-account.d.ts +28 -0
- package/dist/keys/server-account.d.ts.map +1 -0
- package/dist/keys/server-account.js +48 -0
- package/dist/keys/server-account.js.map +1 -0
- package/dist/logger/index.d.ts +5 -0
- package/dist/logger/index.d.ts.map +1 -0
- package/dist/logger/index.js +9 -0
- package/dist/logger/index.js.map +1 -0
- package/dist/logging/access-log.d.ts +15 -0
- package/dist/logging/access-log.d.ts.map +1 -0
- package/dist/logging/access-log.js +25 -0
- package/dist/logging/access-log.js.map +1 -0
- package/dist/logging/access-reader.d.ts +19 -0
- package/dist/logging/access-reader.d.ts.map +1 -0
- package/dist/logging/access-reader.js +50 -0
- package/dist/logging/access-reader.js.map +1 -0
- package/dist/middleware/access-log.d.ts.map +1 -1
- package/dist/middleware/access-log.js +8 -2
- package/dist/middleware/access-log.js.map +1 -1
- package/dist/middleware/builder-check.d.ts +2 -2
- package/dist/middleware/builder-check.d.ts.map +1 -1
- package/dist/middleware/builder-check.js +7 -2
- package/dist/middleware/builder-check.js.map +1 -1
- package/dist/middleware/owner-check.js +1 -1
- package/dist/middleware/owner-check.js.map +1 -1
- package/dist/middleware/web3-auth.d.ts +12 -0
- package/dist/middleware/web3-auth.d.ts.map +1 -1
- package/dist/middleware/web3-auth.js +27 -31
- package/dist/middleware/web3-auth.js.map +1 -1
- package/dist/migrations/local-state.d.ts +21 -0
- package/dist/migrations/local-state.d.ts.map +1 -0
- package/dist/migrations/local-state.js +106 -0
- package/dist/migrations/local-state.js.map +1 -0
- package/dist/routes/access-logs.d.ts +7 -0
- package/dist/routes/access-logs.d.ts.map +1 -1
- package/dist/routes/access-logs.js +11 -17
- package/dist/routes/access-logs.js.map +1 -1
- package/dist/routes/auth-device.d.ts +49 -0
- package/dist/routes/auth-device.d.ts.map +1 -0
- package/dist/routes/auth-device.js +581 -0
- package/dist/routes/auth-device.js.map +1 -0
- package/dist/routes/data.d.ts +42 -1
- package/dist/routes/data.d.ts.map +1 -1
- package/dist/routes/data.js +36 -201
- package/dist/routes/data.js.map +1 -1
- package/dist/routes/grants.d.ts +9 -2
- package/dist/routes/grants.d.ts.map +1 -1
- package/dist/routes/grants.js +18 -196
- package/dist/routes/grants.js.map +1 -1
- package/dist/routes/health.d.ts +7 -1
- package/dist/routes/health.d.ts.map +1 -1
- package/dist/routes/health.js +39 -2
- package/dist/routes/health.js.map +1 -1
- package/dist/routes/mcp.d.ts +93 -6
- package/dist/routes/mcp.d.ts.map +1 -1
- package/dist/routes/mcp.js +526 -53
- package/dist/routes/mcp.js.map +1 -1
- package/dist/routes/oauth-token.d.ts +56 -0
- package/dist/routes/oauth-token.d.ts.map +1 -0
- package/dist/routes/oauth-token.js +49 -0
- package/dist/routes/oauth-token.js.map +1 -0
- package/dist/routes/sync.d.ts +7 -0
- package/dist/routes/sync.d.ts.map +1 -1
- package/dist/routes/sync.js +12 -39
- package/dist/routes/sync.js.map +1 -1
- package/dist/routes/ui-config.d.ts.map +1 -1
- package/dist/routes/ui-config.js +17 -82
- package/dist/routes/ui-config.js.map +1 -1
- package/dist/routes/ui-registration.d.ts +9 -0
- package/dist/routes/ui-registration.d.ts.map +1 -0
- package/dist/routes/ui-registration.js +217 -0
- package/dist/routes/ui-registration.js.map +1 -0
- package/dist/routes/ui.d.ts +1 -0
- package/dist/routes/ui.d.ts.map +1 -1
- package/dist/routes/ui.js +46 -4
- package/dist/routes/ui.js.map +1 -1
- package/dist/storage/hierarchy.d.ts +29 -0
- package/dist/storage/hierarchy.d.ts.map +1 -0
- package/dist/storage/hierarchy.js +241 -0
- package/dist/storage/hierarchy.js.map +1 -0
- package/dist/storage/index-manager.d.ts +53 -0
- package/dist/storage/index-manager.d.ts.map +1 -0
- package/dist/storage/index-manager.js +167 -0
- package/dist/storage/index-manager.js.map +1 -0
- package/dist/storage/index-schema.d.ts +5 -0
- package/dist/storage/index-schema.d.ts.map +1 -0
- package/dist/storage/index-schema.js +62 -0
- package/dist/storage/index-schema.js.map +1 -0
- package/dist/storage/node-data-storage.d.ts +9 -0
- package/dist/storage/node-data-storage.d.ts.map +1 -0
- package/dist/storage/node-data-storage.js +84 -0
- package/dist/storage/node-data-storage.js.map +1 -0
- package/dist/sync-cursor.d.ts +15 -0
- package/dist/sync-cursor.d.ts.map +1 -0
- package/dist/sync-cursor.js +110 -0
- package/dist/sync-cursor.js.map +1 -0
- package/dist/token-store.d.ts +22 -0
- package/dist/token-store.d.ts.map +1 -0
- package/dist/token-store.js +120 -0
- package/dist/token-store.js.map +1 -0
- package/dist/tunnel/auth.d.ts +4 -0
- package/dist/tunnel/auth.d.ts.map +1 -1
- package/dist/tunnel/auth.js +2 -1
- package/dist/tunnel/auth.js.map +1 -1
- package/dist/tunnel/config.d.ts +1 -0
- package/dist/tunnel/config.d.ts.map +1 -1
- package/dist/tunnel/config.js +1 -1
- package/dist/tunnel/config.js.map +1 -1
- package/dist/tunnel/manager.d.ts +21 -1
- package/dist/tunnel/manager.d.ts.map +1 -1
- package/dist/tunnel/manager.js +78 -23
- package/dist/tunnel/manager.js.map +1 -1
- package/dist/tunnel/verify.d.ts +1 -1
- package/dist/tunnel/verify.d.ts.map +1 -1
- package/dist/tunnel/verify.js +8 -2
- package/dist/tunnel/verify.js.map +1 -1
- package/dist/ui/browser_tls_rustls_bg.wasm +0 -0
- package/dist/ui/index.html +2192 -0
- package/dist/ui/ps-lite-debug.d.ts +93 -0
- package/dist/ui/ps-lite-debug.d.ts.map +1 -0
- package/dist/ui/ps-lite-debug.js +129415 -0
- package/dist/ui/ps-lite-debug.js.map +1 -0
- package/package.json +20 -8
- package/dist/middleware/grant-check.d.ts +0 -12
- package/dist/middleware/grant-check.d.ts.map +0 -1
- package/dist/middleware/grant-check.js +0 -90
- package/dist/middleware/grant-check.js.map +0 -1
- package/dist/oauth/provider.d.ts +0 -55
- package/dist/oauth/provider.d.ts.map +0 -1
- package/dist/oauth/provider.js +0 -160
- package/dist/oauth/provider.js.map +0 -1
- package/dist/oauth/routes.d.ts +0 -10
- package/dist/oauth/routes.d.ts.map +0 -1
- package/dist/oauth/routes.js +0 -206
- package/dist/oauth/routes.js.map +0 -1
|
@@ -0,0 +1,2192 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>Personal Server — Dev UI</title>
|
|
7
|
+
<style>
|
|
8
|
+
*,
|
|
9
|
+
*::before,
|
|
10
|
+
*::after {
|
|
11
|
+
box-sizing: border-box;
|
|
12
|
+
margin: 0;
|
|
13
|
+
padding: 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
:root {
|
|
17
|
+
--bg: #0a0a0a;
|
|
18
|
+
--surface: #141414;
|
|
19
|
+
--surface-2: #1e1e1e;
|
|
20
|
+
--border: #2a2a2a;
|
|
21
|
+
--text: #e0e0e0;
|
|
22
|
+
--text-dim: #808080;
|
|
23
|
+
--accent: #00ff88;
|
|
24
|
+
--accent-dim: #00cc6a;
|
|
25
|
+
--error: #ff4444;
|
|
26
|
+
--warn: #ffaa00;
|
|
27
|
+
--mono:
|
|
28
|
+
"SF Mono", "Cascadia Code", "Fira Code", "JetBrains Mono", monospace;
|
|
29
|
+
--sans:
|
|
30
|
+
-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
|
|
31
|
+
--radius: 4px;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
body {
|
|
35
|
+
font-family: var(--mono);
|
|
36
|
+
font-size: 13px;
|
|
37
|
+
background: var(--bg);
|
|
38
|
+
color: var(--text);
|
|
39
|
+
line-height: 1.5;
|
|
40
|
+
min-height: 100vh;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
header {
|
|
44
|
+
display: flex;
|
|
45
|
+
align-items: center;
|
|
46
|
+
gap: 16px;
|
|
47
|
+
padding: 12px 20px;
|
|
48
|
+
border-bottom: 1px solid var(--border);
|
|
49
|
+
background: var(--surface);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
header h1 {
|
|
53
|
+
font-size: 14px;
|
|
54
|
+
font-weight: 600;
|
|
55
|
+
letter-spacing: 0.5px;
|
|
56
|
+
color: var(--accent);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.header-spacer {
|
|
60
|
+
flex: 1;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.runtime-picker {
|
|
64
|
+
display: flex;
|
|
65
|
+
align-items: center;
|
|
66
|
+
gap: 8px;
|
|
67
|
+
color: var(--text-dim);
|
|
68
|
+
font-size: 12px;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.runtime-picker select {
|
|
72
|
+
width: auto;
|
|
73
|
+
min-width: 128px;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
header .status-dot {
|
|
77
|
+
width: 8px;
|
|
78
|
+
height: 8px;
|
|
79
|
+
border-radius: 50%;
|
|
80
|
+
background: var(--accent);
|
|
81
|
+
animation: pulse 2s ease-in-out infinite;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@keyframes pulse {
|
|
85
|
+
0%,
|
|
86
|
+
100% {
|
|
87
|
+
opacity: 1;
|
|
88
|
+
}
|
|
89
|
+
50% {
|
|
90
|
+
opacity: 0.4;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
nav {
|
|
95
|
+
display: flex;
|
|
96
|
+
gap: 0;
|
|
97
|
+
border-bottom: 1px solid var(--border);
|
|
98
|
+
background: var(--surface);
|
|
99
|
+
overflow-x: auto;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
nav button {
|
|
103
|
+
font-family: var(--mono);
|
|
104
|
+
font-size: 12px;
|
|
105
|
+
padding: 10px 16px;
|
|
106
|
+
background: none;
|
|
107
|
+
border: none;
|
|
108
|
+
border-bottom: 2px solid transparent;
|
|
109
|
+
color: var(--text-dim);
|
|
110
|
+
cursor: pointer;
|
|
111
|
+
white-space: nowrap;
|
|
112
|
+
transition:
|
|
113
|
+
color 0.15s,
|
|
114
|
+
border-color 0.15s;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
nav button:hover {
|
|
118
|
+
color: var(--text);
|
|
119
|
+
}
|
|
120
|
+
nav button.active {
|
|
121
|
+
color: var(--accent);
|
|
122
|
+
border-bottom-color: var(--accent);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
main {
|
|
126
|
+
padding: 20px;
|
|
127
|
+
max-width: 1200px;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.panel {
|
|
131
|
+
display: none;
|
|
132
|
+
}
|
|
133
|
+
.panel.active {
|
|
134
|
+
display: block;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.card {
|
|
138
|
+
background: var(--surface);
|
|
139
|
+
border: 1px solid var(--border);
|
|
140
|
+
border-radius: var(--radius);
|
|
141
|
+
padding: 16px;
|
|
142
|
+
margin-bottom: 16px;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.card-title {
|
|
146
|
+
font-size: 11px;
|
|
147
|
+
text-transform: uppercase;
|
|
148
|
+
letter-spacing: 1px;
|
|
149
|
+
color: var(--text-dim);
|
|
150
|
+
margin-bottom: 12px;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.kv-grid {
|
|
154
|
+
display: grid;
|
|
155
|
+
grid-template-columns: auto 1fr;
|
|
156
|
+
gap: 6px 16px;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.kv-grid .label {
|
|
160
|
+
color: var(--text-dim);
|
|
161
|
+
}
|
|
162
|
+
.kv-grid .value {
|
|
163
|
+
color: var(--text);
|
|
164
|
+
word-break: break-all;
|
|
165
|
+
}
|
|
166
|
+
.kv-grid .value.ok {
|
|
167
|
+
color: var(--accent);
|
|
168
|
+
}
|
|
169
|
+
.kv-grid .value.err {
|
|
170
|
+
color: var(--error);
|
|
171
|
+
}
|
|
172
|
+
.kv-grid .value.warn {
|
|
173
|
+
color: var(--warn);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
textarea,
|
|
177
|
+
input,
|
|
178
|
+
select {
|
|
179
|
+
font-family: var(--mono);
|
|
180
|
+
font-size: 13px;
|
|
181
|
+
background: var(--surface-2);
|
|
182
|
+
color: var(--text);
|
|
183
|
+
border: 1px solid var(--border);
|
|
184
|
+
border-radius: var(--radius);
|
|
185
|
+
padding: 8px 10px;
|
|
186
|
+
width: 100%;
|
|
187
|
+
outline: none;
|
|
188
|
+
transition: border-color 0.15s;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
textarea:focus,
|
|
192
|
+
input:focus,
|
|
193
|
+
select:focus {
|
|
194
|
+
border-color: var(--accent);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
textarea {
|
|
198
|
+
resize: vertical;
|
|
199
|
+
min-height: 300px;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
button.btn {
|
|
203
|
+
font-family: var(--mono);
|
|
204
|
+
font-size: 12px;
|
|
205
|
+
padding: 8px 16px;
|
|
206
|
+
border: 1px solid var(--accent);
|
|
207
|
+
border-radius: var(--radius);
|
|
208
|
+
background: transparent;
|
|
209
|
+
color: var(--accent);
|
|
210
|
+
cursor: pointer;
|
|
211
|
+
transition:
|
|
212
|
+
background 0.15s,
|
|
213
|
+
color 0.15s;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
button.btn:hover {
|
|
217
|
+
background: var(--accent);
|
|
218
|
+
color: var(--bg);
|
|
219
|
+
}
|
|
220
|
+
button.btn:disabled {
|
|
221
|
+
opacity: 0.4;
|
|
222
|
+
cursor: not-allowed;
|
|
223
|
+
}
|
|
224
|
+
button.btn.secondary {
|
|
225
|
+
border-color: var(--border);
|
|
226
|
+
color: var(--text-dim);
|
|
227
|
+
}
|
|
228
|
+
button.btn.secondary:hover {
|
|
229
|
+
background: var(--surface-2);
|
|
230
|
+
color: var(--text);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.toolbar {
|
|
234
|
+
display: flex;
|
|
235
|
+
gap: 8px;
|
|
236
|
+
align-items: center;
|
|
237
|
+
margin-bottom: 12px;
|
|
238
|
+
flex-wrap: wrap;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.toolbar input,
|
|
242
|
+
.toolbar select {
|
|
243
|
+
width: auto;
|
|
244
|
+
min-width: 120px;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.msg {
|
|
248
|
+
padding: 8px 12px;
|
|
249
|
+
border-radius: var(--radius);
|
|
250
|
+
font-size: 12px;
|
|
251
|
+
margin-bottom: 12px;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.msg.ok {
|
|
255
|
+
background: rgba(0, 255, 136, 0.1);
|
|
256
|
+
color: var(--accent);
|
|
257
|
+
border: 1px solid rgba(0, 255, 136, 0.2);
|
|
258
|
+
}
|
|
259
|
+
.msg.err {
|
|
260
|
+
background: rgba(255, 68, 68, 0.1);
|
|
261
|
+
color: var(--error);
|
|
262
|
+
border: 1px solid rgba(255, 68, 68, 0.2);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
pre.json {
|
|
266
|
+
background: var(--surface-2);
|
|
267
|
+
border: 1px solid var(--border);
|
|
268
|
+
border-radius: var(--radius);
|
|
269
|
+
padding: 12px;
|
|
270
|
+
overflow-x: auto;
|
|
271
|
+
white-space: pre-wrap;
|
|
272
|
+
word-break: break-all;
|
|
273
|
+
font-size: 12px;
|
|
274
|
+
max-height: 500px;
|
|
275
|
+
overflow-y: auto;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
table {
|
|
279
|
+
width: 100%;
|
|
280
|
+
border-collapse: collapse;
|
|
281
|
+
font-size: 12px;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
th,
|
|
285
|
+
td {
|
|
286
|
+
text-align: left;
|
|
287
|
+
padding: 8px 12px;
|
|
288
|
+
border-bottom: 1px solid var(--border);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
th {
|
|
292
|
+
color: var(--text-dim);
|
|
293
|
+
font-weight: 500;
|
|
294
|
+
text-transform: uppercase;
|
|
295
|
+
font-size: 11px;
|
|
296
|
+
letter-spacing: 0.5px;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
td {
|
|
300
|
+
color: var(--text);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
tr:hover td {
|
|
304
|
+
background: var(--surface-2);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.empty {
|
|
308
|
+
color: var(--text-dim);
|
|
309
|
+
font-style: italic;
|
|
310
|
+
padding: 20px 0;
|
|
311
|
+
text-align: center;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.loading::after {
|
|
315
|
+
content: "";
|
|
316
|
+
display: inline-block;
|
|
317
|
+
width: 12px;
|
|
318
|
+
height: 12px;
|
|
319
|
+
border: 2px solid var(--border);
|
|
320
|
+
border-top-color: var(--accent);
|
|
321
|
+
border-radius: 50%;
|
|
322
|
+
animation: spin 0.6s linear infinite;
|
|
323
|
+
margin-left: 8px;
|
|
324
|
+
vertical-align: middle;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
@keyframes spin {
|
|
328
|
+
to {
|
|
329
|
+
transform: rotate(360deg);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
</style>
|
|
333
|
+
</head>
|
|
334
|
+
<body>
|
|
335
|
+
<header>
|
|
336
|
+
<div class="status-dot"></div>
|
|
337
|
+
<h1>PERSONAL SERVER</h1>
|
|
338
|
+
<div class="header-spacer"></div>
|
|
339
|
+
<label class="runtime-picker">
|
|
340
|
+
<span>Runtime</span>
|
|
341
|
+
<select id="runtime-select">
|
|
342
|
+
<option value="node">Node</option>
|
|
343
|
+
<option value="browser">Browser</option>
|
|
344
|
+
</select>
|
|
345
|
+
</label>
|
|
346
|
+
</header>
|
|
347
|
+
|
|
348
|
+
<nav id="tabs">
|
|
349
|
+
<button class="active" data-tab="health">Health</button>
|
|
350
|
+
<button data-tab="config">Config</button>
|
|
351
|
+
<button data-tab="data">Data</button>
|
|
352
|
+
<button data-tab="grants">Grants</button>
|
|
353
|
+
<button data-tab="logs">Access Logs</button>
|
|
354
|
+
<button data-tab="api">API Tester</button>
|
|
355
|
+
<button data-tab="connect">Connect</button>
|
|
356
|
+
</nav>
|
|
357
|
+
|
|
358
|
+
<main>
|
|
359
|
+
<!-- Health Panel -->
|
|
360
|
+
<div id="panel-health" class="panel active">
|
|
361
|
+
<div class="card">
|
|
362
|
+
<div class="card-title">Server Status</div>
|
|
363
|
+
<div class="toolbar">
|
|
364
|
+
<button class="btn secondary" id="register-node-server-btn">
|
|
365
|
+
Register Server
|
|
366
|
+
</button>
|
|
367
|
+
</div>
|
|
368
|
+
<div
|
|
369
|
+
id="node-registration-status"
|
|
370
|
+
style="margin-bottom: 8px; font-size: 12px; color: var(--text-dim)"
|
|
371
|
+
></div>
|
|
372
|
+
<div id="health-content" class="kv-grid">
|
|
373
|
+
<span class="label">status</span
|
|
374
|
+
><span class="value">loading...</span>
|
|
375
|
+
</div>
|
|
376
|
+
</div>
|
|
377
|
+
</div>
|
|
378
|
+
|
|
379
|
+
<!-- Config Panel -->
|
|
380
|
+
<div id="panel-config" class="panel">
|
|
381
|
+
<div class="card">
|
|
382
|
+
<div class="card-title">Server Configuration</div>
|
|
383
|
+
<div id="config-msg"></div>
|
|
384
|
+
<textarea id="config-editor" spellcheck="false"></textarea>
|
|
385
|
+
<div style="margin-top: 12px; display: flex; gap: 8px">
|
|
386
|
+
<button class="btn" id="save-config-btn">Save</button>
|
|
387
|
+
<button class="btn secondary" id="reload-config-btn">Reload</button>
|
|
388
|
+
</div>
|
|
389
|
+
</div>
|
|
390
|
+
</div>
|
|
391
|
+
|
|
392
|
+
<!-- Data Panel -->
|
|
393
|
+
<div id="panel-data" class="panel">
|
|
394
|
+
<div class="card">
|
|
395
|
+
<div class="card-title">Add Data</div>
|
|
396
|
+
<label
|
|
397
|
+
style="
|
|
398
|
+
display: block;
|
|
399
|
+
margin-bottom: 4px;
|
|
400
|
+
font-size: 11px;
|
|
401
|
+
color: var(--text-dim);
|
|
402
|
+
"
|
|
403
|
+
>Scope</label
|
|
404
|
+
>
|
|
405
|
+
<input
|
|
406
|
+
id="add-data-scope"
|
|
407
|
+
type="text"
|
|
408
|
+
value="instagram.profile"
|
|
409
|
+
style="margin-bottom: 12px"
|
|
410
|
+
/>
|
|
411
|
+
<label
|
|
412
|
+
style="
|
|
413
|
+
display: block;
|
|
414
|
+
margin-bottom: 4px;
|
|
415
|
+
font-size: 11px;
|
|
416
|
+
color: var(--text-dim);
|
|
417
|
+
"
|
|
418
|
+
>JSON Data</label
|
|
419
|
+
>
|
|
420
|
+
<textarea
|
|
421
|
+
id="add-data-json"
|
|
422
|
+
spellcheck="false"
|
|
423
|
+
style="min-height: 120px; margin-bottom: 12px"
|
|
424
|
+
>
|
|
425
|
+
{
|
|
426
|
+
"username": "vana_debug",
|
|
427
|
+
"full_name": "Vana Debug Profile",
|
|
428
|
+
"bio": "Schema-shaped sample profile for Personal Server debug UI testing.",
|
|
429
|
+
"biography_with_entities": {
|
|
430
|
+
"raw_text": "Schema-shaped sample profile for Personal Server debug UI testing.",
|
|
431
|
+
"entities": []
|
|
432
|
+
},
|
|
433
|
+
"pronouns": ["they/them"],
|
|
434
|
+
"bio_links": [
|
|
435
|
+
{
|
|
436
|
+
"title": "Vana",
|
|
437
|
+
"url": "https://www.vana.org",
|
|
438
|
+
"lynx_url": "https://www.vana.org"
|
|
439
|
+
}
|
|
440
|
+
],
|
|
441
|
+
"external_url": "https://www.vana.org",
|
|
442
|
+
"external_url_linkshimmed": "https://www.vana.org",
|
|
443
|
+
"fb_profile_biolink": null,
|
|
444
|
+
"profile_pic_url": "https://www.vana.org/favicon.ico",
|
|
445
|
+
"hd_profile_pic_url": "https://www.vana.org/favicon.ico",
|
|
446
|
+
"pk": "2605080001",
|
|
447
|
+
"id": "2605080001",
|
|
448
|
+
"fbid": null,
|
|
449
|
+
"eimu_id": null,
|
|
450
|
+
"follower_count": 1234,
|
|
451
|
+
"following_count": 321,
|
|
452
|
+
"media_count": 42,
|
|
453
|
+
"highlight_reel_count": 3,
|
|
454
|
+
"pinned_channels_list_count": 0,
|
|
455
|
+
"is_private": false,
|
|
456
|
+
"is_verified": true,
|
|
457
|
+
"is_verified_by_mv4b": false,
|
|
458
|
+
"is_business": false,
|
|
459
|
+
"is_professional_account": true,
|
|
460
|
+
"is_supervised_user": false,
|
|
461
|
+
"is_supervision_enabled": false,
|
|
462
|
+
"is_joined_recently": false,
|
|
463
|
+
"is_embeds_disabled": false,
|
|
464
|
+
"is_regulated_c18": false,
|
|
465
|
+
"hide_like_and_view_counts": false,
|
|
466
|
+
"ai_agent_type": null,
|
|
467
|
+
"has_clips": true,
|
|
468
|
+
"has_channel": false,
|
|
469
|
+
"has_guides": false,
|
|
470
|
+
"has_ar_effects": false,
|
|
471
|
+
"has_chaining": true,
|
|
472
|
+
"country_block": false,
|
|
473
|
+
"should_show_category": true,
|
|
474
|
+
"should_show_public_contacts": false,
|
|
475
|
+
"show_account_transparency_details": true,
|
|
476
|
+
"transparency_label": null,
|
|
477
|
+
"transparency_product": null,
|
|
478
|
+
"business": {
|
|
479
|
+
"is_business_account": false,
|
|
480
|
+
"category_name": "Technology",
|
|
481
|
+
"business_category_name": null,
|
|
482
|
+
"overall_category_name": null,
|
|
483
|
+
"category_enum": null,
|
|
484
|
+
"business_contact_method": null,
|
|
485
|
+
"business_email": null,
|
|
486
|
+
"business_phone_number": null,
|
|
487
|
+
"business_address_json": null
|
|
488
|
+
},
|
|
489
|
+
"viewer_relationship": {
|
|
490
|
+
"followed_by_viewer": true,
|
|
491
|
+
"follows_viewer": false,
|
|
492
|
+
"requested_by_viewer": false,
|
|
493
|
+
"has_requested_viewer": false,
|
|
494
|
+
"blocked_by_viewer": false,
|
|
495
|
+
"has_blocked_viewer": false,
|
|
496
|
+
"restricted_by_viewer": false,
|
|
497
|
+
"is_guardian_of_viewer": false,
|
|
498
|
+
"is_supervised_by_viewer": false,
|
|
499
|
+
"mutual_followed_by_count": 7
|
|
500
|
+
},
|
|
501
|
+
"collected_at": "2026-05-08T00:00:00.000Z",
|
|
502
|
+
"debug_source": "debug-ui"
|
|
503
|
+
}</textarea>
|
|
504
|
+
<button class="btn" id="submit-data-btn">Submit</button>
|
|
505
|
+
<div id="add-data-msg" style="margin-top: 12px"></div>
|
|
506
|
+
</div>
|
|
507
|
+
<div class="card">
|
|
508
|
+
<div class="card-title">Data Scopes</div>
|
|
509
|
+
<div class="toolbar">
|
|
510
|
+
<button class="btn" id="refresh-data-btn">Refresh</button>
|
|
511
|
+
<button class="btn secondary" id="sync-now-btn">Sync Now</button>
|
|
512
|
+
</div>
|
|
513
|
+
<div id="sync-msg" style="margin-bottom: 12px"></div>
|
|
514
|
+
<div id="data-scopes"></div>
|
|
515
|
+
</div>
|
|
516
|
+
<div id="data-detail" class="card" style="display: none">
|
|
517
|
+
<div class="card-title">Scope Data</div>
|
|
518
|
+
<div id="data-detail-content"></div>
|
|
519
|
+
</div>
|
|
520
|
+
</div>
|
|
521
|
+
|
|
522
|
+
<!-- Grants Panel -->
|
|
523
|
+
<div id="panel-grants" class="panel">
|
|
524
|
+
<div class="card">
|
|
525
|
+
<div class="card-title">Active Grants</div>
|
|
526
|
+
<div class="toolbar">
|
|
527
|
+
<button class="btn" id="refresh-grants-btn">Refresh</button>
|
|
528
|
+
</div>
|
|
529
|
+
<div id="grants-content"></div>
|
|
530
|
+
</div>
|
|
531
|
+
</div>
|
|
532
|
+
|
|
533
|
+
<!-- Access Logs Panel -->
|
|
534
|
+
<div id="panel-logs" class="panel">
|
|
535
|
+
<div class="card">
|
|
536
|
+
<div class="card-title">Access Logs</div>
|
|
537
|
+
<div class="toolbar">
|
|
538
|
+
<button class="btn" id="refresh-logs-btn">Refresh</button>
|
|
539
|
+
</div>
|
|
540
|
+
<div id="logs-content"></div>
|
|
541
|
+
</div>
|
|
542
|
+
</div>
|
|
543
|
+
|
|
544
|
+
<!-- API Tester Panel -->
|
|
545
|
+
<div id="panel-api" class="panel">
|
|
546
|
+
<div class="card">
|
|
547
|
+
<div class="card-title">API Tester</div>
|
|
548
|
+
<div class="toolbar">
|
|
549
|
+
<select id="api-method">
|
|
550
|
+
<option>GET</option>
|
|
551
|
+
<option>POST</option>
|
|
552
|
+
<option>PUT</option>
|
|
553
|
+
<option>DELETE</option>
|
|
554
|
+
</select>
|
|
555
|
+
<input
|
|
556
|
+
id="api-path"
|
|
557
|
+
type="text"
|
|
558
|
+
placeholder="/health"
|
|
559
|
+
value="/health"
|
|
560
|
+
style="flex: 1"
|
|
561
|
+
/>
|
|
562
|
+
<button class="btn" id="send-api-btn">Send</button>
|
|
563
|
+
</div>
|
|
564
|
+
<textarea
|
|
565
|
+
id="api-body"
|
|
566
|
+
placeholder="Request body (JSON, for POST/PUT)"
|
|
567
|
+
style="min-height: 100px; margin-bottom: 12px"
|
|
568
|
+
></textarea>
|
|
569
|
+
<div class="card-title">Response</div>
|
|
570
|
+
<div
|
|
571
|
+
id="api-status"
|
|
572
|
+
style="margin-bottom: 8px; font-size: 12px; color: var(--text-dim)"
|
|
573
|
+
></div>
|
|
574
|
+
<pre class="json" id="api-response">No request sent yet.</pre>
|
|
575
|
+
</div>
|
|
576
|
+
</div>
|
|
577
|
+
|
|
578
|
+
<!-- Connect Panel -->
|
|
579
|
+
<div id="panel-connect" class="panel">
|
|
580
|
+
<div class="card">
|
|
581
|
+
<div class="card-title">Connect Simulator</div>
|
|
582
|
+
<label
|
|
583
|
+
style="
|
|
584
|
+
display: block;
|
|
585
|
+
margin-bottom: 4px;
|
|
586
|
+
font-size: 11px;
|
|
587
|
+
color: var(--text-dim);
|
|
588
|
+
"
|
|
589
|
+
>Session Relay URL</label
|
|
590
|
+
>
|
|
591
|
+
<input
|
|
592
|
+
id="connect-relay-url"
|
|
593
|
+
type="text"
|
|
594
|
+
value="https://session-relay-git-dev-opendatalabs.vercel.app"
|
|
595
|
+
style="margin-bottom: 12px"
|
|
596
|
+
/>
|
|
597
|
+
<label
|
|
598
|
+
style="
|
|
599
|
+
display: block;
|
|
600
|
+
margin-bottom: 4px;
|
|
601
|
+
font-size: 11px;
|
|
602
|
+
color: var(--text-dim);
|
|
603
|
+
"
|
|
604
|
+
>Deep Link URL</label
|
|
605
|
+
>
|
|
606
|
+
<input
|
|
607
|
+
id="connect-deep-link"
|
|
608
|
+
type="text"
|
|
609
|
+
placeholder="Paste deep link URL from the test builder app"
|
|
610
|
+
style="margin-bottom: 12px"
|
|
611
|
+
/>
|
|
612
|
+
<div style="display: flex; gap: 8px">
|
|
613
|
+
<button class="btn" id="connect-parse-btn">Parse</button>
|
|
614
|
+
</div>
|
|
615
|
+
<div id="connect-parsed" style="margin-top: 12px; display: none">
|
|
616
|
+
<div class="kv-grid" id="connect-parsed-info"></div>
|
|
617
|
+
</div>
|
|
618
|
+
</div>
|
|
619
|
+
|
|
620
|
+
<div class="card" id="connect-flow-card" style="display: none">
|
|
621
|
+
<div class="card-title">Flow Steps</div>
|
|
622
|
+
<div
|
|
623
|
+
style="
|
|
624
|
+
display: flex;
|
|
625
|
+
gap: 8px;
|
|
626
|
+
flex-wrap: wrap;
|
|
627
|
+
margin-bottom: 12px;
|
|
628
|
+
"
|
|
629
|
+
>
|
|
630
|
+
<button class="btn" id="connect-claim-btn" disabled>
|
|
631
|
+
1. Claim Session
|
|
632
|
+
</button>
|
|
633
|
+
<button class="btn" id="connect-grant-btn" disabled>
|
|
634
|
+
2. Create Grant
|
|
635
|
+
</button>
|
|
636
|
+
<button class="btn" id="connect-approve-btn" disabled>
|
|
637
|
+
3. Approve Session
|
|
638
|
+
</button>
|
|
639
|
+
</div>
|
|
640
|
+
<div style="display: flex; gap: 8px; margin-bottom: 12px">
|
|
641
|
+
<button class="btn" id="connect-auto-btn" disabled>
|
|
642
|
+
Auto-Approve All
|
|
643
|
+
</button>
|
|
644
|
+
<button class="btn secondary" id="connect-deny-btn" disabled>
|
|
645
|
+
Deny
|
|
646
|
+
</button>
|
|
647
|
+
</div>
|
|
648
|
+
<div class="card-title" style="margin-top: 8px">Progress Log</div>
|
|
649
|
+
<div
|
|
650
|
+
id="connect-log"
|
|
651
|
+
style="
|
|
652
|
+
background: var(--surface-2);
|
|
653
|
+
border: 1px solid var(--border);
|
|
654
|
+
border-radius: var(--radius);
|
|
655
|
+
padding: 8px 12px;
|
|
656
|
+
font-size: 11px;
|
|
657
|
+
max-height: 300px;
|
|
658
|
+
overflow-y: auto;
|
|
659
|
+
white-space: pre-wrap;
|
|
660
|
+
"
|
|
661
|
+
></div>
|
|
662
|
+
</div>
|
|
663
|
+
|
|
664
|
+
<div class="card" id="connect-result-card" style="display: none">
|
|
665
|
+
<div class="card-title">Result</div>
|
|
666
|
+
<pre class="json" id="connect-result"></pre>
|
|
667
|
+
</div>
|
|
668
|
+
</div>
|
|
669
|
+
</main>
|
|
670
|
+
|
|
671
|
+
<script>
|
|
672
|
+
const TOKEN = "__DEV_TOKEN__";
|
|
673
|
+
const BASE = window.location.origin;
|
|
674
|
+
window.__PS_LITE_BOOTSTRAP__ = "__PS_LITE_BOOTSTRAP_JSON__";
|
|
675
|
+
const runtimeState = {
|
|
676
|
+
active: "node",
|
|
677
|
+
registration: { ownerPrivateKeyConfigured: false },
|
|
678
|
+
devTokenExpired: false,
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
// --- DOM helpers ---
|
|
682
|
+
function el(id) {
|
|
683
|
+
return document.getElementById(id);
|
|
684
|
+
}
|
|
685
|
+
function clearChildren(node) {
|
|
686
|
+
while (node.firstChild) node.removeChild(node.firstChild);
|
|
687
|
+
}
|
|
688
|
+
function text(str) {
|
|
689
|
+
return document.createTextNode(String(str != null ? str : ""));
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function createEl(tag, attrs, children) {
|
|
693
|
+
const e = document.createElement(tag);
|
|
694
|
+
if (attrs) {
|
|
695
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
696
|
+
if (k === "style" && typeof v === "object") {
|
|
697
|
+
Object.assign(e.style, v);
|
|
698
|
+
} else if (k.startsWith("on") && typeof v === "function") {
|
|
699
|
+
e.addEventListener(k.slice(2), v);
|
|
700
|
+
} else {
|
|
701
|
+
e.setAttribute(k, v);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
if (children) {
|
|
706
|
+
for (const c of Array.isArray(children) ? children : [children]) {
|
|
707
|
+
if (typeof c === "string") e.appendChild(text(c));
|
|
708
|
+
else if (c) e.appendChild(c);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
return e;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function showMsg(container, message, type) {
|
|
715
|
+
clearChildren(container);
|
|
716
|
+
const div = createEl("div", { class: "msg " + type });
|
|
717
|
+
div.textContent = message;
|
|
718
|
+
container.appendChild(div);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function showJson(container, data) {
|
|
722
|
+
clearChildren(container);
|
|
723
|
+
const pre = createEl("pre", { class: "json" });
|
|
724
|
+
pre.textContent =
|
|
725
|
+
typeof data === "string" ? data : JSON.stringify(data, null, 2);
|
|
726
|
+
container.appendChild(pre);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function showEmpty(container, message) {
|
|
730
|
+
clearChildren(container);
|
|
731
|
+
const div = createEl("div", { class: "empty" });
|
|
732
|
+
div.textContent = message;
|
|
733
|
+
container.appendChild(div);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function showLoading(container) {
|
|
737
|
+
clearChildren(container);
|
|
738
|
+
const span = createEl("span", { class: "loading" });
|
|
739
|
+
span.textContent = "Loading";
|
|
740
|
+
container.appendChild(span);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// --- Tabs ---
|
|
744
|
+
el("tabs").addEventListener("click", function (e) {
|
|
745
|
+
if (e.target.tagName !== "BUTTON") return;
|
|
746
|
+
document.querySelectorAll("#tabs button").forEach(function (b) {
|
|
747
|
+
b.classList.remove("active");
|
|
748
|
+
});
|
|
749
|
+
document.querySelectorAll(".panel").forEach(function (p) {
|
|
750
|
+
p.classList.remove("active");
|
|
751
|
+
});
|
|
752
|
+
e.target.classList.add("active");
|
|
753
|
+
el("panel-" + e.target.dataset.tab).classList.add("active");
|
|
754
|
+
|
|
755
|
+
var tab = e.target.dataset.tab;
|
|
756
|
+
if (tab === "health") loadHealth();
|
|
757
|
+
if (tab === "config") loadConfig();
|
|
758
|
+
if (tab === "data") loadScopes();
|
|
759
|
+
if (tab === "grants") loadGrants();
|
|
760
|
+
if (tab === "logs") loadAccessLogs();
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
// --- Fetch helper ---
|
|
764
|
+
async function api(path, opts) {
|
|
765
|
+
opts = opts || {};
|
|
766
|
+
var headers = Object.assign(
|
|
767
|
+
{ Authorization: "Bearer " + TOKEN },
|
|
768
|
+
opts.headers || {},
|
|
769
|
+
);
|
|
770
|
+
var res = await fetch(
|
|
771
|
+
BASE + path,
|
|
772
|
+
Object.assign({}, opts, { headers: headers }),
|
|
773
|
+
);
|
|
774
|
+
var txt = await res.text();
|
|
775
|
+
var json;
|
|
776
|
+
try {
|
|
777
|
+
json = JSON.parse(txt);
|
|
778
|
+
} catch (e) {
|
|
779
|
+
json = txt;
|
|
780
|
+
}
|
|
781
|
+
var result = { status: res.status, ok: res.ok, data: json };
|
|
782
|
+
if (isDevTokenAuthError(result)) {
|
|
783
|
+
runtimeState.devTokenExpired = true;
|
|
784
|
+
throw new Error("Dev UI token expired. Reload this page.");
|
|
785
|
+
}
|
|
786
|
+
return result;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function isDevTokenAuthError(result) {
|
|
790
|
+
var error = result.data && result.data.error;
|
|
791
|
+
var errorCode = error && (error.errorCode || error.code);
|
|
792
|
+
return (
|
|
793
|
+
result.status === 401 &&
|
|
794
|
+
(errorCode === "UNAUTHORIZED" || errorCode === "INVALID_SIGNATURE")
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
async function runtimeApi(path, opts) {
|
|
799
|
+
return runtimeState.active === "browser"
|
|
800
|
+
? psLiteApiRequest(path, opts)
|
|
801
|
+
: api(path, opts);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
async function runtimeListGrants() {
|
|
805
|
+
return runtimeState.active === "browser"
|
|
806
|
+
? psLiteClient().listGrants()
|
|
807
|
+
: api("/v1/grants", { auth: "owner" });
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
async function runtimeCreateGrant(body) {
|
|
811
|
+
return runtimeState.active === "browser"
|
|
812
|
+
? psLiteClient().createGrant(body)
|
|
813
|
+
: api("/v1/grants", {
|
|
814
|
+
method: "POST",
|
|
815
|
+
auth: "owner",
|
|
816
|
+
headers: { "Content-Type": "application/json" },
|
|
817
|
+
body: JSON.stringify(body),
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
async function runtimeRevokeGrant(grantId) {
|
|
822
|
+
var path = "/v1/grants/" + encodeURIComponent(grantId);
|
|
823
|
+
return runtimeState.active === "browser"
|
|
824
|
+
? psLiteClient().revokeGrant(grantId)
|
|
825
|
+
: api(path, { method: "DELETE", auth: "owner" });
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function runtimeName() {
|
|
829
|
+
return runtimeState.active === "browser" ? "Browser" : "Node";
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function registrationRequired(healthData) {
|
|
833
|
+
return Boolean(
|
|
834
|
+
healthData &&
|
|
835
|
+
healthData.registration &&
|
|
836
|
+
healthData.registration.registered === false,
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
async function loadRegistrationStatus() {
|
|
841
|
+
try {
|
|
842
|
+
var result = await api("/ui/api/registration");
|
|
843
|
+
if (result.ok && result.data) {
|
|
844
|
+
runtimeState.registration = result.data;
|
|
845
|
+
}
|
|
846
|
+
} catch (e) {
|
|
847
|
+
if (runtimeState.devTokenExpired) {
|
|
848
|
+
runtimeState.registration = { ownerPrivateKeyConfigured: false };
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
updateRegisterButton();
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function selectedPanel() {
|
|
855
|
+
var active = document.querySelector("#tabs button.active");
|
|
856
|
+
return active ? active.dataset.tab : "health";
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function refreshSelectedPanel() {
|
|
860
|
+
var tab = selectedPanel();
|
|
861
|
+
if (tab === "health") loadHealth();
|
|
862
|
+
if (tab === "config") loadConfig();
|
|
863
|
+
if (tab === "data") loadScopes();
|
|
864
|
+
if (tab === "grants") loadGrants();
|
|
865
|
+
if (tab === "logs") loadAccessLogs();
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function updateRegisterButton() {
|
|
869
|
+
var btn = el("register-node-server-btn");
|
|
870
|
+
if (!btn) return;
|
|
871
|
+
btn.disabled =
|
|
872
|
+
runtimeState.devTokenExpired ||
|
|
873
|
+
!runtimeState.registration.ownerPrivateKeyConfigured;
|
|
874
|
+
btn.title = runtimeState.devTokenExpired
|
|
875
|
+
? "Reload this page to refresh the Dev UI token"
|
|
876
|
+
: btn.disabled
|
|
877
|
+
? "Set VANA_OWNER_PRIVATE_KEY to register servers"
|
|
878
|
+
: "Register selected runtime";
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function isPublicServerUrl(url) {
|
|
882
|
+
try {
|
|
883
|
+
var parsed = new URL(url);
|
|
884
|
+
var host = parsed.hostname.toLowerCase();
|
|
885
|
+
return (
|
|
886
|
+
(parsed.protocol === "https:" || parsed.protocol === "http:") &&
|
|
887
|
+
host !== "localhost" &&
|
|
888
|
+
host !== "127.0.0.1" &&
|
|
889
|
+
host !== "0.0.0.0" &&
|
|
890
|
+
host !== "::1" &&
|
|
891
|
+
host !== "[::1]" &&
|
|
892
|
+
host !== "ps-lite.local" &&
|
|
893
|
+
!host.startsWith("10.") &&
|
|
894
|
+
!host.startsWith("192.168.") &&
|
|
895
|
+
!/^172\.(1[6-9]|2\d|3[0-1])\./.test(host)
|
|
896
|
+
);
|
|
897
|
+
} catch (e) {
|
|
898
|
+
return false;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
async function selectedHealth() {
|
|
903
|
+
if (runtimeState.active === "node") return api("/health");
|
|
904
|
+
|
|
905
|
+
var result = await psLiteClient().status();
|
|
906
|
+
if (result.ok && result.data && !result.data.runtime) {
|
|
907
|
+
result = await psLiteClient().activate();
|
|
908
|
+
}
|
|
909
|
+
var data = result.data || {};
|
|
910
|
+
var runtime = data.runtime || {};
|
|
911
|
+
var relay = data.relay || {};
|
|
912
|
+
var registration = runtime.registration;
|
|
913
|
+
if (registration && relay.publicUrl) {
|
|
914
|
+
registration = Object.assign({}, registration, {
|
|
915
|
+
serverUrl: relay.publicUrl,
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
return {
|
|
919
|
+
status: result.status,
|
|
920
|
+
ok: result.ok,
|
|
921
|
+
data: Object.assign({}, runtime, {
|
|
922
|
+
apiOrigin: relay.publicUrl || runtime.apiOrigin,
|
|
923
|
+
registration: registration,
|
|
924
|
+
relay: relay,
|
|
925
|
+
runtime: {
|
|
926
|
+
kind: "ps-lite",
|
|
927
|
+
available: runtime.active !== false,
|
|
928
|
+
},
|
|
929
|
+
}),
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
el("runtime-select").addEventListener("change", function (e) {
|
|
934
|
+
runtimeState.active = e.target.value;
|
|
935
|
+
el("node-registration-status").textContent = "";
|
|
936
|
+
refreshSelectedPanel();
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
// --- Health ---
|
|
940
|
+
async function loadHealth() {
|
|
941
|
+
var container = el("health-content");
|
|
942
|
+
try {
|
|
943
|
+
await loadRegistrationStatus();
|
|
944
|
+
var result = await selectedHealth();
|
|
945
|
+
var d = result.data;
|
|
946
|
+
clearChildren(container);
|
|
947
|
+
var items = [
|
|
948
|
+
["runtime", runtimeName(), result.ok ? "ok" : "err"],
|
|
949
|
+
["status", d.status, d.status === "healthy" ? "ok" : "err"],
|
|
950
|
+
["version", d.version || "\u2014", ""],
|
|
951
|
+
[
|
|
952
|
+
"uptime",
|
|
953
|
+
typeof d.uptime === "number" ? formatUptime(d.uptime) : "\u2014",
|
|
954
|
+
"",
|
|
955
|
+
],
|
|
956
|
+
[
|
|
957
|
+
"api url",
|
|
958
|
+
d.apiOrigin || BASE,
|
|
959
|
+
d.apiOrigin && d.apiOrigin !== BASE ? "ok" : "warn",
|
|
960
|
+
],
|
|
961
|
+
["owner", d.owner ?? "not configured", d.owner ? "ok" : "err"],
|
|
962
|
+
];
|
|
963
|
+
if (runtimeState.active === "browser") {
|
|
964
|
+
items.push([
|
|
965
|
+
"relay",
|
|
966
|
+
d.relay && d.relay.connected
|
|
967
|
+
? "connected"
|
|
968
|
+
: d.relay
|
|
969
|
+
? d.relay.status
|
|
970
|
+
: "disconnected",
|
|
971
|
+
d.relay && d.relay.connected ? "ok" : "warn",
|
|
972
|
+
]);
|
|
973
|
+
} else if (d.tunnel) {
|
|
974
|
+
var tunnelValue = d.tunnel.status || "unknown";
|
|
975
|
+
if (
|
|
976
|
+
d.tunnel.status === "connected" &&
|
|
977
|
+
d.tunnel.routable === false
|
|
978
|
+
) {
|
|
979
|
+
tunnelValue =
|
|
980
|
+
d.registration && d.registration.registered
|
|
981
|
+
? "connected (not routable)"
|
|
982
|
+
: "connected (not registered)";
|
|
983
|
+
}
|
|
984
|
+
items.push([
|
|
985
|
+
"tunnel",
|
|
986
|
+
tunnelValue,
|
|
987
|
+
d.tunnel.status === "connected" && d.tunnel.routable !== false
|
|
988
|
+
? "ok"
|
|
989
|
+
: "warn",
|
|
990
|
+
]);
|
|
991
|
+
}
|
|
992
|
+
if (d.identity) {
|
|
993
|
+
var registered =
|
|
994
|
+
d.registration && typeof d.registration.registered === "boolean"
|
|
995
|
+
? d.registration.registered
|
|
996
|
+
: d.identity.serverId !== null;
|
|
997
|
+
items.push([
|
|
998
|
+
"address",
|
|
999
|
+
d.identity.address,
|
|
1000
|
+
registered ? "ok" : "warn",
|
|
1001
|
+
]);
|
|
1002
|
+
items.push(["publicKey", truncAddr(d.identity.publicKey), ""]);
|
|
1003
|
+
items.push([
|
|
1004
|
+
"identity",
|
|
1005
|
+
registered ? "registered" : "not registered",
|
|
1006
|
+
registered ? "ok" : "warn",
|
|
1007
|
+
]);
|
|
1008
|
+
} else {
|
|
1009
|
+
items.push(["identity", "not configured", ""]);
|
|
1010
|
+
}
|
|
1011
|
+
for (var i = 0; i < items.length; i++) {
|
|
1012
|
+
var lbl = createEl("span", { class: "label" });
|
|
1013
|
+
lbl.textContent = items[i][0];
|
|
1014
|
+
var val = createEl("span", {
|
|
1015
|
+
class: "value" + (items[i][2] ? " " + items[i][2] : ""),
|
|
1016
|
+
});
|
|
1017
|
+
val.textContent = items[i][1];
|
|
1018
|
+
// Copy-on-click for publicKey and API URLs
|
|
1019
|
+
if (
|
|
1020
|
+
(items[i][0] === "publicKey" && d.identity) ||
|
|
1021
|
+
items[i][0] === "api url"
|
|
1022
|
+
) {
|
|
1023
|
+
var fullValue =
|
|
1024
|
+
items[i][0] === "publicKey" && d.identity
|
|
1025
|
+
? d.identity.publicKey
|
|
1026
|
+
: items[i][1];
|
|
1027
|
+
val.style.cursor = "pointer";
|
|
1028
|
+
val.title = "Click to copy";
|
|
1029
|
+
val.addEventListener(
|
|
1030
|
+
"click",
|
|
1031
|
+
(function (copyValue, displayValue) {
|
|
1032
|
+
return function () {
|
|
1033
|
+
navigator.clipboard.writeText(copyValue);
|
|
1034
|
+
val.textContent = "copied!";
|
|
1035
|
+
setTimeout(function () {
|
|
1036
|
+
val.textContent = displayValue;
|
|
1037
|
+
}, 1200);
|
|
1038
|
+
};
|
|
1039
|
+
})(fullValue, items[i][1]),
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1042
|
+
container.appendChild(lbl);
|
|
1043
|
+
container.appendChild(val);
|
|
1044
|
+
}
|
|
1045
|
+
if (d.identity && !(d.registration && d.registration.registered)) {
|
|
1046
|
+
var noteEl = createEl("span", {
|
|
1047
|
+
class: "label",
|
|
1048
|
+
style: { gridColumn: "1 / -1", marginTop: "8px" },
|
|
1049
|
+
});
|
|
1050
|
+
noteEl.textContent = runtimeState.registration
|
|
1051
|
+
.ownerPrivateKeyConfigured
|
|
1052
|
+
? "Register this runtime once the public API URL is reachable."
|
|
1053
|
+
: "Set VANA_OWNER_PRIVATE_KEY to enable server registration.";
|
|
1054
|
+
noteEl.style.color = "var(--text-dim)";
|
|
1055
|
+
noteEl.style.fontSize = "11px";
|
|
1056
|
+
noteEl.style.fontStyle = "italic";
|
|
1057
|
+
container.appendChild(noteEl);
|
|
1058
|
+
}
|
|
1059
|
+
} catch (e) {
|
|
1060
|
+
clearChildren(container);
|
|
1061
|
+
var lbl2 = createEl("span", { class: "label" });
|
|
1062
|
+
lbl2.textContent = "status";
|
|
1063
|
+
var val2 = createEl("span", { class: "value err" });
|
|
1064
|
+
val2.textContent = "unreachable";
|
|
1065
|
+
container.appendChild(lbl2);
|
|
1066
|
+
container.appendChild(val2);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
async function registerNodeServer() {
|
|
1071
|
+
var statusEl = el("node-registration-status");
|
|
1072
|
+
statusEl.textContent = "Registering " + runtimeName() + " server...";
|
|
1073
|
+
try {
|
|
1074
|
+
var health = await selectedHealth();
|
|
1075
|
+
var serverUrl =
|
|
1076
|
+
health.data &&
|
|
1077
|
+
health.data.registration &&
|
|
1078
|
+
health.data.registration.serverUrl;
|
|
1079
|
+
if (!isPublicServerUrl(serverUrl)) {
|
|
1080
|
+
statusEl.style.color = "var(--error)";
|
|
1081
|
+
statusEl.textContent =
|
|
1082
|
+
"Registration requires a reachable public API URL.";
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
var result = await api("/ui/api/registration/server", {
|
|
1086
|
+
method: "POST",
|
|
1087
|
+
headers: { "Content-Type": "application/json" },
|
|
1088
|
+
body: JSON.stringify({
|
|
1089
|
+
gatewayConfig: health.data.gatewayConfig,
|
|
1090
|
+
registration: health.data.registration,
|
|
1091
|
+
}),
|
|
1092
|
+
});
|
|
1093
|
+
statusEl.style.color = result.ok ? "var(--accent)" : "var(--error)";
|
|
1094
|
+
statusEl.textContent = result.ok
|
|
1095
|
+
? "Registered " + runtimeName() + " server"
|
|
1096
|
+
: (result.data && result.data.error && result.data.error.message) ||
|
|
1097
|
+
"Registration failed";
|
|
1098
|
+
await loadHealth();
|
|
1099
|
+
} catch (e) {
|
|
1100
|
+
statusEl.style.color = "var(--error)";
|
|
1101
|
+
statusEl.textContent = e.message;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
function formatUptime(s) {
|
|
1106
|
+
var h = Math.floor(s / 3600);
|
|
1107
|
+
var m = Math.floor((s % 3600) / 60);
|
|
1108
|
+
var sec = s % 60;
|
|
1109
|
+
return h + "h " + m + "m " + sec + "s";
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
loadHealth();
|
|
1113
|
+
setInterval(loadHealth, 5000);
|
|
1114
|
+
|
|
1115
|
+
// --- Config ---
|
|
1116
|
+
async function loadConfig() {
|
|
1117
|
+
var editor = el("config-editor");
|
|
1118
|
+
var msg = el("config-msg");
|
|
1119
|
+
clearChildren(msg);
|
|
1120
|
+
try {
|
|
1121
|
+
var result = await runtimeApi("/ui/api/config", { auth: "owner" });
|
|
1122
|
+
if (result.ok) {
|
|
1123
|
+
editor.value = JSON.stringify(result.data, null, 2);
|
|
1124
|
+
} else {
|
|
1125
|
+
showMsg(msg, "Failed to load " + runtimeName() + " config", "err");
|
|
1126
|
+
}
|
|
1127
|
+
} catch (e) {
|
|
1128
|
+
showMsg(msg, "Error loading config", "err");
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
async function saveConfig() {
|
|
1133
|
+
var editor = el("config-editor");
|
|
1134
|
+
var msg = el("config-msg");
|
|
1135
|
+
clearChildren(msg);
|
|
1136
|
+
|
|
1137
|
+
var body;
|
|
1138
|
+
try {
|
|
1139
|
+
body = JSON.parse(editor.value);
|
|
1140
|
+
} catch (e) {
|
|
1141
|
+
showMsg(msg, "Invalid JSON", "err");
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
try {
|
|
1146
|
+
var result = await runtimeApi("/ui/api/config", {
|
|
1147
|
+
method: "PUT",
|
|
1148
|
+
headers: { "Content-Type": "application/json" },
|
|
1149
|
+
body: JSON.stringify(body),
|
|
1150
|
+
});
|
|
1151
|
+
if (result.ok) {
|
|
1152
|
+
showMsg(
|
|
1153
|
+
msg,
|
|
1154
|
+
"Config saved. Restart server to apply changes.",
|
|
1155
|
+
"ok",
|
|
1156
|
+
);
|
|
1157
|
+
editor.value = JSON.stringify(result.data.config, null, 2);
|
|
1158
|
+
} else {
|
|
1159
|
+
var errMsg =
|
|
1160
|
+
(result.data && result.data.error && result.data.error.message) ||
|
|
1161
|
+
"Save failed";
|
|
1162
|
+
showMsg(msg, errMsg, "err");
|
|
1163
|
+
}
|
|
1164
|
+
} catch (e) {
|
|
1165
|
+
showMsg(msg, "Error saving config", "err");
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
el("save-config-btn").addEventListener("click", saveConfig);
|
|
1170
|
+
el("reload-config-btn").addEventListener("click", loadConfig);
|
|
1171
|
+
el("register-node-server-btn").addEventListener(
|
|
1172
|
+
"click",
|
|
1173
|
+
registerNodeServer,
|
|
1174
|
+
);
|
|
1175
|
+
|
|
1176
|
+
// --- Data ---
|
|
1177
|
+
async function loadScopes() {
|
|
1178
|
+
var container = el("data-scopes");
|
|
1179
|
+
el("data-detail").style.display = "none";
|
|
1180
|
+
showLoading(container);
|
|
1181
|
+
|
|
1182
|
+
try {
|
|
1183
|
+
var result = await runtimeApi("/v1/data?limit=100");
|
|
1184
|
+
if (!result.ok) {
|
|
1185
|
+
showJson(container, result.data);
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
if (!result.data.scopes || result.data.scopes.length === 0) {
|
|
1189
|
+
showEmpty(container, "No " + runtimeName() + " data scopes found.");
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
var syncResult = await runtimeApi("/v1/sync/status", {
|
|
1194
|
+
auth: "owner",
|
|
1195
|
+
}).catch(function (e) {
|
|
1196
|
+
return { ok: false, data: { error: e.message } };
|
|
1197
|
+
});
|
|
1198
|
+
var health = await selectedHealth().catch(function () {
|
|
1199
|
+
return { data: {} };
|
|
1200
|
+
});
|
|
1201
|
+
var needsRegistration = registrationRequired(health.data);
|
|
1202
|
+
var latestByScope = {};
|
|
1203
|
+
var fileIds = [];
|
|
1204
|
+
for (var vIdx = 0; vIdx < result.data.scopes.length; vIdx++) {
|
|
1205
|
+
var scopeItem = result.data.scopes[vIdx];
|
|
1206
|
+
var scopeName =
|
|
1207
|
+
typeof scopeItem === "string"
|
|
1208
|
+
? scopeItem
|
|
1209
|
+
: scopeItem.scope || JSON.stringify(scopeItem);
|
|
1210
|
+
var versions = await runtimeApi(
|
|
1211
|
+
"/v1/data/" + encodeURIComponent(scopeName) + "/versions?limit=1",
|
|
1212
|
+
).catch(function (e) {
|
|
1213
|
+
return { ok: false, data: { error: e.message } };
|
|
1214
|
+
});
|
|
1215
|
+
var latest =
|
|
1216
|
+
versions.ok &&
|
|
1217
|
+
versions.data &&
|
|
1218
|
+
versions.data.versions &&
|
|
1219
|
+
versions.data.versions.length > 0
|
|
1220
|
+
? versions.data.versions[0]
|
|
1221
|
+
: null;
|
|
1222
|
+
latestByScope[scopeName] = latest;
|
|
1223
|
+
if (latest && latest.fileId) fileIds.push(latest.fileId);
|
|
1224
|
+
}
|
|
1225
|
+
var rpcFiles = await verifyRpcFiles(
|
|
1226
|
+
health.data && health.data.gatewayUrl,
|
|
1227
|
+
Array.from(new Set(fileIds)),
|
|
1228
|
+
).catch(function () {
|
|
1229
|
+
return {};
|
|
1230
|
+
});
|
|
1231
|
+
|
|
1232
|
+
clearChildren(container);
|
|
1233
|
+
var table = createEl("table");
|
|
1234
|
+
var thead = createEl("thead");
|
|
1235
|
+
var headRow = createEl("tr", null, [
|
|
1236
|
+
createEl("th", null, "Scope"),
|
|
1237
|
+
createEl("th", null, "Local"),
|
|
1238
|
+
createEl("th", null, "RPC"),
|
|
1239
|
+
createEl("th", null, "Sync"),
|
|
1240
|
+
createEl("th", null, "Actions"),
|
|
1241
|
+
]);
|
|
1242
|
+
thead.appendChild(headRow);
|
|
1243
|
+
table.appendChild(thead);
|
|
1244
|
+
|
|
1245
|
+
var tbody = createEl("tbody");
|
|
1246
|
+
for (var i = 0; i < result.data.scopes.length; i++) {
|
|
1247
|
+
var scope = result.data.scopes[i];
|
|
1248
|
+
var s =
|
|
1249
|
+
typeof scope === "string"
|
|
1250
|
+
? scope
|
|
1251
|
+
: scope.scope || JSON.stringify(scope);
|
|
1252
|
+
var tr = createEl("tr");
|
|
1253
|
+
var tdScope = createEl("td");
|
|
1254
|
+
tdScope.textContent = s;
|
|
1255
|
+
var latestVersion = latestByScope[s];
|
|
1256
|
+
var tdLocal = createEl("td");
|
|
1257
|
+
tdLocal.textContent =
|
|
1258
|
+
latestVersion && latestVersion.fileId ? "file id" : "unsynced";
|
|
1259
|
+
tdLocal.style.color =
|
|
1260
|
+
latestVersion && latestVersion.fileId
|
|
1261
|
+
? "var(--accent)"
|
|
1262
|
+
: "var(--warn)";
|
|
1263
|
+
var tdRpc = createEl("td");
|
|
1264
|
+
if (latestVersion && latestVersion.fileId) {
|
|
1265
|
+
var rpc = rpcFiles[latestVersion.fileId];
|
|
1266
|
+
tdRpc.textContent = rpc
|
|
1267
|
+
? rpc.registered
|
|
1268
|
+
? "registered"
|
|
1269
|
+
: "missing"
|
|
1270
|
+
: "unknown";
|
|
1271
|
+
tdRpc.style.color =
|
|
1272
|
+
rpc && rpc.registered
|
|
1273
|
+
? "var(--accent)"
|
|
1274
|
+
: rpc
|
|
1275
|
+
? "var(--error)"
|
|
1276
|
+
: "var(--text-dim)";
|
|
1277
|
+
} else {
|
|
1278
|
+
tdRpc.textContent = "\u2014";
|
|
1279
|
+
tdRpc.style.color = "var(--text-dim)";
|
|
1280
|
+
}
|
|
1281
|
+
var tdSync = createEl("td");
|
|
1282
|
+
var syncData = syncResult.data || {};
|
|
1283
|
+
var syncErrors = Array.isArray(syncData.errors)
|
|
1284
|
+
? syncData.errors
|
|
1285
|
+
: [];
|
|
1286
|
+
var hasPendingFiles =
|
|
1287
|
+
typeof syncData.pendingFiles === "number" &&
|
|
1288
|
+
syncData.pendingFiles > 0;
|
|
1289
|
+
if (syncData.enabled === false) {
|
|
1290
|
+
tdSync.textContent = "disabled";
|
|
1291
|
+
tdSync.style.color = "var(--text-dim)";
|
|
1292
|
+
} else if (needsRegistration && hasPendingFiles) {
|
|
1293
|
+
tdSync.textContent = "register server";
|
|
1294
|
+
tdSync.style.color = "var(--warn)";
|
|
1295
|
+
tdSync.title =
|
|
1296
|
+
"Register this runtime before syncing pending files.";
|
|
1297
|
+
} else if (!syncResult.ok) {
|
|
1298
|
+
tdSync.textContent = "unavailable";
|
|
1299
|
+
tdSync.style.color = "var(--warn)";
|
|
1300
|
+
} else if (syncData.syncing) {
|
|
1301
|
+
tdSync.textContent = "syncing";
|
|
1302
|
+
tdSync.style.color = "var(--warn)";
|
|
1303
|
+
} else if (syncErrors.length > 0 && hasPendingFiles) {
|
|
1304
|
+
tdSync.textContent = "error";
|
|
1305
|
+
tdSync.style.color = "var(--error)";
|
|
1306
|
+
tdSync.title = syncErrors[syncErrors.length - 1].message || "";
|
|
1307
|
+
} else if (hasPendingFiles) {
|
|
1308
|
+
tdSync.textContent = "pending";
|
|
1309
|
+
tdSync.style.color = "var(--warn)";
|
|
1310
|
+
} else {
|
|
1311
|
+
tdSync.textContent = "idle";
|
|
1312
|
+
tdSync.style.color = "var(--accent)";
|
|
1313
|
+
}
|
|
1314
|
+
var tdActions = createEl("td");
|
|
1315
|
+
var btnView = createEl("button", { class: "btn secondary" });
|
|
1316
|
+
btnView.textContent = "View Latest";
|
|
1317
|
+
btnView.addEventListener(
|
|
1318
|
+
"click",
|
|
1319
|
+
(function (sc) {
|
|
1320
|
+
return function () {
|
|
1321
|
+
viewScope(sc);
|
|
1322
|
+
};
|
|
1323
|
+
})(s),
|
|
1324
|
+
);
|
|
1325
|
+
var btnVer = createEl("button", {
|
|
1326
|
+
class: "btn secondary",
|
|
1327
|
+
style: { marginLeft: "8px" },
|
|
1328
|
+
});
|
|
1329
|
+
btnVer.textContent = "Versions";
|
|
1330
|
+
btnVer.addEventListener(
|
|
1331
|
+
"click",
|
|
1332
|
+
(function (sc) {
|
|
1333
|
+
return function () {
|
|
1334
|
+
viewVersions(sc);
|
|
1335
|
+
};
|
|
1336
|
+
})(s),
|
|
1337
|
+
);
|
|
1338
|
+
tdActions.appendChild(btnView);
|
|
1339
|
+
tdActions.appendChild(btnVer);
|
|
1340
|
+
tr.appendChild(tdScope);
|
|
1341
|
+
tr.appendChild(tdLocal);
|
|
1342
|
+
tr.appendChild(tdRpc);
|
|
1343
|
+
tr.appendChild(tdSync);
|
|
1344
|
+
tr.appendChild(tdActions);
|
|
1345
|
+
tbody.appendChild(tr);
|
|
1346
|
+
}
|
|
1347
|
+
table.appendChild(tbody);
|
|
1348
|
+
container.appendChild(table);
|
|
1349
|
+
} catch (e) {
|
|
1350
|
+
showMsg(container, "Error: " + e.message, "err");
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
el("refresh-data-btn").addEventListener("click", loadScopes);
|
|
1355
|
+
|
|
1356
|
+
async function triggerSync() {
|
|
1357
|
+
var msg = el("sync-msg");
|
|
1358
|
+
try {
|
|
1359
|
+
var health = await selectedHealth();
|
|
1360
|
+
if (registrationRequired(health.data)) {
|
|
1361
|
+
showMsg(
|
|
1362
|
+
msg,
|
|
1363
|
+
"Register " + runtimeName() + " server before syncing.",
|
|
1364
|
+
"warn",
|
|
1365
|
+
);
|
|
1366
|
+
await loadHealth();
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
showMsg(msg, "Triggering " + runtimeName() + " sync...", "ok");
|
|
1371
|
+
var result = await runtimeApi("/v1/sync/trigger", {
|
|
1372
|
+
method: "POST",
|
|
1373
|
+
auth: "owner",
|
|
1374
|
+
});
|
|
1375
|
+
if (!result.ok) {
|
|
1376
|
+
var errMsg =
|
|
1377
|
+
(result.data && result.data.error && result.data.error.message) ||
|
|
1378
|
+
"Sync trigger failed (" + result.status + ")";
|
|
1379
|
+
showMsg(msg, errMsg, "err");
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
if (result.data && result.data.status === "disabled") {
|
|
1383
|
+
showMsg(msg, "Sync is disabled for " + runtimeName() + ".", "warn");
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1386
|
+
var statusResult = await runtimeApi("/v1/sync/status", {
|
|
1387
|
+
auth: "owner",
|
|
1388
|
+
}).catch(function () {
|
|
1389
|
+
return { ok: false, data: {} };
|
|
1390
|
+
});
|
|
1391
|
+
var syncStatus = statusResult.data || {};
|
|
1392
|
+
var syncErrors = Array.isArray(syncStatus.errors)
|
|
1393
|
+
? syncStatus.errors
|
|
1394
|
+
: [];
|
|
1395
|
+
var pendingFiles =
|
|
1396
|
+
typeof syncStatus.pendingFiles === "number"
|
|
1397
|
+
? syncStatus.pendingFiles
|
|
1398
|
+
: 0;
|
|
1399
|
+
if (syncErrors.length > 0 && pendingFiles > 0) {
|
|
1400
|
+
var latestError = syncErrors[syncErrors.length - 1];
|
|
1401
|
+
showMsg(
|
|
1402
|
+
msg,
|
|
1403
|
+
"Sync finished with errors: " +
|
|
1404
|
+
(latestError.message || "unknown error"),
|
|
1405
|
+
"err",
|
|
1406
|
+
);
|
|
1407
|
+
} else if (pendingFiles > 0) {
|
|
1408
|
+
showMsg(
|
|
1409
|
+
msg,
|
|
1410
|
+
"Sync finished with " + pendingFiles + " pending file(s).",
|
|
1411
|
+
"warn",
|
|
1412
|
+
);
|
|
1413
|
+
} else {
|
|
1414
|
+
showMsg(msg, "Sync finished for " + runtimeName() + ".", "ok");
|
|
1415
|
+
}
|
|
1416
|
+
await loadScopes();
|
|
1417
|
+
} catch (e) {
|
|
1418
|
+
showMsg(msg, "Error: " + e.message, "err");
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
el("sync-now-btn").addEventListener("click", triggerSync);
|
|
1423
|
+
|
|
1424
|
+
async function verifyRpcFiles(gatewayUrl, fileIds) {
|
|
1425
|
+
if (!gatewayUrl || fileIds.length === 0) return {};
|
|
1426
|
+
var result = await api("/ui/api/rpc/files", {
|
|
1427
|
+
method: "POST",
|
|
1428
|
+
headers: { "Content-Type": "application/json" },
|
|
1429
|
+
body: JSON.stringify({ gatewayUrl: gatewayUrl, fileIds: fileIds }),
|
|
1430
|
+
});
|
|
1431
|
+
return result.ok && result.data && result.data.files
|
|
1432
|
+
? result.data.files
|
|
1433
|
+
: {};
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
async function submitData() {
|
|
1437
|
+
var msg = el("add-data-msg");
|
|
1438
|
+
clearChildren(msg);
|
|
1439
|
+
|
|
1440
|
+
var scope = el("add-data-scope").value.trim();
|
|
1441
|
+
if (!scope) {
|
|
1442
|
+
showMsg(msg, "Scope is required.", "err");
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
var raw = el("add-data-json").value.trim();
|
|
1447
|
+
var parsed;
|
|
1448
|
+
try {
|
|
1449
|
+
parsed = JSON.parse(raw);
|
|
1450
|
+
} catch (e) {
|
|
1451
|
+
showMsg(msg, "Invalid JSON: " + e.message, "err");
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
if (
|
|
1456
|
+
parsed === null ||
|
|
1457
|
+
typeof parsed !== "object" ||
|
|
1458
|
+
Array.isArray(parsed)
|
|
1459
|
+
) {
|
|
1460
|
+
showMsg(
|
|
1461
|
+
msg,
|
|
1462
|
+
"JSON must be a plain object (not array or null).",
|
|
1463
|
+
"err",
|
|
1464
|
+
);
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
try {
|
|
1469
|
+
var result = await runtimeApi(
|
|
1470
|
+
"/v1/data/" + encodeURIComponent(scope),
|
|
1471
|
+
{
|
|
1472
|
+
method: "POST",
|
|
1473
|
+
headers: { "Content-Type": "application/json" },
|
|
1474
|
+
body: JSON.stringify(parsed),
|
|
1475
|
+
},
|
|
1476
|
+
);
|
|
1477
|
+
if (result.ok) {
|
|
1478
|
+
var collectedAt =
|
|
1479
|
+
(result.data && result.data.collectedAt) || "unknown";
|
|
1480
|
+
showMsg(
|
|
1481
|
+
msg,
|
|
1482
|
+
'Stored data for scope "' +
|
|
1483
|
+
scope +
|
|
1484
|
+
'" (collectedAt: ' +
|
|
1485
|
+
collectedAt +
|
|
1486
|
+
")",
|
|
1487
|
+
"ok",
|
|
1488
|
+
);
|
|
1489
|
+
loadScopes();
|
|
1490
|
+
} else {
|
|
1491
|
+
var errCode =
|
|
1492
|
+
(result.data && result.data.error && result.data.error.code) ||
|
|
1493
|
+
"";
|
|
1494
|
+
var errMsg =
|
|
1495
|
+
(result.data && result.data.error && result.data.error.message) ||
|
|
1496
|
+
"Request failed (" + result.status + ")";
|
|
1497
|
+
showMsg(msg, errCode ? errCode + ": " + errMsg : errMsg, "err");
|
|
1498
|
+
}
|
|
1499
|
+
} catch (e) {
|
|
1500
|
+
showMsg(msg, "Error: " + e.message, "err");
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
el("submit-data-btn").addEventListener("click", submitData);
|
|
1505
|
+
|
|
1506
|
+
async function viewScope(scope) {
|
|
1507
|
+
var detail = el("data-detail");
|
|
1508
|
+
var content = el("data-detail-content");
|
|
1509
|
+
detail.style.display = "block";
|
|
1510
|
+
showLoading(content);
|
|
1511
|
+
|
|
1512
|
+
try {
|
|
1513
|
+
var path = "/v1/data/" + encodeURIComponent(scope);
|
|
1514
|
+
if (runtimeState.active === "browser") {
|
|
1515
|
+
path += "?grantId=debug-grant";
|
|
1516
|
+
}
|
|
1517
|
+
var result = await runtimeApi(path);
|
|
1518
|
+
showJson(content, result.data);
|
|
1519
|
+
} catch (e) {
|
|
1520
|
+
showMsg(content, "Error: " + e.message, "err");
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
async function viewVersions(scope) {
|
|
1525
|
+
var detail = el("data-detail");
|
|
1526
|
+
var content = el("data-detail-content");
|
|
1527
|
+
detail.style.display = "block";
|
|
1528
|
+
showLoading(content);
|
|
1529
|
+
|
|
1530
|
+
try {
|
|
1531
|
+
var result = await runtimeApi(
|
|
1532
|
+
"/v1/data/" + encodeURIComponent(scope) + "/versions",
|
|
1533
|
+
);
|
|
1534
|
+
if (!result.data.versions || result.data.versions.length === 0) {
|
|
1535
|
+
showEmpty(content, "No versions found.");
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
clearChildren(content);
|
|
1540
|
+
var table = createEl("table");
|
|
1541
|
+
var thead = createEl("thead");
|
|
1542
|
+
var headRow = createEl("tr", null, [
|
|
1543
|
+
createEl("th", null, "File ID"),
|
|
1544
|
+
createEl("th", null, "Collected At"),
|
|
1545
|
+
]);
|
|
1546
|
+
thead.appendChild(headRow);
|
|
1547
|
+
table.appendChild(thead);
|
|
1548
|
+
|
|
1549
|
+
var tbody = createEl("tbody");
|
|
1550
|
+
for (var i = 0; i < result.data.versions.length; i++) {
|
|
1551
|
+
var v = result.data.versions[i];
|
|
1552
|
+
var tr = createEl("tr");
|
|
1553
|
+
var td1 = createEl("td");
|
|
1554
|
+
td1.textContent = v.fileId || "\u2014";
|
|
1555
|
+
var td2 = createEl("td");
|
|
1556
|
+
td2.textContent = v.collectedAt;
|
|
1557
|
+
tr.appendChild(td1);
|
|
1558
|
+
tr.appendChild(td2);
|
|
1559
|
+
tbody.appendChild(tr);
|
|
1560
|
+
}
|
|
1561
|
+
table.appendChild(tbody);
|
|
1562
|
+
content.appendChild(table);
|
|
1563
|
+
} catch (e) {
|
|
1564
|
+
showMsg(content, "Error: " + e.message, "err");
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
// --- Grants ---
|
|
1569
|
+
async function loadGrants() {
|
|
1570
|
+
var container = el("grants-content");
|
|
1571
|
+
showLoading(container);
|
|
1572
|
+
|
|
1573
|
+
try {
|
|
1574
|
+
var result = await runtimeListGrants();
|
|
1575
|
+
if (!result.ok) {
|
|
1576
|
+
showJson(container, result.data);
|
|
1577
|
+
return;
|
|
1578
|
+
}
|
|
1579
|
+
if (!result.data.grants || result.data.grants.length === 0) {
|
|
1580
|
+
showEmpty(container, "No grants found.");
|
|
1581
|
+
return;
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
clearChildren(container);
|
|
1585
|
+
var table = createEl("table");
|
|
1586
|
+
var thead = createEl("thead");
|
|
1587
|
+
var headRow = createEl("tr", null, [
|
|
1588
|
+
createEl("th", null, "ID"),
|
|
1589
|
+
createEl("th", null, "Grantee"),
|
|
1590
|
+
createEl("th", null, "Status"),
|
|
1591
|
+
createEl("th", null, "Created"),
|
|
1592
|
+
createEl("th", null, "Actions"),
|
|
1593
|
+
]);
|
|
1594
|
+
thead.appendChild(headRow);
|
|
1595
|
+
table.appendChild(thead);
|
|
1596
|
+
|
|
1597
|
+
var tbody = createEl("tbody");
|
|
1598
|
+
for (var i = 0; i < result.data.grants.length; i++) {
|
|
1599
|
+
var g = result.data.grants[i];
|
|
1600
|
+
var status = g.revokedAt ? "revoked" : "active";
|
|
1601
|
+
var tr = createEl("tr");
|
|
1602
|
+
var td1 = createEl("td");
|
|
1603
|
+
td1.textContent = String(g.id);
|
|
1604
|
+
var td2 = createEl("td");
|
|
1605
|
+
td2.textContent = g.granteeId || "\u2014";
|
|
1606
|
+
var td3 = createEl("td");
|
|
1607
|
+
var statusSpan = createEl("span");
|
|
1608
|
+
statusSpan.textContent = status;
|
|
1609
|
+
statusSpan.style.color =
|
|
1610
|
+
status === "active" ? "var(--accent)" : "var(--error)";
|
|
1611
|
+
td3.appendChild(statusSpan);
|
|
1612
|
+
var td4 = createEl("td");
|
|
1613
|
+
td4.textContent = g.createdAt || "\u2014";
|
|
1614
|
+
var td5 = createEl("td");
|
|
1615
|
+
if (status === "active") {
|
|
1616
|
+
var revokeBtn = createEl("button", { class: "btn" }, "Revoke");
|
|
1617
|
+
revokeBtn.addEventListener(
|
|
1618
|
+
"click",
|
|
1619
|
+
(function (grantId) {
|
|
1620
|
+
return async function () {
|
|
1621
|
+
revokeBtn.disabled = true;
|
|
1622
|
+
var revokeResult = await runtimeRevokeGrant(grantId);
|
|
1623
|
+
if (!revokeResult.ok) {
|
|
1624
|
+
showJson(container, revokeResult.data);
|
|
1625
|
+
return;
|
|
1626
|
+
}
|
|
1627
|
+
await loadGrants();
|
|
1628
|
+
};
|
|
1629
|
+
})(String(g.id)),
|
|
1630
|
+
);
|
|
1631
|
+
td5.appendChild(revokeBtn);
|
|
1632
|
+
} else {
|
|
1633
|
+
td5.textContent = "\u2014";
|
|
1634
|
+
}
|
|
1635
|
+
tr.appendChild(td1);
|
|
1636
|
+
tr.appendChild(td2);
|
|
1637
|
+
tr.appendChild(td3);
|
|
1638
|
+
tr.appendChild(td4);
|
|
1639
|
+
tr.appendChild(td5);
|
|
1640
|
+
tbody.appendChild(tr);
|
|
1641
|
+
}
|
|
1642
|
+
table.appendChild(tbody);
|
|
1643
|
+
container.appendChild(table);
|
|
1644
|
+
} catch (e) {
|
|
1645
|
+
showMsg(container, "Error: " + e.message, "err");
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
el("refresh-grants-btn").addEventListener("click", loadGrants);
|
|
1650
|
+
|
|
1651
|
+
// --- Access Logs ---
|
|
1652
|
+
async function loadAccessLogs() {
|
|
1653
|
+
var container = el("logs-content");
|
|
1654
|
+
showLoading(container);
|
|
1655
|
+
|
|
1656
|
+
try {
|
|
1657
|
+
var result = await runtimeApi("/v1/access-logs?limit=50", {
|
|
1658
|
+
auth: "owner",
|
|
1659
|
+
});
|
|
1660
|
+
if (!result.ok) {
|
|
1661
|
+
showJson(container, result.data);
|
|
1662
|
+
return;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
var entries = result.data.logs || result.data.entries || result.data;
|
|
1666
|
+
if (!Array.isArray(entries) || entries.length === 0) {
|
|
1667
|
+
showEmpty(container, "No access logs found.");
|
|
1668
|
+
return;
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
clearChildren(container);
|
|
1672
|
+
var table = createEl("table");
|
|
1673
|
+
var thead = createEl("thead");
|
|
1674
|
+
var headRow = createEl("tr", null, [
|
|
1675
|
+
createEl("th", null, "Timestamp"),
|
|
1676
|
+
createEl("th", null, "Builder"),
|
|
1677
|
+
createEl("th", null, "Scope"),
|
|
1678
|
+
createEl("th", null, "Action"),
|
|
1679
|
+
]);
|
|
1680
|
+
thead.appendChild(headRow);
|
|
1681
|
+
table.appendChild(thead);
|
|
1682
|
+
|
|
1683
|
+
var tbody = createEl("tbody");
|
|
1684
|
+
for (var i = 0; i < entries.length; i++) {
|
|
1685
|
+
var entry = entries[i];
|
|
1686
|
+
var tr = createEl("tr");
|
|
1687
|
+
var td1 = createEl("td");
|
|
1688
|
+
td1.textContent = entry.timestamp || "\u2014";
|
|
1689
|
+
var td2 = createEl("td");
|
|
1690
|
+
td2.textContent = truncAddr(entry.builder);
|
|
1691
|
+
var td3 = createEl("td");
|
|
1692
|
+
td3.textContent = entry.scope || "\u2014";
|
|
1693
|
+
var td4 = createEl("td");
|
|
1694
|
+
td4.textContent = entry.action || "\u2014";
|
|
1695
|
+
tr.appendChild(td1);
|
|
1696
|
+
tr.appendChild(td2);
|
|
1697
|
+
tr.appendChild(td3);
|
|
1698
|
+
tr.appendChild(td4);
|
|
1699
|
+
tbody.appendChild(tr);
|
|
1700
|
+
}
|
|
1701
|
+
table.appendChild(tbody);
|
|
1702
|
+
container.appendChild(table);
|
|
1703
|
+
} catch (e) {
|
|
1704
|
+
showMsg(container, "Error: " + e.message, "err");
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
el("refresh-logs-btn").addEventListener("click", loadAccessLogs);
|
|
1709
|
+
|
|
1710
|
+
// --- API Tester ---
|
|
1711
|
+
async function sendApiRequest() {
|
|
1712
|
+
var method = el("api-method").value;
|
|
1713
|
+
var path = el("api-path").value;
|
|
1714
|
+
var bodyText = el("api-body").value.trim();
|
|
1715
|
+
var statusEl2 = el("api-status");
|
|
1716
|
+
var responseEl = el("api-response");
|
|
1717
|
+
|
|
1718
|
+
statusEl2.textContent = "Sending...";
|
|
1719
|
+
responseEl.textContent = "";
|
|
1720
|
+
|
|
1721
|
+
var opts = { method: method };
|
|
1722
|
+
if (bodyText && (method === "POST" || method === "PUT")) {
|
|
1723
|
+
opts.headers = { "Content-Type": "application/json" };
|
|
1724
|
+
opts.body = bodyText;
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
try {
|
|
1728
|
+
var result = await runtimeApi(path, opts);
|
|
1729
|
+
var color =
|
|
1730
|
+
result.status < 300
|
|
1731
|
+
? "var(--accent)"
|
|
1732
|
+
: result.status < 500
|
|
1733
|
+
? "var(--warn)"
|
|
1734
|
+
: "var(--error)";
|
|
1735
|
+
clearChildren(statusEl2);
|
|
1736
|
+
var statusSpan = createEl("span");
|
|
1737
|
+
statusSpan.textContent = result.status;
|
|
1738
|
+
statusSpan.style.color = color;
|
|
1739
|
+
statusEl2.appendChild(statusSpan);
|
|
1740
|
+
statusEl2.appendChild(text(" " + method + " " + path));
|
|
1741
|
+
responseEl.textContent =
|
|
1742
|
+
typeof result.data === "string"
|
|
1743
|
+
? result.data
|
|
1744
|
+
: JSON.stringify(result.data, null, 2);
|
|
1745
|
+
} catch (e) {
|
|
1746
|
+
clearChildren(statusEl2);
|
|
1747
|
+
var errSpan = createEl("span");
|
|
1748
|
+
errSpan.textContent = "Error";
|
|
1749
|
+
errSpan.style.color = "var(--error)";
|
|
1750
|
+
statusEl2.appendChild(errSpan);
|
|
1751
|
+
responseEl.textContent = e.message;
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
el("send-api-btn").addEventListener("click", sendApiRequest);
|
|
1756
|
+
|
|
1757
|
+
// --- Browser Runtime Bridge ---
|
|
1758
|
+
function psLiteClient() {
|
|
1759
|
+
if (!window.psLiteDebug) {
|
|
1760
|
+
throw new Error("Browser runtime bundle is still loading");
|
|
1761
|
+
}
|
|
1762
|
+
return window.psLiteDebug;
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
async function psLiteApiRequest(path, opts) {
|
|
1766
|
+
opts = opts || {};
|
|
1767
|
+
var body;
|
|
1768
|
+
if (opts.body) {
|
|
1769
|
+
try {
|
|
1770
|
+
body = JSON.parse(opts.body);
|
|
1771
|
+
} catch (e) {
|
|
1772
|
+
body = opts.body;
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
var client = psLiteClient();
|
|
1776
|
+
var url = new URL(path, "https://ps-lite.local");
|
|
1777
|
+
var dataPrefix = "/v1/data";
|
|
1778
|
+
if (
|
|
1779
|
+
url.pathname === dataPrefix &&
|
|
1780
|
+
(!opts.method || opts.method.toUpperCase() === "GET")
|
|
1781
|
+
) {
|
|
1782
|
+
return client.listData({
|
|
1783
|
+
scopePrefix: url.searchParams.get("scopePrefix") || undefined,
|
|
1784
|
+
limit: numberParam(url.searchParams.get("limit")),
|
|
1785
|
+
offset: numberParam(url.searchParams.get("offset")),
|
|
1786
|
+
});
|
|
1787
|
+
}
|
|
1788
|
+
if (url.pathname.startsWith(dataPrefix + "/")) {
|
|
1789
|
+
var dataSuffix = url.pathname.slice((dataPrefix + "/").length);
|
|
1790
|
+
if (dataSuffix.endsWith("/versions")) {
|
|
1791
|
+
return client.listVersions(
|
|
1792
|
+
decodeURIComponent(dataSuffix.slice(0, -"/versions".length)),
|
|
1793
|
+
{
|
|
1794
|
+
limit: numberParam(url.searchParams.get("limit")),
|
|
1795
|
+
offset: numberParam(url.searchParams.get("offset")),
|
|
1796
|
+
},
|
|
1797
|
+
);
|
|
1798
|
+
}
|
|
1799
|
+
var scope = decodeURIComponent(dataSuffix);
|
|
1800
|
+
var method = (opts.method || "GET").toUpperCase();
|
|
1801
|
+
if (method === "POST") {
|
|
1802
|
+
return client.postData(scope, body);
|
|
1803
|
+
}
|
|
1804
|
+
if (method === "GET") {
|
|
1805
|
+
return client.readData(scope, {
|
|
1806
|
+
grantId: url.searchParams.get("grantId") || undefined,
|
|
1807
|
+
fileId: url.searchParams.get("fileId") || undefined,
|
|
1808
|
+
at: url.searchParams.get("at") || undefined,
|
|
1809
|
+
});
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
if (
|
|
1813
|
+
url.pathname === "/v1/sync/status" &&
|
|
1814
|
+
(!opts.method || opts.method.toUpperCase() === "GET")
|
|
1815
|
+
) {
|
|
1816
|
+
return client.syncStatus();
|
|
1817
|
+
}
|
|
1818
|
+
if (
|
|
1819
|
+
url.pathname === "/v1/sync/trigger" &&
|
|
1820
|
+
(opts.method || "GET").toUpperCase() === "POST"
|
|
1821
|
+
) {
|
|
1822
|
+
return client.syncTrigger();
|
|
1823
|
+
}
|
|
1824
|
+
if (url.pathname === "/v1/grants") {
|
|
1825
|
+
var grantMethod = (opts.method || "GET").toUpperCase();
|
|
1826
|
+
if (grantMethod === "GET") return client.listGrants();
|
|
1827
|
+
if (grantMethod === "POST") return client.createGrant(body);
|
|
1828
|
+
}
|
|
1829
|
+
if (url.pathname.startsWith("/v1/grants/")) {
|
|
1830
|
+
var revokeMethod = (opts.method || "GET").toUpperCase();
|
|
1831
|
+
if (revokeMethod === "DELETE") {
|
|
1832
|
+
return client.revokeGrant(
|
|
1833
|
+
decodeURIComponent(url.pathname.slice("/v1/grants/".length)),
|
|
1834
|
+
);
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
return psLiteClient().request(path, {
|
|
1838
|
+
method: opts.method || "GET",
|
|
1839
|
+
body: body,
|
|
1840
|
+
auth: opts.auth,
|
|
1841
|
+
headers: opts.headers,
|
|
1842
|
+
});
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
function numberParam(value) {
|
|
1846
|
+
if (value === null || value === "") return undefined;
|
|
1847
|
+
var parsed = Number(value);
|
|
1848
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
window.addEventListener("ps-lite-relay-status", function () {
|
|
1852
|
+
if (runtimeState.active === "browser") refreshSelectedPanel();
|
|
1853
|
+
});
|
|
1854
|
+
window.addEventListener("ps-lite-relay-log", function (event) {
|
|
1855
|
+
if (runtimeState.active === "browser" && selectedPanel() === "health") {
|
|
1856
|
+
el("node-registration-status").textContent = String(event.detail);
|
|
1857
|
+
}
|
|
1858
|
+
});
|
|
1859
|
+
|
|
1860
|
+
// --- Connect Simulator ---
|
|
1861
|
+
var connectState = {
|
|
1862
|
+
relayUrl: "",
|
|
1863
|
+
sessionId: "",
|
|
1864
|
+
secret: "",
|
|
1865
|
+
granteeAddress: "",
|
|
1866
|
+
scopes: [],
|
|
1867
|
+
userAddress: "",
|
|
1868
|
+
serverAddress: "",
|
|
1869
|
+
grantId: "",
|
|
1870
|
+
};
|
|
1871
|
+
|
|
1872
|
+
function connectLog(message, type) {
|
|
1873
|
+
var logEl = el("connect-log");
|
|
1874
|
+
var ts = new Date().toLocaleTimeString();
|
|
1875
|
+
var color =
|
|
1876
|
+
type === "ok"
|
|
1877
|
+
? "var(--accent)"
|
|
1878
|
+
: type === "err"
|
|
1879
|
+
? "var(--error)"
|
|
1880
|
+
: "var(--text-dim)";
|
|
1881
|
+
var line = createEl("div", { style: { color: color } });
|
|
1882
|
+
line.textContent = "[" + ts + "] " + message;
|
|
1883
|
+
logEl.appendChild(line);
|
|
1884
|
+
logEl.scrollTop = logEl.scrollHeight;
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
async function relayFetch(path, opts) {
|
|
1888
|
+
opts = opts || {};
|
|
1889
|
+
var url = connectState.relayUrl + path;
|
|
1890
|
+
var res = await fetch(url, {
|
|
1891
|
+
method: opts.method || "GET",
|
|
1892
|
+
headers: Object.assign(
|
|
1893
|
+
{ "Content-Type": "application/json" },
|
|
1894
|
+
opts.headers || {},
|
|
1895
|
+
),
|
|
1896
|
+
body: opts.body,
|
|
1897
|
+
});
|
|
1898
|
+
var json = await res.json().catch(function () {
|
|
1899
|
+
return {};
|
|
1900
|
+
});
|
|
1901
|
+
return { status: res.status, ok: res.ok, data: json };
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
function connectParse() {
|
|
1905
|
+
var relayUrl = el("connect-relay-url").value.trim().replace(/\/+$/, "");
|
|
1906
|
+
var deepLink = el("connect-deep-link").value.trim();
|
|
1907
|
+
|
|
1908
|
+
if (!deepLink) {
|
|
1909
|
+
connectLog("Deep link URL is required", "err");
|
|
1910
|
+
return;
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
var url;
|
|
1914
|
+
try {
|
|
1915
|
+
url = new URL(deepLink);
|
|
1916
|
+
} catch (e) {
|
|
1917
|
+
connectLog("Invalid deep link URL: " + e.message, "err");
|
|
1918
|
+
return;
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
var sessionId = url.searchParams.get("sessionId");
|
|
1922
|
+
var secret = url.searchParams.get("secret");
|
|
1923
|
+
|
|
1924
|
+
if (!sessionId || !secret) {
|
|
1925
|
+
connectLog(
|
|
1926
|
+
"Deep link must contain sessionId and secret params",
|
|
1927
|
+
"err",
|
|
1928
|
+
);
|
|
1929
|
+
return;
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
connectState.relayUrl = relayUrl;
|
|
1933
|
+
connectState.sessionId = sessionId;
|
|
1934
|
+
connectState.secret = secret;
|
|
1935
|
+
|
|
1936
|
+
// Show parsed info
|
|
1937
|
+
var info = el("connect-parsed-info");
|
|
1938
|
+
clearChildren(info);
|
|
1939
|
+
var items = [
|
|
1940
|
+
["sessionId", sessionId],
|
|
1941
|
+
["secret", secret.slice(0, 8) + "..."],
|
|
1942
|
+
["relay", relayUrl],
|
|
1943
|
+
];
|
|
1944
|
+
for (var i = 0; i < items.length; i++) {
|
|
1945
|
+
var lbl = createEl("span", { class: "label" });
|
|
1946
|
+
lbl.textContent = items[i][0];
|
|
1947
|
+
var val = createEl("span", { class: "value" });
|
|
1948
|
+
val.textContent = items[i][1];
|
|
1949
|
+
info.appendChild(lbl);
|
|
1950
|
+
info.appendChild(val);
|
|
1951
|
+
}
|
|
1952
|
+
el("connect-parsed").style.display = "block";
|
|
1953
|
+
el("connect-flow-card").style.display = "block";
|
|
1954
|
+
|
|
1955
|
+
// Enable buttons
|
|
1956
|
+
el("connect-claim-btn").disabled = false;
|
|
1957
|
+
el("connect-auto-btn").disabled = false;
|
|
1958
|
+
el("connect-deny-btn").disabled = false;
|
|
1959
|
+
el("connect-grant-btn").disabled = true;
|
|
1960
|
+
el("connect-approve-btn").disabled = true;
|
|
1961
|
+
|
|
1962
|
+
connectLog("Parsed deep link — sessionId: " + sessionId, "ok");
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
async function connectClaim() {
|
|
1966
|
+
connectLog("Claiming session...");
|
|
1967
|
+
el("connect-claim-btn").disabled = true;
|
|
1968
|
+
|
|
1969
|
+
try {
|
|
1970
|
+
var result = await relayFetch("/v1/session/claim", {
|
|
1971
|
+
method: "POST",
|
|
1972
|
+
body: JSON.stringify({
|
|
1973
|
+
sessionId: connectState.sessionId,
|
|
1974
|
+
secret: connectState.secret,
|
|
1975
|
+
}),
|
|
1976
|
+
});
|
|
1977
|
+
|
|
1978
|
+
if (!result.ok) {
|
|
1979
|
+
var errMsg =
|
|
1980
|
+
(result.data.error && result.data.error.message) ||
|
|
1981
|
+
"Claim failed (" + result.status + ")";
|
|
1982
|
+
connectLog("Claim failed: " + errMsg, "err");
|
|
1983
|
+
el("connect-claim-btn").disabled = false;
|
|
1984
|
+
return false;
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
connectState.granteeAddress =
|
|
1988
|
+
result.data.granteeAddress ||
|
|
1989
|
+
result.data.session?.granteeAddress ||
|
|
1990
|
+
"";
|
|
1991
|
+
connectState.scopes =
|
|
1992
|
+
result.data.scopes || result.data.session?.scopes || [];
|
|
1993
|
+
|
|
1994
|
+
connectLog(
|
|
1995
|
+
"Session claimed — grantee: " +
|
|
1996
|
+
connectState.granteeAddress +
|
|
1997
|
+
", scopes: [" +
|
|
1998
|
+
connectState.scopes.join(", ") +
|
|
1999
|
+
"]",
|
|
2000
|
+
"ok",
|
|
2001
|
+
);
|
|
2002
|
+
|
|
2003
|
+
el("connect-grant-btn").disabled = false;
|
|
2004
|
+
return true;
|
|
2005
|
+
} catch (e) {
|
|
2006
|
+
connectLog("Claim error: " + e.message, "err");
|
|
2007
|
+
el("connect-claim-btn").disabled = false;
|
|
2008
|
+
return false;
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
async function connectCreateGrant() {
|
|
2013
|
+
connectLog("Creating grant...");
|
|
2014
|
+
el("connect-grant-btn").disabled = true;
|
|
2015
|
+
|
|
2016
|
+
try {
|
|
2017
|
+
// Get owner address from /health
|
|
2018
|
+
var healthResult = await selectedHealth();
|
|
2019
|
+
if (
|
|
2020
|
+
!healthResult.ok ||
|
|
2021
|
+
!healthResult.data ||
|
|
2022
|
+
!healthResult.data.owner
|
|
2023
|
+
) {
|
|
2024
|
+
connectLog(
|
|
2025
|
+
"Could not get owner address from /health. Is VANA_MASTER_KEY_SIGNATURE set?",
|
|
2026
|
+
"err",
|
|
2027
|
+
);
|
|
2028
|
+
el("connect-grant-btn").disabled = false;
|
|
2029
|
+
return false;
|
|
2030
|
+
}
|
|
2031
|
+
connectState.userAddress = healthResult.data.owner;
|
|
2032
|
+
connectState.serverAddress =
|
|
2033
|
+
(healthResult.data.identity &&
|
|
2034
|
+
healthResult.data.identity.address) ||
|
|
2035
|
+
"";
|
|
2036
|
+
connectLog("Owner address: " + connectState.userAddress);
|
|
2037
|
+
|
|
2038
|
+
var grantResult = await runtimeCreateGrant({
|
|
2039
|
+
granteeAddress: connectState.granteeAddress,
|
|
2040
|
+
scopes: connectState.scopes,
|
|
2041
|
+
});
|
|
2042
|
+
|
|
2043
|
+
if (!grantResult.ok) {
|
|
2044
|
+
var errMsg =
|
|
2045
|
+
(grantResult.data.error && grantResult.data.error.message) ||
|
|
2046
|
+
"Create grant failed (" + grantResult.status + ")";
|
|
2047
|
+
connectLog("Grant creation failed: " + errMsg, "err");
|
|
2048
|
+
el("connect-grant-btn").disabled = false;
|
|
2049
|
+
return false;
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
connectState.grantId = String(grantResult.data.grantId);
|
|
2053
|
+
connectLog("Grant created — grantId: " + connectState.grantId, "ok");
|
|
2054
|
+
|
|
2055
|
+
el("connect-approve-btn").disabled = false;
|
|
2056
|
+
return true;
|
|
2057
|
+
} catch (e) {
|
|
2058
|
+
connectLog("Grant error: " + e.message, "err");
|
|
2059
|
+
el("connect-grant-btn").disabled = false;
|
|
2060
|
+
return false;
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
async function connectApprove() {
|
|
2065
|
+
connectLog("Approving session...");
|
|
2066
|
+
el("connect-approve-btn").disabled = true;
|
|
2067
|
+
|
|
2068
|
+
try {
|
|
2069
|
+
var result = await relayFetch(
|
|
2070
|
+
"/v1/session/" + connectState.sessionId + "/approve",
|
|
2071
|
+
{
|
|
2072
|
+
method: "POST",
|
|
2073
|
+
body: JSON.stringify({
|
|
2074
|
+
secret: connectState.secret,
|
|
2075
|
+
grantId: connectState.grantId,
|
|
2076
|
+
userAddress: connectState.userAddress,
|
|
2077
|
+
serverAddress: connectState.serverAddress,
|
|
2078
|
+
scopes: connectState.scopes,
|
|
2079
|
+
}),
|
|
2080
|
+
},
|
|
2081
|
+
);
|
|
2082
|
+
|
|
2083
|
+
if (!result.ok) {
|
|
2084
|
+
var errMsg =
|
|
2085
|
+
(result.data.error && result.data.error.message) ||
|
|
2086
|
+
"Approve failed (" + result.status + ")";
|
|
2087
|
+
connectLog("Approve failed: " + errMsg, "err");
|
|
2088
|
+
el("connect-approve-btn").disabled = false;
|
|
2089
|
+
return false;
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
connectLog("Session approved!", "ok");
|
|
2093
|
+
|
|
2094
|
+
// Show result
|
|
2095
|
+
el("connect-result-card").style.display = "block";
|
|
2096
|
+
el("connect-result").textContent = JSON.stringify(
|
|
2097
|
+
{
|
|
2098
|
+
grantId: connectState.grantId,
|
|
2099
|
+
userAddress: connectState.userAddress,
|
|
2100
|
+
granteeAddress: connectState.granteeAddress,
|
|
2101
|
+
scopes: connectState.scopes,
|
|
2102
|
+
sessionId: connectState.sessionId,
|
|
2103
|
+
},
|
|
2104
|
+
null,
|
|
2105
|
+
2,
|
|
2106
|
+
);
|
|
2107
|
+
|
|
2108
|
+
// Disable all buttons
|
|
2109
|
+
el("connect-auto-btn").disabled = true;
|
|
2110
|
+
el("connect-deny-btn").disabled = true;
|
|
2111
|
+
return true;
|
|
2112
|
+
} catch (e) {
|
|
2113
|
+
connectLog("Approve error: " + e.message, "err");
|
|
2114
|
+
el("connect-approve-btn").disabled = false;
|
|
2115
|
+
return false;
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
async function connectDeny() {
|
|
2120
|
+
connectLog("Denying session...");
|
|
2121
|
+
|
|
2122
|
+
try {
|
|
2123
|
+
var result = await relayFetch(
|
|
2124
|
+
"/v1/session/" + connectState.sessionId + "/deny",
|
|
2125
|
+
{
|
|
2126
|
+
method: "POST",
|
|
2127
|
+
body: JSON.stringify({
|
|
2128
|
+
secret: connectState.secret,
|
|
2129
|
+
reason: "Denied from Dev UI",
|
|
2130
|
+
}),
|
|
2131
|
+
},
|
|
2132
|
+
);
|
|
2133
|
+
|
|
2134
|
+
if (!result.ok) {
|
|
2135
|
+
var errMsg =
|
|
2136
|
+
(result.data.error && result.data.error.message) ||
|
|
2137
|
+
"Deny failed (" + result.status + ")";
|
|
2138
|
+
connectLog("Deny failed: " + errMsg, "err");
|
|
2139
|
+
return;
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
connectLog("Session denied", "ok");
|
|
2143
|
+
|
|
2144
|
+
// Disable all action buttons
|
|
2145
|
+
el("connect-claim-btn").disabled = true;
|
|
2146
|
+
el("connect-grant-btn").disabled = true;
|
|
2147
|
+
el("connect-approve-btn").disabled = true;
|
|
2148
|
+
el("connect-auto-btn").disabled = true;
|
|
2149
|
+
el("connect-deny-btn").disabled = true;
|
|
2150
|
+
} catch (e) {
|
|
2151
|
+
connectLog("Deny error: " + e.message, "err");
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
async function connectAutoApprove() {
|
|
2156
|
+
el("connect-auto-btn").disabled = true;
|
|
2157
|
+
el("connect-deny-btn").disabled = true;
|
|
2158
|
+
connectLog("Starting auto-approve flow...");
|
|
2159
|
+
|
|
2160
|
+
var ok = await connectClaim();
|
|
2161
|
+
if (!ok) {
|
|
2162
|
+
el("connect-auto-btn").disabled = false;
|
|
2163
|
+
el("connect-deny-btn").disabled = false;
|
|
2164
|
+
return;
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
ok = await connectCreateGrant();
|
|
2168
|
+
if (!ok) {
|
|
2169
|
+
el("connect-auto-btn").disabled = false;
|
|
2170
|
+
el("connect-deny-btn").disabled = false;
|
|
2171
|
+
return;
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
await connectApprove();
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
el("connect-parse-btn").addEventListener("click", connectParse);
|
|
2178
|
+
el("connect-claim-btn").addEventListener("click", connectClaim);
|
|
2179
|
+
el("connect-grant-btn").addEventListener("click", connectCreateGrant);
|
|
2180
|
+
el("connect-approve-btn").addEventListener("click", connectApprove);
|
|
2181
|
+
el("connect-deny-btn").addEventListener("click", connectDeny);
|
|
2182
|
+
el("connect-auto-btn").addEventListener("click", connectAutoApprove);
|
|
2183
|
+
|
|
2184
|
+
// --- Utilities ---
|
|
2185
|
+
function truncAddr(addr) {
|
|
2186
|
+
if (!addr || addr.length < 12) return addr || "\u2014";
|
|
2187
|
+
return addr.slice(0, 6) + "..." + addr.slice(-4);
|
|
2188
|
+
}
|
|
2189
|
+
</script>
|
|
2190
|
+
<script type="module" src="/ui/ps-lite-debug.js"></script>
|
|
2191
|
+
</body>
|
|
2192
|
+
</html>
|