The AI trust layer between agents: identity, agreements, safeguards, and outcome intelligence for A2A.

Viewing files at commit 989559e001cdCommit detailsView latest

apps/dashboard/src/main.tsx 1408 lines · 46.3 KB · tsx
1
import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
import { createRoot } from 'react-dom/client';
3
import './styles.css';
4
5
declare const __AUTH0_DOMAIN__: string;
6
declare const __AUTH0_CLIENT_ID__: string;
7
declare const __AUTH0_AUDIENCE__: string;
8
9
type Auth0User = { sub: string; name?: string; email?: string; picture?: string };
10
type AuthSession = { user: Auth0User };
11
type AuthTransaction = { state: string; nonce: string; verifier: string };
12
type Theme = 'dark' | 'light';
13
14
const authTransactionKey = 'openclasp.auth0.transaction';
15
const themeKey = 'openclasp.theme.v1';
16
17
type DashboardData = {
18
  agents: Record<string, any>[];
19
  projects: Record<string, any>[];
20
  installations: Record<string, any>[];
21
  setupRequests: Record<string, any>[];
22
  publications: Record<string, any>[];
23
  interactions: Record<string, any>[];
24
  federatedInteractions: Record<string, any>[];
25
  events: Record<string, any>[];
26
  conflicts: Record<string, any>[];
27
  receipts: Record<string, any>[];
28
  profiles: Record<string, any>[];
29
  runtimes: Record<string, any>[];
30
};
31
32
type Settings = {
33
  displayName: string;
34
  contributionEnabled: boolean;
35
  retentionDays: number;
36
  evidenceSharing: 'never' | 'ask' | 'contract_only';
37
  rawConversationsStored: true;
38
};
39
40
const emptyData: DashboardData = {
41
  agents: [],
42
  projects: [],
43
  installations: [],
44
  setupRequests: [],
45
  publications: [],
46
  interactions: [],
47
  federatedInteractions: [],
48
  events: [],
49
  conflicts: [],
50
  receipts: [],
51
  profiles: [],
52
  runtimes: [],
53
};
54
const defaultSettings: Settings = {
55
  displayName: '',
56
  contributionEnabled: false,
57
  retentionDays: 30,
58
  evidenceSharing: 'ask',
59
  rawConversationsStored: true,
60
};
61
const pages = ['dashboard', 'history', 'agents', 'insights', 'connect', 'settings'] as const;
62
type Page = (typeof pages)[number];
63
64
function base64Url(bytes: Uint8Array) {
65
  let binary = '';
66
  for (const byte of bytes) binary += String.fromCharCode(byte);
67
  return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
68
}
69
70
function randomValue(size = 32) {
71
  return base64Url(crypto.getRandomValues(new Uint8Array(size)));
72
}
73
74
async function beginAuth(provider: 'google' | 'github') {
75
  const verifier = randomValue(48);
76
  const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));
77
  const transaction: AuthTransaction = { state: randomValue(), nonce: randomValue(), verifier };
78
  sessionStorage.setItem(authTransactionKey, JSON.stringify(transaction));
79
  const parameters = new URLSearchParams({
80
    client_id: __AUTH0_CLIENT_ID__,
81
    redirect_uri: `${location.origin}/sso-callback`,
82
    response_type: 'code',
83
    scope: 'openid profile email',
84
    audience: __AUTH0_AUDIENCE__,
85
    state: transaction.state,
86
    nonce: transaction.nonce,
87
    code_challenge: base64Url(new Uint8Array(digest)),
88
    code_challenge_method: 'S256',
89
    connection: provider === 'google' ? 'google-oauth2' : 'github',
90
  });
91
  location.assign(`https://${__AUTH0_DOMAIN__}/authorize?${parameters}`);
