@elinpf/dsh-ops-access-ui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js ADDED
@@ -0,0 +1,1738 @@
1
+ window.__ModuleLoader__.load({ id: "@elinpf/dsh-ops-access-ui", factory: (require) => {
2
+ var module = { exports: {} }; var exports = module.exports;
3
+ "use strict";
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name2 in all)
10
+ __defProp(target, name2, { get: all[name2], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
21
+
22
+ // src/client.ts
23
+ var client_exports = {};
24
+ __export(client_exports, {
25
+ AccessBadge: () => AccessBadge,
26
+ AccessPanel: () => AccessPanel,
27
+ AdminSection: () => AdminSection,
28
+ apiFetchList: () => apiFetchList,
29
+ apiFetchResult: () => apiFetchResult,
30
+ apply: () => apply,
31
+ defaultTtlChoice: () => defaultTtlChoice,
32
+ delegatedAccessCount: () => delegatedAccessCount,
33
+ deleteEntry: () => deleteEntry,
34
+ fetchAdminList: () => fetchAdminList,
35
+ fetchKinds: () => fetchKinds,
36
+ fetchPanelGrants: () => fetchPanelGrants,
37
+ fetchPanelOverview: () => fetchPanelOverview,
38
+ fetchPanelRequests: () => fetchPanelRequests,
39
+ groupBySession: () => groupBySession,
40
+ inject: () => inject,
41
+ liveGrantFor: () => liveGrantFor,
42
+ name: () => name,
43
+ pendingAccessCount: () => pendingAccessCount,
44
+ postPanelDecide: () => postPanelDecide,
45
+ postPanelDeny: () => postPanelDeny,
46
+ postPanelExtend: () => postPanelExtend,
47
+ postPanelGrant: () => postPanelGrant,
48
+ postPanelRevoke: () => postPanelRevoke,
49
+ postPanelRevokeAll: () => postPanelRevokeAll,
50
+ postPanelUndeny: () => postPanelUndeny,
51
+ submitEntry: () => submitEntry
52
+ });
53
+ module.exports = __toCommonJS(client_exports);
54
+ var import_react = require("react");
55
+ var name = "ops-access-ui-client";
56
+ var inject = ["inputTriggers", "slots"];
57
+ function mentionProbeText(c) {
58
+ if (c.probe === void 0) return void 0;
59
+ const label = (s) => s === "verified" ? "\u5DF2\u6838\u9A8C" : s === "mismatch" ? "\u6838\u9A8C\u5931\u8D25" : "\u65E0\u6CD5\u6838\u9A8C";
60
+ const bits = [];
61
+ if (c.probe.ro !== void 0) bits.push("ro " + label(c.probe.ro));
62
+ if (c.probe.rw !== void 0) bits.push("rw " + label(c.probe.rw));
63
+ return bits.length > 0 ? bits.join(" \xB7 ") : void 0;
64
+ }
65
+ async function apiFetchList(url, init, degraded) {
66
+ try {
67
+ const res = await fetch(url, init);
68
+ if (!res.ok) return degraded;
69
+ return await res.json();
70
+ } catch {
71
+ return degraded;
72
+ }
73
+ }
74
+ async function apiFetchResult(url, init, errorPrefix) {
75
+ try {
76
+ const res = await fetch(url, init);
77
+ if (!res.ok) return { ok: false, error: `${errorPrefix}: HTTP ${res.status}` };
78
+ return await res.json();
79
+ } catch (err) {
80
+ return { ok: false, error: String(err?.message ?? err) };
81
+ }
82
+ }
83
+ function fetchAdminList(signal) {
84
+ return apiFetchList("/ops-access/admin/list", { signal }, []);
85
+ }
86
+ function fetchKinds(signal) {
87
+ return apiFetchList("/ops-access/admin/kinds", { signal }, []);
88
+ }
89
+ function submitEntry(body) {
90
+ return apiFetchResult("/ops-access/admin/entry", {
91
+ method: "POST",
92
+ headers: { "content-type": "application/json" },
93
+ body: JSON.stringify(body)
94
+ }, "submitEntry");
95
+ }
96
+ function deleteEntry(kind, name2, tier) {
97
+ const params = new URLSearchParams({ kind, name: name2, tier });
98
+ return apiFetchResult(`/ops-access/admin/entry?${params}`, {
99
+ method: "DELETE"
100
+ }, "deleteEntry");
101
+ }
102
+ async function fetchEntry(kind, name2, tier) {
103
+ const params = new URLSearchParams({ kind, name: name2, tier });
104
+ try {
105
+ const res = await fetch("/ops-access/admin/entry?" + params);
106
+ if (!res.ok) return null;
107
+ return await res.json();
108
+ } catch {
109
+ return null;
110
+ }
111
+ }
112
+ async function fetchPanelGrants(sessionId, signal) {
113
+ try {
114
+ const res = await fetch("/ops-access/grants?session=" + encodeURIComponent(sessionId), { signal });
115
+ if (!res.ok) return null;
116
+ const data = await res.json();
117
+ return { grants: data.grants ?? [], ttlOptions: data.ttlOptions ?? [], denied: Array.isArray(data.denied) ? data.denied : [] };
118
+ } catch {
119
+ return null;
120
+ }
121
+ }
122
+ function postPanelDeny(kind, name2) {
123
+ return apiFetchResult("/ops-access/deny", {
124
+ method: "POST",
125
+ headers: { "content-type": "application/json" },
126
+ body: JSON.stringify({ kind, name: name2 })
127
+ }, "postPanelDeny");
128
+ }
129
+ function postPanelUndeny(kind, name2) {
130
+ return apiFetchResult("/ops-access/undeny", {
131
+ method: "POST",
132
+ headers: { "content-type": "application/json" },
133
+ body: JSON.stringify({ kind, name: name2 })
134
+ }, "postPanelUndeny");
135
+ }
136
+ async function fetchPanelRequests(sessionId, signal) {
137
+ try {
138
+ const res = await fetch("/ops-access/access-requests?session=" + encodeURIComponent(sessionId), { signal });
139
+ if (!res.ok) return null;
140
+ return await res.json();
141
+ } catch {
142
+ return null;
143
+ }
144
+ }
145
+ function postPanelGrant(sessionId, kind, name2, ttlMinutes) {
146
+ return apiFetchResult("/ops-access/grants", {
147
+ method: "POST",
148
+ headers: { "content-type": "application/json" },
149
+ body: JSON.stringify({ session: sessionId, kind, name: name2, ttlMinutes })
150
+ }, "postPanelGrant");
151
+ }
152
+ function postPanelRevoke(sessionId, kind, name2) {
153
+ return apiFetchResult("/ops-access/grants/revoke", {
154
+ method: "POST",
155
+ headers: { "content-type": "application/json" },
156
+ body: JSON.stringify({ session: sessionId, kind, name: name2 })
157
+ }, "postPanelRevoke");
158
+ }
159
+ function postPanelRevokeAll(sessionId) {
160
+ return apiFetchResult("/ops-access/grants/revoke-all", {
161
+ method: "POST",
162
+ headers: { "content-type": "application/json" },
163
+ body: JSON.stringify({ session: sessionId })
164
+ }, "postPanelRevokeAll");
165
+ }
166
+ function postPanelExtend(sessionId, kind, name2, ttlMinutes) {
167
+ return apiFetchResult("/ops-access/grants/extend", {
168
+ method: "POST",
169
+ headers: { "content-type": "application/json" },
170
+ body: JSON.stringify({ session: sessionId, kind, name: name2, ttlMinutes })
171
+ }, "postPanelExtend");
172
+ }
173
+ function postPanelDecide(id, approved, ttlMinutes) {
174
+ return apiFetchResult("/ops-access/access-requests/decide", {
175
+ method: "POST",
176
+ headers: { "content-type": "application/json" },
177
+ body: JSON.stringify(approved ? { id, approved, ttlMinutes } : { id, approved })
178
+ }, "postPanelDecide");
179
+ }
180
+ function pendingAccessCount(session) {
181
+ if (session === null || session === void 0 || !Array.isArray(session.runningCalls)) return 0;
182
+ return session.runningCalls.filter((c) => c.name === "request_access").length;
183
+ }
184
+ function delegatedAccessCount(requests, sessionId) {
185
+ if (!Array.isArray(requests)) return 0;
186
+ return requests.filter((r) => r.parentSession === sessionId).length;
187
+ }
188
+ function AccessBadge(props) {
189
+ const own = pendingAccessCount(props.session);
190
+ const [delegated, setDelegated] = (0, import_react.useState)(0);
191
+ const sid = props.sessionId;
192
+ (0, import_react.useEffect)(() => {
193
+ if (sid === void 0) return;
194
+ let alive = true;
195
+ const poll = async () => {
196
+ const data = await fetchPanelRequests(sid);
197
+ if (alive && data !== null) setDelegated(delegatedAccessCount(data.requests, sid));
198
+ };
199
+ void poll();
200
+ const timer = setInterval(() => {
201
+ void poll();
202
+ }, 4e3);
203
+ return () => {
204
+ alive = false;
205
+ clearInterval(timer);
206
+ };
207
+ }, [sid]);
208
+ const count = own + delegated;
209
+ if (count === 0 || sid === void 0) return null;
210
+ return (0, import_react.createElement)(
211
+ "button",
212
+ {
213
+ className: "ops-access-badge",
214
+ title: delegated > 0 ? "\u6709\u5F85\u5BA1\u6279\u7684\u63D0\u6743\u7533\u8BF7\uFF08\u542B\u59D4\u6D3E\u5B50\u4F1A\u8BDD\uFF09\u2014 \u70B9\u51FB\u6253\u5F00\u6388\u6743\u9762\u677F" : "\u6709\u5F85\u5BA1\u6279\u7684\u63D0\u6743\u7533\u8BF7 \u2014 \u70B9\u51FB\u6253\u5F00\u6388\u6743\u9762\u677F",
215
+ onClick: () => props.openPanel(sid)
216
+ },
217
+ (0, import_react.createElement)("span", { className: "ops-access-badge-dot" }),
218
+ String(count) + " \u4E2A\u63D0\u6743\u7533\u8BF7\u5F85\u5BA1\u6279"
219
+ );
220
+ }
221
+ var CSS = `
222
+ .ops-access-admin-root {
223
+ box-sizing: border-box;
224
+ display: flex;
225
+ flex-direction: column;
226
+ gap: 16px;
227
+ padding: 0 4px;
228
+ font-size: 13px;
229
+ line-height: 20px;
230
+ color: var(--dsw-alias-label-primary, #1f2328);
231
+ }
232
+
233
+ .ops-access-admin-header {
234
+ display: flex;
235
+ align-items: center;
236
+ justify-content: space-between;
237
+ gap: 8px;
238
+ }
239
+
240
+ .ops-access-admin-title {
241
+ font-size: 15px;
242
+ font-weight: 600;
243
+ color: var(--dsw-alias-label-primary, #1f2328);
244
+ }
245
+
246
+ .ops-access-admin-actions {
247
+ display: flex;
248
+ align-items: center;
249
+ gap: 8px;
250
+ }
251
+
252
+ .ops-access-admin-btn {
253
+ display: inline-flex;
254
+ align-items: center;
255
+ gap: 4px;
256
+ padding: 4px 12px;
257
+ border: 1px solid var(--dsw-alias-border-l1, #d0d7de);
258
+ border-radius: 6px;
259
+ background: var(--dsw-alias-bg-primary, #fff);
260
+ color: var(--dsw-alias-label-primary, #1f2328);
261
+ font-size: 13px;
262
+ line-height: 20px;
263
+ cursor: pointer;
264
+ }
265
+
266
+ .ops-access-admin-btn:hover {
267
+ background: var(--dsw-alias-bg-hover, #e9ecef);
268
+ }
269
+
270
+ .ops-access-admin-btn-primary {
271
+ border-color: var(--dsw-alias-state-business-primary, #0969da);
272
+ background: var(--dsw-alias-state-business-primary, #0969da);
273
+ color: #fff;
274
+ }
275
+
276
+ .ops-access-admin-btn-primary:hover {
277
+ opacity: 0.9;
278
+ background: var(--dsw-alias-state-business-primary, #0969da);
279
+ }
280
+
281
+ .ops-access-admin-btn-danger {
282
+ color: #d1242f;
283
+ }
284
+
285
+ .ops-access-admin-btn-danger:hover {
286
+ background: rgba(209, 36, 47, 0.08);
287
+ }
288
+
289
+ .ops-access-admin-btn:disabled {
290
+ opacity: 0.5;
291
+ cursor: not-allowed;
292
+ }
293
+
294
+ .ops-access-admin-cards {
295
+ display: flex;
296
+ flex-direction: column;
297
+ gap: 12px;
298
+ }
299
+
300
+ .ops-access-admin-card {
301
+ border: 1px solid var(--dsw-alias-border-l1, #d0d7de);
302
+ border-radius: 8px;
303
+ overflow: hidden;
304
+ }
305
+
306
+ .ops-access-admin-card-header {
307
+ display: flex;
308
+ align-items: center;
309
+ justify-content: space-between;
310
+ padding: 8px 14px;
311
+ background: var(--dsw-alias-bg-secondary, #f6f8fa);
312
+ border-bottom: 1px solid var(--dsw-alias-border-l1, #e5e7eb);
313
+ }
314
+
315
+ .ops-access-admin-card-title {
316
+ font-size: 14px;
317
+ font-weight: 600;
318
+ color: var(--dsw-alias-label-primary, #1f2328);
319
+ }
320
+
321
+ .ops-access-admin-card-count {
322
+ font-size: 12px;
323
+ color: var(--dsw-alias-label-tertiary, #848d97);
324
+ }
325
+
326
+ .ops-access-admin-card-row {
327
+ display: flex;
328
+ align-items: flex-start;
329
+ justify-content: space-between;
330
+ gap: 8px;
331
+ padding: 8px 14px;
332
+ border-bottom: 1px solid var(--dsw-alias-border-l1, #f0f0f0);
333
+ }
334
+
335
+ .ops-access-admin-card-row:last-child {
336
+ border-bottom: none;
337
+ }
338
+
339
+ .ops-access-admin-card-row-main {
340
+ display: flex;
341
+ flex-direction: column;
342
+ gap: 2px;
343
+ flex: 1;
344
+ min-width: 0;
345
+ }
346
+
347
+ .ops-access-admin-card-row-name {
348
+ font-size: 13px;
349
+ font-weight: 500;
350
+ color: var(--dsw-alias-label-primary, #1f2328);
351
+ word-break: break-all;
352
+ }
353
+
354
+ .ops-access-admin-card-row-id {
355
+ margin-left: 8px;
356
+ font-size: 11px;
357
+ font-weight: 400;
358
+ font-family: var(--dsw-alias-font-mono, ui-monospace, monospace);
359
+ color: var(--dsw-alias-label-tertiary, #848d97);
360
+ }
361
+
362
+ .ops-access-admin-card-row-meta {
363
+ display: flex;
364
+ align-items: center;
365
+ gap: 8px;
366
+ }
367
+
368
+ .ops-access-admin-card-env {
369
+ font-size: 11px;
370
+ color: var(--dsw-alias-label-tertiary, #848d97);
371
+ }
372
+
373
+ .ops-access-admin-card-row-desc {
374
+ font-size: 12px;
375
+ color: var(--dsw-alias-label-secondary, #656d76);
376
+ overflow: hidden;
377
+ text-overflow: ellipsis;
378
+ white-space: nowrap;
379
+ }
380
+
381
+ .ops-access-admin-tier-cell {
382
+ display: inline-flex;
383
+ align-items: center;
384
+ gap: 4px;
385
+ }
386
+
387
+ .ops-access-admin-tier-ok {
388
+ color: var(--dsw-alias-state-success-primary, #1a7f37);
389
+ }
390
+
391
+ .ops-access-admin-tier-err {
392
+ color: #d1242f;
393
+ }
394
+
395
+ .ops-access-admin-tier-na {
396
+ color: var(--dsw-alias-label-tertiary, #848d97);
397
+ }
398
+
399
+ .ops-access-admin-row-actions {
400
+ display: flex;
401
+ gap: 4px;
402
+ }
403
+
404
+ .ops-access-admin-empty {
405
+ padding: 24px 0;
406
+ text-align: center;
407
+ color: var(--dsw-alias-label-tertiary, #848d97);
408
+ }
409
+
410
+ .ops-access-admin-error {
411
+ padding: 8px 12px;
412
+ border-radius: 6px;
413
+ background: rgba(209, 36, 47, 0.08);
414
+ color: #d1242f;
415
+ font-size: 12px;
416
+ }
417
+
418
+ .ops-access-admin-form {
419
+ display: flex;
420
+ flex-direction: column;
421
+ gap: 12px;
422
+ }
423
+
424
+ .ops-access-admin-field {
425
+ display: flex;
426
+ flex-direction: column;
427
+ gap: 4px;
428
+ }
429
+
430
+ .ops-access-admin-label {
431
+ font-size: 12px;
432
+ font-weight: 500;
433
+ color: var(--dsw-alias-label-secondary, #656d76);
434
+ }
435
+
436
+ .ops-access-admin-required {
437
+ color: #d1242f;
438
+ margin-left: 2px;
439
+ }
440
+
441
+ .ops-access-admin-type-hint {
442
+ color: var(--dsw-alias-label-tertiary, #848d97);
443
+ font-size: 11px;
444
+ margin-left: 4px;
445
+ }
446
+
447
+ .ops-access-admin-input {
448
+ padding: 4px 8px;
449
+ border: 1px solid var(--dsw-alias-border-l1, #d0d7de);
450
+ border-radius: 6px;
451
+ background: var(--dsw-alias-bg-primary, #fff);
452
+ color: var(--dsw-alias-label-primary, #1f2328);
453
+ font-size: 13px;
454
+ line-height: 20px;
455
+ }
456
+
457
+ .ops-access-admin-input:focus {
458
+ outline: none;
459
+ border-color: var(--dsw-alias-state-business-primary, #0969da);
460
+ }
461
+
462
+ .ops-access-admin-textarea {
463
+ font-family: var(--dsw-alias-font-mono, ui-monospace, monospace);
464
+ resize: vertical;
465
+ min-height: 120px;
466
+ }
467
+
468
+ .ops-access-admin-radio-group {
469
+ display: flex;
470
+ align-items: center;
471
+ gap: 16px;
472
+ }
473
+
474
+ .ops-access-admin-radio-label {
475
+ display: inline-flex;
476
+ align-items: center;
477
+ gap: 4px;
478
+ cursor: pointer;
479
+ }
480
+
481
+ .ops-access-admin-confirm {
482
+ padding: 12px 16px;
483
+ border: 1px solid var(--dsw-alias-border-l1, #d0d7de);
484
+ border-radius: 8px;
485
+ background: var(--dsw-alias-bg-secondary, #f6f8fa);
486
+ display: flex;
487
+ flex-direction: column;
488
+ gap: 8px;
489
+ }
490
+
491
+ .ops-access-admin-tier-section {
492
+ border: 1px solid var(--dsw-alias-border-l1, #e5e7eb);
493
+ border-radius: 8px;
494
+ padding: 10px 12px;
495
+ display: flex;
496
+ flex-direction: column;
497
+ gap: 8px;
498
+ }
499
+
500
+ .ops-access-admin-tier-fields {
501
+ display: flex;
502
+ flex-direction: column;
503
+ gap: 8px;
504
+ padding-top: 4px;
505
+ border-top: 1px dashed var(--dsw-alias-border-l1, #e5e7eb);
506
+ }
507
+
508
+ /* \u2500\u2500 Access panel (\u6388\u6743\u9762\u677F) \u2500\u2500 */
509
+ .ops-access-panel { display: flex; flex-direction: column; gap: 14px; }
510
+ .ops-access-panel-section { display: flex; flex-direction: column; gap: 6px; }
511
+ .ops-access-panel-heading { font-size: 12px; font-weight: 600; opacity: 0.65; }
512
+ .ops-access-panel-empty { font-size: 12px; opacity: 0.5; padding: 4px 0; }
513
+ .ops-access-panel-message {
514
+ font-size: 12px; padding: 6px 10px; border-radius: 6px; cursor: pointer;
515
+ background: var(--dsw-specific-tip, #f6f8fa);
516
+ border: 1px solid var(--dsw-alias-border-l1, #e5e7eb);
517
+ }
518
+ .ops-access-panel-row {
519
+ display: flex; align-items: center; justify-content: space-between;
520
+ padding: 6px 10px; border-radius: 8px; gap: 8px;
521
+ }
522
+ .ops-access-panel-row.clickable { cursor: pointer; }
523
+ .ops-access-panel-row.clickable:hover { background: var(--dsw-alias-bg-secondary, #f6f8fa); }
524
+ .ops-access-panel-row-title { font-weight: 500; display: flex; align-items: center; gap: 8px; }
525
+ .ops-access-panel-row-detail { font-size: 12px; opacity: 0.55; }
526
+ .ops-access-panel-request {
527
+ border: 1px solid var(--dsw-alias-border-l1, #e5e7eb); border-radius: 8px;
528
+ padding: 8px 10px; display: flex; flex-direction: column; gap: 6px;
529
+ background: var(--dsw-specific-tip, #f6f8fa);
530
+ }
531
+ .ops-access-panel-reason { font-size: 12px; opacity: 0.75; white-space: pre-wrap; }
532
+ .ops-access-panel-badge {
533
+ font-size: 11px; font-weight: 400; padding: 1px 6px; border-radius: 4px;
534
+ background: var(--dsw-alias-bg-secondary, #eaeef2);
535
+ }
536
+ .ops-access-panel-actions { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; padding: 2px 10px 4px; }
537
+ .ops-access-panel-ttl-row { display: inline-flex; gap: 4px; }
538
+ .ops-access-panel-btn {
539
+ border: 1px solid var(--dsw-alias-border-l1, #e5e7eb); border-radius: 6px;
540
+ background: transparent; color: inherit; font-size: 12px; padding: 3px 10px; cursor: pointer;
541
+ }
542
+ .ops-access-panel-btn:hover { background: var(--dsw-alias-bg-secondary, #f6f8fa); }
543
+ .ops-access-panel-btn.primary { border-color: var(--dsw-alias-primary, #0969da); color: var(--dsw-alias-primary, #0969da); }
544
+ .ops-access-panel-btn.danger { border-color: var(--dsw-alias-danger, #cf222e); color: var(--dsw-alias-danger, #cf222e); }
545
+ .ops-access-panel-btn.ttl.selected { background: var(--dsw-alias-primary, #0969da); border-color: var(--dsw-alias-primary, #0969da); color: #fff; }
546
+ .ops-access-panel-confirm {
547
+ display: flex; align-items: center; gap: 8px; font-size: 12px;
548
+ padding: 6px 10px; margin: 2px 0; border-radius: 6px;
549
+ border: 1px dashed var(--dsw-alias-danger, #cf222e);
550
+ }
551
+ .ops-access-panel .danger-zone .ops-access-panel-row-title { color: var(--dsw-alias-danger, #cf222e); }
552
+ .ops-access-panel-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; flex: none; margin-right: 6px; vertical-align: 1px; }
553
+ .ops-access-panel-dot.rw { background: var(--dsw-alias-success, #1a7f37); box-shadow: 0 0 4px var(--dsw-alias-success, #1a7f37); }
554
+ .ops-access-panel-dot.deny { background: var(--dsw-alias-danger, #cf222e); }
555
+ .ops-access-badge {
556
+ display: inline-flex; align-items: center; gap: 6px; width: fit-content;
557
+ padding: 3px 10px; border-radius: 999px; font-size: 12px; cursor: pointer;
558
+ border: 1px solid var(--dsw-alias-border-l1, #e5e7eb);
559
+ background: var(--dsw-alias-bg-primary, #ffffff);
560
+ color: var(--dsw-alias-text-primary, #1f2328);
561
+ }
562
+ .ops-access-badge:hover { background: var(--dsw-alias-bg-secondary, #f6f8fa); }
563
+ .ops-access-admin-probe { font-size: 11px; padding: 0 6px; border-radius: 4px; margin-left: 6px; }
564
+ .ops-access-admin-probe.verified { background: var(--dsw-alias-success, #1a7f37); color: #fff; }
565
+ .ops-access-admin-probe.mismatch { background: var(--dsw-alias-danger, #cf222e); color: #fff; }
566
+ .ops-access-admin-probe.unverifiable { background: var(--dsw-alias-bg-secondary, #eaeef2); opacity: 0.8; }
567
+ .ops-access-badge-dot {
568
+ width: 8px; height: 8px; border-radius: 50%; flex: none;
569
+ background: var(--dsw-alias-danger, #cf222e);
570
+ box-shadow: 0 0 0 3px rgba(207, 34, 46, 0.18);
571
+ }
572
+ `.trim();
573
+ function injectCSS() {
574
+ if (typeof document === "undefined") return;
575
+ if (document.head.querySelector('style[data-plugin="ops-access-admin"]') !== null) return;
576
+ const tag = document.createElement("style");
577
+ tag.dataset.plugin = "ops-access-admin";
578
+ tag.textContent = CSS;
579
+ document.head.appendChild(tag);
580
+ }
581
+ function CheckIcon() {
582
+ return (0, import_react.createElement)("svg", {
583
+ width: 14,
584
+ height: 14,
585
+ viewBox: "0 0 14 14",
586
+ fill: "none",
587
+ "aria-hidden": "true"
588
+ }, (0, import_react.createElement)("path", {
589
+ d: "M11.5 4.5L6 10L2.5 6.5",
590
+ stroke: "currentColor",
591
+ strokeWidth: 1.5,
592
+ strokeLinecap: "round",
593
+ strokeLinejoin: "round"
594
+ }));
595
+ }
596
+ function CrossIcon() {
597
+ return (0, import_react.createElement)("svg", {
598
+ width: 14,
599
+ height: 14,
600
+ viewBox: "0 0 14 14",
601
+ fill: "none",
602
+ "aria-hidden": "true"
603
+ }, (0, import_react.createElement)("path", {
604
+ d: "M4 4L10 10M10 4L4 10",
605
+ stroke: "currentColor",
606
+ strokeWidth: 1.5,
607
+ strokeLinecap: "round"
608
+ }));
609
+ }
610
+ function RefreshIcon() {
611
+ return (0, import_react.createElement)("svg", {
612
+ width: 14,
613
+ height: 14,
614
+ viewBox: "0 0 14 14",
615
+ fill: "none",
616
+ "aria-hidden": "true"
617
+ }, (0, import_react.createElement)("path", {
618
+ d: "M11.5 2.5V5H9M2.5 7.5C2.5 4.74 4.74 2.5 7.5 2.5C9.1 2.5 10.5 3.2 11.5 4.2M2.5 7.5C2.5 10.26 4.74 12.5 7.5 12.5C9.1 12.5 10.5 11.8 11.5 10.8M2.5 7.5V5H5",
619
+ stroke: "currentColor",
620
+ strokeWidth: 1.2,
621
+ strokeLinecap: "round",
622
+ strokeLinejoin: "round"
623
+ }));
624
+ }
625
+ function PlusIcon() {
626
+ return (0, import_react.createElement)("svg", {
627
+ width: 14,
628
+ height: 14,
629
+ viewBox: "0 0 14 14",
630
+ fill: "none",
631
+ "aria-hidden": "true"
632
+ }, (0, import_react.createElement)("path", {
633
+ d: "M7 2.5V11.5M2.5 7H11.5",
634
+ stroke: "currentColor",
635
+ strokeWidth: 1.5,
636
+ strokeLinecap: "round"
637
+ }));
638
+ }
639
+ function TrashIcon() {
640
+ return (0, import_react.createElement)("svg", {
641
+ width: 14,
642
+ height: 14,
643
+ viewBox: "0 0 14 14",
644
+ fill: "none",
645
+ "aria-hidden": "true"
646
+ }, (0, import_react.createElement)("path", {
647
+ d: "M3 4H11M5.5 4V3C5.5 2.5 5.7 2 6.5 2H7.5C8.3 2 8.5 2.5 8.5 3V4M4 4L4.5 12H9.5L10 4",
648
+ stroke: "currentColor",
649
+ strokeWidth: 1.2,
650
+ strokeLinecap: "round",
651
+ strokeLinejoin: "round"
652
+ }));
653
+ }
654
+ function EditIcon() {
655
+ return (0, import_react.createElement)("svg", {
656
+ width: 14,
657
+ height: 14,
658
+ viewBox: "0 0 14 14",
659
+ fill: "none",
660
+ "aria-hidden": "true"
661
+ }, (0, import_react.createElement)("path", {
662
+ d: "M9.5 2.5L11.5 4.5L5 11H3V9L9.5 2.5Z",
663
+ stroke: "currentColor",
664
+ strokeWidth: 1.2,
665
+ strokeLinecap: "round",
666
+ strokeLinejoin: "round"
667
+ }));
668
+ }
669
+ function extractSchemaFields(jsonSchema) {
670
+ const properties = jsonSchema.properties;
671
+ if (typeof properties !== "object" || properties === null) return [];
672
+ const requiredArr = Array.isArray(jsonSchema.required) ? jsonSchema.required : [];
673
+ const requiredSet = new Set(requiredArr);
674
+ return Object.entries(properties).filter(([, v]) => typeof v === "object" && v !== null).map(([name2, v]) => {
675
+ const prop = v;
676
+ return {
677
+ name: name2,
678
+ type: typeof prop.type === "string" ? prop.type : "string",
679
+ required: requiredSet.has(name2)
680
+ };
681
+ });
682
+ }
683
+ function TierBadge({ status }) {
684
+ if (status.ok) {
685
+ const probe = status.probe;
686
+ const chip = probe === void 0 ? null : (0, import_react.createElement)("span", {
687
+ className: "ops-access-admin-probe " + probe.status,
688
+ title: (probe.detail !== void 0 ? probe.detail + " \xB7 " : "") + "probed " + probe.probedAt
689
+ }, probe.status === "verified" ? "\u5DF2\u6838\u9A8C" : probe.status === "mismatch" ? "\u6838\u9A8C\u5931\u8D25" : "\u65E0\u6CD5\u6838\u9A8C");
690
+ return (0, import_react.createElement)(
691
+ "span",
692
+ { className: "ops-access-admin-tier-cell ops-access-admin-tier-ok" },
693
+ (0, import_react.createElement)(CheckIcon),
694
+ chip
695
+ );
696
+ }
697
+ const title = status.error ?? "not registered";
698
+ return (0, import_react.createElement)("span", {
699
+ className: "ops-access-admin-tier-cell ops-access-admin-tier-err",
700
+ title
701
+ }, (0, import_react.createElement)(CrossIcon));
702
+ }
703
+ function AdminListView(props) {
704
+ const { entries, loading, onRefresh, onAdd, onEdit, onDelete } = props;
705
+ const kindOrder = [];
706
+ const byKind = {};
707
+ for (const e of entries) {
708
+ if (!(e.kind in byKind)) {
709
+ kindOrder.push(e.kind);
710
+ byKind[e.kind] = [];
711
+ }
712
+ byKind[e.kind].push(e);
713
+ }
714
+ return (0, import_react.createElement)(
715
+ "div",
716
+ { className: "ops-access-admin-root" },
717
+ (0, import_react.createElement)(
718
+ "div",
719
+ { className: "ops-access-admin-header" },
720
+ (0, import_react.createElement)("span", { className: "ops-access-admin-title" }, "\u51ED\u8BC1\u7BA1\u7406"),
721
+ (0, import_react.createElement)(
722
+ "div",
723
+ { className: "ops-access-admin-actions" },
724
+ (0, import_react.createElement)("button", {
725
+ type: "button",
726
+ className: "ops-access-admin-btn",
727
+ onClick: onRefresh,
728
+ disabled: loading
729
+ }, (0, import_react.createElement)(RefreshIcon), loading ? "\u52A0\u8F7D\u4E2D\u2026" : "\u5237\u65B0"),
730
+ (0, import_react.createElement)("button", {
731
+ type: "button",
732
+ className: "ops-access-admin-btn ops-access-admin-btn-primary",
733
+ onClick: onAdd
734
+ }, (0, import_react.createElement)(PlusIcon), "\u65B0\u589E")
735
+ )
736
+ ),
737
+ entries.length === 0 ? (0, import_react.createElement)(
738
+ "div",
739
+ { className: "ops-access-admin-empty" },
740
+ loading ? "\u52A0\u8F7D\u4E2D\u2026" : "\u6682\u65E0\u51ED\u8BC1\u6761\u76EE"
741
+ ) : (0, import_react.createElement)(
742
+ "div",
743
+ { className: "ops-access-admin-cards" },
744
+ ...kindOrder.map(
745
+ (kind) => (0, import_react.createElement)(
746
+ "div",
747
+ { key: kind, className: "ops-access-admin-card" },
748
+ (0, import_react.createElement)(
749
+ "div",
750
+ { className: "ops-access-admin-card-header" },
751
+ (0, import_react.createElement)("span", { className: "ops-access-admin-card-title" }, kind),
752
+ (0, import_react.createElement)(
753
+ "span",
754
+ { className: "ops-access-admin-card-count" },
755
+ String(byKind[kind].length) + " \u6761"
756
+ )
757
+ ),
758
+ ...byKind[kind].map(
759
+ (entry) => (0, import_react.createElement)(
760
+ "div",
761
+ { key: entry.name, className: "ops-access-admin-card-row" },
762
+ (0, import_react.createElement)(
763
+ "div",
764
+ { className: "ops-access-admin-card-row-main" },
765
+ (0, import_react.createElement)(
766
+ "div",
767
+ { className: "ops-access-admin-card-row-name" },
768
+ entry.envelope.name ?? entry.name,
769
+ entry.envelope.name && (0, import_react.createElement)("span", { className: "ops-access-admin-card-row-id" }, entry.name)
770
+ ),
771
+ (0, import_react.createElement)(
772
+ "div",
773
+ { className: "ops-access-admin-card-row-meta" },
774
+ entry.environment && (0, import_react.createElement)("span", { className: "ops-access-admin-card-env" }, entry.environment),
775
+ (0, import_react.createElement)(TierBadge, { status: entry.tiers.ro }),
776
+ (0, import_react.createElement)(TierBadge, { status: entry.tiers.rw })
777
+ ),
778
+ entry.envelope.description && (0, import_react.createElement)(
779
+ "div",
780
+ { className: "ops-access-admin-card-row-desc" },
781
+ entry.envelope.description
782
+ )
783
+ ),
784
+ (0, import_react.createElement)(
785
+ "div",
786
+ { className: "ops-access-admin-row-actions" },
787
+ (0, import_react.createElement)("button", {
788
+ type: "button",
789
+ className: "ops-access-admin-btn",
790
+ title: "\u7F16\u8F91",
791
+ onClick: () => onEdit(entry)
792
+ }, (0, import_react.createElement)(EditIcon)),
793
+ (0, import_react.createElement)("button", {
794
+ type: "button",
795
+ className: "ops-access-admin-btn ops-access-admin-btn-danger",
796
+ title: "\u5220\u9664",
797
+ onClick: () => onDelete(entry)
798
+ }, (0, import_react.createElement)(TrashIcon))
799
+ )
800
+ )
801
+ )
802
+ )
803
+ )
804
+ )
805
+ );
806
+ }
807
+ function AdminFormView(props) {
808
+ const { error, editEntry, onSubmit, onSubmitDone, onCancel } = props;
809
+ const isEditing = !!editEntry;
810
+ const [kinds, setKinds] = (0, import_react.useState)([]);
811
+ const [loading, setLoading] = (0, import_react.useState)(true);
812
+ const [selectedKind, setSelectedKind] = (0, import_react.useState)(editEntry?.kind ?? "");
813
+ const [profileName, setProfileName] = (0, import_react.useState)(editEntry?.name ?? "");
814
+ const [displayName, setDisplayName] = (0, import_react.useState)("");
815
+ const [description, setDescription] = (0, import_react.useState)("");
816
+ const [environment, setEnvironment] = (0, import_react.useState)("");
817
+ const [roFields, setRoFields] = (0, import_react.useState)({});
818
+ const [rwFields, setRwFields] = (0, import_react.useState)({});
819
+ const [roFileSet, setRoFileSet] = (0, import_react.useState)({});
820
+ const [rwFileSet, setRwFileSet] = (0, import_react.useState)({});
821
+ const [roEnabled, setRoEnabled] = (0, import_react.useState)(!isEditing);
822
+ const [rwEnabled, setRwEnabled] = (0, import_react.useState)(false);
823
+ const [submitError, setSubmitError] = (0, import_react.useState)(null);
824
+ const [submitting, setSubmitting] = (0, import_react.useState)(false);
825
+ const kindDescriptor = kinds.find((k) => k.kind === selectedKind);
826
+ const schemaFields = kindDescriptor ? extractSchemaFields(kindDescriptor.jsonSchema) : [];
827
+ (0, import_react.useEffect)(() => {
828
+ let cancelled = false;
829
+ setLoading(true);
830
+ fetchKinds().then(async (result) => {
831
+ if (cancelled) return;
832
+ setKinds(result);
833
+ if (editEntry) {
834
+ const [roEntry, rwEntry] = await Promise.all([
835
+ fetchEntry(editEntry.kind, editEntry.name, "ro"),
836
+ fetchEntry(editEntry.kind, editEntry.name, "rw")
837
+ ]);
838
+ if (cancelled) return;
839
+ const toStrings = (src) => {
840
+ const out = {};
841
+ if (src?.fields && typeof src.fields === "object") {
842
+ for (const [k, v] of Object.entries(src.fields)) out[k] = v == null ? "" : String(v);
843
+ }
844
+ return out;
845
+ };
846
+ if (roEntry) {
847
+ setRoFields(toStrings(roEntry));
848
+ setRoFileSet(roEntry.fileFields ?? {});
849
+ setRoEnabled(true);
850
+ }
851
+ if (rwEntry) {
852
+ setRwFields(toStrings(rwEntry));
853
+ setRwFileSet(rwEntry.fileFields ?? {});
854
+ setRwEnabled(true);
855
+ }
856
+ const env = roEntry ?? rwEntry;
857
+ if (env?.displayName) setDisplayName(env.displayName);
858
+ if (env?.description) setDescription(env.description);
859
+ if (env?.environment) setEnvironment(env.environment);
860
+ }
861
+ setLoading(false);
862
+ });
863
+ return () => {
864
+ cancelled = true;
865
+ };
866
+ }, []);
867
+ const handleTierFieldChange = (0, import_react.useCallback)((tier, name2, value) => {
868
+ if (tier === "ro") setRoFields((prev) => ({ ...prev, [name2]: value }));
869
+ else setRwFields((prev) => ({ ...prev, [name2]: value }));
870
+ }, []);
871
+ const handleSubmit = (0, import_react.useCallback)(async () => {
872
+ setSubmitError(null);
873
+ if (!selectedKind) {
874
+ setSubmitError("\u8BF7\u9009\u62E9\u51ED\u8BC1\u7C7B\u578B");
875
+ return;
876
+ }
877
+ if (!profileName.trim()) {
878
+ setSubmitError("\u8BF7\u8F93\u5165 ID");
879
+ return;
880
+ }
881
+ const activeTiers = ["ro", "rw"].filter((t) => t === "ro" ? roEnabled : rwEnabled);
882
+ if (activeTiers.length === 0) {
883
+ setSubmitError("\u8BF7\u81F3\u5C11\u542F\u7528\u4E00\u4E2A\u6863\u4F4D (ro / rw)");
884
+ return;
885
+ }
886
+ setSubmitting(true);
887
+ const fileFieldSet = new Set(kindDescriptor?.fileFields ?? []);
888
+ const envelope = {
889
+ displayName: displayName.trim(),
890
+ description: description.trim(),
891
+ environment: environment.trim()
892
+ };
893
+ try {
894
+ for (const t of activeTiers) {
895
+ const tierValues = t === "ro" ? roFields : rwFields;
896
+ if (!isEditing && Object.values(tierValues).every((v) => !String(v ?? "").trim())) continue;
897
+ const baseFields = {};
898
+ const contentFiles = {};
899
+ for (const [k, v] of Object.entries(tierValues)) {
900
+ if (fileFieldSet.has(k)) {
901
+ const content = String(v ?? "");
902
+ if (content.trim() !== "") contentFiles[k] = content;
903
+ } else baseFields[k] = v;
904
+ }
905
+ for (const f of schemaFields) {
906
+ if (f.type === "number" && baseFields[f.name] != null && baseFields[f.name] !== "") {
907
+ baseFields[f.name] = Number(baseFields[f.name]);
908
+ }
909
+ }
910
+ const result = await onSubmit({
911
+ kind: selectedKind,
912
+ name: profileName.trim(),
913
+ tier: t,
914
+ fields: baseFields,
915
+ ...Object.keys(contentFiles).length > 0 ? { contentFiles } : {},
916
+ ...envelope
917
+ });
918
+ if (result && !result.ok) {
919
+ setSubmitting(false);
920
+ setSubmitError(t + ": " + (result.error ?? "\u63D0\u4EA4\u5931\u8D25"));
921
+ return;
922
+ }
923
+ }
924
+ } catch {
925
+ setSubmitting(false);
926
+ setSubmitError("\u63D0\u4EA4\u5931\u8D25");
927
+ return;
928
+ }
929
+ onSubmitDone();
930
+ }, [selectedKind, profileName, displayName, roEnabled, rwEnabled, roFields, rwFields, description, environment, onSubmit, onSubmitDone, schemaFields, kindDescriptor]);
931
+ if (loading) {
932
+ return (0, import_react.createElement)(
933
+ "div",
934
+ { className: "ops-access-admin-root" },
935
+ (0, import_react.createElement)("div", { className: "ops-access-admin-empty" }, isEditing ? "\u52A0\u8F7D\u51ED\u8BC1\u2026" : "\u52A0\u8F7D\u51ED\u8BC1\u7C7B\u578B\u2026")
936
+ );
937
+ }
938
+ if (kinds.length === 0) {
939
+ return (0, import_react.createElement)(
940
+ "div",
941
+ { className: "ops-access-admin-root" },
942
+ (0, import_react.createElement)(
943
+ "div",
944
+ { className: "ops-access-admin-empty" },
945
+ "\u6CA1\u6709\u5DF2\u6CE8\u518C\u7684\u51ED\u8BC1\u7C7B\u578B\u3002\u8BF7\u5148\u5B89\u88C5\u51ED\u8BC1 provider \u63D2\u4EF6\u3002"
946
+ ),
947
+ (0, import_react.createElement)("button", {
948
+ type: "button",
949
+ className: "ops-access-admin-btn",
950
+ onClick: onCancel
951
+ }, "\u8FD4\u56DE")
952
+ );
953
+ }
954
+ const displayError = submitError ?? error;
955
+ const renderTierFields = (tier, values, fileSet) => schemaFields.map((f) => {
956
+ const isContent = kindDescriptor?.fileFields?.includes(f.name);
957
+ return (0, import_react.createElement)(
958
+ "div",
959
+ { key: tier + ":" + f.name, className: "ops-access-admin-field" },
960
+ (0, import_react.createElement)(
961
+ "label",
962
+ { className: "ops-access-admin-label" },
963
+ f.name,
964
+ f.required && (0, import_react.createElement)("span", { className: "ops-access-admin-required" }, "*"),
965
+ (0, import_react.createElement)(
966
+ "span",
967
+ { className: "ops-access-admin-type-hint" },
968
+ isContent ? "\u5185\u5BB9" : f.type
969
+ )
970
+ ),
971
+ isContent ? (0, import_react.createElement)("textarea", {
972
+ className: "ops-access-admin-input ops-access-admin-textarea",
973
+ rows: 8,
974
+ // Saved credential content is never read back: the textarea
975
+ // stays empty and says so; pasting new content overwrites.
976
+ placeholder: isEditing ? fileSet[f.name] ? "\u5DF2\u4FDD\u5B58 \xB7 \u5185\u5BB9\u4E0D\u53EF\u56DE\u8BFB \u2014 \u7C98\u8D34\u65B0\u5185\u5BB9\u4EE5\u8986\u76D6" : "\u672A\u8BBE\u7F6E \u2014 \u7C98\u8D34\u5185\u5BB9\u4EE5\u6DFB\u52A0" : "\u7C98\u8D34\u5B8C\u6574\u5185\u5BB9\u2026",
977
+ value: values[f.name] ?? "",
978
+ onChange: (e) => handleTierFieldChange(tier, f.name, e.target.value)
979
+ }) : (0, import_react.createElement)("input", {
980
+ className: "ops-access-admin-input",
981
+ type: f.type === "number" ? "number" : "text",
982
+ value: values[f.name] ?? "",
983
+ onChange: (e) => handleTierFieldChange(tier, f.name, e.target.value)
984
+ })
985
+ );
986
+ });
987
+ return (0, import_react.createElement)(
988
+ "div",
989
+ { className: "ops-access-admin-root" },
990
+ (0, import_react.createElement)(
991
+ "div",
992
+ { className: "ops-access-admin-header" },
993
+ (0, import_react.createElement)("span", { className: "ops-access-admin-title" }, isEditing ? "\u7F16\u8F91\u51ED\u8BC1" : "\u65B0\u589E\u51ED\u8BC1"),
994
+ (0, import_react.createElement)("button", {
995
+ type: "button",
996
+ className: "ops-access-admin-btn",
997
+ onClick: onCancel
998
+ }, "\u8FD4\u56DE")
999
+ ),
1000
+ displayError && (0, import_react.createElement)("div", { className: "ops-access-admin-error" }, displayError),
1001
+ (0, import_react.createElement)(
1002
+ "div",
1003
+ { className: "ops-access-admin-form" },
1004
+ // ── Common info (shared by both tiers) ──
1005
+ (0, import_react.createElement)(
1006
+ "div",
1007
+ { className: "ops-access-admin-field" },
1008
+ (0, import_react.createElement)(
1009
+ "label",
1010
+ { className: "ops-access-admin-label" },
1011
+ "\u51ED\u8BC1\u7C7B\u578B",
1012
+ (0, import_react.createElement)("span", { className: "ops-access-admin-required" }, "*")
1013
+ ),
1014
+ (0, import_react.createElement)(
1015
+ "select",
1016
+ {
1017
+ className: "ops-access-admin-input",
1018
+ value: selectedKind,
1019
+ disabled: isEditing,
1020
+ onChange: (e) => {
1021
+ setSelectedKind(e.target.value);
1022
+ setRoFields({});
1023
+ setRwFields({});
1024
+ }
1025
+ },
1026
+ (0, import_react.createElement)("option", { value: "" }, "\u2014 \u9009\u62E9 \u2014"),
1027
+ ...kinds.map(
1028
+ (k) => (0, import_react.createElement)("option", { key: k.kind, value: k.kind }, k.kind)
1029
+ )
1030
+ )
1031
+ ),
1032
+ (0, import_react.createElement)(
1033
+ "div",
1034
+ { className: "ops-access-admin-field" },
1035
+ (0, import_react.createElement)(
1036
+ "label",
1037
+ { className: "ops-access-admin-label" },
1038
+ "ID",
1039
+ (0, import_react.createElement)("span", { className: "ops-access-admin-required" }, "*"),
1040
+ (0, import_react.createElement)("span", { className: "ops-access-admin-type-hint" }, "\u5B57\u6BCD/\u6570\u5B57\u5F00\u5934\uFF0C\u53EF\u542B . _ - @\uFF1B\u521B\u5EFA\u540E\u4E0D\u53EF\u6539")
1041
+ ),
1042
+ (0, import_react.createElement)("input", {
1043
+ className: "ops-access-admin-input",
1044
+ type: "text",
1045
+ value: profileName,
1046
+ disabled: isEditing,
1047
+ onChange: (e) => setProfileName(e.target.value)
1048
+ })
1049
+ ),
1050
+ (0, import_react.createElement)(
1051
+ "div",
1052
+ { className: "ops-access-admin-field" },
1053
+ (0, import_react.createElement)(
1054
+ "label",
1055
+ { className: "ops-access-admin-label" },
1056
+ "\u540D\u79F0 (\u53EF\u9009)",
1057
+ (0, import_react.createElement)("span", { className: "ops-access-admin-type-hint" }, "\u663E\u793A\u7528\uFF0C\u968F\u65F6\u53EF\u6539")
1058
+ ),
1059
+ (0, import_react.createElement)("input", {
1060
+ className: "ops-access-admin-input",
1061
+ type: "text",
1062
+ value: displayName,
1063
+ onChange: (e) => setDisplayName(e.target.value)
1064
+ })
1065
+ ),
1066
+ (0, import_react.createElement)(
1067
+ "div",
1068
+ { className: "ops-access-admin-field" },
1069
+ (0, import_react.createElement)("label", { className: "ops-access-admin-label" }, "\u63CF\u8FF0 (\u53EF\u9009)"),
1070
+ (0, import_react.createElement)("input", {
1071
+ className: "ops-access-admin-input",
1072
+ type: "text",
1073
+ value: description,
1074
+ onChange: (e) => setDescription(e.target.value)
1075
+ })
1076
+ ),
1077
+ (0, import_react.createElement)(
1078
+ "div",
1079
+ { className: "ops-access-admin-field" },
1080
+ (0, import_react.createElement)("label", { className: "ops-access-admin-label" }, "\u73AF\u5883 (\u53EF\u9009)"),
1081
+ (0, import_react.createElement)("input", {
1082
+ className: "ops-access-admin-input",
1083
+ type: "text",
1084
+ value: environment,
1085
+ onChange: (e) => setEnvironment(e.target.value)
1086
+ })
1087
+ ),
1088
+ // ── ro tier ──
1089
+ schemaFields.length > 0 && (0, import_react.createElement)(
1090
+ "div",
1091
+ { className: "ops-access-admin-tier-section" },
1092
+ (0, import_react.createElement)(
1093
+ "label",
1094
+ { className: "ops-access-admin-radio-label" },
1095
+ (0, import_react.createElement)("input", {
1096
+ type: "checkbox",
1097
+ checked: roEnabled,
1098
+ onChange: (e) => setRoEnabled(e.target.checked)
1099
+ }),
1100
+ "ro (\u53EA\u8BFB)"
1101
+ ),
1102
+ roEnabled && (0, import_react.createElement)(
1103
+ "div",
1104
+ { className: "ops-access-admin-tier-fields" },
1105
+ ...renderTierFields("ro", roFields, roFileSet)
1106
+ )
1107
+ ),
1108
+ // ── rw tier ──
1109
+ schemaFields.length > 0 && (0, import_react.createElement)(
1110
+ "div",
1111
+ { className: "ops-access-admin-tier-section" },
1112
+ (0, import_react.createElement)(
1113
+ "label",
1114
+ { className: "ops-access-admin-radio-label" },
1115
+ (0, import_react.createElement)("input", {
1116
+ type: "checkbox",
1117
+ checked: rwEnabled,
1118
+ onChange: (e) => setRwEnabled(e.target.checked)
1119
+ }),
1120
+ "rw (\u8BFB\u5199)"
1121
+ ),
1122
+ rwEnabled && (0, import_react.createElement)(
1123
+ "div",
1124
+ { className: "ops-access-admin-tier-fields" },
1125
+ ...renderTierFields("rw", rwFields, rwFileSet)
1126
+ )
1127
+ ),
1128
+ // Submit
1129
+ (0, import_react.createElement)(
1130
+ "div",
1131
+ { className: "ops-access-admin-actions" },
1132
+ (0, import_react.createElement)("button", {
1133
+ type: "button",
1134
+ className: "ops-access-admin-btn ops-access-admin-btn-primary",
1135
+ onClick: handleSubmit,
1136
+ disabled: submitting
1137
+ }, submitting ? "\u63D0\u4EA4\u4E2D\u2026" : "\u63D0\u4EA4"),
1138
+ (0, import_react.createElement)("button", {
1139
+ type: "button",
1140
+ className: "ops-access-admin-btn",
1141
+ onClick: onCancel
1142
+ }, "\u53D6\u6D88")
1143
+ )
1144
+ )
1145
+ );
1146
+ }
1147
+ function AdminSection(_props) {
1148
+ const [view, setView] = (0, import_react.useState)("list");
1149
+ const [entries, setEntries] = (0, import_react.useState)([]);
1150
+ const [listLoading, setListLoading] = (0, import_react.useState)(false);
1151
+ const [formError, setFormError] = (0, import_react.useState)(null);
1152
+ const [editEntry, setEditEntry] = (0, import_react.useState)(null);
1153
+ const [deleteTarget, setDeleteTarget] = (0, import_react.useState)(null);
1154
+ const [deleting, setDeleting] = (0, import_react.useState)(false);
1155
+ const refreshList = (0, import_react.useCallback)(async () => {
1156
+ setListLoading(true);
1157
+ const result = await fetchAdminList();
1158
+ setEntries(result);
1159
+ setListLoading(false);
1160
+ }, []);
1161
+ (0, import_react.useEffect)(() => {
1162
+ refreshList();
1163
+ }, [refreshList]);
1164
+ const handleAdd = (0, import_react.useCallback)(() => {
1165
+ setEditEntry(null);
1166
+ setView("form");
1167
+ setFormError(null);
1168
+ }, []);
1169
+ const handleEdit = (0, import_react.useCallback)((entry) => {
1170
+ setEditEntry({ kind: entry.kind, name: entry.name });
1171
+ setView("form");
1172
+ setFormError(null);
1173
+ }, []);
1174
+ const handleSubmit = (0, import_react.useCallback)(async (body) => {
1175
+ return await submitEntry(body);
1176
+ }, []);
1177
+ const handleSubmitDone = (0, import_react.useCallback)(() => {
1178
+ setView("list");
1179
+ refreshList();
1180
+ }, [refreshList]);
1181
+ const handleDeleteConfirm = (0, import_react.useCallback)(async () => {
1182
+ if (!deleteTarget) return;
1183
+ setDeleting(true);
1184
+ await Promise.all([
1185
+ deleteEntry(deleteTarget.kind, deleteTarget.name, "ro"),
1186
+ deleteEntry(deleteTarget.kind, deleteTarget.name, "rw")
1187
+ ]);
1188
+ setDeleting(false);
1189
+ setDeleteTarget(null);
1190
+ refreshList();
1191
+ }, [deleteTarget, refreshList]);
1192
+ if (deleteTarget) {
1193
+ return (0, import_react.createElement)(
1194
+ "div",
1195
+ { className: "ops-access-admin-root" },
1196
+ (0, import_react.createElement)(
1197
+ "div",
1198
+ { className: "ops-access-admin-confirm" },
1199
+ (0, import_react.createElement)("div", null, `\u786E\u8BA4\u5220\u9664 ${deleteTarget.kind}/${deleteTarget.name}\uFF1F`),
1200
+ (0, import_react.createElement)(
1201
+ "div",
1202
+ { className: "ops-access-admin-actions" },
1203
+ (0, import_react.createElement)("button", {
1204
+ type: "button",
1205
+ className: "ops-access-admin-btn ops-access-admin-btn-danger",
1206
+ onClick: handleDeleteConfirm,
1207
+ disabled: deleting
1208
+ }, deleting ? "\u5220\u9664\u4E2D\u2026" : "\u786E\u8BA4\u5220\u9664"),
1209
+ (0, import_react.createElement)("button", {
1210
+ type: "button",
1211
+ className: "ops-access-admin-btn",
1212
+ onClick: () => setDeleteTarget(null),
1213
+ disabled: deleting
1214
+ }, "\u53D6\u6D88")
1215
+ )
1216
+ )
1217
+ );
1218
+ }
1219
+ if (view === "form") {
1220
+ return (0, import_react.createElement)(AdminFormView, {
1221
+ error: formError,
1222
+ editEntry: editEntry ?? void 0,
1223
+ onSubmit: handleSubmit,
1224
+ onSubmitDone: handleSubmitDone,
1225
+ onCancel: () => setView("list")
1226
+ });
1227
+ }
1228
+ return (0, import_react.createElement)(AdminListView, {
1229
+ entries,
1230
+ loading: listLoading,
1231
+ onRefresh: refreshList,
1232
+ onAdd: handleAdd,
1233
+ onEdit: handleEdit,
1234
+ onDelete: (entry) => setDeleteTarget(entry)
1235
+ });
1236
+ }
1237
+ function liveGrantFor(grants, kind, name2) {
1238
+ return grants.find((g) => g.kind === kind && g.name === name2);
1239
+ }
1240
+ function defaultTtlChoice(options, requested) {
1241
+ const sorted = [...options].sort((a, b) => a - b);
1242
+ const within = sorted.filter((o) => o <= requested);
1243
+ return within.length > 0 ? within[within.length - 1] : sorted[0];
1244
+ }
1245
+ function AccessPanel({ sessionId }) {
1246
+ const [grants, setGrants] = (0, import_react.useState)([]);
1247
+ const [denied, setDenied] = (0, import_react.useState)([]);
1248
+ const [ttlOptions, setTtlOptions] = (0, import_react.useState)([5, 10, 30]);
1249
+ const [requests, setRequests] = (0, import_react.useState)([]);
1250
+ const [candidates, setCandidates] = (0, import_react.useState)([]);
1251
+ const [grantTarget, setGrantTarget] = (0, import_react.useState)(null);
1252
+ const [confirmKey, setConfirmKey] = (0, import_react.useState)(null);
1253
+ const [ttlChoice, setTtlChoice] = (0, import_react.useState)({});
1254
+ const [message, setMessage] = (0, import_react.useState)(null);
1255
+ const refresh = (0, import_react.useCallback)(async () => {
1256
+ const g = await fetchPanelGrants(sessionId);
1257
+ if (g !== null) {
1258
+ setGrants(g.grants);
1259
+ setDenied(g.denied);
1260
+ if (Array.isArray(g.ttlOptions) && g.ttlOptions.length > 0) setTtlOptions(g.ttlOptions);
1261
+ }
1262
+ const r = await fetchPanelRequests(sessionId);
1263
+ if (r !== null) setRequests(r.requests);
1264
+ }, [sessionId]);
1265
+ (0, import_react.useEffect)(() => {
1266
+ void refresh();
1267
+ const timer = setInterval(() => {
1268
+ void refresh();
1269
+ }, 3e3);
1270
+ return () => clearInterval(timer);
1271
+ }, [refresh]);
1272
+ (0, import_react.useEffect)(() => {
1273
+ let cancelled = false;
1274
+ fetch("/ops-access/list?query=").then(async (res) => {
1275
+ if (res.ok && !cancelled) setCandidates(await res.json());
1276
+ }).catch(() => {
1277
+ });
1278
+ return () => {
1279
+ cancelled = true;
1280
+ };
1281
+ }, []);
1282
+ async function run(action, okText) {
1283
+ const res = await action();
1284
+ setMessage(res.ok ? okText : res.error ?? "\u64CD\u4F5C\u5931\u8D25");
1285
+ setConfirmKey(null);
1286
+ setGrantTarget(null);
1287
+ await refresh();
1288
+ }
1289
+ function confirmStrip(key, text, onConfirm) {
1290
+ if (confirmKey !== key) return null;
1291
+ return (0, import_react.createElement)(
1292
+ "div",
1293
+ { className: "ops-access-panel-confirm" },
1294
+ (0, import_react.createElement)("span", null, text),
1295
+ (0, import_react.createElement)("button", { className: "ops-access-panel-btn danger", onClick: onConfirm }, "\u786E\u8BA4"),
1296
+ (0, import_react.createElement)("button", { className: "ops-access-panel-btn", onClick: () => setConfirmKey(null) }, "\u53D6\u6D88")
1297
+ );
1298
+ }
1299
+ function ttlRow(selected, onPick) {
1300
+ return (0, import_react.createElement)(
1301
+ "span",
1302
+ { className: "ops-access-panel-ttl-row" },
1303
+ ttlOptions.map(
1304
+ (ttl) => (0, import_react.createElement)("button", {
1305
+ key: ttl,
1306
+ className: "ops-access-panel-btn ttl" + (ttl === selected ? " selected" : ""),
1307
+ onClick: () => onPick(ttl)
1308
+ }, ttl + " \u5206\u949F")
1309
+ )
1310
+ );
1311
+ }
1312
+ return (0, import_react.createElement)(
1313
+ "div",
1314
+ { className: "ops-access-panel" },
1315
+ message !== null && (0, import_react.createElement)("div", { className: "ops-access-panel-message", onClick: () => setMessage(null) }, message),
1316
+ // ── Pending requests (approval mode) ──
1317
+ requests.length > 0 && (0, import_react.createElement)(
1318
+ "div",
1319
+ { className: "ops-access-panel-section" },
1320
+ (0, import_react.createElement)("div", { className: "ops-access-panel-heading" }, "\u5F85\u5BA1\u6279\u7684\u7533\u8BF7"),
1321
+ requests.map((req) => {
1322
+ const chosen = ttlChoice[req.id] ?? defaultTtlChoice(ttlOptions, req.requestedTtlMinutes);
1323
+ return (0, import_react.createElement)(
1324
+ "div",
1325
+ { key: req.id, className: "ops-access-panel-request" },
1326
+ (0, import_react.createElement)(
1327
+ "div",
1328
+ { className: "ops-access-panel-row-title" },
1329
+ req.kind + "/" + req.name,
1330
+ (0, import_react.createElement)("span", { className: "ops-access-panel-badge" }, "\u7533\u8BF7 " + req.requestedTtlMinutes + " \u5206\u949F"),
1331
+ req.parentSession !== void 0 && (0, import_react.createElement)("span", { className: "ops-access-panel-badge" }, "\u5B50\u4F1A\u8BDD\u7533\u8BF7 \xB7 \u7236\u4F1A\u8BDD " + req.parentSession)
1332
+ ),
1333
+ (0, import_react.createElement)("div", { className: "ops-access-panel-reason" }, req.reason),
1334
+ (0, import_react.createElement)(
1335
+ "div",
1336
+ { className: "ops-access-panel-actions" },
1337
+ ttlRow(chosen, (ttl) => setTtlChoice({ ...ttlChoice, [req.id]: ttl })),
1338
+ (0, import_react.createElement)("button", {
1339
+ className: "ops-access-panel-btn primary",
1340
+ onClick: () => void run(() => postPanelDecide(req.id, true, chosen), "\u5DF2\u6279\u51C6 " + req.kind + "/" + req.name + "\uFF08" + chosen + " \u5206\u949F\uFF09")
1341
+ }, "\u6279\u51C6"),
1342
+ (0, import_react.createElement)("button", {
1343
+ className: "ops-access-panel-btn",
1344
+ onClick: () => void run(() => postPanelDecide(req.id, false), "\u5DF2\u62D2\u7EDD " + req.kind + "/" + req.name)
1345
+ }, "\u62D2\u7EDD")
1346
+ )
1347
+ );
1348
+ })
1349
+ ),
1350
+ // ── Live grants (revoke) ──
1351
+ (0, import_react.createElement)(
1352
+ "div",
1353
+ { className: "ops-access-panel-section" },
1354
+ (0, import_react.createElement)("div", { className: "ops-access-panel-heading" }, "\u672C\u4F1A\u8BDD\u6D3B\u8DC3\u6388\u6743"),
1355
+ grants.length === 0 && (0, import_react.createElement)("div", { className: "ops-access-panel-empty" }, "\u65E0\u6D3B\u8DC3\u6388\u6743"),
1356
+ grants.map((g) => {
1357
+ const key = g.kind + "/" + g.name;
1358
+ return (0, import_react.createElement)(
1359
+ "div",
1360
+ { key, className: "ops-access-panel-grant" },
1361
+ (0, import_react.createElement)(
1362
+ "div",
1363
+ {
1364
+ className: "ops-access-panel-row clickable",
1365
+ onClick: () => setConfirmKey(confirmKey === "row:" + key ? null : "row:" + key)
1366
+ },
1367
+ (0, import_react.createElement)("span", { className: "ops-access-panel-row-title" }, key),
1368
+ (0, import_react.createElement)("span", { className: "ops-access-panel-row-detail" }, "\u5269 " + g.remainingMinutes + " \u5206\u949F \xB7 " + g.approvedBy)
1369
+ ),
1370
+ // The row expands into extend (per-TTL confirm, same two-click
1371
+ // discipline as the grant flow) and revoke. Renewal is from NOW —
1372
+ // repeated extends never accumulate past one TTL tier.
1373
+ confirmKey === "row:" + key && (0, import_react.createElement)(
1374
+ "div",
1375
+ { className: "ops-access-panel-actions" },
1376
+ ttlRow(0, (ttl) => setConfirmKey("extend:" + key + ":" + ttl)),
1377
+ (0, import_react.createElement)("button", {
1378
+ className: "ops-access-panel-btn",
1379
+ onClick: () => setConfirmKey("revoke:" + key)
1380
+ }, "\u6536\u56DE\u2026")
1381
+ ),
1382
+ ttlOptions.map((ttl) => confirmStrip("extend:" + key + ":" + ttl, "\u5EF6\u957F " + key + " \xB7 \u4ECE\u73B0\u5728\u8D77 " + ttl + " \u5206\u949F\uFF1F", () => void run(() => postPanelExtend(sessionId, g.kind, g.name, ttl), "\u5DF2\u5EF6\u957F " + key + "\uFF08" + ttl + " \u5206\u949F\uFF09"))),
1383
+ confirmStrip("revoke:" + key, "\u6536\u56DE " + key + " \u7684\u63D0\u6743\u6743\u9650\uFF1F", () => void run(() => postPanelRevoke(sessionId, g.kind, g.name), "\u5DF2\u6536\u56DE " + key))
1384
+ );
1385
+ }),
1386
+ grants.length > 1 && (0, import_react.createElement)(
1387
+ "div",
1388
+ null,
1389
+ (0, import_react.createElement)("div", {
1390
+ className: "ops-access-panel-row clickable danger-zone",
1391
+ onClick: () => setConfirmKey(confirmKey === "revoke-all" ? null : "revoke-all")
1392
+ }, (0, import_react.createElement)("span", { className: "ops-access-panel-row-title" }, "\u6536\u56DE\u672C\u4F1A\u8BDD\u5168\u90E8\u6388\u6743")),
1393
+ confirmStrip("revoke-all", "\u6536\u56DE\u5168\u90E8 " + grants.length + " \u9879\u6388\u6743\uFF1F", () => void run(() => postPanelRevokeAll(sessionId), "\u5DF2\u6536\u56DE\u5168\u90E8\u6388\u6743"))
1394
+ )
1395
+ ),
1396
+ // ── Grant flow (pick profile → TTL → confirm) ──
1397
+ (0, import_react.createElement)(
1398
+ "div",
1399
+ { className: "ops-access-panel-section" },
1400
+ (0, import_react.createElement)("div", { className: "ops-access-panel-heading" }, "\u6388\u4E88\u63D0\u6743\u8BBF\u95EE"),
1401
+ candidates.length === 0 && (0, import_react.createElement)("div", { className: "ops-access-panel-empty" }, "\u65E0\u53EF\u6388\u6743\u6863\u6848"),
1402
+ candidates.map((c) => {
1403
+ const key = c.kind + "/" + c.name;
1404
+ const open = grantTarget === key;
1405
+ const live = liveGrantFor(grants, c.kind, c.name);
1406
+ const locked = denied.find((d) => d.kind === c.kind && d.name === c.name);
1407
+ return (0, import_react.createElement)(
1408
+ "div",
1409
+ { key, className: "ops-access-panel-candidate" },
1410
+ (0, import_react.createElement)(
1411
+ "div",
1412
+ {
1413
+ className: "ops-access-panel-row clickable",
1414
+ onClick: () => {
1415
+ setGrantTarget(open ? null : key);
1416
+ setConfirmKey(null);
1417
+ }
1418
+ },
1419
+ (0, import_react.createElement)("span", { className: "ops-access-panel-row-title" }, key),
1420
+ (0, import_react.createElement)(
1421
+ "span",
1422
+ { className: "ops-access-panel-row-detail" },
1423
+ // Access-state marker on the right: lit green = this session
1424
+ // holds an rw grant for the profile right now; no dot = plain
1425
+ // ro. (A red 'deny' state is reserved for the future lockdown.)
1426
+ locked !== void 0 ? (0, import_react.createElement)("span", { className: "ops-access-panel-dot deny", title: "\u5DF2\u5C01\u7981 \xB7 " + locked.reason }) : live !== void 0 && (0, import_react.createElement)("span", {
1427
+ className: "ops-access-panel-dot rw",
1428
+ title: "\u672C\u4F1A\u8BDD\u5DF2\u6388\u6743 rw \xB7 \u5269 " + live.remainingMinutes + " \u5206\u949F"
1429
+ }),
1430
+ locked !== void 0 ? "\u5DF2\u5C01\u7981 \xB7 " + locked.reason : live !== void 0 ? "\u5DF2\u6388\u6743 \xB7 \u5269 " + live.remainingMinutes + " \u5206\u949F" : c.rw ? "rw \u5C31\u7EEA" : c.ro ? "\u9650\u65F6\u4F7F\u7528" : "\u672A\u914D\u7F6E",
1431
+ c.environment ? " \xB7 " + c.environment : ""
1432
+ )
1433
+ ),
1434
+ open && (0, import_react.createElement)(
1435
+ "div",
1436
+ { className: "ops-access-panel-actions" },
1437
+ locked !== void 0 ? (0, import_react.createElement)("button", { className: "ops-access-panel-btn", onClick: () => setConfirmKey("undeny:" + key) }, "\u89E3\u5C01\u2026") : ttlRow(0, (ttl) => setConfirmKey("grant:" + key + ":" + ttl)),
1438
+ locked === void 0 && (0, import_react.createElement)("button", {
1439
+ className: "ops-access-panel-btn",
1440
+ onClick: () => setConfirmKey("deny:" + key)
1441
+ }, "\u5C01\u7981\u2026")
1442
+ ),
1443
+ open && locked === void 0 && ttlOptions.map((ttl) => confirmStrip("grant:" + key + ":" + ttl, "\u6388\u4E88 " + key + " \xB7 " + ttl + " \u5206\u949F\uFF1F", () => void run(() => postPanelGrant(sessionId, c.kind, c.name, ttl), "\u5DF2\u6388\u4E88 " + key + "\uFF08" + ttl + " \u5206\u949F\uFF09"))),
1444
+ confirmStrip("deny:" + key, "\u5C01\u7981 " + key + "\uFF1F\u8FDE\u53EA\u8BFB\u8BBF\u95EE\u4E5F\u4F1A\u9501\u5B9A\uFF0C\u5176\u6240\u6709\u4F1A\u8BDD\u7684\u6388\u6743\u7ACB\u5373\u6536\u56DE", () => void run(() => postPanelDeny(c.kind, c.name), "\u5DF2\u5C01\u7981 " + key + "\uFF08\u8FDE\u53EA\u8BFB\u4E5F\u9501\u5B9A\uFF09")),
1445
+ confirmStrip("undeny:" + key, "\u89E3\u5C01 " + key + "\uFF1F\u6062\u590D\u6B63\u5E38\u7684 ro/rw \u6388\u6743\u6D41\u7A0B", () => void run(() => postPanelUndeny(c.kind, c.name), "\u5DF2\u89E3\u5C01 " + key))
1446
+ );
1447
+ })
1448
+ )
1449
+ );
1450
+ }
1451
+ async function fetchPanelOverview(signal) {
1452
+ try {
1453
+ const res = await fetch("/ops-access/grants/all", { signal });
1454
+ if (!res.ok) return null;
1455
+ const data = await res.json();
1456
+ return {
1457
+ grants: Array.isArray(data.grants) ? data.grants : [],
1458
+ requests: Array.isArray(data.requests) ? data.requests : [],
1459
+ denied: Array.isArray(data.denied) ? data.denied : [],
1460
+ ttlOptions: Array.isArray(data.ttlOptions) && data.ttlOptions.length > 0 ? data.ttlOptions : [5, 10, 30]
1461
+ };
1462
+ } catch {
1463
+ return null;
1464
+ }
1465
+ }
1466
+ function groupBySession(grants) {
1467
+ const map = /* @__PURE__ */ new Map();
1468
+ for (const g of grants) {
1469
+ const list = map.get(g.session);
1470
+ if (list) list.push(g);
1471
+ else map.set(g.session, [g]);
1472
+ }
1473
+ return [...map.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([session, gs]) => ({ session, grants: gs }));
1474
+ }
1475
+ function AccessOverviewPanel(_props) {
1476
+ const [grants, setGrants] = (0, import_react.useState)([]);
1477
+ const [requests, setRequests] = (0, import_react.useState)([]);
1478
+ const [denied, setDenied] = (0, import_react.useState)([]);
1479
+ const [ttlOptions, setTtlOptions] = (0, import_react.useState)([5, 10, 30]);
1480
+ const [ttlChoice, setTtlChoice] = (0, import_react.useState)({});
1481
+ const [confirmKey, setConfirmKey] = (0, import_react.useState)(null);
1482
+ const [message, setMessage] = (0, import_react.useState)(null);
1483
+ const refresh = (0, import_react.useCallback)(async () => {
1484
+ const data = await fetchPanelOverview();
1485
+ if (data !== null) {
1486
+ setGrants(data.grants);
1487
+ setRequests(data.requests);
1488
+ setDenied(data.denied);
1489
+ setTtlOptions(data.ttlOptions);
1490
+ }
1491
+ }, []);
1492
+ (0, import_react.useEffect)(() => {
1493
+ void refresh();
1494
+ const timer = setInterval(() => {
1495
+ void refresh();
1496
+ }, 3e3);
1497
+ return () => clearInterval(timer);
1498
+ }, [refresh]);
1499
+ async function run(action, okText) {
1500
+ const res = await action();
1501
+ setMessage(res.ok ? okText : res.error ?? "\u64CD\u4F5C\u5931\u8D25");
1502
+ setConfirmKey(null);
1503
+ await refresh();
1504
+ }
1505
+ function confirmStrip(key, text, onConfirm) {
1506
+ if (confirmKey !== key) return null;
1507
+ return (0, import_react.createElement)(
1508
+ "div",
1509
+ { className: "ops-access-panel-confirm" },
1510
+ (0, import_react.createElement)("span", null, text),
1511
+ (0, import_react.createElement)("button", { className: "ops-access-panel-btn danger", onClick: onConfirm }, "\u786E\u8BA4"),
1512
+ (0, import_react.createElement)("button", { className: "ops-access-panel-btn", onClick: () => setConfirmKey(null) }, "\u53D6\u6D88")
1513
+ );
1514
+ }
1515
+ const groups = groupBySession(grants);
1516
+ return (0, import_react.createElement)(
1517
+ "div",
1518
+ null,
1519
+ message !== null && (0, import_react.createElement)("div", { className: "ops-access-panel-message" }, message),
1520
+ (0, import_react.createElement)(
1521
+ "div",
1522
+ { className: "ops-access-panel-section" },
1523
+ (0, import_react.createElement)("div", { className: "ops-access-panel-heading" }, "\u6D3B\u8DC3\u6388\u6743\uFF08\u5168\u90E8\u4F1A\u8BDD\uFF09"),
1524
+ groups.length === 0 && (0, import_react.createElement)("div", { className: "ops-access-panel-empty" }, "\u5F53\u524D\u65E0\u4EFB\u4F55\u4F1A\u8BDD\u6301\u6709\u63D0\u6743\u6388\u6743"),
1525
+ groups.map((group) => (0, import_react.createElement)(
1526
+ "div",
1527
+ { key: group.session, className: "ops-access-panel-candidate" },
1528
+ (0, import_react.createElement)(
1529
+ "div",
1530
+ { className: "ops-access-panel-row" },
1531
+ (0, import_react.createElement)("span", { className: "ops-access-panel-row-title" }, group.session),
1532
+ (0, import_react.createElement)("span", { className: "ops-access-panel-row-detail" }, group.grants.length + " \u9879\u6388\u6743")
1533
+ ),
1534
+ group.grants.map((g) => {
1535
+ const key = g.session + ":" + g.kind + "/" + g.name;
1536
+ return (0, import_react.createElement)(
1537
+ "div",
1538
+ { key, className: "ops-access-panel-grant" },
1539
+ (0, import_react.createElement)(
1540
+ "div",
1541
+ {
1542
+ className: "ops-access-panel-row clickable",
1543
+ onClick: () => setConfirmKey(confirmKey === "row:" + key ? null : "row:" + key)
1544
+ },
1545
+ (0, import_react.createElement)("span", { className: "ops-access-panel-row-title" }, g.kind + "/" + g.name),
1546
+ (0, import_react.createElement)("span", { className: "ops-access-panel-row-detail" }, "\u5269 " + g.remainingMinutes + " \u5206\u949F \xB7 " + g.approvedBy + " \xB7 " + g.reason)
1547
+ ),
1548
+ confirmStrip("row:" + key, "\u6536\u56DE " + group.session + " \u5BF9 " + g.kind + "/" + g.name + " \u7684\u6388\u6743\uFF1F", () => void run(() => postPanelRevoke(g.session, g.kind, g.name), "\u5DF2\u6536\u56DE " + g.kind + "/" + g.name))
1549
+ );
1550
+ }),
1551
+ (0, import_react.createElement)(
1552
+ "div",
1553
+ null,
1554
+ (0, import_react.createElement)("div", {
1555
+ className: "ops-access-panel-row clickable danger-zone",
1556
+ onClick: () => setConfirmKey(confirmKey === "all:" + group.session ? null : "all:" + group.session)
1557
+ }, (0, import_react.createElement)("span", { className: "ops-access-panel-row-title" }, "\u6536\u56DE\u8BE5\u4F1A\u8BDD\u5168\u90E8\u6388\u6743")),
1558
+ confirmStrip("all:" + group.session, "\u6536\u56DE " + group.session + " \u7684\u5168\u90E8 " + group.grants.length + " \u9879\u6388\u6743\uFF1F", () => void run(() => postPanelRevokeAll(group.session), "\u5DF2\u6536\u56DE " + group.session + " \u7684\u5168\u90E8\u6388\u6743"))
1559
+ )
1560
+ )),
1561
+ groups.length > 1 && (0, import_react.createElement)(
1562
+ "div",
1563
+ null,
1564
+ (0, import_react.createElement)("div", {
1565
+ className: "ops-access-panel-row clickable danger-zone",
1566
+ onClick: () => setConfirmKey(confirmKey === "all-sessions" ? null : "all-sessions")
1567
+ }, (0, import_react.createElement)("span", { className: "ops-access-panel-row-title" }, "\u6536\u56DE\u5168\u90E8\u4F1A\u8BDD\u7684\u5168\u90E8\u6388\u6743")),
1568
+ confirmStrip("all-sessions", "\u6536\u56DE " + groups.length + " \u4E2A\u4F1A\u8BDD\u7684\u5168\u90E8\u6388\u6743\uFF1F", () => void run(async () => {
1569
+ const results = await Promise.all(groups.map((g) => postPanelRevokeAll(g.session)));
1570
+ const failed = results.filter((r) => !r.ok);
1571
+ return failed.length === 0 ? { ok: true } : { ok: false, error: failed.length + " \u4E2A\u4F1A\u8BDD\u6536\u56DE\u5931\u8D25" };
1572
+ }, "\u5DF2\u6536\u56DE\u5168\u90E8\u4F1A\u8BDD\u7684\u6388\u6743"))
1573
+ )
1574
+ ),
1575
+ (0, import_react.createElement)(
1576
+ "div",
1577
+ { className: "ops-access-panel-section" },
1578
+ (0, import_react.createElement)("div", { className: "ops-access-panel-heading" }, "\u5F85\u51B3\u7533\u8BF7\uFF08\u5168\u90E8\u4F1A\u8BDD\uFF09"),
1579
+ requests.length === 0 && (0, import_react.createElement)("div", { className: "ops-access-panel-empty" }, "\u65E0\u5F85\u51B3\u7533\u8BF7"),
1580
+ requests.map((r) => {
1581
+ const chosen = ttlChoice[r.id] ?? defaultTtlChoice(ttlOptions, r.requestedTtlMinutes);
1582
+ return (0, import_react.createElement)(
1583
+ "div",
1584
+ { key: r.id, className: "ops-access-panel-request" },
1585
+ (0, import_react.createElement)(
1586
+ "div",
1587
+ { className: "ops-access-panel-row-title" },
1588
+ r.kind + "/" + r.name,
1589
+ (0, import_react.createElement)("span", { className: "ops-access-panel-badge" }, "\u7533\u8BF7 " + r.requestedTtlMinutes + " \u5206\u949F"),
1590
+ r.parentSession !== void 0 ? (0, import_react.createElement)("span", { className: "ops-access-panel-badge" }, "\u5B50\u4F1A\u8BDD\u7533\u8BF7 \xB7 \u7236\u4F1A\u8BDD " + r.parentSession) : (0, import_react.createElement)("span", { className: "ops-access-panel-badge" }, r.session)
1591
+ ),
1592
+ (0, import_react.createElement)("div", { className: "ops-access-panel-reason" }, r.reason),
1593
+ (0, import_react.createElement)(
1594
+ "div",
1595
+ { className: "ops-access-panel-actions" },
1596
+ (0, import_react.createElement)(
1597
+ "span",
1598
+ { className: "ops-access-panel-ttl-row" },
1599
+ ttlOptions.map(
1600
+ (ttl) => (0, import_react.createElement)("button", {
1601
+ key: ttl,
1602
+ className: "ops-access-panel-btn ttl" + (ttl === chosen ? " selected" : ""),
1603
+ onClick: () => setTtlChoice({ ...ttlChoice, [r.id]: ttl })
1604
+ }, ttl + " \u5206\u949F")
1605
+ )
1606
+ ),
1607
+ (0, import_react.createElement)("button", {
1608
+ className: "ops-access-panel-btn primary",
1609
+ onClick: () => void run(() => postPanelDecide(r.id, true, chosen), "\u5DF2\u6279\u51C6 " + r.kind + "/" + r.name + "\uFF08" + chosen + " \u5206\u949F\uFF09")
1610
+ }, "\u6279\u51C6"),
1611
+ (0, import_react.createElement)("button", {
1612
+ className: "ops-access-panel-btn",
1613
+ onClick: () => void run(() => postPanelDecide(r.id, false), "\u5DF2\u62D2\u7EDD " + r.kind + "/" + r.name)
1614
+ }, "\u62D2\u7EDD")
1615
+ )
1616
+ );
1617
+ })
1618
+ ),
1619
+ (0, import_react.createElement)(
1620
+ "div",
1621
+ { className: "ops-access-panel-section" },
1622
+ (0, import_react.createElement)("div", { className: "ops-access-panel-heading" }, "\u5C01\u7981\uFF08\u8FDE\u53EA\u8BFB\u4E5F\u9501\u5B9A\uFF09"),
1623
+ denied.length === 0 && (0, import_react.createElement)("div", { className: "ops-access-panel-empty" }, "\u65E0\u5C01\u7981\u6863\u6848"),
1624
+ denied.map((d) => {
1625
+ const key = d.kind + "/" + d.name;
1626
+ return (0, import_react.createElement)(
1627
+ "div",
1628
+ { key, className: "ops-access-panel-grant" },
1629
+ (0, import_react.createElement)(
1630
+ "div",
1631
+ { className: "ops-access-panel-row" },
1632
+ (0, import_react.createElement)("span", { className: "ops-access-panel-dot deny", title: "\u5DF2\u5C01\u7981" }),
1633
+ (0, import_react.createElement)("span", { className: "ops-access-panel-row-title" }, key),
1634
+ (0, import_react.createElement)("span", { className: "ops-access-panel-row-detail" }, d.reason + " \xB7 " + d.deniedBy),
1635
+ (0, import_react.createElement)("button", { className: "ops-access-panel-btn", onClick: () => setConfirmKey("undeny:" + key) }, "\u89E3\u5C01\u2026")
1636
+ ),
1637
+ confirmStrip("undeny:" + key, "\u89E3\u5C01 " + key + "\uFF1F\u6062\u590D\u6B63\u5E38\u7684 ro/rw \u6388\u6743\u6D41\u7A0B", () => void run(() => postPanelUndeny(d.kind, d.name), "\u5DF2\u89E3\u5C01 " + key))
1638
+ );
1639
+ })
1640
+ )
1641
+ );
1642
+ }
1643
+ function apply(ctx) {
1644
+ const inputTriggers = ctx.get("inputTriggers");
1645
+ if (inputTriggers !== void 0) {
1646
+ const source = {
1647
+ trigger: "@",
1648
+ name: "access",
1649
+ order: 2,
1650
+ async candidates(_session, { query, signal }) {
1651
+ let list;
1652
+ try {
1653
+ const res = await fetch(`/ops-access/list?query=${encodeURIComponent(query)}`, { signal });
1654
+ if (!res.ok) return [];
1655
+ list = await res.json();
1656
+ } catch {
1657
+ return [];
1658
+ }
1659
+ return list.map((c) => {
1660
+ const base = c.ro ? c.description : [c.description, "ro \u672A\u6CE8\u518C\uFF08\u53EF\u7531 rw \u6D3E\u751F\uFF09"].filter(Boolean).join(" \u2014 ");
1661
+ const probe = mentionProbeText(c);
1662
+ return {
1663
+ name: `${c.kind}/${c.name}`,
1664
+ description: [base, probe].filter(Boolean).join(" \xB7 ") || void 0,
1665
+ hint: c.environment,
1666
+ value: c.mention
1667
+ };
1668
+ });
1669
+ },
1670
+ onPick({ candidate }) {
1671
+ const mention = candidate.value;
1672
+ if (mention === void 0) return void 0;
1673
+ return {
1674
+ insert: {
1675
+ source: "access",
1676
+ ref: mention,
1677
+ label: candidate.name,
1678
+ clipboardText: `@${candidate.name}`
1679
+ }
1680
+ };
1681
+ },
1682
+ codec: {
1683
+ clipboardText: (ref) => ref,
1684
+ serialize: (ref) => Promise.resolve(ref)
1685
+ }
1686
+ };
1687
+ ctx.effect(() => inputTriggers.registerSource(source));
1688
+ }
1689
+ injectCSS();
1690
+ const slots = ctx.get("slots");
1691
+ if (slots !== void 0) {
1692
+ ctx.effect(() => slots.inject(
1693
+ "settings.section",
1694
+ () => slots.register(
1695
+ { name: "settings.section", id: "ops-access-admin", order: 20, label: "\u51ED\u8BC1\u7BA1\u7406" },
1696
+ AdminSection
1697
+ )
1698
+ ));
1699
+ }
1700
+ ;
1701
+ ctx.inject(["opsPanels"], (pctx) => {
1702
+ const opsPanels = pctx.opsPanels;
1703
+ if (opsPanels === void 0) return;
1704
+ const effect = pctx.effect.bind(pctx);
1705
+ effect(
1706
+ () => opsPanels.registerPanel({
1707
+ command: "access",
1708
+ title: "\u8BBF\u95EE\u6388\u6743",
1709
+ component: AccessPanel
1710
+ })
1711
+ );
1712
+ effect(
1713
+ () => opsPanels.registerPanel({
1714
+ command: "access-all",
1715
+ title: "\u6388\u6743\u603B\u89C8\uFF08\u8DE8\u4F1A\u8BDD\uFF09",
1716
+ component: AccessOverviewPanel
1717
+ })
1718
+ );
1719
+ if (slots !== void 0) {
1720
+ effect(
1721
+ () => slots.inject(
1722
+ "conversation.input.dock",
1723
+ () => slots.register(
1724
+ { name: "conversation.input.dock", id: "ops-access-badge", order: 30 },
1725
+ (props) => AccessBadge({
1726
+ sessionId: props.sessionId,
1727
+ session: props.session,
1728
+ openPanel: (sid) => opsPanels.open(sid, "access")
1729
+ })
1730
+ )
1731
+ )
1732
+ );
1733
+ }
1734
+ });
1735
+ }
1736
+
1737
+ return module.exports; } });
1738
+ //# sourceMappingURL=client.js.map