@volter/world-console 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,815 @@
1
+ // The console's client (apps/console/README.md): every read and every write below is a door
2
+ // of the host (`/-/worlds`, `/-/ping`) or of a world (`/-/<org>/<world>/…`), called with the token the
3
+ // person typed, from the same origin the host serves. The client holds no state a door did not
4
+ // answer, and offers nothing `volter world` or `curl` could not do. Locators are roles and text.
5
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
6
+ import { createRoot } from 'react-dom/client';
7
+
8
+ type Twin = { twin: string; protocol?: { major?: number; standing?: string } | null; root?: { url?: string } | null; credential?: { placedAt?: string; fingerprint?: string } | null; refresh?: { at?: string; observedAt?: string; failed?: { at?: string; reason?: string; count?: number } } | null; position?: number; lastReceipt?: { status?: string; externalId?: string; reason?: string } | null };
9
+ type Row = { org: string; world: string; name: string; base: string; token: string; readToken: string; twins: Array<{ vendor: string; protocol?: { major?: number } | null; root?: unknown }> };
10
+ type Entry = { id: string; service: string; op: string; operation?: string; subject: { type: string; id: string }; occurredAt: string; actor?: { kind: string; id?: string }; provenance?: string; batch?: string; receipt?: { status?: string; externalId?: string; reason?: string }; landsId?: string; fields?: Record<string, unknown> };
11
+ type Changeset = { name: string; actions?: unknown[]; receipts?: unknown; verification?: unknown; approvals?: unknown[] };
12
+ type Mode = { kind: 'admin'; worlds: Row[] } | { kind: 'world'; worlds: Array<{ name: string; base: string; twins: Row['twins'] }> };
13
+
14
+ const TOKEN_KEY = 'volter.console.token';
15
+ const ROOT = document.getElementById('root')!;
16
+ const BASE = ROOT.dataset.base ?? '/-/console';
17
+ const ORIGIN = window.location.origin;
18
+ // where the Worlds are served: another origin when the console has one of its own (a local host's
19
+ // console-apart listener), so no twin-served script shares this page's storage; else this one
20
+ const WORLDS = (ROOT.dataset.worlds ?? ORIGIN).replace(/\/+$/, '');
21
+ /** The Worlds a World token may open, when the host does not list them (a hosted host keeps its tenants' names to itself). */
22
+ const NAMES = (ROOT.dataset.names ?? '').split(',').filter((n) => n !== '');
23
+ const SITE = 'https://volter.world'; // the product site: pricing, status
24
+ const WORLD_LOGO = 'https://brand.volter.ai/logo/twin-world/svg?size=64&variant=mono'; // Volter World's mark, served by the brand (brand.volter.ai)
25
+ // the platform serves the console as its portal: a session token is injected (no sign-in screen)
26
+ // and `managed` is set, so a world opens on its own host's console rather than under this origin
27
+ const INJECTED = ROOT.dataset.token ?? '';
28
+ const MANAGED = ROOT.dataset.managed === '1';
29
+
30
+ // the tab's own storage, never localStorage: the token the person typed ends with the tab and is not left on
31
+ // an origin that also serves the Worlds' twins (a served World mounts this console beside them)
32
+ const keep = (t: string): void => { try { if (t) sessionStorage.setItem(TOKEN_KEY, t); else sessionStorage.removeItem(TOKEN_KEY); localStorage.removeItem(TOKEN_KEY); } catch { /* private window */ } };
33
+ // a host console opened FROM the portal is handed the world's token in the URL fragment (never sent
34
+ // to a server); it is taken once and dropped from the address bar
35
+ const PORTAL_KEY = 'volter.console.portal';
36
+ const fromFragment = (): string => {
37
+ const h = window.location.hash;
38
+ const m = /(?:^#|&)token=([^&]+)/.exec(h); const pm = /(?:^#|&)portal=([^&]+)/.exec(h);
39
+ if (pm) { try { sessionStorage.setItem(PORTAL_KEY, decodeURIComponent(pm[1]!)); } catch { /* fine */ } }
40
+ if (!m) return '';
41
+ try { window.history.replaceState(null, '', window.location.pathname + window.location.search); } catch { /* fine */ }
42
+ return decodeURIComponent(m[1]!);
43
+ };
44
+ /** The portal this host console was opened from, when it was: the way back for a person. */
45
+ const portal = (): string => { try { return sessionStorage.getItem(PORTAL_KEY) ?? ''; } catch { return ''; } };
46
+ const held = (): string => { if (INJECTED) return INJECTED; const f = fromFragment(); if (f) { keep(f); return f; } try { return sessionStorage.getItem(TOKEN_KEY) ?? ''; } catch { return ''; } };
47
+ const when = (iso?: string | null): string => (iso ? new Date(iso).toLocaleString() : '—');
48
+
49
+ async function door(token: string, path: string, init: RequestInit = {}): Promise<Response> {
50
+ return fetch(`${ORIGIN}${path}`, { ...init, headers: { ...(init.headers ?? {}), 'x-volter-token': token, ...(init.body && !(init.headers as Record<string, string> | undefined)?.['content-type'] ? { 'content-type': 'application/json' } : {}) } });
51
+ }
52
+
53
+ /** The path under the console's base: [] worlds; [org, world] a world; [org, world, twin] a twin. */
54
+ function route(): string[] { return window.location.pathname.slice(BASE.length).split('/').filter(Boolean).map(decodeURIComponent); }
55
+ function go(parts: string[]): void { window.history.pushState(null, '', `${BASE}/${parts.map(encodeURIComponent).join('/')}`); window.dispatchEvent(new PopStateEvent('popstate')); }
56
+
57
+ function App(): React.JSX.Element {
58
+ const [token, setToken] = useState<string>(held());
59
+ const [draft, setDraft] = useState<string>('');
60
+ const [mode, setMode] = useState<Mode | null>(null);
61
+ const [error, setError] = useState<string | null>(null);
62
+ const [parts, setParts] = useState<string[]>(route());
63
+ useEffect(() => { const on = () => setParts(route()); window.addEventListener('popstate', on); return () => window.removeEventListener('popstate', on); }, []);
64
+
65
+ /** The mode is what the origin answers: a host's inventory to the admin token, else the worlds this token opens. */
66
+ const load = useCallback(async (t: string) => {
67
+ setError(null);
68
+ if (!t) { setMode(null); return; }
69
+ const inventory = await door(t, '/-/worlds');
70
+ if (inventory.ok) { const { worlds } = (await inventory.json()) as { worlds: Row[] }; setMode({ kind: 'admin', worlds }); return; }
71
+ if (inventory.status === 429) { setError('You are asking faster than the platform allows — wait a moment and reload.'); setMode(null); return; } // never misread a rate limit as "not a host"
72
+ // A host's inventory accepts only its admin token. A world token gets 401 here but
73
+ // still opens its own world below; only a managed portal's 401 means sign-in expired.
74
+ if (inventory.status === 401 && MANAGED) { setError('Your sign-in no longer opens this console — sign in again.'); setMode(null); return; }
75
+ if (inventory.status >= 500) { const body = (await inventory.json().catch(() => ({}))) as { error?: string }; setError(`The platform could not list your worlds${body.error ? `: ${body.error}` : ''}. Reload; if it stays, the status page says what is wrong.`); setMode(null); return; }
76
+ let worlds = NAMES;
77
+ if (worlds.length === 0) {
78
+ const ping = await fetch(`${ORIGIN}/-/ping`); // not a platform answer: perhaps this origin is a bare host
79
+ if (!ping.ok) { setError(`${ORIGIN} does not answer as a host`); setMode(null); return; }
80
+ worlds = ((await ping.json()) as { worlds?: string[] }).worlds ?? [];
81
+ if (worlds.length === 0) { setError(`${ORIGIN} does not list its worlds: open the console with the World's URL (volter world console <url>/<org>/<world>)`); setMode(null); return; }
82
+ }
83
+ const opened: Array<{ name: string; base: string; twins: Row['twins'] }> = [];
84
+ for (const name of worlds) { const r = await door(t, `/-/${name}/twins`); if (r.ok) { const b = (await r.json()) as { twins?: Array<{ twin: string; protocol?: { major?: number } | null; root?: unknown }> }; opened.push({ name, base: `${WORLDS}/${name}`, twins: (b.twins ?? []).map((x) => ({ vendor: x.twin, protocol: x.protocol ?? null, root: x.root ?? null })) }); } }
85
+ if (opened.length === 0) { setError('this token opens no world here'); setMode(null); return; }
86
+ setMode({ kind: 'world', worlds: opened });
87
+ }, []);
88
+ useEffect(() => { void load(token); }, [token, load]);
89
+
90
+ const worldToken = (name: string): string => (mode?.kind === 'admin' ? mode.worlds.find((w) => w.name === name)?.token ?? token : token);
91
+ const [who, setWho] = useState<string>(''); const [support, setSupport] = useState<{ until: string; reason: string } | null>(null);
92
+ useEffect(() => { if (!MANAGED || !token) return; void door(token, '/-/orgs').then(async (r) => { if (r.ok) { const b = (await r.json()) as { person?: { email?: string; name?: string; support?: { until: string; reason: string } } }; setSupport(b.person?.support ?? null); setWho(b.person?.email ?? b.person?.name ?? ''); } }); }, [token]);
93
+
94
+ if (!token || (!mode && !error)) {
95
+ return (
96
+ <main>
97
+ <h1><img src={WORLD_LOGO} alt="" width={24} height={24} style={{ verticalAlign: 'middle' }} /> Volter World</h1>
98
+ {token && !mode && !error ? <p>Opening…</p> : (
99
+ <form onSubmit={(e) => { e.preventDefault(); keep(draft.trim()); setToken(draft.trim()); }}>
100
+ <p>A host's admin token opens every world it serves; a world's token opens that world.</p>
101
+ <label>Token <input name="token" type="password" value={draft} onChange={(e) => setDraft(e.target.value)} autoFocus /></label>
102
+ <button type="submit">Open</button>
103
+ </form>
104
+ )}
105
+ </main>
106
+ );
107
+ }
108
+ const signOut = (): void => { if (mode) for (const w of mode.worlds) navigator.sendBeacon(`${w.base.replace(/\/[^/]+\/[^/]+$/, '')}/-/${w.name}/session/end`); keep(''); setToken(''); setDraft(''); setMode(null); go([]); if (MANAGED) window.location.assign(`${ORIGIN}/-/sign-out`); };
109
+ if (error || !mode) {
110
+ return <main><h1><img src={WORLD_LOGO} alt="" width={24} height={24} style={{ verticalAlign: 'middle' }} /> Volter World</h1><p role="alert">{error ?? 'nothing answered'}</p><button onClick={signOut}>Use another token</button></main>;
111
+ }
112
+ const [org, world, twin] = parts;
113
+ return (
114
+ <main>
115
+ {support ? <p role="alert">Viewing as Volter support — read only, until {when(support.until)}. Reason on record: {support.reason}.</p> : null}
116
+ <header>
117
+ <h1><a href={`${BASE}/`} onClick={(e) => { e.preventDefault(); go([]); }}><img src={WORLD_LOGO} alt="" width={24} height={24} style={{ verticalAlign: 'middle' }} /> Volter World</a></h1>
118
+ <nav aria-label="breadcrumb">
119
+ {org && world ? <a href={`${BASE}/${org}/${world}`} onClick={(e) => { e.preventDefault(); go([org, world]); }}>{org}/{world}</a> : null}
120
+ {twin ? <span> / {twin}</span> : null}
121
+ </nav>
122
+ {portal() ? <a href={`${portal()}/-/console/`}>← back to your worlds</a> : null}
123
+ <span className="mode">{MANAGED ? (who ? `signed in as ${who}` : 'signed in') : mode.kind === 'admin' ? 'host: admin' : 'world token'}</span>
124
+ {MANAGED ? <a href={`${ORIGIN}/-/account`}>Account</a> : null}
125
+ {MANAGED ? <a href={`${SITE}/status.html`}>Status</a> : null}
126
+ {MANAGED ? <a href="#help">Help</a> : null}
127
+ <button onClick={signOut}>Sign out</button>
128
+ </header>
129
+ {org && world
130
+ ? <WorldView name={`${org}/${world}`} token={worldToken(`${org}/${world}`)} twin={twin} admin={mode.kind === 'admin' ? token : null} onRemoved={() => { void load(token); go([]); }} />
131
+ : <Worlds mode={mode} admin={mode.kind === 'admin' ? token : null} reload={() => load(token)} />}
132
+ </main>
133
+ );
134
+ }
135
+
136
+ function Worlds({ mode, admin, reload }: { mode: Mode; admin: string | null; reload: () => Promise<void> }): React.JSX.Element {
137
+ const [org, setOrg] = useState(''); const [world, setWorld] = useState(''); const [vendors, setVendors] = useState('');
138
+ const [note, setNote] = useState<string | null>(null);
139
+ const [myOrgs, setMyOrgs] = useState<string[]>([]);
140
+ // the org switcher (punchlist 2.2): a person in several orgs works in ONE at a time — remembered per tab
141
+ const remembered = (): string => { try { return sessionStorage.getItem('volter.console.org') ?? ''; } catch { return ''; } };
142
+ const [current, setCurrent] = useState<string>(remembered());
143
+ const switchOrg = (name: string): void => { setCurrent(name); setOrg(name); try { sessionStorage.setItem('volter.console.org', name); } catch { /* no storage */ } };
144
+ const loadOrgs = useCallback(async () => {
145
+ if (!MANAGED || !admin) return;
146
+ const r = await door(admin, '/-/orgs'); if (!r.ok) return;
147
+ const names = ((await r.json()) as { orgs: Array<{ slug: string | null; name: string }> }).orgs.map((o) => o.slug ?? o.name);
148
+ setMyOrgs(names);
149
+ const pick = names.length === 1 ? names[0]! : names.includes(remembered()) ? remembered() : '';
150
+ if (pick) { setOrg(pick); setCurrent(pick); }
151
+ // the org worked in no longer exists (deleted, or left): stand in the first one that does
152
+ setCurrent((c) => (c && !names.includes(c) ? (names[0] ?? '') : c)); setOrg((o) => (o && !names.includes(o) ? (names[0] ?? '') : o));
153
+ }, [admin]);
154
+ useEffect(() => { void loadOrgs(); }, [loadOrgs]);
155
+ const [worldsVersion, setWorldsVersion] = useState(0);
156
+ // a world deleted from the portal: the platform's door (walk 3b: nothing removed a world, so an org could never be deleted)
157
+ const deleteWorld = async (name: string): Promise<void> => {
158
+ setNote(null);
159
+ const r = await door(admin!, `/-/worlds/${name}`, { method: 'DELETE' });
160
+ const body = (await r.json().catch(() => ({}))) as { error?: string };
161
+ setNote(r.ok ? `deleted ${name}` : body.error ?? `answered ${r.status}`);
162
+ await reload(); setWorldsVersion((v) => v + 1);
163
+ };
164
+ const [provisioning, setProvisioning] = useState(false);
165
+ const provision = async (e: React.FormEvent): Promise<void> => {
166
+ e.preventDefault(); setNote(null); setProvisioning(true);
167
+ try {
168
+ const r = await door(admin!, '/-/worlds', { method: 'POST', body: JSON.stringify({ org, world, vendors: vendors.split(',').map((v) => v.trim()).filter(Boolean) }) });
169
+ const body = (await r.json()) as { error?: string; name?: string };
170
+ if (!r.ok) { setNote(body.error ?? `the host answered ${r.status}`); return; }
171
+ setNote(`provisioned ${body.name}`); setWorld(''); setVendors(''); await reload(); setWorldsVersion((v) => v + 1); // the org stays selected: the next world usually goes to the same org
172
+ } finally { setProvisioning(false); }
173
+ };
174
+ const allRows: Array<{ name: string; base: string; token?: string; twins: Row['twins'] }> = mode.worlds;
175
+ const rows = MANAGED && current && myOrgs.length > 1 ? allRows.filter((r) => r.name.startsWith(`${current}/`)) : allRows;
176
+ return (
177
+ <section aria-labelledby="worlds">
178
+ {MANAGED && admin ? <GettingStarted token={admin} orgs={myOrgs} worlds={allRows.length} firstWorld={rows[0]?.name} version={worldsVersion} /> : null}
179
+ <h2 id="worlds">Worlds</h2>
180
+ {MANAGED && myOrgs.length > 1 ? <p><label>Current org <select aria-label="Current org" value={current} onChange={(e) => switchOrg(e.target.value)}><option value="">all my orgs</option>{myOrgs.map((n) => <option key={n} value={n}>{n}</option>)}</select></label></p> : null}
181
+ <table>
182
+ <thead><tr><th>world</th><th>address</th><th>twins</th>{MANAGED ? <th>actions</th> : null}</tr></thead>
183
+ <tbody>
184
+ {rows.map((w) => (
185
+ <tr key={w.name}>
186
+ {/* managed: a world opens on its OWN host's console (the portal never proxies a world). self-hosted: in-page */}
187
+ <td>{MANAGED
188
+ ? <a href={`${w.base.replace(/\/[^/]+\/[^/]+$/, '')}/-/console/${w.name}#token=${encodeURIComponent(w.token ?? '')}&portal=${encodeURIComponent(ORIGIN)}`}>{w.name}</a>
189
+ : <a href={`${BASE}/${w.name}`} onClick={(e) => { e.preventDefault(); go(w.name.split('/')); }}>{w.name}</a>}</td>
190
+ <td><code>{w.base}</code></td>
191
+ <td>{w.twins.map((t) => t.vendor).join(', ') || '—'}</td>
192
+ {MANAGED ? <td><button type="button" aria-label={`Delete ${w.name}`} onClick={() => { if (window.confirm(`Delete the world ${w.name}? Its host stops serving it and its history goes with it.`)) void deleteWorld(w.name); }}>Delete</button></td> : null}
193
+ </tr>
194
+ ))}
195
+ </tbody>
196
+ </table>
197
+ {admin && MANAGED ? <Orgs token={admin} version={worldsVersion} only={myOrgs.length > 1 ? current : ''} onChanged={async () => { await loadOrgs(); await reload(); }} /> : null}
198
+ {admin && MANAGED ? <PersonalTokens token={admin} onChanged={() => setWorldsVersion((v) => v + 1)} /> : null}
199
+ {admin && MANAGED ? <Help token={admin} orgs={myOrgs} onChanged={() => setWorldsVersion((v) => v + 1)} /> : null}
200
+ {admin ? (
201
+ <form onSubmit={provision} aria-labelledby="provision">
202
+ <h3 id="provision">Provision a world</h3>
203
+ {MANAGED && myOrgs.length > 0
204
+ ? <label>Org <select name="org" aria-label="Org" value={org} onChange={(e) => setOrg(e.target.value)} required>{myOrgs.map((o) => <option key={o} value={o}>{o}</option>)}</select></label>
205
+ : <label>Org <input name="org" value={org} onChange={(e) => setOrg(e.target.value)} required /></label>}
206
+ <label>World <input name="world" value={world} onChange={(e) => setWorld(e.target.value)} required /></label>
207
+ <label>Twins <input name="vendors" value={vendors} onChange={(e) => setVendors(e.target.value)} placeholder="github, slack" required /></label>
208
+ <button type="submit" disabled={provisioning}>{provisioning ? 'Provisioning… (a host boots the world; ten seconds or so)' : 'Provision'}</button>
209
+ {note ? <p role="status">{note}</p> : null}
210
+ </form>
211
+ ) : null}
212
+ </section>
213
+ );
214
+ }
215
+
216
+ /** Managed only: the person's orgs (the Volter identity service's, through the platform) and a form to create one. */
217
+ function Orgs({ token, version, only, onChanged }: { token: string; version: number; only: string; onChanged: () => Promise<void> }): React.JSX.Element {
218
+ const [orgs, setOrgs] = useState<Array<{ id: string; slug: string | null; name: string; role: string; held?: string | null; flags?: Record<string, boolean> }> | null>(null);
219
+ const [name, setName] = useState(''); const [note, setNote] = useState<string | null>(null); const [accepted, setAccepted] = useState(false);
220
+ const [tick, setTick] = useState(0); // every re-read of the orgs is an act done: the Activity lists re-read too
221
+ const load = useCallback(async () => { const r = await door(token, '/-/orgs'); setOrgs(r.ok ? ((await r.json()) as { orgs: Array<{ id: string; slug: string | null; name: string; role: string; held?: string | null; flags?: Record<string, boolean> }> }).orgs : []); setTick((t) => t + 1); }, [token]);
222
+ useEffect(() => { void load(); }, [load]);
223
+ const create = async (e: React.FormEvent): Promise<void> => {
224
+ e.preventDefault(); setNote(null);
225
+ const r = await door(token, '/-/orgs', { method: 'POST', body: JSON.stringify({ name, accepted }) });
226
+ const body = (await r.json()) as { error?: string; slug?: string; name?: string };
227
+ if (!r.ok) { setNote(body.error ?? `answered ${r.status}`); return; }
228
+ setNote(`created org ${body.slug ?? body.name}`); setName(''); await load(); await onChanged();
229
+ };
230
+ return (
231
+ <section aria-labelledby="orgs">
232
+ <h3 id="orgs">Your orgs</h3>
233
+ {orgs === null ? <p>Loading…</p> : orgs.length === 0 ? <p>You belong to no org yet — create one to provision worlds into.</p> : (
234
+ <ul>{orgs.filter((o) => !only || (o.slug ?? o.name) === only).map((o) => <li key={o.id}>{o.slug && o.name && o.name !== o.slug ? <>{o.name} <span className="mode">({o.slug})</span></> : (o.slug ?? o.name)} <span className="mode">({o.role})</span>{o.held ? <p role="alert">Held: {o.held}.</p> : <><Billing token={token} org={o.slug ?? o.id} version={version} flags={o.flags ?? {}} /><Members token={token} org={o.slug ?? o.id} admin={o.role === 'admin' || o.role === 'org:admin'} onChanged={() => setTick((t) => t + 1)} /><OrgSettings token={token} org={o.slug ?? o.id} name={o.name} admin={o.role === 'admin' || o.role === 'org:admin'} worlds={0} version={version + tick} flags={o.flags ?? {}} onChanged={async (said?: string) => { await load(); if (said) setNote(said); await onChanged(); }} />{(o.role === 'admin' || o.role === 'org:admin') ? <Activity token={token} org={o.slug ?? o.id} version={version + tick} /> : null}</>}</li>)}</ul>
235
+ )}
236
+ <form onSubmit={create} aria-labelledby="create-org">
237
+ <h4 id="create-org">Create an org</h4>
238
+ <label>Org name <input name="orgName" value={name} onChange={(e) => setName(e.target.value)} placeholder="acme" required /></label>
239
+ <label><input name="accepted" type="checkbox" checked={accepted} onChange={(e) => setAccepted(e.target.checked)} required /> I agree to the <a href={`${ORIGIN}/terms`} target="_blank" rel="noreferrer">terms of service</a> and the <a href={`${ORIGIN}/privacy`} target="_blank" rel="noreferrer">privacy policy</a></label>
240
+ <button type="submit">Create org</button>
241
+ {note ? <p role="status">{note}</p> : null}
242
+ </form>
243
+ </section>
244
+ );
245
+ }
246
+
247
+ type PlanCard = { id: string; name: string; priceMonthly: number; limits: { worlds: number | null; twinsPerWorld: number | null; worldHours: number | null; members: number | null }; pitch: string; features: string[] };
248
+ type BillingInfo = { plan: { id: string; name: string; priceMonthly: number; limits: { worlds: number | null; twinsPerWorld: number | null; worldHours: number | null; members: number | null } }; plans: PlanCard[]; pendingCheckout: { id: string; url: string } | null; paid: boolean; lastTickAt: string | null; subscription: { status: string; currentPeriodEnd: string | null; trialEnd: string | null; cancelAtPeriodEnd: boolean } | null; usageByDay?: Array<{ day: string; hours: number; worlds: number }>; spendCap?: boolean; overageCentsPerHour?: number | null; reminders?: { trialEnding: string | null; renewal: string | null; sent: string[] }; usage: Record<string, number>; meters: Array<{ name: string; label: string }>; worlds: number; restriction: { status: string; graceEnds: string; meter: string; reason?: string } | null };
249
+
250
+ /** The org's plan, quota and usage — Polar's customer state as the platform reads it — with the way to pay and the way to the billing portal. */
251
+ function Billing({ token, org, version, flags }: { token: string; org: string; version: number; flags?: Record<string, boolean> }): React.JSX.Element {
252
+ const [info, setInfo] = useState<BillingInfo | null>(null);
253
+ const [checkout, setCheckout] = useState<string | null>(null);
254
+ const [portal, setPortal] = useState<string | null>(null);
255
+ const [note, setNote] = useState<string | null>(null);
256
+ const fresh = typeof window !== 'undefined' && new URLSearchParams(window.location.search).has('upgraded'); // back from checkout: Polar's state now, not the cache
257
+ const [failed, setFailed] = useState<string | null>(null);
258
+ const load = useCallback(async () => {
259
+ let r = await door(token, `/-/orgs/${org}/billing${fresh ? '?fresh=1' : ''}`);
260
+ if (r.status === 401) { await new Promise((res) => setTimeout(res, 800)); r = await door(token, `/-/orgs/${org}/billing${fresh ? '?fresh=1' : ''}`); } // the session cookie rotates; one retry before an error is shown (walk 3b)
261
+ if (r.ok) { setInfo((await r.json()) as BillingInfo); setFailed(null); } else { setInfo(null); setFailed(((await r.json().catch(() => ({}))) as { error?: string }).error ?? `answered ${r.status}`); }
262
+ }, [token, org, fresh, version]); // version: the worlds changed — quota and usage with them
263
+ useEffect(() => { void load(); }, [load]);
264
+ const upgrade = async (): Promise<void> => {
265
+ setNote(null);
266
+ const r = await door(token, `/-/orgs/${org}/checkout`, { method: 'POST', body: '{}' });
267
+ const body = (await r.json()) as { url?: string; error?: string; pending?: boolean };
268
+ if (!r.ok || !body.url) { setNote(body.error ?? `answered ${r.status}`); return; }
269
+ setCheckout(body.url); if (body.pending) setNote('You already have an open checkout — this is the same link. Pay there and come back.');
270
+ };
271
+ const setCap = async (on: boolean): Promise<void> => { setFailed(null); const r = await door(token, `/-/orgs/${org}/billing`, { method: 'PATCH', body: JSON.stringify({ spendCap: on }) }); if (!r.ok) { const body = (await r.json().catch(() => ({}))) as { error?: string }; setFailed(body.error ?? `answered ${r.status}`); return; } await load(); };
272
+ const manage = async (): Promise<void> => {
273
+ setNote(null);
274
+ const r = await door(token, `/-/orgs/${org}/billing-portal`, { method: 'POST', body: '{}' });
275
+ const body = (await r.json()) as { url?: string; error?: string };
276
+ if (!r.ok || !body.url) { setNote(body.error ?? `answered ${r.status}`); return; }
277
+ setPortal(body.url);
278
+ };
279
+ if (!info) return <p className="mode" role={failed ? 'alert' : undefined}>{failed ? `Billing: ${failed}` : 'Billing: loading…'}</p>;
280
+ const lim = (n: number | null): string => (n === null ? 'unlimited' : String(n));
281
+ const when = (iso: string | null): string => (iso ? day(iso) : '');
282
+ return (
283
+ <div className="billing" aria-label={`Billing for ${org}`}>
284
+ <p><strong>Plan: {info.plan.name}</strong>{info.plan.priceMonthly ? ` — $${(info.plan.priceMonthly / 100).toFixed(0)}/month` : ''}
285
+ {info.subscription?.trialEnd ? ` · trial ends ${when(info.subscription.trialEnd)} — the card is charged then` : ''}
286
+ {info.subscription?.cancelAtPeriodEnd ? ` · cancels ${when(info.subscription.currentPeriodEnd)}` : info.subscription?.currentPeriodEnd ? ` · renews ${when(info.subscription.currentPeriodEnd)}` : ''}</p>
287
+ <p>Worlds: {info.worlds} of {lim(info.plan.limits.worlds)} · twins per world: {lim(info.plan.limits.twinsPerWorld)} · members: {lim(info.plan.limits.members)}</p>
288
+ <ul className="usage">{info.meters.map((m) => <li key={m.name}>{m.label}: {info.usage[m.name] ?? 0}{m.name === 'world_hours' && info.plan.limits.worldHours !== null ? ` of ${info.plan.limits.worldHours} this period` : ''}</li>)}</ul>
289
+ {info.plan.id !== 'free' && info.overageCentsPerHour ? <p className="mode">{info.spendCap !== false
290
+ ? <>Spend cap is on: past the plan's {info.plan.limits.worldHours} world-hours, provisioning and refresh are held after a grace period. Need more? <button type="button" onClick={() => void setCap(false)}>Switch the spend cap off</button> — hours beyond the plan are then billed at ${(info.overageCentsPerHour / 100).toFixed(2)} each.</>
291
+ : <>Spend cap is off: hours beyond the plan's {info.plan.limits.worldHours} are billed at ${(info.overageCentsPerHour / 100).toFixed(2)} each and nothing is held. <button type="button" onClick={() => void setCap(true)}>Switch the spend cap on</button></>}</p> : null}
292
+ {info.reminders?.trialEnding ? <p className="mode">We email the admins on {day(info.reminders.trialEnding)}, seven days before the trial ends{info.reminders.sent.some((k) => k.startsWith('trial-ending')) ? ' — sent' : ''}.</p> : info.reminders?.renewal ? <p className="mode">We email the admins on {day(info.reminders.renewal)}, seven days before the renewal{info.reminders.sent.some((k) => k.startsWith('renewal')) ? ' — sent' : ''}.</p> : null}
293
+ {flags?.usage_by_day && info.usageByDay && info.usageByDay.length > 0 ? <div aria-label="World-hours by day"><h5 className="mode">World-hours by day this period</h5><ul className="bars">{info.usageByDay.map((d) => { const max = Math.max(1, ...info.usageByDay!.map((x) => x.hours)); return <li key={d.day}><span className="mode">{d.day.slice(5)}</span><span className="bar" style={{ width: `${Math.max(2, Math.round((d.hours / max) * 100))}%` }} aria-hidden="true" /><span>{d.hours} h · {d.worlds} world{d.worlds === 1 ? '' : 's'}</span></li>; })}</ul></div> : null}
294
+ <p className="mode">{info.lastTickAt ? `Usage metered hourly; last metered ${info.lastTickAt.slice(0, 16).replace('T', ' ')} UTC.` : 'Usage is metered hourly; the first hour has not been metered yet.'}</p>
295
+ {info.paid ? <p role="status">Payment received — your org is on {info.plan.name}.</p> : null}
296
+ {info.pendingCheckout && !checkout ? <p role="status">Your checkout at Polar is still open: <a href={info.pendingCheckout.url}>continue to checkout at Polar</a>. Reload after paying.</p> : null}
297
+ {info.plan.id === 'free' ? (() => { const team = info.plans.find((p) => p.id === 'team'); return team ? <div className="plan-preview" aria-label="Team plan"><p><strong>Team — ${(team.priceMonthly / 100).toFixed(0)}/month.</strong> {team.pitch}</p><ul>{team.features.map((f) => <li key={f}>{f}</li>)}</ul></div> : null; })() : null}
298
+ {info.restriction ? <p role="alert">{restrictionCopy(info)}</p> : null}
299
+ {info.plan.id === 'free' ? <button type="button" onClick={() => void upgrade()}>{trialDaysOf(info) && !info.subscription ? `Start a ${trialDaysOf(info)}-day trial of Team` : 'Upgrade to Team'}</button> : <button type="button" onClick={() => void manage()}>Manage billing</button>}
300
+ {checkout ? <p role="status">Your checkout is ready: <a href={checkout}>continue to checkout at Polar</a>. You come back here when it is paid.</p> : null}
301
+ {portal ? <p role="status"><a href={portal}>Open the billing portal</a> — invoices, payment method, cancellation.</p> : null}
302
+ {note ? <p role="status">{note}</p> : null}
303
+ </div>
304
+ );
305
+ }
306
+
307
+ /** A date in the person's own calendar — a UTC slice reads a day ahead in the Americas (walk 5). */
308
+ const day = (iso: string): string => { const t = Date.parse(iso); return Number.isNaN(t) ? iso : new Date(t).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' }); };
309
+
310
+ /** The paid plan's trial, days, when it offers one (the biller's field; layer 6). */
311
+ const trialDaysOf = (info: BillingInfo): number | null => (info.plans.find((p) => p.id === 'team') as { trialDays?: number | null } | undefined)?.trialDays ?? null;
312
+
313
+ /** What the restriction machine's position means for this org, in the person's words — the reason named (layer 5: Twenty's NO_SUBSCRIPTION, Dub's downgrade copy). */
314
+ function restrictionCopy(info: BillingInfo): string {
315
+ const r = info.restriction!; const until = when(r.graceEnds); const allows = info.plan.limits.worlds ?? 0;
316
+ if (r.meter === 'worlds') {
317
+ const lead = r.reason === 'no_subscription' ? `Your Team subscription ended, so this org is on the Free plan, which allows ${allows} world${allows === 1 ? '' : 's'}; it holds ${info.worlds}.` : `The ${info.plan.name} plan allows ${allows} world${allows === 1 ? '' : 's'}; this org holds ${info.worlds}.`;
318
+ return r.status === 'restricted' ? `${lead} Nothing was deleted, but the grace period ended ${until}: the worlds you hold no longer refresh and new ones are refused, until you upgrade or delete worlds down to ${allows}.` : `${lead} Nothing is deleted, and the worlds you hold keep working — refreshing and taking pushes — until ${until}. New worlds beyond ${allows} are refused from now on. After ${until} the ones you hold stop refreshing too, until you upgrade or delete worlds down to ${allows}.`;
319
+ }
320
+ return r.status === 'restricted' ? `Restricted: the ${info.plan.name} plan's world-hours were exceeded and the grace period ended ${until} — upgrade to provision and refresh again.` : `Over the plan's world-hours: you can keep working until ${until}; after that, provisioning and refresh are restricted until you upgrade or the next period.`;
321
+ }
322
+
323
+ type MemberRow = { id: string; userId: string; email: string | null; name: string | null; role: string };
324
+ type PendingRow = { id: string; email: string; role: string; createdAt: string | null; expiresAt: string | null };
325
+ /** The org's members — the Volter identity service's memberships — with the admin's invite, remove and role. */
326
+ function Members({ token, org, admin, onChanged }: { token: string; org: string; admin: boolean; onChanged?: () => void }): React.JSX.Element {
327
+ const [members, setMembers] = useState<MemberRow[] | null>(null); const [pending, setPending] = useState<PendingRow[]>([]);
328
+ const [email, setEmail] = useState(''); const [note, setNote] = useState<string | null>(null);
329
+ const load = useCallback(async () => { const r = await door(token, `/-/orgs/${org}/members`); const body = r.ok ? ((await r.json()) as { members: MemberRow[]; pending?: PendingRow[] }) : { members: [] as MemberRow[], pending: [] as PendingRow[] }; setMembers(body.members); setPending(body.pending ?? []); }, [token, org]);
330
+ useEffect(() => { void load(); }, [load]);
331
+ const act = async (label: string, path: string, init: RequestInit): Promise<void> => {
332
+ setNote(null); const r = await door(token, path, init); const body = (await r.json().catch(() => ({}))) as { error?: string };
333
+ setNote(r.ok ? label : body.error ?? `answered ${r.status}`); await load(); if (r.ok) onChanged?.(); // the Activity list re-reads (walk 4: no row for the invite until a reload)
334
+ };
335
+ return (
336
+ <div className="members" aria-label={`Members of ${org}`}>
337
+ <h4>Members</h4>
338
+ {members === null ? <p className="mode">loading…</p> : (
339
+ <ul>{members.map((m) => <li key={m.id}>{m.email ?? m.userId}{m.name ? ` (${m.name})` : ''} <span className="mode">{m.role.replace('org:', '')}</span>
340
+ {admin ? <> <button type="button" aria-label={`${m.role.endsWith('admin') ? 'Make member' : 'Make admin'}: ${m.email ?? m.userId}`} onClick={() => void act(`role changed for ${m.email ?? m.userId}`, `/-/orgs/${org}/members/${m.userId}`, { method: 'PATCH', body: JSON.stringify({ role: m.role.endsWith('admin') ? 'member' : 'admin' }) })}>{m.role.endsWith('admin') ? 'Make member' : 'Make admin'}</button> <button type="button" aria-label={`Remove ${m.email ?? m.userId}`} onClick={() => { if (window.confirm(`Remove ${m.email ?? m.userId} from ${org}? They lose access to its worlds.`)) void act(`removed ${m.email ?? m.userId}`, `/-/orgs/${org}/members/${m.userId}`, { method: 'DELETE' }); }}>Remove {m.email ?? m.userId}</button></> : null}
341
+ </li>)}</ul>
342
+ )}
343
+ {pending.length > 0 ? <><h5 className="mode">Invited, not yet joined</h5><ul>{pending.map((p) => <li key={p.id}>{p.email} <span className="mode">invited{p.createdAt ? ` ${day(p.createdAt)}` : ''}{p.expiresAt ? ` · expires ${day(p.expiresAt)}` : ''}</span>{admin ? <> <button type="button" aria-label={`Revoke invitation: ${p.email}`} onClick={() => void act(`invitation to ${p.email} revoked`, `/-/orgs/${org}/invitations/${p.id}`, { method: 'DELETE' })}>Revoke invitation</button></> : null}</li>)}</ul></> : null}
344
+ {admin ? <form onSubmit={(e) => { e.preventDefault(); const to = email; void door(token, `/-/orgs/${org}/members`, { method: 'POST', body: JSON.stringify({ email: to }) }).then(async (r) => { const body = (await r.json().catch(() => ({}))) as { error?: string; pending?: boolean }; setNote(r.ok ? (body.pending ? `invited ${to} — they get an email and join when they accept` : `added ${to} — we emailed them`) : body.error ?? `answered ${r.status}`); await load(); if (r.ok) { setEmail(''); onChanged?.(); } }); }}>
345
+ <label>Add a member by email <input name="inviteEmail" type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="teammate@example.com" required /></label>
346
+ <button type="submit">Add member</button>
347
+ <p className="mode">Someone with an account here is added at once; anyone else is invited by email and joins when they accept.</p>
348
+ </form> : null}
349
+ {note ? <p role="status">{note}</p> : null}
350
+ </div>
351
+ );
352
+ }
353
+
354
+ /** The org itself: its name, and the danger zone — deletion, refused while it holds worlds. */
355
+ function OrgSettings({ token, org, name, admin, onChanged, version, flags }: { token: string; org: string; name: string; admin: boolean; worlds: number; onChanged: (said?: string) => Promise<void>; version?: number; flags?: Record<string, boolean> }): React.JSX.Element | null {
356
+ const [newName, setNewName] = useState(name); const [note, setNote] = useState<string | null>(null); const [confirm, setConfirm] = useState('');
357
+ if (!admin) return null;
358
+ const rename = async (e: React.FormEvent): Promise<void> => {
359
+ e.preventDefault(); setNote(null);
360
+ const r = await door(token, `/-/orgs/${org}`, { method: 'PATCH', body: JSON.stringify({ name: newName }) });
361
+ const body = (await r.json().catch(() => ({}))) as { error?: string };
362
+ setNote(r.ok ? `renamed to ${newName}` : body.error ?? `answered ${r.status}`); if (r.ok) await onChanged();
363
+ };
364
+ const removeEverything = async (): Promise<void> => {
365
+ setNote(null);
366
+ const r = await door(token, `/-/orgs/${org}?everything=1`, { method: 'DELETE' });
367
+ const body = (await r.json().catch(() => ({}))) as { error?: string };
368
+ if (r.ok) { await onChanged(`deleted ${org} and its worlds`); return; } // said above the list: this org's own card is about to go
369
+ setNote(body.error ?? `answered ${r.status}`);
370
+ };
371
+ const remove = async (): Promise<void> => {
372
+ setNote(null);
373
+ const r = await door(token, `/-/orgs/${org}`, { method: 'DELETE' });
374
+ const body = (await r.json().catch(() => ({}))) as { error?: string };
375
+ setNote(r.ok ? `deleted ${org}` : body.error ?? `answered ${r.status}`); if (r.ok) await onChanged();
376
+ };
377
+ return (
378
+ <details className="org-settings"><summary>Org settings</summary>
379
+ <form onSubmit={rename}><label>New name <input name="orgRename" value={newName} onChange={(e) => setNewName(e.target.value)} required /></label><p className="mode">Only the name changes. The org's address, <code>{org}/…</code>, stays — nothing your code or the CLI points at moves.</p><button type="submit">Rename</button></form>
380
+ {admin ? <Labs token={token} org={org} onChanged={onChanged} /> : null}
381
+ {admin && flags?.webhooks ? <Webhooks token={token} org={org} /> : null}
382
+ {admin ? <SupportAccess token={token} org={org} version={version ?? 0} /> : null}
383
+ {admin ? <SecuritySettings token={token} org={org} onChanged={onChanged} /> : null}
384
+ {admin ? <OrgTokens token={token} org={org} /> : null}
385
+ <div className="danger" aria-label="Danger zone">
386
+ <h5>Danger zone</h5>
387
+ <p>Deleting the org removes it for every member. It is refused while the org holds worlds — delete those first.</p>
388
+ <label>Type the org's name to confirm <input name="confirmDelete" value={confirm} onChange={(e) => setConfirm(e.target.value)} placeholder={org} /></label>
389
+ <button type="button" disabled={confirm !== org} onClick={() => void remove()}>Delete org</button>
390
+ <button type="button" disabled={confirm !== org} onClick={() => { if (window.confirm(`Delete ${org} AND every world it holds? Their hosts stop serving them and their history goes with them.`)) void removeEverything(); }}>Delete org and its worlds</button>
391
+ </div>
392
+ {note ? <p role="status">{note}</p> : null}
393
+ </details>
394
+ );
395
+ }
396
+
397
+ /** Scopes (layer 6): `object:action` as Sentry's and PostHog's — a token opens only what its scopes name. */
398
+ const SCOPE_OBJECTS = ['worlds', 'orgs', 'members', 'billing', 'tokens', 'activity'] as const;
399
+ function scopesFor(access: 'all' | 'read' | 'custom', custom: Record<string, 'none' | 'read' | 'write'>): string[] {
400
+ if (access === 'all') return ['*'];
401
+ if (access === 'read') return SCOPE_OBJECTS.map((o) => `${o}:read`);
402
+ const chosen = SCOPE_OBJECTS.filter((o) => custom[o] && custom[o] !== 'none').map((o) => `${o}:${custom[o]}`);
403
+ return chosen.length ? chosen : ['worlds:read'];
404
+ }
405
+ function scopeWords(scopes?: string[]): string {
406
+ if (!scopes || scopes.includes('*')) return 'all access';
407
+ if (SCOPE_OBJECTS.every((o) => scopes.includes(`${o}:read`)) && scopes.every((s) => s.endsWith(':read'))) return 'read only';
408
+ return scopes.join(', ');
409
+ }
410
+ function ScopePicker({ access, setAccess, custom, setCustom }: { access: 'all' | 'read' | 'custom'; setAccess: (a: 'all' | 'read' | 'custom') => void; custom: Record<string, 'none' | 'read' | 'write'>; setCustom: (c: Record<string, 'none' | 'read' | 'write'>) => void }): React.JSX.Element {
411
+ return (
412
+ <>
413
+ <label>Access <select name="tokenAccess" aria-label="Access" value={access} onChange={(e) => setAccess(e.target.value as 'all' | 'read' | 'custom')}><option value="all">all access — every scope you hold (*)</option><option value="read">read only — every object's :read scope</option><option value="custom">choose per object — read or read-and-write scopes</option></select></label>
414
+ <p className="mode">Scopes are <code>object:action</code> over worlds, orgs, members, billing, tokens and activity — the same names the API reference gives each endpoint.</p>
415
+ {access === 'custom' ? <ul className="usage">{SCOPE_OBJECTS.map((o) => <li key={o}><label>{o} <select aria-label={`Scope: ${o}`} value={custom[o] ?? 'none'} onChange={(e) => setCustom({ ...custom, [o]: e.target.value as 'none' | 'read' | 'write' })}><option value="none">no access</option><option value="read">read</option><option value="write">read and write</option></select></label></li>)}</ul> : null}
416
+ </>
417
+ );
418
+ }
419
+
420
+ /** Labs (layer 8: Twenty's lab, Sentry's Early Adopter, Cal.com's opt-in): early-access features, on per org, or all with one switch. */
421
+ function Labs({ token, org, onChanged }: { token: string; org: string; onChanged: () => Promise<void> }): React.JSX.Element {
422
+ type Feature = { key: string; name: string; description: string; since: string; on: boolean };
423
+ const [features, setFeatures] = useState<Feature[] | null>(null); const [early, setEarly] = useState(false); const [note, setNote] = useState<string | null>(null);
424
+ const load = useCallback(async () => { const r = await door(token, `/-/orgs/${org}/labs`); if (r.ok) { const b = (await r.json()) as { features: Feature[]; earlyAdopter: boolean }; setFeatures(b.features); setEarly(b.earlyAdopter); } else setFeatures([]); }, [token, org]);
425
+ useEffect(() => { void load(); }, [load]);
426
+ const set = async (patch: { flags?: Record<string, boolean>; earlyAdopter?: boolean }, label: string): Promise<void> => { setNote(null); const r = await door(token, `/-/orgs/${org}/labs`, { method: 'PATCH', body: JSON.stringify(patch) }); const body = (await r.json().catch(() => ({}))) as { error?: string }; setNote(r.ok ? label : body.error ?? `answered ${r.status}`); await load(); if (r.ok) await onChanged(); };
427
+ return (
428
+ <div aria-label="Labs">
429
+ <h4>Labs</h4>
430
+ <p className="mode">Features that are new. Turn one on for this org, or turn on every new one as it lands.</p>
431
+ <label><input type="checkbox" name="earlyAdopter" checked={early} onChange={(e) => { setEarly(e.target.checked); void set({ earlyAdopter: e.target.checked }, e.target.checked ? 'early adopter: every new feature is on' : 'early adopter off'); }} /> Early adopter — every new feature is on for this org as soon as it lands</label>
432
+ {features === null ? <p className="mode">loading…</p> : <ul>{features.map((f) => <li key={f.key}><label><input type="checkbox" name={`lab:${f.key}`} checked={early || f.on} disabled={early} onChange={(e) => { const on = e.target.checked; setFeatures((list) => (list ?? []).map((x) => (x.key === f.key ? { ...x, on } : x))); void set({ flags: { [f.key]: on } }, `${f.name} ${on ? 'on' : 'off'}`); }} /> <strong>{f.name}</strong> <span className="mode">since {day(f.since)}</span> — {f.description}</label></li>)}</ul>}
433
+ {note ? <p role="status">{note}</p> : null}
434
+ </div>
435
+ );
436
+ }
437
+
438
+ /** Webhooks out (layer 7): endpoints with the events they want, the secret once, a test event, the last deliveries (Dub's, Polar's, Cal.com's shape). */
439
+ function Webhooks({ token, org }: { token: string; org: string }): React.JSX.Element {
440
+ type Endpoint = { id: string; url: string; events: string[]; description: string; createdAt: string; consecutiveFailures: number; lastFailedAt: string | null; disabledAt: string | null };
441
+ type Delivery = { id: string; event: string; attempt: number; state: string; nextAt: string; attempts: Array<{ at: string; status: number | null; ok: boolean; response: string }> };
442
+ const [list, setList] = useState<Endpoint[] | null>(null); const [names, setNames] = useState<string[]>([]);
443
+ const [url, setUrl] = useState(''); const [description, setDescription] = useState(''); const [all, setAll] = useState(true); const [chosen, setChosen] = useState<string[]>([]);
444
+ const [secret, setSecret] = useState<string | null>(null); const [note, setNote] = useState<string | null>(null); const [open, setOpen] = useState<string | null>(null); const [deliveries, setDeliveries] = useState<Delivery[]>([]);
445
+ const load = useCallback(async () => { const r = await door(token, `/-/orgs/${org}/webhooks`); if (r.ok) { const b = (await r.json()) as { webhooks: Endpoint[]; events: string[] }; setList(b.webhooks); setNames(b.events); } else setList([]); }, [token, org]);
446
+ useEffect(() => { void load(); }, [load]);
447
+ const showDeliveries = async (id: string): Promise<void> => { setOpen(id); const r = await door(token, `/-/orgs/${org}/webhooks/${id}/deliveries`); setDeliveries(r.ok ? ((await r.json()) as { deliveries: Delivery[] }).deliveries : []); };
448
+ const add = async (e: React.FormEvent): Promise<void> => {
449
+ e.preventDefault(); setNote(null); setSecret(null);
450
+ const r = await door(token, `/-/orgs/${org}/webhooks`, { method: 'POST', body: JSON.stringify({ url, description, events: all ? ['*'] : chosen }) });
451
+ const body = (await r.json().catch(() => ({}))) as { secret?: string; error?: string };
452
+ if (!r.ok || !body.secret) { setNote(body.error ?? `answered ${r.status}`); return; }
453
+ setSecret(body.secret); setUrl(''); setDescription(''); await load();
454
+ };
455
+ const act = async (label: string, path: string, init: RequestInit): Promise<void> => { setNote(null); const r = await door(token, path, init); const body = (await r.json().catch(() => ({}))) as { error?: string; delivery?: Delivery }; setNote(r.ok ? (body.delivery ? `${label}: ${body.delivery.state === 'delivered' ? `delivered (${body.delivery.attempts[0]?.status})` : `not delivered (${body.delivery.attempts[0]?.status ?? body.delivery.attempts[0]?.response}); retrying at ${when(body.delivery.nextAt)}`}` : label) : body.error ?? `answered ${r.status}`); await load(); if (open) await showDeliveries(open); };
456
+ const state = (w: Endpoint): string => w.disabledAt ? `disabled ${day(w.disabledAt)}` : w.consecutiveFailures > 0 ? `failing (${w.consecutiveFailures} in a row, last ${when(w.lastFailedAt ?? w.createdAt)})` : 'ok';
457
+ return (
458
+ <div aria-label="Webhooks">
459
+ <h4>Webhooks</h4>
460
+ <p className="mode">Events the org raises — a world provisioned, a member added, the plan changed — sent to an address of yours as JSON, signed with Standard Webhooks (<code>webhook-id</code>, <code>webhook-timestamp</code>, <code>webhook-signature</code>). Ten attempts with backoff; twenty failures in a row disable the endpoint and email the admins.</p>
461
+ {note ? <p role="status">{note}</p> : null}
462
+ {list === null ? <p className="mode">loading…</p> : list.length === 0 ? <p className="mode">No endpoints yet.</p> : <ul>{list.map((w) => <li key={w.id}><code>{w.url}</code>{w.description ? ` — ${w.description}` : ''} <span className="mode">· {w.events.includes('*') ? 'every event' : w.events.join(', ')} · {state(w)}</span> <button type="button" aria-label={`Send a test event: ${w.url}`} onClick={() => void act('test event', `/-/orgs/${org}/webhooks/${w.id}/test`, { method: 'POST' })}>Send a test event</button> <button type="button" aria-label={`Deliveries: ${w.url}`} onClick={() => void showDeliveries(w.id)}>Deliveries</button> {w.disabledAt ? <button type="button" aria-label={`Enable: ${w.url}`} onClick={() => void act('endpoint enabled', `/-/orgs/${org}/webhooks/${w.id}`, { method: 'PATCH', body: JSON.stringify({ enabled: true }) })}>Enable</button> : <button type="button" aria-label={`Disable: ${w.url}`} onClick={() => void act('endpoint disabled', `/-/orgs/${org}/webhooks/${w.id}`, { method: 'PATCH', body: JSON.stringify({ enabled: false }) })}>Disable</button>} <button type="button" aria-label={`Remove endpoint: ${w.url}`} onClick={() => { if (window.confirm(`Remove the endpoint ${w.url}? Nothing more is sent to it.`)) void act('endpoint removed', `/-/orgs/${org}/webhooks/${w.id}`, { method: 'DELETE' }); }}>Remove</button>
463
+ {open === w.id ? <div aria-label={`Deliveries to ${w.url}`}>{deliveries.length === 0 ? <p className="mode">No deliveries yet.</p> : <table><thead><tr><th>when</th><th>event</th><th>attempt</th><th>status</th><th>answer</th></tr></thead><tbody>{deliveries.map((d) => <tr key={d.id}><td><code>{when(d.attempts[d.attempts.length - 1]?.at ?? d.nextAt)}</code></td><td>{d.event}</td><td>{d.attempt}{d.state === 'pending' ? ` · next ${when(d.nextAt)}` : ` · ${d.state}`}</td><td>{d.attempts[d.attempts.length - 1]?.status ?? '—'}</td><td className="mode">{(d.attempts[d.attempts.length - 1]?.response ?? '').slice(0, 80)}</td></tr>)}</tbody></table>}</div> : null}
464
+ </li>)}</ul>}
465
+ <form onSubmit={add}>
466
+ <label>Endpoint URL <input name="webhookUrl" type="url" value={url} onChange={(e) => setUrl(e.target.value)} placeholder="https://example.com/hooks/volter" required /></label>
467
+ <label>Description (optional) <input name="webhookDescription" value={description} onChange={(e) => setDescription(e.target.value)} /></label>
468
+ <label><input type="checkbox" name="webhookAll" checked={all} onChange={(e) => setAll(e.target.checked)} /> Every event</label>
469
+ {!all ? <ul className="usage">{names.map((n) => <li key={n}><label><input type="checkbox" name={`webhookEvent:${n}`} checked={chosen.includes(n)} onChange={(e) => setChosen(e.target.checked ? [...chosen, n] : chosen.filter((x) => x !== n))} /> {n}</label></li>)}</ul> : null}
470
+ <button type="submit">Add endpoint</button>
471
+ </form>
472
+ {secret ? <p role="status">The endpoint's secret, shown once — your receiver verifies each delivery with it: <code>{secret}</code> <button type="button" onClick={() => { void navigator.clipboard?.writeText(secret).then(() => setNote('copied')); }}>Copy</button></p> : null}
473
+ </div>
474
+ );
475
+ }
476
+
477
+ /** Support access (layer 6): the standing consent, and its revocation. */
478
+ function SupportAccess({ token, org, version }: { token: string; org: string; version: number }): React.JSX.Element | null {
479
+ const [access, setAccess] = useState<{ until: string } | null | undefined>(undefined); const [note, setNote] = useState<string | null>(null);
480
+ const load = useCallback(async () => { const r = await door(token, `/-/orgs/${org}/support-access`); setAccess(r.ok ? ((await r.json()) as { supportAccess: { until: string } | null }).supportAccess : null); }, [token, org, version]); // re-read after the Help form grants it
481
+ useEffect(() => { void load(); }, [load]);
482
+ if (access === undefined) return null;
483
+ return (
484
+ <div aria-label="Support access">
485
+ <h4>Support access</h4>
486
+ {access ? <p>Volter support may open this org, read only, until {when(access.until)}. Every time they do is in the Activity log with their reason. <button type="button" onClick={() => void door(token, `/-/orgs/${org}/support-access`, { method: 'DELETE' }).then(async (r) => { setNote(r.ok ? 'support access revoked' : `answered ${r.status}`); await load(); })}>Revoke support access</button></p> : <p className="mode">Not allowed. You can allow it for 7 days from the Help form when you write to us.</p>}
487
+ {note ? <p role="status">{note}</p> : null}
488
+ </div>
489
+ );
490
+ }
491
+
492
+ /** Help (layer 6: Supabase's support form — a category, a severity, what it concerns, and the consent switch). */
493
+ function Help({ token, orgs, onChanged }: { token: string; orgs: string[]; onChanged?: () => void }): React.JSX.Element {
494
+ const [category, setCategory] = useState('worlds'); const [severity, setSeverity] = useState('normal'); const [subject, setSubject] = useState(''); const [message, setMessage] = useState('');
495
+ const [org, setOrg] = useState(orgs[0] ?? ''); const [world, setWorld] = useState(''); const [allow, setAllow] = useState(false); const [note, setNote] = useState<string | null>(null);
496
+ useEffect(() => { if (!org && orgs[0]) setOrg(orgs[0]); }, [orgs, org]);
497
+ const send = async (e: React.FormEvent): Promise<void> => {
498
+ e.preventDefault(); setNote(null);
499
+ const r = await door(token, '/-/support', { method: 'POST', body: JSON.stringify({ category, severity, subject, message, ...(org ? { org } : {}), ...(world ? { world } : {}), ...(allow && org ? { allowAccessDays: 7 } : {}) }) });
500
+ const body = (await r.json().catch(() => ({}))) as { id?: string; error?: string; copyTo?: string | null; access?: { until: string } | null };
501
+ if (!r.ok || !body.id) { setNote(body.error ?? `answered ${r.status}`); return; }
502
+ setNote(`Sent — your ticket is ${body.id}${body.copyTo ? `; we emailed a copy to ${body.copyTo}` : ''}. A person answers, usually within a working day.${body.access ? ` Support may open ${org} until ${when(body.access.until)}; revoke it under Org settings.` : ''}`);
503
+ setSubject(''); setMessage(''); setWorld(''); setAllow(false); onChanged?.();
504
+ };
505
+ return (
506
+ <section aria-labelledby="help" id="help">
507
+ <h3 id="help">Help</h3>
508
+ <p className="mode">Ask a person. Say what you saw and where; the console's address, your org and the world go along with it.</p>
509
+ <form onSubmit={send}>
510
+ <label>About <select name="helpCategory" aria-label="About" value={category} onChange={(e) => setCategory(e.target.value)}><option value="worlds">a world or a twin</option><option value="billing">billing</option><option value="account">my account or team</option><option value="security">security</option><option value="other">something else</option></select></label>
511
+ <label>How urgent <select name="helpSeverity" aria-label="How urgent" value={severity} onChange={(e) => setSeverity(e.target.value)}><option value="low">low — a question</option><option value="normal">normal</option><option value="high">high — something is broken for us</option><option value="urgent">urgent — we cannot work</option></select></label>
512
+ {orgs.length > 0 ? <label>Org <select name="helpOrg" aria-label="Help org" value={org} onChange={(e) => setOrg(e.target.value)}>{orgs.map((o) => <option key={o} value={o}>{o}</option>)}</select></label> : null}
513
+ <label>World (optional) <input name="helpWorld" value={world} onChange={(e) => setWorld(e.target.value)} placeholder="acme/team" /></label>
514
+ <label>Subject <input name="helpSubject" value={subject} onChange={(e) => setSubject(e.target.value)} required /></label>
515
+ <label>What happened <textarea name="helpMessage" value={message} onChange={(e) => setMessage(e.target.value)} rows={5} required /></label>
516
+ {org ? <label><input type="checkbox" name="helpAllow" checked={allow} onChange={(e) => setAllow(e.target.checked)} /> Allow Volter support to open this org, read only, for 7 days — so they can see what you see. Admins only; revocable under Org settings.</label> : null}
517
+ <button type="submit">Send to support</button>
518
+ </form>
519
+ {note ? <p role="status">{note}</p> : null}
520
+ </section>
521
+ );
522
+ }
523
+
524
+ /** Security (layer 6): the switches the peers share — PostHog's members_can_invite, members_can_use_personal_api_keys; Twenty's approved domains. */
525
+ function SecuritySettings({ token, org, onChanged }: { token: string; org: string; onChanged: () => Promise<void> }): React.JSX.Element {
526
+ type Sec = { membersCanInvite: boolean; membersCanUsePersonalTokens: boolean; approvedEmailDomains: string[] };
527
+ const [sec, setSec] = useState<Sec | null>(null); const [domains, setDomains] = useState(''); const [note, setNote] = useState<string | null>(null);
528
+ const load = useCallback(async () => { const r = await door(token, `/-/orgs/${org}/security`); if (r.ok) { const s = ((await r.json()) as { security: Sec }).security; setSec(s); setDomains(s.approvedEmailDomains.join(', ')); } }, [token, org]);
529
+ useEffect(() => { void load(); }, [load]);
530
+ const save = async (e: React.FormEvent): Promise<void> => {
531
+ e.preventDefault(); if (!sec) return; setNote(null);
532
+ const r = await door(token, `/-/orgs/${org}/security`, { method: 'PATCH', body: JSON.stringify({ ...sec, approvedEmailDomains: domains.split(',').map((d) => d.trim()).filter(Boolean) }) });
533
+ const body = (await r.json().catch(() => ({}))) as { error?: string };
534
+ setNote(r.ok ? 'security settings saved' : body.error ?? `answered ${r.status}`); await load(); if (r.ok) await onChanged();
535
+ };
536
+ if (!sec) return <p className="mode">loading…</p>;
537
+ return (
538
+ <form onSubmit={save} aria-label="Security">
539
+ <h4>Security</h4>
540
+ <label><input type="checkbox" name="membersCanInvite" checked={sec.membersCanInvite} onChange={(e) => setSec({ ...sec, membersCanInvite: e.target.checked })} /> Members may add and invite members (otherwise admins only)</label>
541
+ <label><input type="checkbox" name="membersCanUsePersonalTokens" checked={sec.membersCanUsePersonalTokens} onChange={(e) => setSec({ ...sec, membersCanUsePersonalTokens: e.target.checked })} /> Personal access tokens may open this org (otherwise only org tokens and signed-in sessions)</label>
542
+ <label>Approved email domains for invitations <input name="approvedEmailDomains" value={domains} onChange={(e) => setDomains(e.target.value)} placeholder="acme.test, acme.com — empty means any" /></label>
543
+ <button type="submit">Save security settings</button>
544
+ {note ? <p role="status">{note}</p> : null}
545
+ </form>
546
+ );
547
+ }
548
+
549
+ /** Org tokens (layer 6): the org's CI credential — admin-made, org-bound, scoped, expiring, shown once (Polar's organization token, Twenty's workspace key). */
550
+ function OrgTokens({ token, org }: { token: string; org: string }): React.JSX.Element {
551
+ const [list, setList] = useState<Array<{ id: string; name: string; createdAt: string; last4: string; lastUsedAt?: string; expiresAt?: string | null; scopes?: string[] }> | null>(null);
552
+ const [name, setName] = useState('ci'); const [expiry, setExpiry] = useState<'30' | '90' | 'never'>('90'); const [made, setMade] = useState<string | null>(null); const [note, setNote] = useState<string | null>(null);
553
+ const [access, setAccess] = useState<'all' | 'read' | 'custom'>('read'); const [custom, setCustom] = useState<Record<string, 'none' | 'read' | 'write'>>({});
554
+ const load = useCallback(async () => { const r = await door(token, `/-/orgs/${org}/tokens`); setList(r.ok ? ((await r.json()) as { tokens: typeof list }).tokens : []); }, [token, org]);
555
+ useEffect(() => { void load(); }, [load]);
556
+ const create = async (e: React.FormEvent): Promise<void> => {
557
+ e.preventDefault(); setNote(null); setMade(null);
558
+ const r = await door(token, `/-/orgs/${org}/tokens`, { method: 'POST', body: JSON.stringify({ name, expiresInDays: expiry === 'never' ? null : Number(expiry), scopes: scopesFor(access, custom) }) });
559
+ const body = (await r.json().catch(() => ({}))) as { token?: string; error?: string };
560
+ if (!r.ok || !body.token) { setNote(body.error ?? `answered ${r.status}`); return; }
561
+ setMade(body.token); await load();
562
+ };
563
+ const revoke = async (id: string, n: string): Promise<void> => { setNote(null); const r = await door(token, `/-/orgs/${org}/tokens/${id}`, { method: 'DELETE' }); setNote(r.ok ? `org token ${n} revoked` : `answered ${r.status}`); await load(); };
564
+ return (
565
+ <div aria-label="Org tokens">
566
+ <h4>Org tokens</h4>
567
+ <p className="mode">A token that acts as the org, not as a person — for CI and scripts: <code>volter login {ORIGIN} --token …</code> then <code>volter remote add origin {org}/&lt;world&gt;</code>. Read only by default.</p>
568
+ {list === null ? <p className="mode">loading…</p> : list.length === 0 ? <p className="mode">No org tokens yet.</p> : <ul>{list.map((t) => <li key={t.id}>{t.name} <span className="mode">…{t.last4} · made {day(t.createdAt)}{t.lastUsedAt ? ` · last used ${day(t.lastUsedAt)}` : ' · never used'}{t.expiresAt === null ? ' · never expires' : t.expiresAt ? ` · expires ${day(t.expiresAt)}` : ''} · {scopeWords(t.scopes)}</span> <button type="button" aria-label={`Revoke org token: ${t.name}`} onClick={() => void revoke(t.id, t.name)}>Revoke org token</button></li>)}</ul>}
569
+ <form onSubmit={create}><label>Org token name <input name="orgTokenName" value={name} onChange={(e) => setName(e.target.value)} required /></label><label>Expires <select name="orgTokenExpiry" aria-label="Org token expires" value={expiry} onChange={(e) => setExpiry(e.target.value as '30' | '90' | 'never')}><option value="30">in 30 days</option><option value="90">in 90 days</option><option value="never">never</option></select></label><ScopePicker access={access} setAccess={setAccess} custom={custom} setCustom={setCustom} /><button type="submit">Create org token</button></form>
570
+ {made ? <p role="status">The org token, shown once — copy it now: <code>{made}</code> <button type="button" onClick={() => { void navigator.clipboard?.writeText(made).then(() => setNote('copied')); }}>Copy</button></p> : null}
571
+ {note ? <p role="status">{note}</p> : null}
572
+ </div>
573
+ );
574
+ }
575
+
576
+ /** You: the personal access tokens that sign the CLI in as you — shown once at creation, revocable. */
577
+ function PersonalTokens({ token, onChanged }: { token: string; onChanged?: () => void }): React.JSX.Element {
578
+ const [list, setList] = useState<Array<{ id: string; name: string; createdAt: string; last4: string; lastUsedAt?: string; expiresAt?: string | null; scopes?: string[] }> | null>(null);
579
+ const [name, setName] = useState('cli'); const [expiry, setExpiry] = useState<'30' | '90' | 'never'>('30');
580
+ const [access, setAccess] = useState<'all' | 'read' | 'custom'>('all'); const [custom, setCustom] = useState<Record<string, 'none' | 'read' | 'write'>>({}); const [made, setMade] = useState<string | null>(null); const [note, setNote] = useState<string | null>(null);
581
+ const load = useCallback(async () => { const r = await door(token, '/-/tokens'); setList(r.ok ? ((await r.json()) as { tokens: typeof list }).tokens : []); }, [token]);
582
+ useEffect(() => { void load(); }, [load]);
583
+ const create = async (e: React.FormEvent): Promise<void> => {
584
+ e.preventDefault(); setNote(null); setMade(null);
585
+ const r = await door(token, '/-/tokens', { method: 'POST', body: JSON.stringify({ name, expiresInDays: expiry === 'never' ? null : Number(expiry), scopes: scopesFor(access, custom) }) });
586
+ const body = (await r.json().catch(() => ({}))) as { token?: string; error?: string };
587
+ if (!r.ok || !body.token) { setNote(body.error ?? `answered ${r.status}`); return; }
588
+ setMade(body.token); await load(); onChanged?.();
589
+ };
590
+ const revoke = async (id: string): Promise<void> => { setNote(null); const r = await door(token, `/-/tokens/${id}`, { method: 'DELETE' }); setNote(r.ok ? 'token revoked' : `answered ${r.status}`); await load(); };
591
+ return (
592
+ <section aria-labelledby="you">
593
+ <h3 id="you">You</h3>
594
+ <h4>Personal access tokens</h4>
595
+ <p>A token signs the <code>volter</code> command in as you: <code>volter login {ORIGIN} --token …</code>, then <code>volter remote add origin org/world</code>.</p>
596
+ {list === null ? <p className="mode">loading…</p> : list.length === 0 ? <p className="mode">No tokens yet.</p> : <ul>{list.map((t) => <li key={t.id}>{t.name} <span className="mode">…{t.last4} · made {day(t.createdAt)}{t.lastUsedAt ? ` · last used ${day(t.lastUsedAt)}` : ' · never used'}{t.expiresAt === null ? ' · never expires' : t.expiresAt ? (Date.parse(t.expiresAt) <= Date.now() ? ` · expired ${day(t.expiresAt)}` : ` · expires ${day(t.expiresAt)}`) : ''} · {scopeWords(t.scopes)}</span> <button type="button" onClick={() => void revoke(t.id)}>Revoke</button></li>)}</ul>}
597
+ <form onSubmit={create}><label>Token name <input name="tokenName" value={name} onChange={(e) => setName(e.target.value)} required /></label><label>Expires <select name="tokenExpiry" aria-label="Expires" value={expiry} onChange={(e) => setExpiry(e.target.value as '30' | '90' | 'never')}><option value="30">in 30 days</option><option value="90">in 90 days</option><option value="never">never</option></select></label><ScopePicker access={access} setAccess={setAccess} custom={custom} setCustom={setCustom} /><button type="submit">Create token</button></form>
598
+ {made ? <p role="status">Your new token, shown once — copy it now: <code>{made}</code> <button type="button" onClick={() => { void navigator.clipboard?.writeText(made).then(() => setNote('copied')); }}>Copy</button></p> : null}
599
+ {note ? <p role="status">{note}</p> : null}
600
+ </section>
601
+ );
602
+ }
603
+
604
+ /** The first run (punchlist 3.5): what a person does next, with the real commands — dismissed once done. */
605
+ function GettingStarted({ token, orgs, worlds, firstWorld, version }: { token: string; orgs: string[]; worlds: number; firstWorld?: string | undefined; version: number }): React.JSX.Element | null {
606
+ const KEY = 'volter.console.onboarding';
607
+ const [dismissed, setDismissed] = useState<boolean>(() => { try { return localStorage.getItem(KEY) === 'done'; } catch { return false; } });
608
+ const [tokensMade, setTokensMade] = useState<boolean | null>(null);
609
+ useEffect(() => { void door(token, '/-/tokens').then(async (r) => setTokensMade(r.ok ? ((await r.json()) as { tokens: unknown[] }).tokens.length > 0 : false)); }, [token, version]); // re-read after a token is made (walk 3: the step stayed unticked)
610
+ if (dismissed) return null;
611
+ const org = orgs[0]; const step = (done: boolean, text: React.ReactNode): React.JSX.Element => <li aria-checked={done} role="checkbox">{done ? '✓' : '○'} {text}</li>;
612
+ return (
613
+ <section aria-labelledby="getting-started" className="onboarding">
614
+ <h2 id="getting-started">Getting started</h2>
615
+ <ol>
616
+ {step(orgs.length > 0, "Create an org — the unit that holds worlds, members and a plan.")}
617
+ {step(worlds > 0, "Provision a world into it with the twins your app needs.")}
618
+ {step(tokensMade === true, <>Make a personal access token (under You), then sign the CLI in: <code>volter login {ORIGIN} --token …</code></>)}
619
+ {step(false, <>From your terminal, not this page: point your app's world at it — <code>volter remote add origin {firstWorld ?? `${org ?? '<org>'}/<world>`}</code>, then <code>volter world push</code>; its first write shows in the world's log.</>)}
620
+ </ol>
621
+ <button type="button" onClick={() => { try { localStorage.setItem(KEY, 'done'); } catch { /* no storage */ } setDismissed(true); }}>Dismiss</button>
622
+ </section>
623
+ );
624
+ }
625
+
626
+ type AuditRow = { at: string; event: string; actor: { id?: string; email?: string; operator?: boolean; platform?: boolean }; target?: string };
627
+ /** What happened in the org (punchlist 4.1): the audit log, newest first, for admins; exported as the file it is kept as. */
628
+ function Activity({ token, org, version }: { token: string; org: string; version: number }): React.JSX.Element {
629
+ const [rows, setRows] = useState<AuditRow[] | null>(null);
630
+ useEffect(() => { void door(token, `/-/orgs/${org}/audit`).then(async (r) => setRows(r.ok ? ((await r.json()) as { entries: AuditRow[] }).entries : [])); }, [token, org, version]); // re-read after every act on the page (walk 3d: rows only appeared on reload)
631
+ const who = (a: AuditRow['actor']): string => a.platform ? 'the platform' : a.operator ? 'the operator' : a.email ?? a.id ?? '?';
632
+ return (
633
+ <details className="org-settings"><summary>Activity</summary>
634
+ {rows === null ? <p className="mode">loading…</p> : rows.length === 0 ? <p className="mode">Nothing yet.</p> : (
635
+ <table><thead><tr><th>when</th><th>what</th><th>who</th><th>target</th></tr></thead>
636
+ <tbody>{rows.slice(0, 50).map((r, i) => <tr key={`${r.at}-${i}`}><td><code>{r.at.slice(0, 16).replace('T', ' ')}</code></td><td>{r.event.toLowerCase().replace('_', ' ')}</td><td>{who(r.actor)}</td><td>{r.target ?? ''}</td></tr>)}</tbody></table>
637
+ )}
638
+ <p><a href={`${ORIGIN}/-/orgs/${org}/audit.jsonl`} download={`${org}-activity.jsonl`}>Download the log</a> (a <code>.jsonl</code> file, one act a line) · <a href={`${ORIGIN}/-/orgs/${org}/export`} download={`${org}-export.json`}>Export the org</a> (a <code>.json</code> file: the record, the worlds, the members, this log — what the platform knows). Your browser saves them.</p>
639
+ </details>
640
+ );
641
+ }
642
+
643
+ function WorldView({ name, token, twin, admin, onRemoved }: { name: string; token: string; twin?: string; admin: string | null; onRemoved: () => void }): React.JSX.Element {
644
+ const [twins, setTwins] = useState<Twin[] | null>(null);
645
+ const [changesets, setChangesets] = useState<Changeset[] | null>(null);
646
+ const [note, setNote] = useState<string | null>(null);
647
+ const [tokens, setTokens] = useState<{ token: string; readToken: string } | null>(null);
648
+ type Report = { hours: number; requests: number; twins: Array<{ twin: string; requests: number; byStatus: Record<string, number>; p50Ms: number | null; p95Ms: number | null; topRoutes: Array<{ route: string; requests: number }> }> };
649
+ const [report, setReport] = useState<Report | null>(null);
650
+ const refresh = useCallback(async () => {
651
+ const t = await door(token, `/-/${name}/twins`);
652
+ setTwins(t.ok ? ((await t.json()) as { twins: Twin[] }).twins : []);
653
+ const rq = await door(token, `/-/${name}/requests?hours=24`); setReport(rq.ok ? ((await rq.json()) as Report) : null);
654
+ const c = await door(token, `/-/${name}/changesets`);
655
+ setChangesets(c.ok ? ((await c.json()) as { changesets: Changeset[] }).changesets : []);
656
+ }, [name, token]);
657
+ useEffect(() => { void refresh(); }, [refresh]);
658
+ const act = async (label: string, path: string, init: RequestInit): Promise<void> => {
659
+ setNote(null);
660
+ const r = await door(token, path, init);
661
+ const body = (await r.json().catch(() => ({}))) as { error?: string };
662
+ setNote(r.ok ? `${label}: done` : `${label}: ${body.error ?? `answered ${r.status}`}`);
663
+ await refresh();
664
+ };
665
+ const rotate = async (): Promise<void> => {
666
+ if (!window.confirm(`Rotate the tokens of ${name}? Every holder of the current ones loses access.`)) return;
667
+ const r = await door(admin!, `/-/worlds/${name}/rotate`, { method: 'POST' });
668
+ if (r.ok) { setTokens((await r.json()) as { token: string; readToken: string }); setNote('rotated: the new tokens are shown once'); } else setNote(`rotate: answered ${r.status}`);
669
+ };
670
+ const remove = async (): Promise<void> => {
671
+ if (!window.confirm(`Remove ${name} and its whole tree from this host?`)) return;
672
+ const r = await door(admin!, `/-/worlds/${name}`, { method: 'DELETE' });
673
+ if (r.ok) onRemoved(); else setNote(`remove: answered ${r.status}`);
674
+ };
675
+ if (twin) return <TwinView name={name} token={token} twin={twin} />;
676
+ return (
677
+ <section aria-labelledby="world">
678
+ <h2 id="world">{name}</h2>
679
+ <p><code>{WORLDS}/{name}</code> — point an app's SDK at <code>{WORLDS}/{name}/&lt;twin&gt;</code></p>
680
+ {note ? <p role="status">{note}</p> : null}
681
+ {tokens ? <p role="status">token <code>{tokens.token}</code> · read token <code>{tokens.readToken}</code></p> : null}
682
+ <h3>Twins</h3>
683
+ {twins === null ? <p>Loading…</p> : (
684
+ <table>
685
+ <thead><tr><th>twin</th><th>protocol</th><th>root</th><th>credential</th><th>last refresh</th><th>position</th><th>last receipt</th><th>actions</th></tr></thead>
686
+ <tbody>
687
+ {twins.map((t) => (
688
+ <tr key={t.twin}>
689
+ <td><a href={`${BASE}/${name}/${t.twin}`} onClick={(e) => { e.preventDefault(); go([...name.split('/'), t.twin]); }}>{t.twin}</a></td>
690
+ <td>{t.protocol?.major !== undefined ? `protocol ${t.protocol.major}${t.protocol.standing && t.protocol.standing !== 'current' ? ` (${t.protocol.standing})` : ''}` : '—'}</td>
691
+ <td>{t.root?.url ? <code>{t.root.url}</code> : 'simulated'}</td>
692
+ <td>{t.credential ? `sealed ${t.credential.fingerprint ?? ''} · ${when(t.credential.placedAt)}` : 'none'}</td>
693
+ <td>{t.refresh?.failed ? <span role="alert">failed ×{t.refresh.failed.count ?? 1}: {t.refresh.failed.reason ?? ''}</span> : when(t.refresh?.observedAt ?? t.refresh?.at)}</td>
694
+ <td>{t.position ?? 0}</td>
695
+ <td>{t.lastReceipt ? `${t.lastReceipt.status ?? ''} ${t.lastReceipt.externalId ?? ''} ${t.lastReceipt.reason ?? ''}`.trim() : '—'}</td>
696
+ <td>
697
+ <button onClick={() => { if (window.confirm(`Ask the vendor behind ${t.twin} now?`)) void act(`refresh ${t.twin}`, `/-/${name}/twins/${t.twin}/refresh`, { method: 'POST' }); }} disabled={!t.root}>Refresh</button>{' '}
698
+ <button onClick={() => { const url = window.prompt(`The root of ${t.twin}: the vendor's URL (empty clears it)`, t.root?.url ?? ''); if (url === null) return; void act(`root ${t.twin}`, `/-/${name}/twins/${t.twin}/root`, { method: 'PUT', body: JSON.stringify(url ? { url } : null) }); }}>Set root</button>{' '}
699
+ <button onClick={() => { const secret = window.prompt(`Seal the real credential of ${t.twin} (never shown again)`); if (!secret) return; void act(`seal ${t.twin}`, `/-/${name}/twins/${t.twin}/credential`, { method: 'PUT', body: secret, headers: { 'content-type': 'text/plain' } }); }}>Seal credential</button>
700
+ </td>
701
+ </tr>
702
+ ))}
703
+ </tbody>
704
+ </table>
705
+ )}
706
+ <h3>Requests (last 24 hours)</h3>
707
+ {report === null ? <p className="mode">No request report for this world.</p> : report.requests === 0 ? <p className="mode">No requests in the last 24 hours.</p> : (
708
+ <table aria-label="Requests by twin">
709
+ <thead><tr><th>twin</th><th>requests</th><th>by status</th><th>p50</th><th>p95</th><th>top routes</th></tr></thead>
710
+ <tbody>{report.twins.map((t) => <tr key={t.twin}><td>{t.twin}</td><td>{t.requests}</td><td>{Object.entries(t.byStatus).map(([k, v]) => `${k}: ${v}`).join(' · ') || '—'}</td><td>{t.p50Ms === null ? '—' : `${t.p50Ms} ms`}</td><td>{t.p95Ms === null ? '—' : `${t.p95Ms} ms`}</td><td>{t.topRoutes.slice(0, 5).map((r) => <div key={r.route}><code>{r.route}</code> <span className="mode">×{r.requests}</span></div>)}</td></tr>)}</tbody>
711
+ </table>
712
+ )}
713
+ <h3>Changesets</h3>
714
+ {changesets === null ? <p>Loading…</p> : changesets.length === 0 ? <p>No changeset has landed.</p> : (
715
+ <table>
716
+ <thead><tr><th>name</th><th>changes</th><th>approvals</th><th>actions</th></tr></thead>
717
+ <tbody>
718
+ {changesets.map((c) => (
719
+ <tr key={c.name}>
720
+ <td>{c.name}</td>
721
+ <td>{Array.isArray(c.actions) ? c.actions.length : '—'}</td>
722
+ <td>{Array.isArray(c.approvals) ? c.approvals.length : 0}</td>
723
+ <td>
724
+ <button onClick={() => void act(`verify ${c.name}`, `/-/${name}/changesets/${encodeURIComponent(c.name)}/verify`, { method: 'POST' })}>Verify</button>{' '}
725
+ <button onClick={() => { const as = window.prompt(`Approve ${c.name} as`); if (!as) return; void act(`approve ${c.name}`, `/-/${name}/changesets/${encodeURIComponent(c.name)}/approve`, { method: 'POST', body: JSON.stringify({ as }) }); }}>Approve</button>{' '}
726
+ <button onClick={() => { if (window.confirm(`Deploy ${c.name} against every twin's root?`)) void act(`deploy ${c.name}`, `/-/${name}/deploy/${encodeURIComponent(c.name)}`, { method: 'POST' }); }}>Deploy</button>
727
+ </td>
728
+ </tr>
729
+ ))}
730
+ </tbody>
731
+ </table>
732
+ )}
733
+ {admin ? (
734
+ <p className="admin">
735
+ <button onClick={() => void rotate()}>Rotate tokens</button>{' '}
736
+ <button onClick={() => void remove()}>Remove world</button>
737
+ </p>
738
+ ) : null}
739
+ </section>
740
+ );
741
+ }
742
+
743
+ function TwinView({ name, token, twin }: { name: string; token: string; twin: string }): React.JSX.Element {
744
+ const [tab, setTab] = useState<'log' | 'tree'>('log');
745
+ const [log, setLog] = useState<{ position: number; entries: Entry[] } | null>(null);
746
+ const [tree, setTree] = useState<Array<Record<string, unknown>> | null>(null);
747
+ useEffect(() => {
748
+ void (async () => {
749
+ const head = await door(token, `/-/${name}/log/${twin}?limit=1`);
750
+ if (!head.ok) { setLog({ position: 0, entries: [] }); return; }
751
+ const { position } = (await head.json()) as { position: number };
752
+ const after = Math.max(0, position - 200);
753
+ const r = await door(token, `/-/${name}/log/${twin}?after=${after}&limit=200&receipts=1`);
754
+ const body = (await r.json()) as { position: number; entries: Entry[] };
755
+ setLog({ position: body.position, entries: [...body.entries].reverse() });
756
+ const c = await door(token, `/-/${name}/checkpoint/${twin}`);
757
+ setTree(c.ok ? ((await c.json()) as { tree: Array<Record<string, unknown>> }).tree : []);
758
+ })();
759
+ }, [name, token, twin]);
760
+ // the twin's mirror (its vendor's own UI over this World's state), when its pack has one. Opening it
761
+ // posts the token to the World's own session door on the Worlds' origin — a form, so the session is
762
+ // set there first-party and the browser goes on to the mirror; the token never enters a URL
763
+ const [mirror, setMirror] = useState(false);
764
+ useEffect(() => { void door(token, `/-/${name}/twins/${twin}/status`).then(async (r) => setMirror(r.ok && ((await r.json()) as { mirror?: boolean }).mirror === true), () => setMirror(false)); }, [name, token, twin]);
765
+ const mirrorAt = `${WORLDS}/${name}/${twin}/mirror/`;
766
+ const openMirror = (): void => {
767
+ const form = document.createElement('form');
768
+ form.method = 'POST'; form.action = `${WORLDS}/-/${name}/session`;
769
+ for (const [k, v] of [['token', token], ['next', `/${name}/${twin}/mirror/`]]) { const input = document.createElement('input'); input.type = 'hidden'; input.name = k!; input.value = v!; form.append(input); }
770
+ document.body.append(form); form.submit(); form.remove();
771
+ };
772
+ const receipt = (e: Entry): string => (e.operation === 'observed' ? `observed${e.provenance === 'placeholder' ? ' (default data)' : ' (vendor)'}` : e.receipt ? `${e.receipt.status ?? ''} ${e.receipt.externalId ?? ''} ${e.receipt.reason ?? ''}`.trim() : e.provenance === 'placeholder' ? 'default data' : 'unpushed');
773
+ return (
774
+ <section aria-labelledby="twin">
775
+ <h2 id="twin">{twin} in {name}</h2>
776
+ <p>position {log?.position ?? '…'} · <code>{WORLDS}/{name}/{twin}</code>{mirror ? <> · <a href={mirrorAt} onClick={(e) => { e.preventDefault(); openMirror(); }}>Open as {twin} shows it</a></> : null}</p>
777
+ <div role="tablist">
778
+ <button role="tab" aria-selected={tab === 'log'} onClick={() => setTab('log')}>Log</button>
779
+ <button role="tab" aria-selected={tab === 'tree'} onClick={() => setTab('tree')}>Tree</button>
780
+ </div>
781
+ {tab === 'log' ? (
782
+ log === null ? <p>Loading…</p> : log.entries.length === 0 ? <p>No changes yet.</p> : (
783
+ <table>
784
+ <thead><tr><th>when</th><th>operation</th><th>subject</th><th>actor</th><th>standing</th></tr></thead>
785
+ <tbody>
786
+ {log.entries.map((e) => (
787
+ <tr key={e.id}>
788
+ <td>{when(e.occurredAt)}</td>
789
+ <td>{e.operation ?? e.op}</td>
790
+ <td><code>{e.subject.type}:{e.subject.id}</code></td>
791
+ <td>{e.actor ? `${e.actor.kind}${e.actor.id ? ` ${e.actor.id}` : ''}` : '—'}</td>
792
+ <td>{receipt(e)}</td>
793
+ </tr>
794
+ ))}
795
+ </tbody>
796
+ </table>
797
+ )
798
+ ) : (
799
+ tree === null ? <p>Loading…</p> : tree.length === 0 ? <p>The tree is empty.</p> : (
800
+ <table>
801
+ <thead><tr><th>type</th><th>id</th><th>fields</th></tr></thead>
802
+ <tbody>
803
+ {tree.filter((r) => r.deleted !== true).slice(0, 500).map((r) => {
804
+ const { type, id, ...rest } = r as { type: string; id: string } & Record<string, unknown>;
805
+ return <tr key={`${type}:${id}`}><td>{type}</td><td><code>{id}</code></td><td><pre>{JSON.stringify(rest, null, 1).slice(0, 600)}</pre></td></tr>;
806
+ })}
807
+ </tbody>
808
+ </table>
809
+ )
810
+ )}
811
+ </section>
812
+ );
813
+ }
814
+
815
+ createRoot(ROOT).render(<App />);