92
}
93
94
async function signOut() {
95
  await fetch('/api/session', { method: 'DELETE', credentials: 'same-origin' });
96
  const parameters = new URLSearchParams({
97
    client_id: __AUTH0_CLIENT_ID__,
98
    returnTo: `${location.origin}/login`,
99
  });
100
  location.assign(`https://${__AUTH0_DOMAIN__}/v2/logout?${parameters}`);
101
}
102
103
async function api(path: string, init?: RequestInit) {
104
  const headers = new Headers(init?.headers);
105
  if (init?.body && !headers.has('content-type')) headers.set('content-type', 'application/json');
106
  return fetch(path, {
107
    ...init,
108
    credentials: 'same-origin',
109
    headers,
110
  }).then(async (response) => {
111
    if (!response.ok)
112
      throw new Error((await response.text()) || `Request failed: ${response.status}`);
113
    return response.json();
114
  });
115
}
116
117
function route(): Page {
118
  const value = location.pathname.slice(1) || 'dashboard';
119
  return pages.includes(value as Page) ? (value as Page) : 'dashboard';
120
}
121
122
function initialTheme(): Theme {
123
  const saved = localStorage.getItem(themeKey);
124
  if (saved === 'dark' || saved === 'light') return saved;
125
  return matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
126
}
127
128
function App() {
129
  const [session, setSession] = useState<AuthSession | null>();
130
  const [theme, setTheme] = useState<Theme>(initialTheme);
131
  const [page, setPage] = useState<Page>(route());
132
  const [data, setData] = useState<DashboardData>(emptyData);
133
  const [settings, setSettings] = useState<Settings>(defaultSettings);
134
  const [loading, setLoading] = useState(true);
135
  const [error, setError] = useState('');
136
  const refreshDashboard = useCallback(async () => {
137
    setData((await api('/v0.1/dashboard')) as DashboardData);
138
  }, []);
139
140
  useEffect(() => {
141
    fetch('/api/session', { credentials: 'same-origin' })
142
      .then(async (response) => {
143
        if (!response.ok) return null;
144
        return (await response.json()) as AuthSession;
145
      })
146
      .then(setSession)
147
      .catch(() => setSession(null));
148
  }, []);
149
150
  useEffect(() => {
151
    document.documentElement.dataset.theme = theme;
152
    localStorage.setItem(themeKey, theme);
153
  }, [theme]);
154
155
  useEffect(() => {
156
    const onPopState = () => setPage(route());
157
    addEventListener('popstate', onPopState);
158
    return () => removeEventListener('popstate', onPopState);
159
  }, []);
160
161
  useEffect(() => {
162
    if (session === undefined) return;
163
    if (!session && location.pathname !== '/login') history.replaceState({}, '', '/login');
164
    if (!session) return;
165
    if (location.pathname === '/login') history.replaceState({}, '', '/dashboard');
166
    setPage(route());
167
    Promise.all([api('/v0.1/dashboard'), api('/v0.1/settings')])
168
      .then(([dashboard, accountSettings]) => {
169
        setData(dashboard as DashboardData);
170
        setSettings(accountSettings as Settings);
171
      })
172
      .catch((reason: unknown) =>
173
        setError(reason instanceof Error ? reason.message : 'Load failed'),
174
      )
175
      .finally(() => setLoading(false));
176
  }, [session]);
177
178
  useEffect(() => {
179
    if (!session) return;
180
    const refresh = () => {
181
      if (document.visibilityState === 'visible') void refreshDashboard().catch(() => undefined);
182
    };
183
    const timer = window.setInterval(refresh, 10_000);
184
    document.addEventListener('visibilitychange', refresh);
185
    return () => {
186
      window.clearInterval(timer);
187
      document.removeEventListener('visibilitychange', refresh);
188
    };
189
  }, [session, refreshDashboard]);
190
191
  const navigate = (next: Page) => {
192
    history.pushState({}, '', `/${next}`);
193
    setPage(next);
194
  };
195
196
  if (session === undefined) return <Loading />;
197
  if (!session) return <Login />;
198
199
  return (
200
    <div className="appShell">
201
      <aside>
202
        <div className="brand">
203
          <div className="mark">OC</div>
204
          <div>
205
            <strong>OpenClasp</strong>
206
            <small>ASSURANCE NETWORK</small>
207
          </div>
208
        </div>
209
        <nav>
210
          <Nav page="dashboard" active={page} onClick={navigate} label="Overview" glyph="⌂" />
211
          <Nav page="history" active={page} onClick={navigate} label="History" glyph="≡" />
212
          <Nav page="agents" active={page} onClick={navigate} label="Agents" glyph="◇" />
213
          <Nav page="insights" active={page} onClick={navigate} label="Insights" glyph="⌁" />
214
          <Nav page="connect" active={page} onClick={navigate} label="Connect" glyph="+" />
215
          <Nav page="settings" active={page} onClick={navigate} label="Settings" glyph="⚙" />
216
        </nav>
217
        <button
218
          className="themeSwitch"
219
          onClick={() => setTheme((current) => (current === 'dark' ? 'light' : 'dark'))}
220
          aria-label={`Switch to ${theme === 'dark' ? 'light' : 'dark'} theme`}
221
        >
222
          <span>{theme === 'dark' ? '☼' : '◐'}</span>
223
          {theme === 'dark' ? 'Light theme' : 'Dark theme'}
224
        </button>
225
        <div className="privacyStamp">
226
          <span className="liveDot" />
227
          <div>
228
            <strong>Privacy separated</strong>
229
            <small>Gateway bodies expire in 24h</small>
230
          </div>
231
        </div>
232
        <button className="account" onClick={() => void signOut()}>
233
          <span>{initials(session.user.name || session.user.email || 'OC')}</span>
234
          <div>
235
            <strong>{session.user.name || 'OpenClasp user'}</strong>
236
            <small>{session.user.email || 'Sign out'}</small>
237
          </div>
238
          <b>↗</b>
239
        </button>
240
      </aside>
241
      <main>
242
        {error && <div className="errorBar">{error}</div>}
243
        {loading ? (
244
          <Loading compact />
245
        ) : (
246
          <PageContent
247
            page={page}
248
            data={data}
249
            settings={settings}
250
            setSettings={setSettings}
251
            navigate={navigate}
252
            refreshDashboard={refreshDashboard}
253
          />
254
        )}
255
      </main>
256
    </div>
257
  );
258
}
259
260
function Login() {
261
  const [error, setError] = useState('');
262
  const continueWith = async (provider: 'google' | 'github') => {
263
    setError('');
264
    try {
265
      await beginAuth(provider);
266
    } catch {
267
      setError(`${provider === 'google' ? 'Google' : 'GitHub'} sign-in is not configured yet.`);
268
    }
269
  };
270
  return (
271
    <div className="loginPage">
272
      <section className="loginPitch">
273
        <div className="brand">
274
          <div className="mark">OC</div>
275
          <strong>OpenClasp</strong>
276
        </div>
277
        <div>
278
          <p className="eyebrow">AGENT-TO-AGENT ASSURANCE</p>
279
          <h1>Know who your agent is dealing with.</h1>
280
          <p>
281
            Verified identities, signed outcomes, private warnings, and contextual
282
            reliability—without using message bodies for scoring.
283
          </p>
284
        </div>
285
        <div className="loginProof">
286
          <span>01</span> User-owned history<span>02</span> No universal trust score<span>03</span>{' '}
287
          Explicit network consent
288
        </div>
289
      </section>
290
      <section className="loginCard">
291
        <div>
292
          <p className="eyebrow">SECURE ACCESS</p>
293
          <h2>Sign in to OpenClasp</h2>
294
          <p>
295
            Manage connected agents, review signed history, and control what contributes to the
296
            network.
297
          </p>
298
        </div>
299
        <div className="socialButtons">
300
          <button onClick={() => void continueWith('google')}>
301
            <span className="google">G</span> Continue with Google
302
          </button>
303
          <button onClick={() => void continueWith('github')}>
304
            <span>◉</span> Continue with GitHub
305
          </button>
306
        </div>
307
        {error && <div className="loginError">{error}</div>}
308
        <small>
309
          Google and GitHub handle authentication. OpenClasp never receives your password.
310
        </small>
311
      </section>
312
    </div>
313
  );
314
}
315
316
let callbackStarted = false;
317
function AuthCallback() {
318
  const [error, setError] = useState('');
319
  useEffect(() => {
320
    if (callbackStarted) return;
321
    callbackStarted = true;
322
    const finish = async () => {
323
      const query = new URLSearchParams(location.search);
324
      const oauthError = query.get('error_description') ?? query.get('error');
325
      if (oauthError) throw new Error(oauthError);
326
      const code = query.get('code');
327
      const returnedState = query.get('state');
328
      const rawTransaction = sessionStorage.getItem(authTransactionKey);
329
      if (!code || !returnedState || !rawTransaction) throw new Error('Missing OAuth transaction');
330
      const transaction = JSON.parse(rawTransaction) as AuthTransaction;
331
      sessionStorage.removeItem(authTransactionKey);
332
      if (returnedState !== transaction.state) throw new Error('OAuth state mismatch');
333
      const response = await fetch('/api/session', {
334
        method: 'POST',
335
        credentials: 'same-origin',
336
        headers: { 'content-type': 'application/json' },
337
        body: JSON.stringify({
338
          code,
339
          codeVerifier: transaction.verifier,
340
        }),
341
      });
342
      if (!response.ok) throw new Error('Auth0 token exchange failed');
343
      location.replace('/dashboard');
344
    };
345
    void finish().catch((reason: unknown) =>
346
      setError(reason instanceof Error ? reason.message : 'Authentication failed'),
347
    );
348
  }, []);
349
  if (error)
350
    return (
351
      <div className="loading">
352
        <span className="mark">OC</span>
353
        <p>{error}</p>
354
        <a href="/login">Return to login</a>
355
      </div>
356
    );
357
  return <Loading />;
358
}
359
360
function PageContent({
361
  page,
362
  data,
363
  settings,
364
  setSettings,
365
  navigate,
366
  refreshDashboard,
367
}: {
368
  page: Page;
369
  data: DashboardData;
370
  settings: Settings;
371
  setSettings: React.Dispatch<React.SetStateAction<Settings>>;
372
  navigate: (page: Page) => void;
373
  refreshDashboard: () => Promise<void>;
374
}) {
375
  if (page === 'history') return <History data={data} />;
376
  if (page === 'agents')
377
    return <Agents data={data} navigate={navigate} refreshDashboard={refreshDashboard} />;
378
  if (page === 'insights') return <Insights data={data} />;
379
  if (page === 'connect') return <Connect data={data} refreshDashboard={refreshDashboard} />;
380
  if (page === 'settings') return <SettingsPage settings={settings} setSettings={setSettings} />;
381
  return <Overview data={data} navigate={navigate} refreshDashboard={refreshDashboard} />;
382
}
383
384
function Overview({
385
  data,
386
  navigate,
387
  refreshDashboard,
388
}: {
389
  data: DashboardData;
390
  navigate: (page: Page) => void;
391
  refreshDashboard: () => Promise<void>;
392
}) {
393
  const completed = data.receipts.filter((item) => item.outcome === 'success').length;
394
  const warnings = data.events.filter((item) =>
395
    ['policy_warning', 'policy_violation', 'objection'].includes(String(item.eventType)),
396
  ).length;
397
  const publishedIds = new Set(
398
    data.publications
399
      .filter((publication) => publication.published)
400
      .map((publication) => String(publication.agentId)),
401
  );
402
  const readyAgents = data.agents.filter((agent) => publishedIds.has(String(agent.agentId))).length;
403
  const onlineAgents = data.agents.filter((agent) => agent.presence?.status === 'online').length;
404
  const pendingInvitations = data.federatedInteractions.filter(
405
    (interaction) => interaction.status === 'pending',
406
  ).length;
407
  return (
408
    <>
409
      <PageHead
410
        eyebrow="NETWORK OVERVIEW"
411
        title="Assurance, at a glance."
412
        action="Connect agent"
413
        onAction={() => navigate('connect')}
414
      />
415
      <section
416
        className={`readiness ${readyAgents === data.agents.length && readyAgents ? 'ready' : ''}`}
417
      >
418
        <div>
419
          <span className="statusOrb">{readyAgents ? '✓' : '!'}</span>
420
          <div>
421
            <strong>
422
              {readyAgents === data.agents.length && readyAgents
423
                ? 'Your agent network is ready'
424
                : `${readyAgents} of ${data.agents.length} agents can receive A2A work`}
425
            </strong>
426
            <small>
427
              Safe matching requests activate automatically. Everything else waits for approval.
428
            </small>
429
          </div>
430
        </div>
431
        <button className="secondary" onClick={() => navigate('agents')}>
432
          Manage automation
433
        </button>
434
      </section>
435
      <section className="metrics">
436
        <Metric
437
          label="Online agents"
438
          value={onlineAgents}
439
          note={`${data.agents.length - onlineAgents} offline · 2 min window`}
440
        />
441
        <Metric
442
          label="Interactions"
443
          value={data.interactions.length + data.federatedInteractions.length}
444
          note="local and shared"
445
        />
446
        <Metric label="Successful outcomes" value={completed} note="receipt-backed" />
447
        <Metric
448
          label="Needs review"
449
          value={warnings + pendingInvitations}
450
          note="warnings or approvals"
451
          warn={warnings + pendingInvitations > 0}
452
        />
453
      </section>
454
      <Invitations data={data} refreshDashboard={refreshDashboard} />
455
      <section className="contentGrid">
456
        <Panel title="Recent activity" subtitle="Structured events and signed outcomes">
457
          <Timeline events={data.events.slice(-6).reverse()} />
458
          <TextButton onClick={() => navigate('history')}>View complete history →</TextButton>
459
        </Panel>
460
        <Panel
461
          title="Reliability context"
462
          subtitle="Task-specific profiles, never a universal score"
463
        >
464
          {data.profiles.length ? (
465
            data.profiles
466
              .slice(0, 4)
467
              .map((profile) => (
468
                <Profile key={`${profile.agentId}-${profile.taskCategory}`} profile={profile} />
469
              ))
470
          ) : (
471
            <Empty
472
              title="No reliability profile yet"
473
              text="Profiles appear after signed receipts and eligible bilateral feedback."
474
            />
475
          )}
476
        </Panel>
477
      </section>
478
    </>
479
  );
480
}
481
482
function History({ data }: { data: DashboardData }) {
483
  const records = useMemo(
484
    () =>
485
      [
486
        ...data.interactions.map((value) => ({ ...value, _kind: 'interaction' })),
487
        ...data.federatedInteractions.map((value) => ({
488
          ...value,
489
          _kind: 'federated interaction',
490
        })),
491
        ...data.events.map((value) => ({ ...value, _kind: 'event' })),
492
        ...data.receipts.map((value) => ({ ...value, _kind: 'receipt' })),
493
        ...data.conflicts.map((value) => ({ ...value, _kind: 'dispute' })),
494
      ].sort((a, b) => Date.parse(timestamp(b)) - Date.parse(timestamp(a))),
495
    [data],
496
  );
497
  return (
498
    <>
499
      <PageHead eyebrow="AUDIT HISTORY" title="What happened, and what proves it." />
500
      <Panel
501
        title="Account history"
502
        subtitle="Only structured metadata, signatures, hashes, and permitted evidence are hosted"
503
      >
504
        {records.length ? (
505
          records.map((item, index) => (
506
            <HistoryRow
507
              key={String(
508
                (item as Record<string, any>).eventId ??
509
                  (item as Record<string, any>).receiptId ??
510
                  (item as Record<string, any>).interactionId ??
511
                  index,
512
              )}
513
              item={item}
514
            />
515
          ))
516
        ) : (
517
          <Empty
518
            title="No interactions recorded"
519
            text="Connect an agent and start an assured interaction. Raw message bodies will not appear here."
520
          />
521
        )}
522
      </Panel>
523
    </>
524
  );
525
}
526
527
function Agents({
528
  data,
529
  navigate,
530
  refreshDashboard,
531
}: {
532
  data: DashboardData;
533
  navigate: (page: Page) => void;
534
  refreshDashboard: () => Promise<void>;
535
}) {
536
  const [working, setWorking] = useState('');
537
  const [error, setError] = useState('');
538
  const [runtimeSecrets, setRuntimeSecrets] = useState<Record<string, string>>({});
539
  const saveAutomation = async (
540
    agentId: string,
541
    value: {
542
      autoPublish: boolean;
543
      autoAcceptPolicy: 'off' | 'safe_matching';
544
      autoAcceptTaskCategories: string[];
545
    },
546
  ) => {
547
    setWorking(agentId);
548
    setError('');
549
    try {
550
      await api(`/v0.1/agents/${encodeURIComponent(agentId)}/automation`, {
551
        method: 'PUT',
552
        body: JSON.stringify(value),
553
      });
554
      await refreshDashboard();
555
    } catch (reason) {
556
      setError(reason instanceof Error ? reason.message : 'Directory update failed');
557
    } finally {
558
      setWorking('');
559
    }
560
  };
561
  const saveRuntime = async (agentId: string, endpoint: string) => {
562
    setWorking(`runtime:${agentId}`);
563
    setError('');
564
    try {
565
      const result = (await api(`/v0.1/agents/${encodeURIComponent(agentId)}/runtime`, {
566
        method: 'PUT',
567
        body: JSON.stringify({ endpoint }),
568
      })) as { signingSecret: string };
569
      setRuntimeSecrets((current) => ({ ...current, [agentId]: result.signingSecret }));
570
      await refreshDashboard();
571
    } catch (reason) {
572
      setError(reason instanceof Error ? reason.message : 'Runtime verification failed');
573
    } finally {
574
      setWorking('');
575
    }
576
  };
577
  const disableRuntime = async (agentId: string) => {
578
    setWorking(`runtime:${agentId}`);
579
    setError('');
580
    try {
581
      await api(`/v0.1/agents/${encodeURIComponent(agentId)}/runtime`, { method: 'DELETE' });
582
      await refreshDashboard();
583
    } catch (reason) {
584
      setError(reason instanceof Error ? reason.message : 'Runtime update failed');
585
    } finally {
586
      setWorking('');
587
    }
588
  };
589
  return (
590
    <>
591
      <PageHead
592
        eyebrow="IDENTITY REGISTRY"
593
        title="Your connected agents."
594
        action="Connect agent"
595
        onAction={() => navigate('connect')}
596
      />
597
      <div className="notice">
598
        <strong>One approval, then automatic.</strong> OpenClasp creates and hosts the agent's A2A
599
        endpoint. Safe matching tasks can be accepted automatically; sensitive or mismatched
600
        requests still require review.
601
      </div>
602
      {error && <div className="errorBar">{error}</div>}
603
      <section className="agentGrid">
604
        {data.agents.length ? (
605
          data.agents.map((agent) => (
606
            <AgentCard
607
              key={agent.agentId}
608
              agent={agent}
609
              projectName={
610
                data.projects.find((project) => project.projectId === agent.projectId)?.name
611
              }
612
              published={data.publications.some(
613
                (publication) => publication.agentId === agent.agentId && publication.published,
614
              )}
615
              working={working === agent.agentId}
616
              onSave={(value) => saveAutomation(agent.agentId, value)}
617
              runtime={data.runtimes.find((runtime) => runtime.agentId === agent.agentId)}
618
              runtimeSecret={runtimeSecrets[agent.agentId]}
619
              runtimeWorking={working === `runtime:${agent.agentId}`}
620
              onRuntime={(endpoint) => saveRuntime(agent.agentId, endpoint)}
621
              onDisableRuntime={() => disableRuntime(agent.agentId)}
622
            />
623
          ))
624
        ) : (
625
          <Empty
626
            title="No agents connected"
627
            text="Install the remote MCP endpoint in an agent, sign in, then create and register its identity."
628
            action="Open connection guide"
629
            onAction={() => navigate('connect')}
630
          />
631
        )}
632
      </section>
633
    </>
634
  );
635
}
636
637
function Invitations({
638
  data,
639
  refreshDashboard,
640
}: {
641
  data: DashboardData;
642
  refreshDashboard: () => Promise<void>;
643
}) {
644
  const [working, setWorking] = useState('');
645
  const [error, setError] = useState('');
646
  const ownedAgentIds = useMemo(
647
    () => new Set(data.agents.map((agent) => String(agent.agentId))),
648
    [data.agents],
649
  );
650
  const incoming = data.federatedInteractions.filter(
651
    (interaction) =>
652
      interaction.status === 'pending' && ownedAgentIds.has(String(interaction.responderAgentId)),
653
  );
654
  const respond = async (interactionId: string, agentId: string, decision: 'accept' | 'reject') => {
655
    setWorking(interactionId);
656
    setError('');
657
    try {
658
      await api(`/v0.1/federated-interactions/${encodeURIComponent(interactionId)}/respond`, {
659
        method: 'POST',
660
        body: JSON.stringify({ agentId, decision }),
661
      });
662
      await refreshDashboard();
663
    } catch (reason) {
664
      setError(reason instanceof Error ? reason.message : 'Could not respond to invitation');
665
    } finally {
666
      setWorking('');
667
    }
668
  };
669
  if (!incoming.length && !data.federatedInteractions.length) return null;
670
  return (
671
    <Panel
672
      title={incoming.length ? `Agent invitations (${incoming.length})` : 'Shared interactions'}
673
      subtitle="Both accounts see the same immutable contract and acceptance state"
674
    >
675
      {error ? <div className="errorBar">{error}</div> : null}
676
      {(incoming.length ? incoming : data.federatedInteractions.slice(0, 5)).map((interaction) => (
677
        <article className="setupRequest" key={interaction.interactionId}>
678
          <div>
679
            <strong>{interaction.contract?.purpose ?? interaction.interactionId}</strong>
680
            <small>
681
              {interaction.initiatorAgentId} → {interaction.responderAgentId} · {interaction.status}
682
            </small>
683
            <small>Contract: {String(interaction.termsHash).slice(0, 16)}…</small>
684
            {interaction.status === 'active' &&
685
            interaction.acceptances?.[interaction.responderAgentId]?.method ===
686
              'policy_auto_accept' ? (
687
              <small className="autoAccepted">✓ Auto-approved by the responder's safe policy</small>
688
            ) : null}
689
          </div>
690
          {interaction.status === 'pending' &&
691
          ownedAgentIds.has(String(interaction.responderAgentId)) ? (
692
            <div className="decisionButtons">
693
              <button
694
                className="secondary"
695
                disabled={working === interaction.interactionId}
696
                onClick={() =>
697
                  void respond(interaction.interactionId, interaction.responderAgentId, 'reject')
698
                }
699
              >
700
                Reject
701
              </button>
702
              <button
703
                className="primary"
704
                disabled={working === interaction.interactionId}
705
                onClick={() =>
706
                  void respond(interaction.interactionId, interaction.responderAgentId, 'accept')
707
                }
708
              >
709
                {working === interaction.interactionId ? 'Working…' : 'Accept contract'}
710
              </button>
711
            </div>
712
          ) : null}
713
        </article>
714
      ))}
715
    </Panel>
716
  );
717
}
718
719
function Insights({ data }: { data: DashboardData }) {
720
  return (
721
    <>
722
      <PageHead eyebrow="BEHAVIOURAL CONTEXT" title="Evidence, not reputation theatre." />
723
      <div className="notice">
724
        <strong>Context matters.</strong> OpenClasp profiles an agent by task category and version.
725
        It does not produce a single universal trust score.
726
      </div>
727
      <section className="agentGrid">
728
        {data.profiles.length ? (
729
          data.profiles.map((profile) => (
730
            <Panel
731
              key={`${profile.agentId}-${profile.taskCategory}`}
732
              title={profile.agentId}
733
              subtitle={`${profile.taskCategory} · v${profile.agentVersion}`}
734
            >
735
              <Profile profile={profile} expanded />
736
            </Panel>
737
          ))
738
        ) : (
739
          <Empty
740
            title="Not enough verified evidence"
741
            text="Insights grow only from signed outcomes, eligible feedback, corrections, disputes, and version-aware history."
742
          />
743
        )}
744
      </section>
745
    </>
746
  );
747
}
748
749
function Connect({
750
  data,
751
  refreshDashboard,
752
}: {
753
  data: DashboardData;
754
  refreshDashboard: () => Promise<void>;
755
}) {
756
  const endpoint = 'https://openclasp.vercel.app/mcp';
757
  const [copied, setCopied] = useState(false);
758
  const [working, setWorking] = useState('');
759
  const [decisionError, setDecisionError] = useState('');
760
  const pending = data.setupRequests.filter((request) => request.status === 'pending');
761
  useEffect(() => {
762
    const poll = () => void refreshDashboard().catch(() => undefined);
763
    poll();
764
    const timer = window.setInterval(poll, 5000);
765
    return () => window.clearInterval(timer);
766
  }, [refreshDashboard]);
767
  const decide = async (requestId: string, decision: 'approve' | 'reject') => {
768
    setWorking(requestId);
769
    setDecisionError('');
770
    try {
771
      await api(`/v0.1/onboarding/${encodeURIComponent(requestId)}/${decision}`, {
772
        method: 'POST',
773
      });
774
      await refreshDashboard();
775
    } catch (reason) {
776
      setDecisionError(reason instanceof Error ? reason.message : 'Could not update setup request');
777
    } finally {
778
      setWorking('');
779
    }
780
  };
781
  return (
782
    <>
783
      <PageHead eyebrow="AGENT CONNECTION" title="Add OpenClasp to an agent." />
784
      {pending.length > 0 && (
785
        <section className="setupRequests">
786
          <div>
787
            <p className="eyebrow">CONFIRMATION REQUIRED</p>
788
            <h2>Approve agent setup</h2>
789
            <p>
790
              An agent proposed this identity. Confirm it before OpenClasp binds the installation.
791
            </p>
792
          </div>
793
          {pending.map((request) => (
794
            <article className="setupRequest" key={request.requestId}>
795
              <div>
796
                <strong>{request.agentName ?? 'Switch installation'}</strong>
797
                <small>
798
                  {request.projectName ?? request.existingAgentId} · {request.framework}
799
                </small>
800
                {!!request.capabilities?.length && (
801
                  <div className="tags">
802
                    {request.capabilities.slice(0, 5).map((capability: string) => (
803
                      <span key={capability}>{capability}</span>
804
                    ))}
805
                  </div>
806
                )}
807
                {!!request.limitations?.length && (
808
                  <small>Limitations: {request.limitations.join(' · ')}</small>
809
                )}
810
                <div className="automationPreview">
811
                  <span>{request.autoPublish ? 'Public after approval' : 'Private'}</span>
812
                  <span>
813
                    {request.autoAcceptPolicy === 'safe_matching'
814
                      ? 'Auto-accept safe matches'
815
                      : 'Review every request'}
816
                  </span>
817
                </div>
818
              </div>
819
              <div className="decisionButtons">
820
                <button
821
                  className="secondary"
822
                  disabled={working === request.requestId}
823
                  onClick={() => void decide(request.requestId, 'reject')}
824
                >
825
                  Reject
826
                </button>
827
                <button
828
                  className="primary"
829
                  disabled={working === request.requestId}
830
                  onClick={() => void decide(request.requestId, 'approve')}
831
                >
832
                  {working === request.requestId ? 'Working…' : 'Approve & automate'}
833
                </button>
834
              </div>
835
            </article>
836
          ))}
837
          {decisionError && <div className="loginError">{decisionError}</div>}
838
        </section>
839
      )}
840
      <section className="connectLayout">
841
        <Panel title="Connect once" subtitle="OpenClasp handles the routine steps afterward">
842
          <div className="endpoint">
843
            <code>{endpoint}</code>
844
            <button
845
              onClick={() => {
846
                void navigator.clipboard.writeText(endpoint);
847
                setCopied(true);
848
              }}
849
            >
850
              {copied ? 'Copied' : 'Copy'}
851
            </button>
852
          </div>
853
          <ol>
854
            <li>Add this MCP URL and complete the OAuth sign-in.</li>
855
            <li>
856
              Tell the agent: <code>Set yourself up on OpenClasp</code>.
857
            </li>
858
            <li>
859
              Approve its identity and automation policy here once. That is the last routine manual
860
              step.
861
            </li>
862
          </ol>
863
        </Panel>
864
        <Panel
865
          title="What becomes automatic"
866
          subtitle="Safe defaults with deterministic approval boundaries"
867
        >
868
          <ul className="checkList">
869
            <li>Agent Card publication and discovery</li>
870
            <li>Contract defaults inferred from the task</li>
871
            <li>Safe matching invitations accepted immediately</li>
872
            <li>Ready-to-send A2A endpoint, headers, and metadata</li>
873
            <li>Risky requests routed to human review</li>
874
          </ul>
875
        </Panel>
876
      </section>
877
    </>
878
  );
879
}
880
881
function SettingsPage({
882
  settings,
883
  setSettings,
884
}: {
885
  settings: Settings;
886
  setSettings: React.Dispatch<React.SetStateAction<Settings>>;
887
}) {
888
  const [saving, setSaving] = useState(false);
889
  const [saved, setSaved] = useState(false);
890
  const save = useCallback(async () => {
891
    setSaving(true);
892
    setSaved(false);
893
    const updated = (await api('/v0.1/settings', {
894
      method: 'PUT',
895
      body: JSON.stringify(settings),
896
    })) as Settings;
897
    setSettings(updated);
898
    setSaving(false);
899
    setSaved(true);
900
  }, [settings, setSettings]);
901
  return (
902
    <>
903
      <PageHead eyebrow="ACCOUNT CONTROLS" title="Privacy and network settings." />
904
      <section className="settingsCard">
905
        <Setting label="Display name" description="Shown only inside your OpenClasp account.">
906
          <input
907
            value={settings.displayName}
908
            onChange={(event) =>
909
              setSettings((value) => ({ ...value, displayName: event.target.value }))
910
            }
911
            placeholder="Team or operator name"
912
          />
913
        </Setting>
914
        <Setting
915
          label="Network contribution"
916
          description="Allow eligible structured aggregates to improve contextual intelligence. Raw conversations are never included."
917
        >
918
          <Toggle
919
            checked={settings.contributionEnabled}
920
            onChange={(checked) =>
921
              setSettings((value) => ({ ...value, contributionEnabled: checked }))
922
            }
923
          />
924
        </Setting>
925
        <Setting
926
          label="Evidence sharing"
927
          description="Controls when permitted evidence references may be shared with interaction participants."
928
        >
929
          <select
930
            value={settings.evidenceSharing}
931
            onChange={(event) =>
932
              setSettings((value) => ({
933
                ...value,
934
                evidenceSharing: event.target.value as Settings['evidenceSharing'],
935
              }))
936
            }
937
          >
938
            <option value="never">Never</option>
939
            <option value="ask">Ask every time</option>
940
            <option value="contract_only">When contract permits</option>
941
          </select>
942
        </Setting>
943
        <Setting
944
          label="Structured record retention"
945
          description="Retention for hosted account records. Signed protocol records may require separate revocation markers."
946
        >
947
          <select
948
            value={settings.retentionDays}
949
            onChange={(event) =>
950
              setSettings((value) => ({ ...value, retentionDays: Number(event.target.value) }))
951
            }
952
          >
953
            <option value={7}>7 days</option>
954
            <option value={30}>30 days</option>
955
            <option value={90}>90 days</option>
956
            <option value={365}>1 year</option>
957
            <option value={0}>No automatic deletion</option>
958
          </select>
959
        </Setting>
960
        <Setting
961
          label="Gateway message bodies"
962
          description="Encrypted at rest, deleted on acknowledgement, and automatically expired after 24 hours. Never used for profiles."
963
        >
964
          <span className="locked">24H MAX</span>
965
        </Setting>
966
        <div className="saveRow">
967
          <button className="primary" onClick={() => void save()} disabled={saving}>
968
            {saving ? 'Saving…' : 'Save settings'}
969
          </button>
970
          {saved && <span>Saved</span>}
971
        </div>
972
      </section>
973
    </>
974
  );
975
}
976
977
function PageHead({
978
  eyebrow,
979
  title,
980
  action,
981
  onAction,
982
}: {
983
  eyebrow: string;
984
  title: string;
985
  action?: string;
986
  onAction?: () => void;
987
}) {
988
  return (
989
    <header className="pageHead">
990
      <div>
991
        <p className="eyebrow">{eyebrow}</p>
992
        <h1>{title}</h1>
993
      </div>
994
      {action && (
995
        <button className="primary" onClick={onAction}>
996
          {action}
997
        </button>
998
      )}
999
    </header>
1000
  );
1001
}
1002
function Nav({
1003
  page,
1004
  active,
1005
  onClick,
1006
  label,
1007
  glyph,
1008
}: {
1009
  page: Page;
1010
  active: Page;
1011
  onClick: (page: Page) => void;
1012
  label: string;
1013
  glyph: string;
1014
}) {
1015
  return (
1016
    <button className={active === page ? 'active' : ''} onClick={() => onClick(page)}>
1017
      <span>{glyph}</span>
1018
      {label}
1019
    </button>
1020
  );
1021
}
1022
function Metric({
1023
  label,
1024
  value,
1025
  note,
1026
  warn,
1027
}: {
1028
  label: string;
1029
  value: number;
1030
  note: string;
1031
  warn?: boolean;
1032
}) {
1033
  return (
1034
    <article className={warn ? 'metric warn' : 'metric'}>
1035
      <span>{label}</span>
1036
      <strong>{String(value).padStart(2, '0')}</strong>
1037
      <small>{note}</small>
1038
    </article>
1039
  );
1040
}
1041
function Panel({
1042
  title,
1043
  subtitle,
1044
  children,
1045
}: React.PropsWithChildren<{ title: string; subtitle: string }>) {
1046
  return (
1047
    <section className="panel">
1048
      <div className="panelHead">
1049
        <div>
1050
          <h2>{title}</h2>
1051
          <p>{subtitle}</p>
1052
        </div>
1053
      </div>
1054
      {children}
1055
    </section>
1056
  );
1057
}
1058
function Timeline({ events }: { events: Record<string, any>[] }) {
1059
  return events.length ? (
1060
    <div className="timeline">
1061
      {events.map((event) => (
1062
        <HistoryRow key={event.eventId} item={{ ...event, _kind: 'event' }} />
1063
      ))}
1064
    </div>
1065
  ) : (
1066
    <Empty
1067
      title="Nothing recorded yet"
1068
      text="Activity appears after your connected agents submit signed structured events."
1069
    />
1070
  );
1071
}
1072
function HistoryRow({ item }: { item: Record<string, any> }) {
1073
  const kind = String(item.eventType ?? item._kind ?? 'record').replaceAll('_', ' ');
1074
  const id = String(item.agentId ?? item.interactionId ?? item.receiptId ?? 'OpenClasp');
1075
  return (
1076
    <article className="historyRow">
1077
      <span className={`eventDot ${item.eventType ?? item._kind}`} />
1078
      <div>
1079
        <strong>{kind}</strong>
1080
        <small>
1081
          {id} · {new Date(timestamp(item)).toLocaleString()}
1082
        </small>
1083
      </div>
1084
      <b>{item.visibility ?? item.outcome ?? item.status ?? 'SIGNED'}</b>
1085
    </article>
1086
  );
1087
}
1088
function AgentCard({
1089
  agent,
1090
  projectName,
1091
  published,
1092
  working,
1093
  onSave,
1094
  runtime,
1095
  runtimeSecret,
1096
  runtimeWorking,
1097
  onRuntime,
1098
  onDisableRuntime,
1099
}: {
1100
  agent: Record<string, any>;
1101
  projectName?: string;
1102
  published: boolean;
1103
  working: boolean;
1104
  onSave: (value: {
1105
    autoPublish: boolean;
1106
    autoAcceptPolicy: 'off' | 'safe_matching';
1107
    autoAcceptTaskCategories: string[];
1108
  }) => void;
1109
  runtime: Record<string, any> | undefined;
1110
  runtimeSecret: string | undefined;
1111
  runtimeWorking: boolean;
1112
  onRuntime: (endpoint: string) => void;
1113
  onDisableRuntime: () => void;
1114
}) {
1115
  const [editing, setEditing] = useState(false);
1116
  const [autoPublish, setAutoPublish] = useState(Boolean(agent.autoPublish ?? published));
1117
  const [autoAcceptPolicy, setAutoAcceptPolicy] = useState<'off' | 'safe_matching'>(
1118
    agent.autoAcceptPolicy === 'safe_matching' ? 'safe_matching' : 'off',
1119
  );
1120
  const [categories, setCategories] = useState<string>(
1121
    (agent.autoAcceptTaskCategories?.length
1122
      ? agent.autoAcceptTaskCategories
1123
      : (agent.capabilities ?? [])
1124
    ).join(', '),
1125
  );
1126
  const [runtimeEndpoint, setRuntimeEndpoint] = useState(String(runtime?.endpoint ?? ''));
1127
  const ready = published;
1128
  const online = agent.presence?.status === 'online';
1129
  const endpoint = `${window.location.origin}/a2a/${encodeURIComponent(agent.agentId)}`;
1130
  const identityLabel = agent.revoked
1131
    ? 'REVOKED'
1132
    : agent.identityMode === 'oauth_installation'
1133
      ? 'AUTHENTICATED'
1134
      : 'VERIFIED';
1135
  return (
1136
    <article className="agentCard">
1137
      <div className="agentTop">
1138
        <span className="agentGlyph">◇</span>
1139
        <div className="agentBadges">
1140
          <b className={online ? 'onlineBadge' : 'offlineBadge'}>
1141
            ● {online ? 'ONLINE' : 'OFFLINE'}
1142
          </b>
1143
          <b className={agent.revoked ? 'bad' : ''}>{identityLabel}</b>
1144
          <b className={ready ? 'readyBadge' : 'needsBadge'}>{ready ? 'READY' : 'SETUP NEEDED'}</b>
1145
        </div>
1146
      </div>
1147
      <h2>{agent.name ?? agent.agentId}</h2>
1148
      <p>{projectName ?? `Version ${agent.agentVersion ?? '1.0.0'}`}</p>
1149
      <div className="tags">
1150
        {(agent.capabilities ?? []).slice(0, 4).map((capability: string) => (
1151
          <span key={capability}>{capability}</span>
1152
        ))}
1153
      </div>
1154
      <small>
1155
        {agent.framework ? `${agent.framework} · ` : ''}
1156
        {agent.identityMode === 'oauth_installation' ? 'OAuth-bound · ' : 'Ed25519 · '}Created{' '}
1157
        {new Date(agent.createdAt).toLocaleDateString()}
1158
      </small>
1159
      <small>
1160
        {agent.presence?.lastSeenAt
1161
          ? `Last MCP activity ${new Date(agent.presence.lastSeenAt).toLocaleString()}`
1162
          : 'No MCP activity recorded yet'}
1163
      </small>
1164
      {published ? (
1165
        <a
1166
          href={`/agents/${encodeURIComponent(agent.agentId)}/card.json`}
1167
          target="_blank"
1168
          rel="noreferrer"
1169
        >
1170
          Public Agent Card ↗
1171
        </a>
1172
      ) : null}
1173
      <div className="automationSummary">
1174
        <span>{published ? '● Public discovery' : '○ Private'}</span>
1175
        <span>↗ OpenClasp A2A gateway</span>
1176
        <span>
1177
          {agent.autoAcceptPolicy === 'safe_matching'
1178
            ? '⚡ Safe tasks automatic'
1179
            : '◷ Manual approval'}
1180
        </span>
1181
      </div>
1182
      <div className="runtimeBox">
1183
        <div>
1184
          <strong>Autonomous runtime</strong>
1185
          <span className={runtime?.status === 'verified' ? 'runtimeLive' : 'runtimeMissing'}>
1186
            {runtime?.status === 'verified' ? 'VERIFIED' : 'NOT CONNECTED'}
1187
          </span>
1188
        </div>
1189
        <p>
1190
          Connect the agent worker running on your cloud. OpenClasp verifies it before sending work.
1191
        </p>
1192
        <input
1193
          type="url"
1194
          value={runtimeEndpoint}
1195
          onChange={(event) => setRuntimeEndpoint(event.target.value)}
1196
          placeholder="https://agent.example.com/openclasp"
1197
        />
1198
        <div className="agentActions">
1199
          {runtime?.status === 'verified' ? (
1200
            <button className="secondary" disabled={runtimeWorking} onClick={onDisableRuntime}>
1201
              Disable runtime
1202
            </button>
1203
          ) : null}
1204
          <button
1205
            className="primary"
1206
            disabled={runtimeWorking || !runtimeEndpoint.trim()}
1207
            onClick={() => onRuntime(runtimeEndpoint.trim())}
1208
          >
1209
            {runtimeWorking ? 'Verifying…' : runtime ? 'Rotate connection' : 'Verify & connect'}
1210
          </button>
1211
        </div>
1212
        {runtimeSecret ? (
1213
          <div className="runtimeSecret">
1214
            <strong>Copy this signing secret now. It will not be shown again.</strong>
1215
            <code>{runtimeSecret}</code>
1216
          </div>
1217
        ) : null}
1218
        {runtime?.lastError ? <small>Last delivery error: {runtime.lastError}</small> : null}
1219
      </div>
1220
      {editing ? (
1221
        <div className="automationForm">
1222
          <label>
1223
            <span>Hosted A2A endpoint</span>
1224
            <input type="url" value={endpoint} readOnly />
1225
          </label>
1226
          <label>
1227
            <span>Invitation policy</span>
1228
            <select
1229
              value={autoAcceptPolicy}
1230
              onChange={(event) =>
1231
                setAutoAcceptPolicy(event.target.value as 'off' | 'safe_matching')
1232
              }
1233
            >
1234
              <option value="safe_matching">Auto-accept safe matches</option>
1235
              <option value="off">Review every request</option>
1236
            </select>
1237
          </label>
1238
          <label>
1239
            <span>Safe task categories</span>
1240
            <input
1241
              value={categories}
1242
              onChange={(event) => setCategories(event.target.value)}
1243
              placeholder="research, planning, coding"
1244
            />
1245
          </label>
1246
          <label className="checkControl">
1247
            <input
1248
              type="checkbox"
1249
              checked={autoPublish}
1250
              onChange={(event) => setAutoPublish(event.target.checked)}
1251
            />
1252
            <span>Publish and keep Agent Card updated</span>
1253
          </label>
1254
          <div className="agentActions">
1255
            <button className="secondary" onClick={() => setEditing(false)}>
1256
              Cancel
1257
            </button>
1258
            <button
1259
              className="primary"
1260
              disabled={working || agent.status === 'revoked'}
1261
              onClick={() =>
1262
                onSave({
1263
                  autoPublish,
1264
                  autoAcceptPolicy,
1265
                  autoAcceptTaskCategories: categories
1266
                    .split(',')
1267
                    .map((value) => value.trim())
1268
                    .filter(Boolean),
1269
                })
1270
              }
1271
            >
1272
              {working ? 'Saving…' : 'Save automation'}
1273
            </button>
1274
          </div>
1275
        </div>
1276
      ) : (
1277
        <button
1278
          className="secondary"
1279
          disabled={working || agent.status === 'revoked'}
1280
          onClick={() => setEditing(true)}
1281
        >
1282
          Configure automation
1283
        </button>
1284
      )}
1285
    </article>
1286
  );
1287
}
1288
function Profile({ profile, expanded }: { profile: Record<string, any>; expanded?: boolean }) {
1289
  const dimensions = [
1290
    ['Completion', profile.completion],
1291
    ['Scope', profile.scope],
1292
    ['Evidence', profile.evidence],
1293
    ...(expanded
1294
      ? [
1295
          ['Communication', profile.communication],
1296
          ['Deadline', profile.deadline],
1297
        ]
1298
      : []),
1299
  ] as [string, number][];
1300
  return (
1301
    <article className="profile">
1302
      <div>
1303
        <strong>{profile.agentId}</strong>
1304
        <small>
1305
          {profile.taskCategory} · {profile.sampleSize} outcome(s)
1306
        </small>
1307
      </div>
1308
      {dimensions.map(([label, value]) => (
1309
        <Meter key={label} label={label} value={value ?? 0} />
1310
      ))}
1311
    </article>
1312
  );
1313
}
1314
function Meter({ label, value }: { label: string; value: number }) {
1315
  return (
1316
    <div className="meter">
1317
      <span>{label}</span>
1318
      <i>
1319
        <b style={{ width: `${Math.round(value * 100)}%` }} />
1320
      </i>
1321
      <strong>{Math.round(value * 100)}%</strong>
1322
    </div>
1323
  );
1324
}
1325
function Empty({
1326
  title,
1327
  text,
1328
  action,
1329
  onAction,
1330
}: {
1331
  title: string;
1332
  text: string;
1333
  action?: string;
1334
  onAction?: () => void;
1335
}) {
1336
  return (
1337
    <div className="empty">
1338
      <span>◇</span>
1339
      <strong>{title}</strong>
1340
      <p>{text}</p>
1341
      {action && (
1342
        <button className="secondary" onClick={onAction}>
1343
          {action}
1344
        </button>
1345
      )}
1346
    </div>
1347
  );
1348
}
1349
function TextButton({ children, onClick }: React.PropsWithChildren<{ onClick: () => void }>) {
1350
  return (
1351
    <button className="textButton" onClick={onClick}>
1352
      {children}
1353
    </button>
1354
  );
1355
}
1356
function Setting({
1357
  label,
1358
  description,
1359
  children,
1360
}: React.PropsWithChildren<{ label: string; description: string }>) {
1361
  return (
1362
    <div className="setting">
1363
      <div>
1364
        <strong>{label}</strong>
1365
        <p>{description}</p>
1366
      </div>
1367
      <div>{children}</div>
1368
    </div>
1369
  );
1370
}
1371
function Toggle({ checked, onChange }: { checked: boolean; onChange: (checked: boolean) => void }) {
1372
  return (
1373
    <button
1374
      className={checked ? 'toggle on' : 'toggle'}
1375
      role="switch"
1376
      aria-checked={checked}
1377
      onClick={() => onChange(!checked)}
1378
    >
1379
      <span />
1380
    </button>
1381
  );
1382
}
1383
function Loading({ compact }: { compact?: boolean }) {
1384
  return (
1385
    <div className={compact ? 'loading compact' : 'loading'}>
1386
      <span className="mark">OC</span>
1387
      <p>Verifying session…</p>
1388
    </div>
1389
  );
1390
}
1391
function initials(value: string) {
1392
  return value
1393
    .split(/\s+|@/)
1394
    .slice(0, 2)
1395
    .map((part) => part[0]?.toUpperCase())
1396
    .join('');
1397
}
1398
function timestamp(item: Record<string, any>) {
1399
  return String(item.timestamp ?? item.completedAt ?? item.createdAt ?? new Date(0).toISOString());
1400
}
1401
1402
if (!__AUTH0_DOMAIN__ || !__AUTH0_CLIENT_ID__ || !__AUTH0_AUDIENCE__)
1403
  throw new Error('Auth0 public configuration is incomplete');
1404
createRoot(document.getElementById('root')!).render(
1405
  <React.StrictMode>
1406
    {location.pathname === '/sso-callback' ? <AuthCallback /> : <App />}
1407
  </React.StrictMode>,
1408
);