@zackbart/connecta 0.6.1 → 0.7.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/CHANGELOG.md +294 -0
- package/README.md +24 -20
- package/dist/auth/bearer.d.ts +4 -3
- package/dist/auth/bearer.d.ts.map +1 -1
- package/dist/auth/bearer.js +10 -8
- package/dist/auth/bearer.js.map +1 -1
- package/dist/auth/clerk.d.ts +8 -7
- package/dist/auth/clerk.d.ts.map +1 -1
- package/dist/auth/clerk.js +27 -8
- package/dist/auth/clerk.js.map +1 -1
- package/dist/connector-scope.d.ts +13 -0
- package/dist/connector-scope.d.ts.map +1 -0
- package/dist/connector-scope.js +35 -0
- package/dist/connector-scope.js.map +1 -0
- package/dist/connectors/api.d.ts +5 -5
- package/dist/connectors/api.d.ts.map +1 -1
- package/dist/connectors/remote-mcp.d.ts +27 -4
- package/dist/connectors/remote-mcp.d.ts.map +1 -1
- package/dist/connectors/remote-mcp.js +400 -19
- package/dist/connectors/remote-mcp.js.map +1 -1
- package/dist/credential-health.d.ts +8 -5
- package/dist/credential-health.d.ts.map +1 -1
- package/dist/credential-health.js +99 -51
- package/dist/credential-health.js.map +1 -1
- package/dist/credentials.d.ts +51 -2
- package/dist/credentials.d.ts.map +1 -1
- package/dist/credentials.js +68 -3
- package/dist/credentials.js.map +1 -1
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +10 -8
- package/dist/execute.js.map +1 -1
- package/dist/index.d.ts +84 -83
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +74 -24
- package/dist/index.js.map +1 -1
- package/dist/meta-tools.d.ts +28 -3
- package/dist/meta-tools.d.ts.map +1 -1
- package/dist/meta-tools.js +131 -16
- package/dist/meta-tools.js.map +1 -1
- package/dist/registry.d.ts +3 -2
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +4 -3
- package/dist/registry.js.map +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +152 -52
- package/dist/server.js.map +1 -1
- package/dist/toolkits.js +1 -1
- package/dist/types.d.ts +41 -27
- package/dist/types.d.ts.map +1 -1
- package/dist/ui.d.ts +24 -10
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +594 -172
- package/dist/ui.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -2
- package/src/auth/bearer.ts +10 -8
- package/src/auth/clerk.ts +28 -9
- package/src/connector-scope.ts +41 -0
- package/src/connectors/api.ts +5 -5
- package/src/connectors/remote-mcp.ts +489 -35
- package/src/credential-health.ts +120 -57
- package/src/credentials.ts +96 -3
- package/src/execute.ts +18 -7
- package/src/index.ts +174 -107
- package/src/meta-tools.ts +173 -24
- package/src/registry.ts +4 -3
- package/src/server.ts +191 -70
- package/src/toolkits.ts +1 -1
- package/src/types.ts +41 -27
- package/src/ui.ts +631 -170
- package/src/version.ts +1 -1
package/dist/ui.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { credentialTestRule } from "./credentials.js";
|
|
1
|
+
import { credentialTestRule, describeUndeclaredCredentialFields, storedCredentialShape, } from "./credentials.js";
|
|
2
|
+
import { closeConnectorScope } from "./connector-scope.js";
|
|
2
3
|
/** Connecta's default monochrome "C" mark. */
|
|
3
4
|
export const CONNECTA_FAVICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
|
4
5
|
<style>
|
|
@@ -119,8 +120,8 @@ const URL_STRIPPED_CHARS = /[\t\n\r]/g;
|
|
|
119
120
|
* default href is the relative `/favicon.svg`, which `isSafeHttpUrl` alone would
|
|
120
121
|
* reject — and it is kept narrow on both ends.
|
|
121
122
|
*
|
|
122
|
-
* Root-relative only, because
|
|
123
|
-
* depths and a document-relative path would resolve differently
|
|
123
|
+
* Root-relative only, because operator and OAuth callback pages sit at
|
|
124
|
+
* different depths and a document-relative path would resolve differently.
|
|
124
125
|
*
|
|
125
126
|
* "Root-relative" is enforced structurally: exactly one leading `/` followed by
|
|
126
127
|
* a character that is neither `/` nor `\`. Both of those would make the value an
|
|
@@ -147,7 +148,7 @@ export function isSafeIconHref(href) {
|
|
|
147
148
|
}
|
|
148
149
|
/**
|
|
149
150
|
* True only for an absolute `https:` URL — the gate every `uiAuth` URL passes:
|
|
150
|
-
* `frontendApiUrl`, which becomes the
|
|
151
|
+
* `frontendApiUrl`, which becomes the operator shell's sign-in loader source,
|
|
151
152
|
* and `signInUrl`/`signUpUrl`, which ClerkJS uses as *navigation targets* when
|
|
152
153
|
* the operator signs in. With those three gated, no operator-config value
|
|
153
154
|
* reaches the browser in a URL position — attribute or navigation — without
|
|
@@ -156,7 +157,7 @@ export function isSafeIconHref(href) {
|
|
|
156
157
|
* Stricter than `isSafeHttpUrl` on purpose: no `http:` carve-out, no loopback
|
|
157
158
|
* carve-out, and no relative form. Nobody types `frontendApiUrl` — the shipped
|
|
158
159
|
* Clerk adapter derives it from the publishable key, and Clerk's Frontend API is
|
|
159
|
-
* always https — and a cleartext script source on
|
|
160
|
+
* always https — and a cleartext script source on an operator page would be a
|
|
160
161
|
* downgrade even where a browser's mixed-content rules had not already blocked
|
|
161
162
|
* it. `signInUrl`/`signUpUrl` *are* typed by the operator, but what belongs
|
|
162
163
|
* there is a hosted Account Portal address (`https://accounts.<domain>` or
|
|
@@ -207,6 +208,32 @@ export function droppedUiAuthUrls(uiAuth) {
|
|
|
207
208
|
: []),
|
|
208
209
|
];
|
|
209
210
|
}
|
|
211
|
+
const OPERATOR_PAGE_LABELS = {
|
|
212
|
+
connections: "Connections",
|
|
213
|
+
credentials: "Credentials",
|
|
214
|
+
activity: "Activity",
|
|
215
|
+
};
|
|
216
|
+
export function operatorPageForPath(path) {
|
|
217
|
+
if (path === "/")
|
|
218
|
+
return "connections";
|
|
219
|
+
if (path === "/credentials")
|
|
220
|
+
return "credentials";
|
|
221
|
+
if (path === "/activity")
|
|
222
|
+
return "activity";
|
|
223
|
+
return undefined;
|
|
224
|
+
}
|
|
225
|
+
export function operatorPageTitle(page, configuredTitle) {
|
|
226
|
+
return `${OPERATOR_PAGE_LABELS[page]} — ${configuredTitle}`;
|
|
227
|
+
}
|
|
228
|
+
export function credentialManagementCapability(input) {
|
|
229
|
+
if (!input.eligibleClerkOperator)
|
|
230
|
+
return "requires_clerk";
|
|
231
|
+
if (!input.hasCredentialSlots)
|
|
232
|
+
return "no_slots";
|
|
233
|
+
if (!input.hasCredentialVault)
|
|
234
|
+
return "vault_not_configured";
|
|
235
|
+
return "available";
|
|
236
|
+
}
|
|
210
237
|
/**
|
|
211
238
|
* Filter by connector identity/description or tool name/description. A
|
|
212
239
|
* connector-level match stays visible even when it currently exposes no tools
|
|
@@ -239,9 +266,12 @@ export function filterUiConnectors(connectors, query) {
|
|
|
239
266
|
* isolated: they surface status "error" with an empty tool list rather than
|
|
240
267
|
* failing the whole payload.
|
|
241
268
|
*/
|
|
242
|
-
export async function buildUiData(registry, baseUrl, serverInfo, credentialVault, activityEnabled = false
|
|
269
|
+
export async function buildUiData(registry, baseUrl, serverInfo, credentialVault, activityEnabled = false, credentialManagement = credentialVault
|
|
270
|
+
? "available"
|
|
271
|
+
: "requires_clerk") {
|
|
243
272
|
const requestScope = {};
|
|
244
|
-
const
|
|
273
|
+
const connectorSet = registry.listConnectors();
|
|
274
|
+
const connectors = await Promise.all(connectorSet.map(async (c) => {
|
|
245
275
|
const status = await registry.statusFor(c.id, baseUrl, requestScope);
|
|
246
276
|
const credentialCheck = await registry.credentialHealthFor(c.id);
|
|
247
277
|
let tools = [];
|
|
@@ -267,7 +297,7 @@ export async function buildUiData(registry, baseUrl, serverInfo, credentialVault
|
|
|
267
297
|
// One rule, shared with the test route: only the hook matching the
|
|
268
298
|
// declared credential shape can run, so the button is offered only
|
|
269
299
|
// where a click can succeed (src/credentials.ts).
|
|
270
|
-
const
|
|
300
|
+
const testRule = credentialTestRule(c);
|
|
271
301
|
const credentialFields = (metadata) => c.credential?.fields?.map((field) => {
|
|
272
302
|
const fieldMetadata = metadata?.fields?.[field.name];
|
|
273
303
|
return {
|
|
@@ -292,6 +322,7 @@ export async function buildUiData(registry, baseUrl, serverInfo, credentialVault
|
|
|
292
322
|
try {
|
|
293
323
|
const metadata = await credentialVault.metadata(c.id);
|
|
294
324
|
const fields = credentialFields(metadata);
|
|
325
|
+
const shape = storedCredentialShape(c.credential, metadata?.fields ?? null);
|
|
295
326
|
credential = {
|
|
296
327
|
label: c.credential.label,
|
|
297
328
|
...(c.credential.description
|
|
@@ -301,9 +332,7 @@ export async function buildUiData(registry, baseUrl, serverInfo, credentialVault
|
|
|
301
332
|
? { placeholder: c.credential.placeholder }
|
|
302
333
|
: {}),
|
|
303
334
|
...(fields?.length ? { fields } : {}),
|
|
304
|
-
configured:
|
|
305
|
-
? fields.every((field) => field.configured)
|
|
306
|
-
: Boolean(metadata),
|
|
335
|
+
configured: shape.state === "valid",
|
|
307
336
|
removable: Boolean(metadata),
|
|
308
337
|
...(metadata
|
|
309
338
|
? {
|
|
@@ -311,7 +340,18 @@ export async function buildUiData(registry, baseUrl, serverInfo, credentialVault
|
|
|
311
340
|
updatedAt: metadata.updatedAt,
|
|
312
341
|
}
|
|
313
342
|
: {}),
|
|
314
|
-
testable,
|
|
343
|
+
testable: testRule.mode !== null && shape.state !== "mismatch",
|
|
344
|
+
...(shape.state === "mismatch"
|
|
345
|
+
? { error: shape.message }
|
|
346
|
+
: {}),
|
|
347
|
+
// A dropped field leaves its secret in the vault, and the field
|
|
348
|
+
// list below only renders fields the connector still declares —
|
|
349
|
+
// so without this line there is nowhere an operator could see it.
|
|
350
|
+
...(shape.state === "valid" && shape.undeclared.length
|
|
351
|
+
? {
|
|
352
|
+
notice: describeUndeclaredCredentialFields(shape.undeclared),
|
|
353
|
+
}
|
|
354
|
+
: {}),
|
|
315
355
|
};
|
|
316
356
|
}
|
|
317
357
|
catch {
|
|
@@ -327,7 +367,7 @@ export async function buildUiData(registry, baseUrl, serverInfo, credentialVault
|
|
|
327
367
|
...(fields?.length ? { fields } : {}),
|
|
328
368
|
configured: false,
|
|
329
369
|
removable: true,
|
|
330
|
-
testable,
|
|
370
|
+
testable: testRule.mode !== null,
|
|
331
371
|
error: "Stored credential could not be read.",
|
|
332
372
|
};
|
|
333
373
|
}
|
|
@@ -356,8 +396,15 @@ export async function buildUiData(registry, baseUrl, serverInfo, credentialVault
|
|
|
356
396
|
: {}),
|
|
357
397
|
...(credential ? { credential } : {}),
|
|
358
398
|
};
|
|
359
|
-
}))
|
|
360
|
-
|
|
399
|
+
})).finally(async () => {
|
|
400
|
+
await Promise.all(connectorSet.map((connector) => closeConnectorScope(connector, registry.contextFor(connector.id, baseUrl, requestScope))));
|
|
401
|
+
});
|
|
402
|
+
return {
|
|
403
|
+
serverInfo,
|
|
404
|
+
connectors,
|
|
405
|
+
activityEnabled,
|
|
406
|
+
credentialManagement,
|
|
407
|
+
};
|
|
361
408
|
}
|
|
362
409
|
function escapeHtmlAttr(value) {
|
|
363
410
|
return value
|
|
@@ -373,12 +420,10 @@ function jsonForInlineScript(value) {
|
|
|
373
420
|
.replaceAll("&", "\\u0026");
|
|
374
421
|
}
|
|
375
422
|
/**
|
|
376
|
-
*
|
|
377
|
-
*
|
|
378
|
-
* normal sign-in flow and a short-lived session token; bearer-only deployments
|
|
379
|
-
* retain the manual-token fallback.
|
|
423
|
+
* Every operator page serves this same data-free shell. Connector, credential,
|
|
424
|
+
* and activity data arrives only through the authenticated `/ui/*` APIs.
|
|
380
425
|
*/
|
|
381
|
-
export function renderUiHtml(uiAuth, mcpUrl = "/mcp", branding, nonce) {
|
|
426
|
+
export function renderUiHtml(uiAuth, mcpUrl = "/mcp", branding, nonce, page = "connections") {
|
|
382
427
|
const clerk = uiAuth?.kind === "clerk" ? uiAuth : undefined;
|
|
383
428
|
// The Clerk loader's origin. A value that fails the gate is dropped rather
|
|
384
429
|
// than escaped into the page: the loader tag is simply not emitted, the gate
|
|
@@ -408,8 +453,8 @@ export function renderUiHtml(uiAuth, mcpUrl = "/mcp", branding, nonce) {
|
|
|
408
453
|
}
|
|
409
454
|
: (uiAuth ?? { kind: "bearer" });
|
|
410
455
|
const brand = resolveBranding(branding);
|
|
411
|
-
const title = brand.pageTitle;
|
|
412
|
-
// When
|
|
456
|
+
const title = operatorPageTitle(page, brand.pageTitle);
|
|
457
|
+
// When an operator shell ships a nonce-based CSP, every script it emits must
|
|
413
458
|
// carry that nonce to run; without a nonce the markup is unchanged.
|
|
414
459
|
const nonceAttr = nonce ? ` nonce="${nonce}"` : "";
|
|
415
460
|
// Top-left corner. With an owner set it reads "<owner> <product>"; without
|
|
@@ -448,6 +493,7 @@ ${clerkScript}
|
|
|
448
493
|
--paper: #fff;
|
|
449
494
|
--rule: #ccc;
|
|
450
495
|
--muted: #666;
|
|
496
|
+
--trace: #f5f5f5;
|
|
451
497
|
--shell: 70rem;
|
|
452
498
|
--pad: 1rem;
|
|
453
499
|
--gap: 1.5rem;
|
|
@@ -494,6 +540,15 @@ ${clerkScript}
|
|
|
494
540
|
outline: 1px solid var(--ink);
|
|
495
541
|
}
|
|
496
542
|
:is(a, button, summary):focus-visible { outline-offset: 2px; }
|
|
543
|
+
.skip-link {
|
|
544
|
+
background: var(--paper);
|
|
545
|
+
left: var(--pad);
|
|
546
|
+
padding: .5rem;
|
|
547
|
+
position: fixed;
|
|
548
|
+
top: -4rem;
|
|
549
|
+
z-index: 10;
|
|
550
|
+
}
|
|
551
|
+
.skip-link:focus { top: var(--pad); }
|
|
497
552
|
|
|
498
553
|
.shell {
|
|
499
554
|
margin: 0 auto;
|
|
@@ -512,6 +567,15 @@ ${clerkScript}
|
|
|
512
567
|
.cap, .meta { color: var(--muted); font-size: .9em; }
|
|
513
568
|
.mono { font-family: var(--mono); font-size: .78rem; }
|
|
514
569
|
.hidden { display: none !important; }
|
|
570
|
+
.visually-hidden {
|
|
571
|
+
clip: rect(0 0 0 0);
|
|
572
|
+
clip-path: inset(50%);
|
|
573
|
+
height: 1px;
|
|
574
|
+
overflow: hidden;
|
|
575
|
+
position: absolute;
|
|
576
|
+
white-space: nowrap;
|
|
577
|
+
width: 1px;
|
|
578
|
+
}
|
|
515
579
|
|
|
516
580
|
.masthead {
|
|
517
581
|
align-items: start;
|
|
@@ -536,6 +600,17 @@ ${clerkScript}
|
|
|
536
600
|
justify-content: flex-end;
|
|
537
601
|
min-width: 0;
|
|
538
602
|
}
|
|
603
|
+
.page-nav,
|
|
604
|
+
.session-actions {
|
|
605
|
+
display: flex;
|
|
606
|
+
flex-wrap: wrap;
|
|
607
|
+
gap: .5rem var(--gap);
|
|
608
|
+
}
|
|
609
|
+
.mast-actions :is(a, button) {
|
|
610
|
+
align-items: center;
|
|
611
|
+
display: inline-flex;
|
|
612
|
+
min-height: 2rem;
|
|
613
|
+
}
|
|
539
614
|
.navlink,
|
|
540
615
|
.linklike {
|
|
541
616
|
text-decoration: underline;
|
|
@@ -545,7 +620,7 @@ ${clerkScript}
|
|
|
545
620
|
.navlink { text-decoration-color: transparent; }
|
|
546
621
|
.navlink:hover,
|
|
547
622
|
.navlink:focus-visible,
|
|
548
|
-
.navlink
|
|
623
|
+
.navlink[aria-current="page"] { text-decoration-color: currentColor; }
|
|
549
624
|
.linklike { text-decoration-color: currentColor; }
|
|
550
625
|
.linklike:hover,
|
|
551
626
|
.linklike:focus-visible { text-decoration-color: transparent; }
|
|
@@ -593,7 +668,24 @@ ${clerkScript}
|
|
|
593
668
|
#notice:empty { display: none; }
|
|
594
669
|
#notice:not(:empty) { text-decoration: underline; }
|
|
595
670
|
.error-notice, .msg { text-decoration: underline; }
|
|
596
|
-
.card
|
|
671
|
+
.card,
|
|
672
|
+
.credential-card,
|
|
673
|
+
.activity-item {
|
|
674
|
+
padding-left: 1.25rem;
|
|
675
|
+
position: relative;
|
|
676
|
+
}
|
|
677
|
+
.card::before,
|
|
678
|
+
.credential-card::before,
|
|
679
|
+
.activity-item::before {
|
|
680
|
+
background: var(--rule);
|
|
681
|
+
bottom: 0;
|
|
682
|
+
content: "";
|
|
683
|
+
left: .25rem;
|
|
684
|
+
position: absolute;
|
|
685
|
+
top: 0;
|
|
686
|
+
width: 1px;
|
|
687
|
+
}
|
|
688
|
+
.card { border-top: 1px solid var(--rule); padding-bottom: .75rem; padding-top: .75rem; }
|
|
597
689
|
.connector-head {
|
|
598
690
|
display: grid;
|
|
599
691
|
gap: var(--gap);
|
|
@@ -604,14 +696,25 @@ ${clerkScript}
|
|
|
604
696
|
display: flex;
|
|
605
697
|
gap: .5rem;
|
|
606
698
|
}
|
|
699
|
+
.connector-title .dot,
|
|
700
|
+
.activity-stamp .dot {
|
|
701
|
+
margin-left: -1.25rem;
|
|
702
|
+
}
|
|
703
|
+
.activity-stamp {
|
|
704
|
+
align-items: baseline;
|
|
705
|
+
display: flex;
|
|
706
|
+
gap: .75rem;
|
|
707
|
+
}
|
|
607
708
|
.card h2 { overflow-wrap: anywhere; }
|
|
608
709
|
.connector-state { text-align: right; }
|
|
609
710
|
.dot {
|
|
711
|
+
background: var(--paper);
|
|
610
712
|
border: 1px solid var(--ink);
|
|
611
713
|
display: inline-block;
|
|
612
714
|
flex: none;
|
|
613
715
|
height: .5rem;
|
|
614
716
|
width: .5rem;
|
|
717
|
+
z-index: 1;
|
|
615
718
|
}
|
|
616
719
|
.dot.ok { background: var(--ink); }
|
|
617
720
|
.dot.auth_required {
|
|
@@ -621,7 +724,8 @@ ${clerkScript}
|
|
|
621
724
|
.connector-message,
|
|
622
725
|
.connector-auth { margin-top: .75rem; }
|
|
623
726
|
|
|
624
|
-
.credential { border-
|
|
727
|
+
.credential-ledger { border-bottom: 1px solid var(--rule); }
|
|
728
|
+
.credential-card { border-top: 1px solid var(--rule); padding-bottom: .75rem; padding-top: .75rem; }
|
|
625
729
|
.credential-head {
|
|
626
730
|
align-items: baseline;
|
|
627
731
|
display: flex;
|
|
@@ -630,7 +734,27 @@ ${clerkScript}
|
|
|
630
734
|
justify-content: space-between;
|
|
631
735
|
}
|
|
632
736
|
.credential-copy { margin-top: .25rem; max-width: 40rem; }
|
|
737
|
+
.credential-field-summary {
|
|
738
|
+
border-top: 1px solid var(--rule);
|
|
739
|
+
margin-top: .75rem;
|
|
740
|
+
}
|
|
741
|
+
.credential-field-summary > div {
|
|
742
|
+
border-bottom: 1px solid var(--rule);
|
|
743
|
+
display: flex;
|
|
744
|
+
flex-wrap: wrap;
|
|
745
|
+
gap: .25rem var(--gap);
|
|
746
|
+
justify-content: space-between;
|
|
747
|
+
padding: .5rem 0;
|
|
748
|
+
}
|
|
633
749
|
.credential-actions { display: flex; flex-wrap: wrap; gap: var(--gap); margin-top: .75rem; }
|
|
750
|
+
.credential-actions button,
|
|
751
|
+
.credential-form button,
|
|
752
|
+
.activity-controls button,
|
|
753
|
+
.activity-more {
|
|
754
|
+
align-items: center;
|
|
755
|
+
display: inline-flex;
|
|
756
|
+
min-height: 2.75rem;
|
|
757
|
+
}
|
|
634
758
|
.credential-form {
|
|
635
759
|
align-items: center;
|
|
636
760
|
display: flex;
|
|
@@ -674,7 +798,8 @@ ${clerkScript}
|
|
|
674
798
|
display: grid;
|
|
675
799
|
gap: .25rem var(--gap);
|
|
676
800
|
grid-template-columns: minmax(9rem, .85fr) minmax(12rem, 1.4fr) minmax(8rem, .9fr);
|
|
677
|
-
padding: .75rem
|
|
801
|
+
padding-bottom: .75rem;
|
|
802
|
+
padding-top: .75rem;
|
|
678
803
|
}
|
|
679
804
|
.activity-time,
|
|
680
805
|
.activity-actor,
|
|
@@ -685,6 +810,16 @@ ${clerkScript}
|
|
|
685
810
|
.activity-item.timeout .activity-outcome { text-decoration: underline; }
|
|
686
811
|
.activity-empty { border-top: 1px solid var(--rule); padding: .75rem 0; }
|
|
687
812
|
.activity-more { margin-top: .75rem; }
|
|
813
|
+
.unavailable {
|
|
814
|
+
background: var(--trace);
|
|
815
|
+
border-bottom: 1px solid var(--rule);
|
|
816
|
+
border-top: 1px solid var(--rule);
|
|
817
|
+
padding: .75rem;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
@media (prefers-reduced-motion: reduce) {
|
|
821
|
+
html:focus-within { scroll-behavior: auto; }
|
|
822
|
+
}
|
|
688
823
|
|
|
689
824
|
@media (max-width: 36.99rem) {
|
|
690
825
|
.pgrid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
@@ -693,7 +828,12 @@ ${clerkScript}
|
|
|
693
828
|
.masthead .brand { grid-column: 1; }
|
|
694
829
|
.mast-nav { grid-column: 1 / -1; grid-row: 2; justify-content: flex-start; }
|
|
695
830
|
.product { display: none; }
|
|
696
|
-
.mast-actions {
|
|
831
|
+
.mast-actions {
|
|
832
|
+
align-items: flex-start;
|
|
833
|
+
flex-direction: column;
|
|
834
|
+
font-size: .875rem;
|
|
835
|
+
gap: .25rem;
|
|
836
|
+
}
|
|
697
837
|
.lead { margin-top: 4rem; }
|
|
698
838
|
.section,
|
|
699
839
|
.section + .section { margin-top: 2.5rem; }
|
|
@@ -702,49 +842,58 @@ ${clerkScript}
|
|
|
702
842
|
.activity-item { grid-template-columns: 1fr; }
|
|
703
843
|
.connector-state { text-align: left; }
|
|
704
844
|
.credential-field { align-items: start; grid-template-columns: 1fr; gap: .25rem; }
|
|
845
|
+
input { min-height: 2.75rem; }
|
|
705
846
|
}
|
|
706
847
|
</style>
|
|
707
848
|
</head>
|
|
708
849
|
<body>
|
|
850
|
+
<a class="skip-link" href="#operatorContent">Skip to operator page</a>
|
|
709
851
|
<header class="masthead shell pgrid">
|
|
710
852
|
${owner}
|
|
711
853
|
<div class="mast-nav">
|
|
712
854
|
${product}
|
|
713
|
-
<
|
|
714
|
-
<
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
855
|
+
<div id="appNav" class="mast-actions hidden">
|
|
856
|
+
<nav class="page-nav" aria-label="Operator pages">
|
|
857
|
+
<a id="connectionsNav" class="navlink" href="/"
|
|
858
|
+
data-operator-page="connections"${page === "connections" ? ' aria-current="page"' : ""}>Connections</a>
|
|
859
|
+
<a id="credentialsNav" class="navlink hidden" href="/credentials"
|
|
860
|
+
data-operator-page="credentials"${page === "credentials" ? ' aria-current="page"' : ""}>Credentials</a>
|
|
861
|
+
<a id="activityNav" class="navlink hidden" href="/activity"
|
|
862
|
+
data-operator-page="activity"${page === "activity" ? ' aria-current="page"' : ""}>Activity</a>
|
|
863
|
+
</nav>
|
|
864
|
+
<div class="session-actions" aria-label="Session actions">
|
|
865
|
+
<button id="change" class="navlink hidden" type="button">Change token</button>
|
|
866
|
+
<button id="signout" class="navlink hidden" type="button">Sign out</button>
|
|
867
|
+
</div>
|
|
868
|
+
</div>
|
|
721
869
|
</div>
|
|
722
870
|
</header>
|
|
723
871
|
|
|
724
|
-
<main id="
|
|
725
|
-
<section
|
|
726
|
-
<
|
|
727
|
-
|
|
728
|
-
<
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
<
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
<
|
|
737
|
-
|
|
872
|
+
<main id="operatorContent" class="page shell" tabindex="-1">
|
|
873
|
+
<section id="gate" class="hidden">
|
|
874
|
+
<div class="lead pgrid">
|
|
875
|
+
<h1 id="gateHeading" class="pcap" tabindex="-1">${OPERATOR_PAGE_LABELS[page]}</h1>
|
|
876
|
+
<div class="pbody lead-copy">
|
|
877
|
+
<p>${escapeHtmlAttr(brand.description)}</p>
|
|
878
|
+
<p id="gateCopy" class="meta"></p>
|
|
879
|
+
<div id="tokenGate" class="row gate-actions hidden">
|
|
880
|
+
<input id="token" type="password" placeholder="Bearer token" autocomplete="off"
|
|
881
|
+
aria-label="Bearer token">
|
|
882
|
+
<button id="save" class="linklike" type="button">Open operator pages</button>
|
|
883
|
+
</div>
|
|
884
|
+
<div id="clerkGate" class="actions gate-actions hidden">
|
|
885
|
+
<button id="signin" class="linklike" type="button">Team sign in</button>
|
|
886
|
+
<button id="gateSignout" class="linklike hidden" type="button">Sign out</button>
|
|
887
|
+
</div>
|
|
888
|
+
<p id="err" role="alert"></p>
|
|
738
889
|
</div>
|
|
739
|
-
<p id="err"></p>
|
|
740
890
|
</div>
|
|
741
891
|
</section>
|
|
742
|
-
</main>
|
|
743
892
|
|
|
744
|
-
<
|
|
745
|
-
<section id="
|
|
893
|
+
<div id="app" class="hidden">
|
|
894
|
+
<section id="connectionsView"${page === "connections" ? "" : ' class="hidden"'}>
|
|
746
895
|
<div class="lead pgrid">
|
|
747
|
-
<h1 class="pcap">
|
|
896
|
+
<h1 id="connectionsHeading" class="pcap" tabindex="-1">Connections</h1>
|
|
748
897
|
<div class="pbody lead-copy">
|
|
749
898
|
<p>Use this endpoint to give an MCP client access to the tools below.</p>
|
|
750
899
|
<div class="endpoint">
|
|
@@ -753,50 +902,80 @@ ${clerkScript}
|
|
|
753
902
|
<button id="copyMcpUrl" class="linklike" type="button">Copy URL</button>
|
|
754
903
|
</div>
|
|
755
904
|
</div>
|
|
756
|
-
<p class="cap" id="serverInfo">${escapeHtmlAttr(brand.productName)}
|
|
905
|
+
<p class="cap" id="serverInfo">${escapeHtmlAttr(brand.productName)} operator</p>
|
|
757
906
|
</div>
|
|
758
907
|
</div>
|
|
759
|
-
<section class="section pgrid" aria-labelledby="
|
|
760
|
-
<h2 class="pcap" id="
|
|
908
|
+
<section class="section pgrid" aria-labelledby="connectorLedgerHeading">
|
|
909
|
+
<h2 class="pcap" id="connectorLedgerHeading">Connectors</h2>
|
|
761
910
|
<div class="pbody">
|
|
762
911
|
<div class="row toolbar">
|
|
763
912
|
<input id="filter" type="search" placeholder="Filter connectors or tools…"
|
|
764
913
|
aria-label="Filter connectors or tools">
|
|
765
914
|
</div>
|
|
766
|
-
<
|
|
767
|
-
<div id="list" class="connector-tools"></div>
|
|
915
|
+
<div id="list" class="connector-tools" aria-busy="false"></div>
|
|
768
916
|
</div>
|
|
769
917
|
</section>
|
|
770
918
|
</section>
|
|
771
|
-
|
|
919
|
+
|
|
920
|
+
<section id="credentialsView"${page === "credentials" ? "" : ' class="hidden"'}>
|
|
921
|
+
<div class="lead pgrid">
|
|
922
|
+
<h1 id="credentialsHeading" class="pcap" tabindex="-1">Credentials</h1>
|
|
923
|
+
<div class="pbody">
|
|
924
|
+
<p class="activity-copy">Rotate operator-managed connector credentials. Stored values are never returned or displayed.</p>
|
|
925
|
+
<p id="credentialNotice" class="meta" role="status" aria-live="polite"
|
|
926
|
+
tabindex="-1"></p>
|
|
927
|
+
<div id="credentialUnavailable" class="unavailable hidden"></div>
|
|
928
|
+
<div id="credentialList" class="credential-ledger" aria-busy="false"></div>
|
|
929
|
+
</div>
|
|
930
|
+
</div>
|
|
931
|
+
</section>
|
|
932
|
+
|
|
933
|
+
<section id="activityView"${page === "activity" ? "" : ' class="hidden"'}>
|
|
772
934
|
<div class="lead pgrid">
|
|
773
|
-
<h1 class="pcap">
|
|
935
|
+
<h1 id="activityHeading" class="pcap" tabindex="-1">Activity</h1>
|
|
774
936
|
<div class="pbody">
|
|
775
937
|
<p class="activity-copy" id="activitySummary">Arguments and results are never stored.</p>
|
|
776
|
-
<div class="
|
|
777
|
-
<
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
938
|
+
<div id="activityUnavailable" class="unavailable hidden">
|
|
939
|
+
Activity history is not configured. Add an <span class="mono">activity.store</span>
|
|
940
|
+
with a list reader to enable this page.
|
|
941
|
+
</div>
|
|
942
|
+
<div id="activityAvailable">
|
|
943
|
+
<div class="row activity-controls">
|
|
944
|
+
<input id="activitySearch" type="search"
|
|
945
|
+
placeholder="Search user, tool, or outcome…"
|
|
946
|
+
aria-label="Search loaded activity">
|
|
947
|
+
<button id="refreshActivity" class="linklike" type="button">Refresh</button>
|
|
948
|
+
</div>
|
|
949
|
+
<p id="activityNotice" class="meta" role="status" aria-live="polite"></p>
|
|
950
|
+
<div id="activityList" class="activity-ledger" aria-busy="false"></div>
|
|
951
|
+
<button id="moreActivity" class="linklike activity-more hidden" type="button">Load older</button>
|
|
781
952
|
</div>
|
|
782
|
-
<p id="activityNotice" class="meta" role="status" aria-live="polite"></p>
|
|
783
|
-
<div id="activityList" class="activity-ledger"></div>
|
|
784
|
-
<button id="moreActivity" class="linklike activity-more hidden" type="button">Load older</button>
|
|
785
953
|
</div>
|
|
786
954
|
</div>
|
|
787
955
|
</section>
|
|
956
|
+
</div>
|
|
788
957
|
</main>
|
|
789
958
|
|
|
790
959
|
<script${nonceAttr}>
|
|
791
960
|
const AUTH = ${jsonForInlineScript(auth)};
|
|
792
961
|
const MCP_URL = ${jsonForInlineScript(mcpUrl)};
|
|
962
|
+
const INITIAL_PAGE = ${jsonForInlineScript(page)};
|
|
963
|
+
const TITLE_SUFFIX = ${jsonForInlineScript(brand.pageTitle)};
|
|
793
964
|
const filterUiConnectors = ${filterUiConnectors.toString()};
|
|
794
965
|
const KEY = "connecta:token";
|
|
795
966
|
const $ = (id) => document.getElementById(id);
|
|
967
|
+
const PAGE_META = {
|
|
968
|
+
connections: { path: "/", label: "Connections" },
|
|
969
|
+
credentials: { path: "/credentials", label: "Credentials" },
|
|
970
|
+
activity: { path: "/activity", label: "Activity" },
|
|
971
|
+
};
|
|
796
972
|
let DATA = null;
|
|
797
973
|
let ACTIVITY = [];
|
|
798
974
|
let ACTIVITY_CURSOR = null;
|
|
799
975
|
let ACTIVITY_LOADED = false;
|
|
976
|
+
let CURRENT_PAGE = INITIAL_PAGE;
|
|
977
|
+
let SESSION_GENERATION = 0;
|
|
978
|
+
let ACTIVITY_GENERATION = 0;
|
|
800
979
|
$("mcpUrl").textContent = MCP_URL;
|
|
801
980
|
|
|
802
981
|
function esc(s) {
|
|
@@ -820,8 +999,9 @@ function formatDate(value) {
|
|
|
820
999
|
}
|
|
821
1000
|
|
|
822
1001
|
function setNotice(message, isError) {
|
|
823
|
-
$("
|
|
824
|
-
$("
|
|
1002
|
+
$("credentialNotice").textContent = message || "";
|
|
1003
|
+
$("credentialNotice").classList.toggle("error-notice", Boolean(isError));
|
|
1004
|
+
$("credentialNotice").setAttribute("role", isError ? "alert" : "status");
|
|
825
1005
|
}
|
|
826
1006
|
|
|
827
1007
|
async function sessionToken() {
|
|
@@ -830,7 +1010,45 @@ async function sessionToken() {
|
|
|
830
1010
|
: localStorage.getItem(KEY);
|
|
831
1011
|
}
|
|
832
1012
|
|
|
1013
|
+
function clearActivityState() {
|
|
1014
|
+
ACTIVITY_GENERATION += 1;
|
|
1015
|
+
ACTIVITY = [];
|
|
1016
|
+
ACTIVITY_CURSOR = null;
|
|
1017
|
+
ACTIVITY_LOADED = false;
|
|
1018
|
+
$("activityList").innerHTML = "";
|
|
1019
|
+
$("activityList").setAttribute("aria-busy", "false");
|
|
1020
|
+
$("activityNotice").textContent = "";
|
|
1021
|
+
$("activityNotice").setAttribute("role", "status");
|
|
1022
|
+
$("activitySummary").textContent = "Arguments and results are never stored.";
|
|
1023
|
+
$("activitySearch").value = "";
|
|
1024
|
+
$("refreshActivity").disabled = false;
|
|
1025
|
+
$("moreActivity").disabled = false;
|
|
1026
|
+
$("moreActivity").classList.add("hidden");
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
function clearIdentityState() {
|
|
1030
|
+
SESSION_GENERATION += 1;
|
|
1031
|
+
DATA = null;
|
|
1032
|
+
clearActivityState();
|
|
1033
|
+
$("list").innerHTML = "";
|
|
1034
|
+
$("filter").value = "";
|
|
1035
|
+
$("credentialList").innerHTML = "";
|
|
1036
|
+
$("credentialList").setAttribute("aria-busy", "false");
|
|
1037
|
+
$("credentialNotice").textContent = "";
|
|
1038
|
+
$("credentialNotice").setAttribute("role", "status");
|
|
1039
|
+
$("credentialNotice").classList.remove("error-notice");
|
|
1040
|
+
$("credentialUnavailable").textContent = "";
|
|
1041
|
+
$("credentialUnavailable").classList.add("hidden");
|
|
1042
|
+
$("credentialList").classList.add("hidden");
|
|
1043
|
+
$("activityUnavailable").classList.add("hidden");
|
|
1044
|
+
$("activityAvailable").classList.add("hidden");
|
|
1045
|
+
$("credentialsNav").classList.add("hidden");
|
|
1046
|
+
$("activityNav").classList.add("hidden");
|
|
1047
|
+
$("serverInfo").textContent = ${escapeScriptString(brand.productName + " operator")};
|
|
1048
|
+
}
|
|
1049
|
+
|
|
833
1050
|
function showGate(msg) {
|
|
1051
|
+
clearIdentityState();
|
|
834
1052
|
$("app").classList.add("hidden");
|
|
835
1053
|
$("appNav").classList.add("hidden");
|
|
836
1054
|
$("gate").classList.remove("hidden");
|
|
@@ -838,8 +1056,8 @@ function showGate(msg) {
|
|
|
838
1056
|
if (AUTH.kind === "clerk") {
|
|
839
1057
|
const signedIn = Boolean(window.Clerk && Clerk.user);
|
|
840
1058
|
$("gateCopy").textContent = signedIn
|
|
841
|
-
? "Signed in with Clerk, but this account cannot open
|
|
842
|
-
: "Sign in with Clerk to open
|
|
1059
|
+
? "Signed in with Clerk, but this account cannot open deployment-wide operator pages."
|
|
1060
|
+
: "Sign in with Clerk to open this operator page.";
|
|
843
1061
|
$("signin").classList.toggle("hidden", signedIn);
|
|
844
1062
|
$("gateSignout").classList.toggle("hidden", !signedIn);
|
|
845
1063
|
} else {
|
|
@@ -847,20 +1065,87 @@ function showGate(msg) {
|
|
|
847
1065
|
}
|
|
848
1066
|
}
|
|
849
1067
|
|
|
1068
|
+
function pageForPath(path) {
|
|
1069
|
+
if (path === "/credentials") return "credentials";
|
|
1070
|
+
if (path === "/activity") return "activity";
|
|
1071
|
+
return "connections";
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
function credentialUnavailableCopy(capability) {
|
|
1075
|
+
if (capability === "no_slots") {
|
|
1076
|
+
return "No connectors declare operator-managed credential slots. Connector credentials remain configuration-as-code until a slot is declared.";
|
|
1077
|
+
}
|
|
1078
|
+
if (capability === "vault_not_configured") {
|
|
1079
|
+
return "Credential storage is not configured. Set credentials.encryptionKey before managing connector credentials here.";
|
|
1080
|
+
}
|
|
1081
|
+
return "Credential management requires an eligible Clerk operator. Bearer-authenticated sessions can inspect connections but cannot manage stored credentials.";
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
function updateCapabilities() {
|
|
1085
|
+
const credentialsAvailable =
|
|
1086
|
+
DATA?.credentialManagement === "available";
|
|
1087
|
+
$("credentialsNav").classList.toggle("hidden", !credentialsAvailable);
|
|
1088
|
+
$("activityNav").classList.toggle("hidden", !DATA?.activityEnabled);
|
|
1089
|
+
$("credentialUnavailable").classList.toggle("hidden", credentialsAvailable);
|
|
1090
|
+
$("credentialList").classList.toggle("hidden", !credentialsAvailable);
|
|
1091
|
+
$("credentialUnavailable").textContent = credentialsAvailable
|
|
1092
|
+
? ""
|
|
1093
|
+
: credentialUnavailableCopy(DATA?.credentialManagement);
|
|
1094
|
+
$("activityUnavailable").classList.toggle("hidden", Boolean(DATA?.activityEnabled));
|
|
1095
|
+
$("activityAvailable").classList.toggle("hidden", !DATA?.activityEnabled);
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
function activatePage(page, options) {
|
|
1099
|
+
const next = PAGE_META[page] ? page : "connections";
|
|
1100
|
+
CURRENT_PAGE = next;
|
|
1101
|
+
for (const name of Object.keys(PAGE_META)) {
|
|
1102
|
+
$(name + "View").classList.toggle("hidden", name !== next);
|
|
1103
|
+
const link = $(name + "Nav");
|
|
1104
|
+
if (name === next) link.setAttribute("aria-current", "page");
|
|
1105
|
+
else link.removeAttribute("aria-current");
|
|
1106
|
+
}
|
|
1107
|
+
$("gateHeading").textContent = PAGE_META[next].label;
|
|
1108
|
+
document.title = PAGE_META[next].label + " — " + TITLE_SUFFIX;
|
|
1109
|
+
if (DATA) {
|
|
1110
|
+
updateCapabilities();
|
|
1111
|
+
if (next === "connections") renderConnections();
|
|
1112
|
+
if (next === "credentials") renderCredentials();
|
|
1113
|
+
if (next === "activity" && DATA.activityEnabled && !ACTIVITY_LOADED) {
|
|
1114
|
+
loadActivity(true);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
if (next !== "credentials") $("credentialList").innerHTML = "";
|
|
1118
|
+
// Focus what is actually on screen. While gated the page views are hidden, so
|
|
1119
|
+
// focusing their heading is a silent no-op that drops focus to <body> and
|
|
1120
|
+
// restarts the next Tab from the top of the document — the gate's own h1 is
|
|
1121
|
+
// the visible heading, and activatePage has just relabelled it.
|
|
1122
|
+
if (options?.focus) $(DATA ? next + "Heading" : "gateHeading").focus();
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
function navigateTo(page, href) {
|
|
1126
|
+
history.pushState({ operatorPage: page }, "", href);
|
|
1127
|
+
activatePage(page, { focus: true });
|
|
1128
|
+
}
|
|
1129
|
+
|
|
850
1130
|
async function load() {
|
|
1131
|
+
const generation = SESSION_GENERATION;
|
|
851
1132
|
let token;
|
|
852
1133
|
try {
|
|
853
1134
|
token = await sessionToken();
|
|
854
1135
|
} catch (e) {
|
|
1136
|
+
if (generation !== SESSION_GENERATION) return;
|
|
855
1137
|
return showGate("Could not read the Clerk session: " + e.message);
|
|
856
1138
|
}
|
|
1139
|
+
if (generation !== SESSION_GENERATION) return;
|
|
857
1140
|
if (!token) return showGate("");
|
|
858
1141
|
let res;
|
|
859
1142
|
try {
|
|
860
1143
|
res = await fetch("/ui/data", { headers: { Authorization: "Bearer " + token } });
|
|
861
1144
|
} catch (e) {
|
|
1145
|
+
if (generation !== SESSION_GENERATION) return;
|
|
862
1146
|
return showGate("Network error: " + e.message);
|
|
863
1147
|
}
|
|
1148
|
+
if (generation !== SESSION_GENERATION) return;
|
|
864
1149
|
if (res.status === 401 || res.status === 403) {
|
|
865
1150
|
if (AUTH.kind === "clerk") {
|
|
866
1151
|
return showGate(
|
|
@@ -873,25 +1158,22 @@ async function load() {
|
|
|
873
1158
|
return showGate("Token rejected — enter a valid bearer token.");
|
|
874
1159
|
}
|
|
875
1160
|
if (!res.ok) return showGate("Error " + res.status);
|
|
876
|
-
|
|
1161
|
+
let data;
|
|
1162
|
+
try {
|
|
1163
|
+
data = await res.json();
|
|
1164
|
+
} catch (e) {
|
|
1165
|
+
if (generation !== SESSION_GENERATION) return;
|
|
1166
|
+
return showGate("Operator data could not be read.");
|
|
1167
|
+
}
|
|
1168
|
+
if (generation !== SESSION_GENERATION) return;
|
|
1169
|
+
DATA = data;
|
|
877
1170
|
$("gate").classList.add("hidden");
|
|
878
1171
|
$("app").classList.remove("hidden");
|
|
879
1172
|
$("appNav").classList.remove("hidden");
|
|
880
1173
|
const si = DATA.serverInfo || {};
|
|
881
1174
|
$("serverInfo").textContent = (si.name || ${escapeScriptString(brand.productName)}) + " v" + (si.version || "?");
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
function showView(view) {
|
|
887
|
-
const activity = view === "activity";
|
|
888
|
-
$("configView").classList.toggle("hidden", activity);
|
|
889
|
-
$("activityView").classList.toggle("hidden", !activity);
|
|
890
|
-
$("configTab").classList.toggle("active", !activity);
|
|
891
|
-
$("activityTab").classList.toggle("active", activity);
|
|
892
|
-
$("configTab").setAttribute("aria-pressed", String(!activity));
|
|
893
|
-
$("activityTab").setAttribute("aria-pressed", String(activity));
|
|
894
|
-
if (activity && !ACTIVITY_LOADED) loadActivity(true);
|
|
1175
|
+
updateCapabilities();
|
|
1176
|
+
activatePage(CURRENT_PAGE);
|
|
895
1177
|
}
|
|
896
1178
|
|
|
897
1179
|
function actorLabel(actor) {
|
|
@@ -930,12 +1212,21 @@ function renderActivity() {
|
|
|
930
1212
|
}
|
|
931
1213
|
for (const event of visible) {
|
|
932
1214
|
const item = document.createElement("article");
|
|
933
|
-
|
|
934
|
-
|
|
1215
|
+
const outcomeClass = ["success", "error", "timeout"].includes(event.outcome)
|
|
1216
|
+
? event.outcome
|
|
1217
|
+
: "error";
|
|
1218
|
+
item.className = "activity-item " + outcomeClass;
|
|
1219
|
+
const retryCopy = event.attempts > 1
|
|
1220
|
+
? " · " + esc(event.attempts) + " attempts"
|
|
1221
|
+
: "";
|
|
935
1222
|
const errorCopy = event.errorCode ? " · " + esc(event.errorCode) : "";
|
|
936
1223
|
item.innerHTML =
|
|
937
|
-
'<div
|
|
938
|
-
|
|
1224
|
+
'<div class="activity-stamp"><span class="dot ' +
|
|
1225
|
+
(outcomeClass === "success" ? "ok" : "") +
|
|
1226
|
+
'" aria-hidden="true"></span><div><time class="activity-time" datetime="' +
|
|
1227
|
+
esc(event.occurredAt) + '">' +
|
|
1228
|
+
esc(formatDate(event.occurredAt)) +
|
|
1229
|
+
'</time><div class="activity-actor">' + esc(actorLabel(event.actor)) + '</div></div></div>' +
|
|
939
1230
|
'<div><div class="activity-address">' + esc(event.address) +
|
|
940
1231
|
'</div><div class="activity-detail">' + esc(event.source) + retryCopy +
|
|
941
1232
|
errorCopy + '</div></div>' +
|
|
@@ -948,22 +1239,45 @@ function renderActivity() {
|
|
|
948
1239
|
|
|
949
1240
|
async function loadActivity(reset) {
|
|
950
1241
|
if (!DATA?.activityEnabled) return;
|
|
1242
|
+
const sessionGeneration = SESSION_GENERATION;
|
|
1243
|
+
if (reset) ACTIVITY_GENERATION += 1;
|
|
1244
|
+
const activityGeneration = ACTIVITY_GENERATION;
|
|
1245
|
+
const isCurrent = () =>
|
|
1246
|
+
sessionGeneration === SESSION_GENERATION &&
|
|
1247
|
+
activityGeneration === ACTIVITY_GENERATION;
|
|
951
1248
|
$("activityNotice").textContent = "Loading activity…";
|
|
1249
|
+
$("activityNotice").setAttribute("role", "status");
|
|
1250
|
+
$("activityList").setAttribute("aria-busy", "true");
|
|
952
1251
|
$("refreshActivity").disabled = true;
|
|
953
1252
|
$("moreActivity").disabled = true;
|
|
954
1253
|
try {
|
|
955
1254
|
const token = await sessionToken();
|
|
956
|
-
if (!
|
|
1255
|
+
if (!isCurrent()) return;
|
|
1256
|
+
if (!token) return showGate("Your session has expired.");
|
|
957
1257
|
const cursor = reset ? null : ACTIVITY_CURSOR;
|
|
958
1258
|
const params = new URLSearchParams({ limit: "50" });
|
|
959
1259
|
if (cursor) params.set("cursor", cursor);
|
|
960
1260
|
const res = await fetch("/ui/activity?" + params, {
|
|
961
1261
|
headers: { Authorization: "Bearer " + token },
|
|
962
1262
|
});
|
|
1263
|
+
if (!isCurrent()) return;
|
|
963
1264
|
let payload = {};
|
|
964
1265
|
try { payload = await res.json(); } catch (e) {}
|
|
1266
|
+
if (!isCurrent()) return;
|
|
1267
|
+
if (res.status === 401) {
|
|
1268
|
+
return showGate("Your session was not accepted. Sign in again.");
|
|
1269
|
+
}
|
|
1270
|
+
if (res.status === 403) {
|
|
1271
|
+
clearActivityState();
|
|
1272
|
+
$("activityNotice").setAttribute("role", "alert");
|
|
1273
|
+
$("activityNotice").textContent =
|
|
1274
|
+
"This identity may not read activity history.";
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
965
1277
|
if (!res.ok) {
|
|
966
|
-
throw new Error(
|
|
1278
|
+
throw new Error(
|
|
1279
|
+
payload.error || "Activity could not be loaded (" + res.status + ")."
|
|
1280
|
+
);
|
|
967
1281
|
}
|
|
968
1282
|
ACTIVITY = reset
|
|
969
1283
|
? (payload.events || [])
|
|
@@ -973,14 +1287,19 @@ async function loadActivity(reset) {
|
|
|
973
1287
|
$("activityNotice").textContent = "";
|
|
974
1288
|
renderActivity();
|
|
975
1289
|
} catch (e) {
|
|
1290
|
+
if (!isCurrent()) return;
|
|
1291
|
+
$("activityNotice").setAttribute("role", "alert");
|
|
976
1292
|
$("activityNotice").textContent = e.message || "Activity could not be loaded.";
|
|
977
1293
|
} finally {
|
|
978
|
-
|
|
979
|
-
|
|
1294
|
+
if (isCurrent()) {
|
|
1295
|
+
$("activityList").setAttribute("aria-busy", "false");
|
|
1296
|
+
$("refreshActivity").disabled = false;
|
|
1297
|
+
$("moreActivity").disabled = false;
|
|
1298
|
+
}
|
|
980
1299
|
}
|
|
981
1300
|
}
|
|
982
1301
|
|
|
983
|
-
function
|
|
1302
|
+
function renderConnections() {
|
|
984
1303
|
const q = $("filter").value.trim().toLowerCase();
|
|
985
1304
|
const list = $("list");
|
|
986
1305
|
list.innerHTML = "";
|
|
@@ -1028,66 +1347,8 @@ function render() {
|
|
|
1028
1347
|
esc(c.authorizationUrl) + "</p>";
|
|
1029
1348
|
}
|
|
1030
1349
|
if (c.credential) {
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
const removable = configured || Boolean(cred.removable);
|
|
1034
|
-
const state = configured
|
|
1035
|
-
? "configured · ••••" + esc(cred.lastFour || "")
|
|
1036
|
-
: "not configured";
|
|
1037
|
-
const updated = configured && cred.updatedAt
|
|
1038
|
-
? " · updated " + esc(formatDate(cred.updatedAt))
|
|
1039
|
-
: "";
|
|
1040
|
-
head += '<section class="credential" aria-label="' + esc(cred.label) + '">';
|
|
1041
|
-
head += '<div class="credential-head"><span class="credential-label">' +
|
|
1042
|
-
esc(cred.label) + '</span><span class="credential-state">' + state +
|
|
1043
|
-
updated + "</span></div>";
|
|
1044
|
-
if (cred.description) {
|
|
1045
|
-
head += '<p class="credential-copy meta">' + esc(cred.description) + "</p>";
|
|
1046
|
-
}
|
|
1047
|
-
if (cred.error) {
|
|
1048
|
-
head += '<div class="msg">' + esc(cred.error) + "</div>";
|
|
1049
|
-
}
|
|
1050
|
-
if (AUTH.kind === "clerk") {
|
|
1051
|
-
head += '<div class="credential-actions">';
|
|
1052
|
-
head += '<button class="linklike" type="button" data-credential-action="edit" data-connector="' +
|
|
1053
|
-
esc(c.id) + '">' + (removable ? "Replace" : "Add credential") + "</button>";
|
|
1054
|
-
if (configured && cred.testable) {
|
|
1055
|
-
head += '<button class="linklike" type="button" data-credential-action="test" data-connector="' +
|
|
1056
|
-
esc(c.id) + '">Test</button>';
|
|
1057
|
-
}
|
|
1058
|
-
if (removable) {
|
|
1059
|
-
head += '<button type="button" class="linklike danger" data-credential-action="remove" data-connector="' +
|
|
1060
|
-
esc(c.id) + '">Remove</button>';
|
|
1061
|
-
}
|
|
1062
|
-
head += "</div>";
|
|
1063
|
-
head += '<div class="credential-form hidden" data-credential-form="' +
|
|
1064
|
-
esc(c.id) + '">';
|
|
1065
|
-
if (cred.fields && cred.fields.length) {
|
|
1066
|
-
head += '<div class="credential-fields">';
|
|
1067
|
-
for (const field of cred.fields) {
|
|
1068
|
-
head += '<div class="credential-field"><label>' +
|
|
1069
|
-
esc(field.label) + '</label><input type="' +
|
|
1070
|
-
esc(field.inputType || "password") + '" data-credential-field="' +
|
|
1071
|
-
esc(field.name) + '" aria-label="' + esc(field.label) +
|
|
1072
|
-
'" placeholder="' + esc(field.placeholder || field.label) +
|
|
1073
|
-
'" autocomplete="' +
|
|
1074
|
-
(field.inputType === "password" ? "new-password" : "off") +
|
|
1075
|
-
'" autocapitalize="none" spellcheck="false"></div>';
|
|
1076
|
-
}
|
|
1077
|
-
head += "</div>";
|
|
1078
|
-
} else {
|
|
1079
|
-
head += '<input type="password" data-credential-input="' +
|
|
1080
|
-
esc(c.id) + '" aria-label="' + esc(cred.label) + '" placeholder="' +
|
|
1081
|
-
esc(cred.placeholder || "Paste credential") +
|
|
1082
|
-
'" autocomplete="new-password" autocapitalize="none" spellcheck="false">';
|
|
1083
|
-
}
|
|
1084
|
-
head += '<button class="linklike" type="button" data-credential-action="save" data-connector="' +
|
|
1085
|
-
esc(c.id) + '">Save</button><button class="linklike" type="button" data-credential-action="cancel" data-connector="' +
|
|
1086
|
-
esc(c.id) + '">Cancel</button></div>';
|
|
1087
|
-
} else {
|
|
1088
|
-
head += '<p class="credential-copy meta">Team sign in is required to manage this credential.</p>';
|
|
1089
|
-
}
|
|
1090
|
-
head += "</section>";
|
|
1350
|
+
head += '<p class="connector-auth"><a class="linklike" href="/credentials" ' +
|
|
1351
|
+
'data-operator-page="credentials">Manage credential →</a></p>';
|
|
1091
1352
|
}
|
|
1092
1353
|
let body = "";
|
|
1093
1354
|
if (tools.length) {
|
|
@@ -1105,12 +1366,122 @@ function render() {
|
|
|
1105
1366
|
list.appendChild(el);
|
|
1106
1367
|
}
|
|
1107
1368
|
if (!list.children.length) {
|
|
1108
|
-
list.innerHTML = '<p class="empty">
|
|
1369
|
+
list.innerHTML = '<p class="empty">' +
|
|
1370
|
+
(q
|
|
1371
|
+
? "No connectors or tools match this filter."
|
|
1372
|
+
: "No connectors are declared in this deployment.") +
|
|
1373
|
+
"</p>";
|
|
1109
1374
|
}
|
|
1110
1375
|
}
|
|
1111
1376
|
|
|
1112
|
-
|
|
1377
|
+
function renderCredentials() {
|
|
1378
|
+
const list = $("credentialList");
|
|
1379
|
+
list.innerHTML = "";
|
|
1380
|
+
if (DATA.credentialManagement !== "available") return;
|
|
1381
|
+
for (const c of DATA.connectors.filter((connector) => connector.credential)) {
|
|
1382
|
+
const cred = c.credential;
|
|
1383
|
+
const configured = Boolean(cred.configured);
|
|
1384
|
+
const removable = configured || Boolean(cred.removable);
|
|
1385
|
+
const state = configured
|
|
1386
|
+
? cred.fields?.length
|
|
1387
|
+
? "configured"
|
|
1388
|
+
: "configured · ••••" + esc(cred.lastFour || "")
|
|
1389
|
+
: "not configured";
|
|
1390
|
+
const updated = configured && cred.updatedAt
|
|
1391
|
+
? " · updated " + esc(formatDate(cred.updatedAt))
|
|
1392
|
+
: "";
|
|
1393
|
+
const el = document.createElement("section");
|
|
1394
|
+
el.className = "credential-card";
|
|
1395
|
+
el.id = "credential-" + c.id;
|
|
1396
|
+
el.setAttribute("aria-labelledby", "credential-title-" + c.id);
|
|
1397
|
+
let body = '<div class="credential-head"><div class="connector-title">' +
|
|
1398
|
+
'<span class="dot ' + (configured ? "ok" : "auth_required") +
|
|
1399
|
+
'" aria-hidden="true"></span><h2 id="credential-title-' + esc(c.id) + '">' +
|
|
1400
|
+
esc(c.title || c.id) + '</h2></div><span class="credential-state">' +
|
|
1401
|
+
state + updated + "</span></div>";
|
|
1402
|
+
body += '<p class="mono">' + esc(c.id) + " · " + esc(cred.label) + "</p>";
|
|
1403
|
+
if (cred.description) {
|
|
1404
|
+
body += '<p class="credential-copy meta">' + esc(cred.description) + "</p>";
|
|
1405
|
+
}
|
|
1406
|
+
if (cred.fields?.length) {
|
|
1407
|
+
body += '<div class="credential-field-summary">';
|
|
1408
|
+
for (const field of cred.fields) {
|
|
1409
|
+
const fieldState = field.configured
|
|
1410
|
+
? "configured · ••••" + esc(field.lastFour || "") +
|
|
1411
|
+
(field.updatedAt ? " · updated " + esc(formatDate(field.updatedAt)) : "")
|
|
1412
|
+
: "not configured";
|
|
1413
|
+
body += '<div><span>' + esc(field.label) +
|
|
1414
|
+
'</span><span class="meta">' + fieldState + "</span></div>";
|
|
1415
|
+
}
|
|
1416
|
+
body += "</div>";
|
|
1417
|
+
}
|
|
1418
|
+
if (c.credentialCheck) {
|
|
1419
|
+
const check = c.credentialCheck;
|
|
1420
|
+
const verdict = check.state === "ok"
|
|
1421
|
+
? "healthy"
|
|
1422
|
+
: check.state === "auth_required"
|
|
1423
|
+
? "needs authorization"
|
|
1424
|
+
: "check failed";
|
|
1425
|
+
body += '<p class="connector-check meta">Liveness: ' + esc(verdict) +
|
|
1426
|
+
" · " + esc(formatDate(check.checkedAt)) +
|
|
1427
|
+
(check.message ? " — " + esc(check.message) : "") + "</p>";
|
|
1428
|
+
}
|
|
1429
|
+
if (cred.error) body += '<div class="msg">' + esc(cred.error) + "</div>";
|
|
1430
|
+
// Leftover stored fields are not an error — the credential still works, so
|
|
1431
|
+
// this stays muted copy rather than the msg block an actual failure earns.
|
|
1432
|
+
if (cred.notice) {
|
|
1433
|
+
body += '<p class="credential-copy meta">' + esc(cred.notice) + "</p>";
|
|
1434
|
+
}
|
|
1435
|
+
body += '<div class="credential-actions">';
|
|
1436
|
+
body += '<button class="linklike" type="button" data-credential-action="edit" data-connector="' +
|
|
1437
|
+
esc(c.id) + '">' + (removable ? "Replace" : "Add credential") + "</button>";
|
|
1438
|
+
if (configured && cred.testable) {
|
|
1439
|
+
body += '<button class="linklike" type="button" data-credential-action="test" data-connector="' +
|
|
1440
|
+
esc(c.id) + '">Test</button>';
|
|
1441
|
+
}
|
|
1442
|
+
if (removable) {
|
|
1443
|
+
body += '<button type="button" class="linklike danger" data-credential-action="remove" data-connector="' +
|
|
1444
|
+
esc(c.id) + '">Remove</button>';
|
|
1445
|
+
}
|
|
1446
|
+
body += "</div>";
|
|
1447
|
+
body += '<div class="credential-form hidden" data-credential-form="' +
|
|
1448
|
+
esc(c.id) + '">';
|
|
1449
|
+
if (cred.fields && cred.fields.length) {
|
|
1450
|
+
body += '<div class="credential-fields">';
|
|
1451
|
+
for (let index = 0; index < cred.fields.length; index += 1) {
|
|
1452
|
+
const field = cred.fields[index];
|
|
1453
|
+
const inputId = "credential-input-" + c.id + "-" + index;
|
|
1454
|
+
body += '<div class="credential-field"><label for="' + esc(inputId) + '">' +
|
|
1455
|
+
esc(field.label) + '</label><input id="' + esc(inputId) + '" type="' +
|
|
1456
|
+
esc(field.inputType || "password") + '" data-credential-field="' +
|
|
1457
|
+
esc(field.name) + '" placeholder="' + esc(field.placeholder || field.label) +
|
|
1458
|
+
'" autocomplete="' +
|
|
1459
|
+
(field.inputType === "password" ? "new-password" : "off") +
|
|
1460
|
+
'" autocapitalize="none" spellcheck="false"></div>';
|
|
1461
|
+
}
|
|
1462
|
+
body += "</div>";
|
|
1463
|
+
} else {
|
|
1464
|
+
const inputId = "credential-input-" + c.id;
|
|
1465
|
+
body += '<label class="visually-hidden" for="' + esc(inputId) + '">' +
|
|
1466
|
+
esc(cred.label) + '</label><input id="' + esc(inputId) +
|
|
1467
|
+
'" type="password" data-credential-input="' +
|
|
1468
|
+
esc(c.id) + '" aria-label="' + esc(cred.label) + '" placeholder="' +
|
|
1469
|
+
esc(cred.placeholder || "Paste credential") +
|
|
1470
|
+
'" autocomplete="new-password" autocapitalize="none" spellcheck="false">';
|
|
1471
|
+
}
|
|
1472
|
+
body += '<button class="linklike" type="button" data-credential-action="save" data-connector="' +
|
|
1473
|
+
esc(c.id) + '">Save</button><button class="linklike" type="button" data-credential-action="cancel" data-connector="' +
|
|
1474
|
+
esc(c.id) + '">Cancel</button></div>';
|
|
1475
|
+
el.innerHTML = body;
|
|
1476
|
+
list.appendChild(el);
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
async function credentialRequest(connector, method, action, body, generation) {
|
|
1113
1481
|
const token = await sessionToken();
|
|
1482
|
+
if (generation !== SESSION_GENERATION) {
|
|
1483
|
+
throw new Error("The operator session changed.");
|
|
1484
|
+
}
|
|
1114
1485
|
if (!token) throw new Error("Your Clerk session has expired.");
|
|
1115
1486
|
const suffix = action ? "/" + action : "";
|
|
1116
1487
|
const res = await fetch(
|
|
@@ -1136,12 +1507,13 @@ function credentialForm(connector) {
|
|
|
1136
1507
|
CSS.escape(connector) + '"]');
|
|
1137
1508
|
}
|
|
1138
1509
|
|
|
1139
|
-
$("
|
|
1510
|
+
$("credentialList").onclick = async (event) => {
|
|
1140
1511
|
const button = event.target.closest("[data-credential-action]");
|
|
1141
1512
|
if (!button) return;
|
|
1142
1513
|
const connector = button.dataset.connector;
|
|
1143
1514
|
const action = button.dataset.credentialAction;
|
|
1144
1515
|
const form = credentialForm(connector);
|
|
1516
|
+
const generation = SESSION_GENERATION;
|
|
1145
1517
|
|
|
1146
1518
|
if (action === "edit") {
|
|
1147
1519
|
form.classList.remove("hidden");
|
|
@@ -1151,6 +1523,10 @@ $("list").onclick = async (event) => {
|
|
|
1151
1523
|
if (action === "cancel") {
|
|
1152
1524
|
form.querySelectorAll("input").forEach((input) => { input.value = ""; });
|
|
1153
1525
|
form.classList.add("hidden");
|
|
1526
|
+
document.querySelector(
|
|
1527
|
+
'[data-credential-action="edit"][data-connector="' +
|
|
1528
|
+
CSS.escape(connector) + '"]'
|
|
1529
|
+
)?.focus();
|
|
1154
1530
|
return;
|
|
1155
1531
|
}
|
|
1156
1532
|
if (action === "remove" && !window.confirm(
|
|
@@ -1158,6 +1534,7 @@ $("list").onclick = async (event) => {
|
|
|
1158
1534
|
)) return;
|
|
1159
1535
|
|
|
1160
1536
|
setNotice("");
|
|
1537
|
+
$("credentialList").setAttribute("aria-busy", "true");
|
|
1161
1538
|
const buttons = [...document.querySelectorAll(
|
|
1162
1539
|
'[data-connector="' + CSS.escape(connector) + '"]'
|
|
1163
1540
|
)];
|
|
@@ -1172,42 +1549,65 @@ $("list").onclick = async (event) => {
|
|
|
1172
1549
|
if (!value) throw new Error("Complete every credential field before saving.");
|
|
1173
1550
|
values[input.dataset.credentialField] = value;
|
|
1174
1551
|
}
|
|
1175
|
-
await credentialRequest(connector, "PUT", "", { values });
|
|
1552
|
+
await credentialRequest(connector, "PUT", "", { values }, generation);
|
|
1176
1553
|
} else {
|
|
1177
1554
|
const input = form.querySelector("[data-credential-input]");
|
|
1178
1555
|
const value = input.value.trim();
|
|
1179
1556
|
if (!value) throw new Error("Paste a credential before saving.");
|
|
1180
|
-
await credentialRequest(connector, "PUT", "", { value });
|
|
1557
|
+
await credentialRequest(connector, "PUT", "", { value }, generation);
|
|
1181
1558
|
}
|
|
1559
|
+
if (generation !== SESSION_GENERATION) return;
|
|
1182
1560
|
form.querySelectorAll("input").forEach((input) => { input.value = ""; });
|
|
1183
1561
|
setNotice("Credential saved.");
|
|
1184
1562
|
await load();
|
|
1563
|
+
if (generation !== SESSION_GENERATION) return;
|
|
1564
|
+
$("credentialNotice").focus();
|
|
1185
1565
|
} else if (action === "remove") {
|
|
1186
|
-
await credentialRequest(connector, "DELETE");
|
|
1566
|
+
await credentialRequest(connector, "DELETE", "", null, generation);
|
|
1567
|
+
if (generation !== SESSION_GENERATION) return;
|
|
1187
1568
|
setNotice("Credential removed.");
|
|
1188
1569
|
await load();
|
|
1570
|
+
if (generation !== SESSION_GENERATION) return;
|
|
1571
|
+
$("credentialNotice").focus();
|
|
1189
1572
|
} else if (action === "test") {
|
|
1190
|
-
const result = await credentialRequest(
|
|
1573
|
+
const result = await credentialRequest(
|
|
1574
|
+
connector,
|
|
1575
|
+
"POST",
|
|
1576
|
+
"test",
|
|
1577
|
+
null,
|
|
1578
|
+
generation,
|
|
1579
|
+
);
|
|
1580
|
+
if (generation !== SESSION_GENERATION) return;
|
|
1191
1581
|
setNotice(
|
|
1192
1582
|
result.message || (result.ok ? "Credential is valid." : "Credential test failed."),
|
|
1193
1583
|
!result.ok,
|
|
1194
1584
|
);
|
|
1195
1585
|
}
|
|
1196
1586
|
} catch (e) {
|
|
1587
|
+
if (generation !== SESSION_GENERATION) return;
|
|
1197
1588
|
setNotice(e.message || "Credential action failed.", true);
|
|
1198
1589
|
} finally {
|
|
1199
|
-
|
|
1590
|
+
if (generation === SESSION_GENERATION) {
|
|
1591
|
+
$("credentialList").setAttribute("aria-busy", "false");
|
|
1592
|
+
buttons.forEach((item) => { item.disabled = false; });
|
|
1593
|
+
}
|
|
1200
1594
|
}
|
|
1201
1595
|
};
|
|
1202
1596
|
|
|
1203
|
-
$("save").onclick = () => {
|
|
1597
|
+
$("save").onclick = async () => {
|
|
1204
1598
|
const v = $("token").value.trim();
|
|
1205
1599
|
if (!v) return;
|
|
1600
|
+
clearIdentityState();
|
|
1206
1601
|
localStorage.setItem(KEY, v);
|
|
1207
1602
|
$("token").value = "";
|
|
1208
|
-
load();
|
|
1603
|
+
await load();
|
|
1604
|
+
if (DATA) $(CURRENT_PAGE + "Heading").focus();
|
|
1605
|
+
};
|
|
1606
|
+
$("change").onclick = () => {
|
|
1607
|
+
localStorage.removeItem(KEY);
|
|
1608
|
+
showGate("");
|
|
1609
|
+
$("token").focus();
|
|
1209
1610
|
};
|
|
1210
|
-
$("change").onclick = () => { localStorage.removeItem(KEY); showGate(""); };
|
|
1211
1611
|
$("copyMcpUrl").onclick = async () => {
|
|
1212
1612
|
const button = $("copyMcpUrl");
|
|
1213
1613
|
try {
|
|
@@ -1222,14 +1622,35 @@ $("signin").onclick = () => Clerk.redirectToSignIn({
|
|
|
1222
1622
|
signInFallbackRedirectUrl: window.location.href,
|
|
1223
1623
|
signUpFallbackRedirectUrl: window.location.href,
|
|
1224
1624
|
});
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
$("
|
|
1625
|
+
function signOut() {
|
|
1626
|
+
clearIdentityState();
|
|
1627
|
+
return Clerk.signOut({ redirectUrl: window.location.href });
|
|
1628
|
+
}
|
|
1629
|
+
$("gateSignout").onclick = signOut;
|
|
1630
|
+
$("signout").onclick = signOut;
|
|
1631
|
+
$("filter").oninput = () => { if (DATA) renderConnections(); };
|
|
1230
1632
|
$("refreshActivity").onclick = () => loadActivity(true);
|
|
1231
1633
|
$("moreActivity").onclick = () => loadActivity(false);
|
|
1232
1634
|
$("activitySearch").oninput = () => renderActivity();
|
|
1635
|
+
document.addEventListener("click", (event) => {
|
|
1636
|
+
const link = event.target.closest("a[data-operator-page]");
|
|
1637
|
+
if (
|
|
1638
|
+
!link ||
|
|
1639
|
+
event.defaultPrevented ||
|
|
1640
|
+
event.button !== 0 ||
|
|
1641
|
+
event.metaKey ||
|
|
1642
|
+
event.ctrlKey ||
|
|
1643
|
+
event.shiftKey ||
|
|
1644
|
+
event.altKey
|
|
1645
|
+
) return;
|
|
1646
|
+
const target = new URL(link.href, window.location.href);
|
|
1647
|
+
if (target.origin !== window.location.origin) return;
|
|
1648
|
+
event.preventDefault();
|
|
1649
|
+
navigateTo(link.dataset.operatorPage, target.pathname + target.search + target.hash);
|
|
1650
|
+
});
|
|
1651
|
+
window.addEventListener("popstate", () => {
|
|
1652
|
+
activatePage(pageForPath(window.location.pathname), { focus: true });
|
|
1653
|
+
});
|
|
1233
1654
|
|
|
1234
1655
|
async function init() {
|
|
1235
1656
|
if (AUTH.kind === "clerk") {
|
|
@@ -1253,6 +1674,7 @@ async function init() {
|
|
|
1253
1674
|
$("tokenGate").classList.remove("hidden");
|
|
1254
1675
|
$("change").classList.remove("hidden");
|
|
1255
1676
|
}
|
|
1677
|
+
activatePage(pageForPath(window.location.pathname));
|
|
1256
1678
|
await load();
|
|
1257
1679
|
}
|
|
1258
1680
|
|