@volter/world-console 2.0.1 → 2.0.2

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.
@@ -2,12 +2,15 @@
2
2
  // of the host (`/-/worlds`, `/-/ping`) or of a world (`/-/<org>/<world>/…`), called with the token the
3
3
  // person typed, from the same origin the host serves. The client holds no state a door did not
4
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';
5
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
6
+ import { Background, BackgroundVariant, Controls, MiniMap, NodeToolbar, Panel, Position, ReactFlow, ReactFlowProvider, useNodesState, useReactFlow, type Node, type NodeProps } from '@xyflow/react';
7
+ import { applyTheme, ThemeChoice, At, Brand, ConfirmHost, MenuDismiss, confirmAct, CopyField, counted, Dialog, DrawerButton, failure, listed, Mark, Menu, Pending, Toaster, toast, useDoor as useKitDoor, vendorName, when, ago, type Load } from './kit/kit.tsx';
6
8
  import { createRoot } from 'react-dom/client';
9
+ import { agentPrompt } from '../../../packages/cli/src/agent-prompt.ts';
7
10
 
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 };
11
+ 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; mirror?: boolean };
9
12
  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> };
13
+ 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>; traceparent?: string };
11
14
  type Changeset = { name: string; actions?: unknown[]; receipts?: unknown; verification?: unknown; approvals?: unknown[] };
12
15
  type Mode = { kind: 'admin'; worlds: Row[] } | { kind: 'world'; worlds: Array<{ name: string; base: string; twins: Row['twins'] }> };
13
16
 
@@ -20,82 +23,263 @@ const ORIGIN = window.location.origin;
20
23
  const WORLDS = (ROOT.dataset.worlds ?? ORIGIN).replace(/\/+$/, '');
21
24
  /** The Worlds a World token may open, when the host does not list them (a hosted host keeps its tenants' names to itself). */
22
25
  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';
26
+ // Served on a World's own origin, beside the World's twins and mirrors (same-origin frames of this tab), the console
27
+ // keeps no token any script there could read: a token (typed, or carried in the fragment) is exchanged once for the
28
+ // World's HttpOnly session, and the doors ride that cookie. SESSION stands in for the token from then on.
29
+ const ON_WORLD = ROOT.dataset.worldOrigin === '1';
30
+ // the browser's fetch as it was when the console loaded, before any mirror frame existed: a same-origin mirror that
31
+ // later replaces `parent.fetch` does not see the console's requests through it. Hardening, not isolation (such a frame
32
+ // reaches much else); a sign-out's fresh document is what ends its reach into this one
33
+ const nativeFetch = window.fetch.bind(window);
34
+ const SESSION = '(session)';
35
+ /** Stands in for the token while a pass the platform signed (in the fragment) is exchanged for the World's session. */
36
+ const PASSED = '(pass)';
29
37
 
30
38
  // the tab's own storage, never localStorage: the token the person typed ends with the tab and is not left on
31
39
  // 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
40
+ const keep = (t: string): void => { try { if (t && !ON_WORLD) sessionStorage.setItem(TOKEN_KEY, t); else sessionStorage.removeItem(TOKEN_KEY); localStorage.removeItem(TOKEN_KEY); } catch { /* private window */ } };
41
+ // a token handed in the URL fragment (never sent to a server) is taken once and dropped from the address bar
35
42
  const PORTAL_KEY = 'volter.console.portal';
43
+ // the way back to the platform a person came from: only the issuer the World verified on their pass (the session door
44
+ // says it), never an address from this page's URL, which anyone can write
45
+ const keepPortal = (issuer: unknown): void => { try { if (typeof issuer === 'string' && /^https?:$/.test(new URL(issuer).protocol)) sessionStorage.setItem(PORTAL_KEY, new URL(issuer).origin); } catch { /* fine */ } };
36
46
  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]!);
47
+ return FRAGMENT.get('token') ?? '';
43
48
  };
44
49
  /** 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() : '—');
50
+ // a fragment is anyone's to write: only an http(s) origin is a way back, never a path or another scheme
51
+ const portal = (): string => { try { const p = sessionStorage.getItem(PORTAL_KEY) ?? ''; return p && /^https?:$/.test(new URL(p).protocol) ? new URL(p).origin : ''; } catch { return ''; } };
52
+ const held = (): string => {
53
+ const f = fromFragment();
54
+ // on a World origin a carried token is only exchanged (App's load); without one, the session is the credential,
55
+ // unless this document is the fresh one a sign-out loaded (`?signin`): then a token is asked for
56
+ if (ON_WORLD) {
57
+ keep('');
58
+ // a person the platform sent here with a pass: it is spent on the World's session (App's load)
59
+ if (FRAGMENT.get('pass')) return PASSED;
60
+ const signingIn = new URLSearchParams(window.location.search).has('signin');
61
+ if (signingIn) { try { window.history.replaceState(null, '', window.location.pathname + window.location.hash); } catch { /* fine */ } }
62
+ return f || (signingIn ? '' : SESSION);
63
+ }
64
+ if (f) { keep(f); return f; }
65
+ try { return sessionStorage.getItem(TOKEN_KEY) ?? ''; } catch { return ''; }
66
+ };
48
67
 
49
68
  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' } : {}) } });
69
+ // SESSION: no token header; the World's session cookie (same-origin) is the credential
70
+ return nativeFetch(`${ORIGIN}${path}`, { ...init, headers: { ...(init.headers ?? {}), ...(token === SESSION ? {} : { 'x-volter-token': token }), ...(init.body && !(init.headers as Record<string, string> | undefined)?.['content-type'] ? { 'content-type': 'application/json' } : {}) } });
51
71
  }
52
72
 
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); }
73
+ /** One door read with this token (the kit's useDoor). */
74
+ function useDoor<T>(token: string, path: string | null, version = 0): Load<T> { return useKitDoor<T>((p) => door(token, p), path, version, token); }
75
+
76
+ /** A datetime-local input's value for an instant, in the viewer's zone. */
77
+ const localInput = (iso: string): string => { const d = new Date(iso); const p = (n: number): string => String(n).padStart(2, '0'); return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}T${p(d.getHours())}:${p(d.getMinutes())}`; };
78
+
79
+ /** The first whole minute at or after an instant: a datetime-local picks minutes, and the clock only moves forward. */
80
+ const nextMinute = (iso: string): string => new Date(Math.ceil(Date.parse(iso) / 60_000) * 60_000).toISOString();
81
+
82
+ /** An entry's standing: observed from the vendor, a receipt, default data, or not yet pushed. */
83
+ const standing = (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');
84
+ const actorOf = (e: Entry): string => (e.actor ? `${e.actor.kind}${e.actor.id ? ` ${e.actor.id}` : ''}` : '—');
85
+ /** The trace id of a W3C traceparent (`00-<32 hex>-<16 hex>-<2 hex>`), or null. */
86
+ const traceOf = (tp?: string): { trace: string; span: string } | null => { const m = /^[0-9a-f]{2}-([0-9a-f]{32})-([0-9a-f]{16})-[0-9a-f]{2}$/.exec(tp ?? ''); return m ? { trace: m[1]!, span: m[2]! } : null; };
87
+
88
+ /** A past moment of a World, opened by this tab: a branch the host made, held with its tokens for the tab's life. */
89
+ type Snapshot = { name: string; parent: string; token: string; readToken: string; at: string; expiresAt: string | null; /** where the parent's pages live, the way back */ parentOrigin?: string; /** this tab's own "View as of" made it here: only such a branch is ever removed */ made?: boolean };
90
+ const SNAP_KEY = 'volter.console.snapshots';
91
+ const snapshots = (): Record<string, Snapshot> => { try { return JSON.parse(sessionStorage.getItem(SNAP_KEY) ?? '{}') as Record<string, Snapshot>; } catch { return {}; } };
92
+ // on a World origin a snapshot's tokens stay in this module (no frame can reach it), never in storage
93
+ const SNAP_TOKENS = new Map<string, { token: string; readToken: string }>(); // cache: this tab's snapshot tokens on a World origin
94
+ const keepSnapshot = (s: Snapshot): void => {
95
+ if (ON_WORLD) { if (s.token || s.readToken) SNAP_TOKENS.set(s.name, { token: s.token, readToken: s.readToken }); s = { ...s, token: '', readToken: '' }; }
96
+ try { sessionStorage.setItem(SNAP_KEY, JSON.stringify({ ...snapshots(), [s.name]: s })); } catch { /* no storage */ }
97
+ };
98
+ /** The read token a snapshot this tab made opens with, wherever it is kept. */
99
+ const snapshotToken = (name: string): string => SNAP_TOKENS.get(name)?.readToken || snapshots()[name]?.readToken || '';
100
+ const forgetSnapshot = (name: string): void => { try { const all = snapshots(); delete all[name]; sessionStorage.setItem(SNAP_KEY, JSON.stringify(all)); } catch { /* no storage */ } };
101
+
102
+ // A World's pages live at the World's own origin, where its browser session lives (a host that gives
103
+ // each World one answers `POST …/session` elsewhere with a 409 naming it). The console moves there
104
+ // carrying, in the URL's fragment (which no server sees), the token, a snapshot this tab made, or the
105
+ // snapshot a page on another origin is returning from.
106
+ const FRAGMENT = new URLSearchParams(window.location.hash.slice(1));
107
+ const CLOSING = FRAGMENT.get('close') ?? '';
108
+ /** `value` as a World origin this console may send the tab to, or null: an http(s) origin and nothing else (no
109
+ * path, no `javascript:`), with this page's scheme and port, that is this page's own host or a World origin of this
110
+ * host: `<world>--<org>.` before this page's host (a console on the host's origin), or before the same host as this
111
+ * page's (a sibling World origin). A fragment is anyone's to write, so nothing it carries reaches a navigation
112
+ * unchecked; a host that merely shares a last label (`evil.dev`, another IP) is none of these. */
113
+ /** `value` as a pure http(s) origin (no path, no other scheme), or null. */
114
+ function httpOrigin(value: unknown): URL | null {
115
+ if (typeof value !== 'string') return null;
116
+ let at: URL; try { at = new URL(value); } catch { return null; }
117
+ return (at.protocol === 'http:' || at.protocol === 'https:') && at.origin === value.replace(/\/+$/, '') ? at : null;
118
+ }
119
+ function worldOriginOf(value: unknown): string | null {
120
+ const at = httpOrigin(value); if (!at) return null;
121
+ const here = new URL(ORIGIN);
122
+ if (at.protocol !== here.protocol || at.port !== here.port) return null;
123
+ if (at.hostname === here.hostname) return at.origin;
124
+ // a World origin's first label is `<world>--<org>` (world-origins.ts)
125
+ const label = /^[a-z0-9](?:[a-z0-9]|-(?!-))*--[a-z0-9](?:[a-z0-9]|-(?!-))*\./;
126
+ if (!label.test(at.hostname)) return null;
127
+ const rest = at.hostname.replace(label, '');
128
+ return rest === here.hostname || (label.test(here.hostname) && rest === here.hostname.replace(label, '')) ? at.origin : null;
129
+ }
130
+ /** A carried snapshot, kept only when it has a snapshot's shape; its way back only when it is a World origin. */
131
+ function carriedSnapshot(raw: string): Snapshot | null {
132
+ let s: Record<string, unknown>; try { s = JSON.parse(raw) as Record<string, unknown>; } catch { return null; }
133
+ const world = /^[a-z0-9][a-z0-9-]*\/[a-z0-9][a-z0-9-]*$/i;
134
+ if (typeof s.name !== 'string' || !world.test(s.name) || typeof s.parent !== 'string' || !world.test(s.parent) || typeof s.at !== 'string') return null;
135
+ const back = s.parentOrigin === undefined ? undefined : worldOriginOf(s.parentOrigin);
136
+ return { name: s.name, parent: s.parent, token: typeof s.token === 'string' ? s.token : '', readToken: typeof s.readToken === 'string' ? s.readToken : '', at: s.at, expiresAt: typeof s.expiresAt === 'string' ? s.expiresAt : null, ...(back ? { parentOrigin: back } : {}) };
137
+ }
138
+ // a carried snapshot is kept to be viewed, never as one this tab made (`made` is never carried), and never over a
139
+ // record this tab already holds: a fragment cannot create or replace what a later fragment's `close` is checked against
140
+ { const carried = FRAGMENT.get('snapshot'); if (carried) { const s = carriedSnapshot(carried); if (s && !snapshots()[s.name]) keepSnapshot(s); } }
141
+ // the console's home, when a World's pages moved it away from it: the host's origin, where the list of
142
+ // Worlds (and an admin's token) lives
143
+ const HOME_KEY = 'volter.console.home';
144
+ /** A carried home only when it is this host's own: an http(s) origin (never a `javascript:` or a path) whose host is this
145
+ * page's, or the one this World's origin sits under (`<world>--<org>.<that host>`), on any port. A fragment is anyone's to
146
+ * write; a look-alike site never becomes the logo's link. */
147
+ const homeOrigin = (value: unknown): string | null => {
148
+ const at = httpOrigin(value); if (!at) return null;
149
+ const here = new URL(ORIGIN).hostname;
150
+ const under = here.replace(/^[a-z0-9](?:[a-z0-9]|-(?!-))*--[a-z0-9](?:[a-z0-9]|-(?!-))*\./, '');
151
+ return at.hostname === here || at.hostname === under ? at.origin : null;
152
+ };
153
+ { const carried = homeOrigin(FRAGMENT.get('home')); if (carried) { try { sessionStorage.setItem(HOME_KEY, carried); } catch { /* fine */ } } }
154
+ // what the fragment carried is read (FRAGMENT, above); it leaves the address bar now, whoever supplied the token
155
+ if (['token', 'pass', 'portal', 'snapshot', 'home', 'close'].some((k) => FRAGMENT.has(k))) { try { window.history.replaceState(null, '', window.location.pathname + window.location.search); } catch { /* fine */ } }
156
+ const home = (): string => { try { return homeOrigin(sessionStorage.getItem(HOME_KEY)) ?? ''; } catch { return ''; } };
157
+ function moveTo(origin: string, token: string, name: string, path?: string): void {
158
+ const carried = new URLSearchParams({ token, home: home() || ORIGIN });
159
+ const snap = snapshots()[name]; if (snap) carried.set('snapshot', JSON.stringify(snap));
160
+ window.location.replace(`${origin}${path ?? `${window.location.pathname}${window.location.search}`}#${carried.toString()}`);
161
+ }
162
+
163
+ /** The path under the console's base: [] worlds; [org, world] a world; [org, world, twin] a twin; [org, world, '~', tab, arg?] a tab of the world. */
164
+ function route(): string[] { return window.location.pathname.slice(BASE.length).split('/').filter(Boolean).map((p) => { try { return decodeURIComponent(p); } catch { return ''; } }); }
165
+ /** An org's or a World's name: letters, digits and single hyphens (world-origins.ts), never a separator or a dot. */
166
+ const WORLD_PART = /^[a-z0-9](?:[a-z0-9]|-(?!-))*$/i;
167
+ /** The World a path names, `<org>/<world>`, or '' when its segments are not names: nothing decoded from a path (an
168
+ * encoded `/` or `..`) ever reaches a door's path. */
169
+ const worldOf = (o?: string, w?: string): string => (o && w && WORLD_PART.test(o) && WORLD_PART.test(w) ? `${o}/${w}` : '');
55
170
  function go(parts: string[]): void { window.history.pushState(null, '', `${BASE}/${parts.map(encodeURIComponent).join('/')}`); window.dispatchEvent(new PopStateEvent('popstate')); }
56
171
 
172
+ /** A World this machine serves on loopback opens its session for its own page with no token (served-world.ts
173
+ * localSession): nothing is ever pasted, and a restart or a rotated token is picked up by asking again. Asked of the
174
+ * World in the path, else of each World the origin names. The answer's `token` comes only where the World has no
175
+ * origin of its own (a shared origin, where a session cannot reach its doors); else the session is the credential.
176
+ * 'moved': the World's session lives at its own origin, where the tab now goes. null: no World here opens itself
177
+ * (a remote host), and the page asks for a token. */
178
+ async function localSession(): Promise<{ token: string } | 'moved' | null> {
179
+ const [o, w] = route();
180
+ const here = worldOf(o, w);
181
+ let names = here ? [here] : NAMES;
182
+ if (names.length === 0) {
183
+ const ping = await nativeFetch(`${ORIGIN}/-/ping`).catch(() => null);
184
+ names = ping?.ok ? (((await ping.json().catch(() => ({}))) as { worlds?: string[] }).worlds ?? []) : [];
185
+ }
186
+ names = names.filter((n) => { const [a, b, ...rest] = n.split('/'); return rest.length === 0 && worldOf(a, b) === n; });
187
+ for (const name of names) {
188
+ const r = await nativeFetch(`${ORIGIN}/-/${name}/session`, { method: 'POST' }).catch(() => null);
189
+ if (!r) continue;
190
+ const b = (await r.json().catch(() => ({}))) as { local?: boolean; token?: string; origin?: string };
191
+ // the host's own answer names the World's origin (an http(s) origin only)
192
+ const own = httpOrigin(b.origin)?.origin;
193
+ if (r.status === 409 && own && own !== ORIGIN) { window.location.replace(`${own}${BASE}/${name}`); return 'moved'; }
194
+ if (r.ok && b.local) return { token: typeof b.token === 'string' && b.token ? b.token : SESSION };
195
+ }
196
+ return null;
197
+ }
198
+
57
199
  function App(): React.JSX.Element {
58
200
  const [token, setToken] = useState<string>(held());
201
+ // a World this machine serves (localSession): no sign-in, no sign-out; `probed`: that was asked, so a sign-in form
202
+ // is shown only once it is known nothing opens by itself
203
+ const [local, setLocal] = useState(false); const [probed, setProbed] = useState(false);
59
204
  const [draft, setDraft] = useState<string>('');
60
205
  const [mode, setMode] = useState<Mode | null>(null);
61
206
  const [error, setError] = useState<string | null>(null);
62
207
  const [parts, setParts] = useState<string[]>(route());
63
208
  useEffect(() => { const on = () => setParts(route()); window.addEventListener('popstate', on); return () => window.removeEventListener('popstate', on); }, []);
209
+ // a snapshot's page on its own origin sent the way back here (`#close=<branch>`): this World's token
210
+ // removes the branch it was
211
+ useEffect(() => {
212
+ const [ro, rw] = route(); const here = worldOf(ro, rw); const [o, w] = here ? here.split('/') : [];
213
+ // only a snapshot this tab made of this World: a fragment is anyone's to write
214
+ const made = snapshots()[CLOSING];
215
+ if (!CLOSING || !token || !o || !w || !made?.made || made.parent !== `${o}/${w}` || worldOf(...CLOSING.split('/')) !== CLOSING) return;
216
+ try { window.history.replaceState(null, '', window.location.pathname + window.location.search); } catch { /* fine */ }
217
+ void door(token, `/-/${o}/${w}/branches/${CLOSING}`, { method: 'DELETE' }).catch(() => null).then(() => forgetSnapshot(CLOSING));
218
+ }, [token]);
64
219
 
65
220
  /** The mode is what the origin answers: a host's inventory to the admin token, else the worlds this token opens. */
66
221
  const load = useCallback(async (t: string) => {
67
222
  setError(null);
68
- if (!t) { setMode(null); return; }
69
- const inventory = await door(t, '/-/worlds');
223
+ if (!t) {
224
+ const found = await localSession();
225
+ if (found === 'moved') return;
226
+ if (found) { setLocal(true); if (found.token !== SESSION) keep(found.token); setToken(found.token); return; }
227
+ setMode(null); setProbed(true); return;
228
+ }
229
+ // on a World's own origin the session is the credential: a local World opens it again itself (after a restart, a
230
+ // cleared cookie or a rotated token), and says it is local
231
+ if (ON_WORLD && t === SESSION) { const found = await localSession(); if (found === 'moved') return; if (found) setLocal(true); }
232
+ // on a World origin a pass the platform signed for this person is spent on the World's session, once
233
+ if (ON_WORLD && t === PASSED) {
234
+ const [ro, rw] = route(); const here = worldOf(ro, rw); const [o, w] = here ? here.split('/') : [];
235
+ const r = o && w ? await nativeFetch(`${ORIGIN}/-/${o}/${w}/session`, { method: 'POST', headers: { 'x-volter-pass': FRAGMENT.get('pass') ?? '' } }).catch(() => null) : null;
236
+ if (!r) { setError(`${ORIGIN} did not answer: no session was opened`); setMode(null); return; }
237
+ if (!r.ok) { setError(`${await failure(r)}. Open it again from the platform.`); setMode(null); return; }
238
+ keepPortal(((await r.json().catch(() => ({}))) as { issuer?: string }).issuer);
239
+ setToken(SESSION); return;
240
+ }
241
+ // on a World origin a token is exchanged for the World's session before anything renders, and then dropped
242
+ if (ON_WORLD && t !== SESSION) {
243
+ const [ro, rw] = route(); const here = worldOf(ro, rw); const [o, w] = here ? here.split('/') : [];
244
+ const r = o && w ? await nativeFetch(`${ORIGIN}/-/${o}/${w}/session`, { method: 'POST', headers: { 'x-volter-token': t } }).catch(() => null) : null;
245
+ // a refused token, a session kept elsewhere and a host that failed are three different things to say
246
+ if (!r) { setError(`${ORIGIN} did not answer: no session was opened`); setMode(null); return; }
247
+ if (!r.ok) {
248
+ const why = await failure(r);
249
+ setError(r.status === 401 || r.status === 403 ? `That token does not open ${o}/${w}.` : r.status === 409 ? `${o}/${w}'s session lives at another origin: ${why}` : `The World could not open a session (${r.status}): ${why}`);
250
+ setMode(null); return;
251
+ }
252
+ setToken(SESSION); return;
253
+ }
254
+ // a World's own origin serves that World, never the host's inventory: asked only where a host could answer
255
+ const inventory = ON_WORLD ? new Response(null, { status: 404 }) : await door(t, '/-/worlds');
70
256
  if (inventory.ok) { const { worlds } = (await inventory.json()) as { worlds: Row[] }; setMode({ kind: 'admin', worlds }); return; }
71
257
  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
258
  // A host's inventory accepts only its admin token. A world token gets 401 here but
73
259
  // 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
260
  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
261
  let worlds = NAMES;
77
262
  if (worlds.length === 0) {
78
- const ping = await fetch(`${ORIGIN}/-/ping`); // not a platform answer: perhaps this origin is a bare host
263
+ const ping = await nativeFetch(`${ORIGIN}/-/ping`); // not a platform answer: perhaps this origin is a bare host
79
264
  if (!ping.ok) { setError(`${ORIGIN} does not answer as a host`); setMode(null); return; }
80
265
  worlds = ((await ping.json()) as { worlds?: string[] }).worlds ?? [];
81
266
  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
267
  }
83
268
  const opened: Array<{ name: string; base: string; twins: Row['twins'] }> = [];
84
269
  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; }
270
+ if (opened.length === 0) { setError('This link or key opens no World here: it was revoked, or it has ended.'); setMode(null); return; }
86
271
  setMode({ kind: 'world', worlds: opened });
87
272
  }, []);
88
273
  useEffect(() => { void load(token); }, [token, load]);
89
274
 
90
275
  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]);
276
+ const [who, setWho] = useState<string>('');
93
277
 
94
278
  if (!token || (!mode && !error)) {
95
279
  return (
96
280
  <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> : (
281
+ <Brand href={`${BASE}/`} />
282
+ {(token && !mode && !error) || (!token && !probed) ? <p>Opening…</p> : (
99
283
  <form onSubmit={(e) => { e.preventDefault(); keep(draft.trim()); setToken(draft.trim()); }}>
100
284
  <p>A host's admin token opens every world it serves; a world's token opens that world.</p>
101
285
  <label>Token <input name="token" type="password" value={draft} onChange={(e) => setDraft(e.target.value)} autoFocus /></label>
@@ -105,542 +289,1100 @@ function App(): React.JSX.Element {
105
289
  </main>
106
290
  );
107
291
  }
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`); };
292
+ const signOut = (): void => {
293
+ // a local World has no sign-in to leave: its page opens itself again
294
+ if (local) { window.location.reload(); return; }
295
+ if (ON_WORLD) {
296
+ // on a World origin the page is replaced, not re-rendered: nothing a frame beside the console may have hooked
297
+ // into this document survives to see the next token typed
298
+ const [ro, rw] = route(); const here = worldOf(ro, rw); const [o, w] = here ? here.split('/') : [];
299
+ // opened from a platform, the way out is back to it (where the person signs in); otherwise the World asks for a token
300
+ const fresh = (): void => window.location.replace(portal() ? `${portal()}/` : `${ORIGIN}${BASE}/${o && w ? `${o}/${w}` : ''}?signin`);
301
+ setTimeout(fresh, 2000); // the page is replaced whether or not the host answers
302
+ if (o && w) void nativeFetch(`${ORIGIN}/-/${o}/${w}/session/end`, { method: 'POST' }).catch(() => null).then(fresh); else fresh();
303
+ return;
304
+ }
305
+ if (mode) for (const w of mode.worlds) navigator.sendBeacon(`${w.base.replace(/\/[^/]+\/[^/]+$/, '')}/-/${w.name}/session/end`); keep(''); setToken(''); setDraft(''); setMode(null); go([]);
306
+ };
109
307
  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>;
308
+ return <main><Brand href={`${BASE}/`} /><p role="alert">{error ?? 'nothing answered'}</p>{portal() ? <a className="button" href={`${portal()}/`}>Back to {new URL(portal()).host}</a> : <button onClick={signOut}>{local ? 'Try again' : 'Use another token'}</button>}</main>;
111
309
  }
112
- const [org, world, twin] = parts;
310
+ // `/<org>/<world>/<twin>` is a twin; `/<org>/<world>/~/<tab>[/<arg>]` is a tab of the World — `~`
311
+ // is never a vendor name, so a tab can never shadow a twin
312
+ const [org, world, third, tabPart, argPart] = parts;
313
+ const tabbed = third === '~';
314
+ // a vendor's name and a tab's argument (a vendor, a trace id) are names too: nothing else reaches a door or a frame
315
+ const PART = /^[A-Za-z0-9_-]+$/;
316
+ const twin = tabbed || !third || !PART.test(third) ? undefined : third;
317
+ const tabArg = argPart && PART.test(argPart) ? argPart : undefined;
318
+ // only a World's name opens a World page (and so reaches its doors); any other path is the list of Worlds
319
+ const name = worldOf(org, world);
320
+ const snapshot = name ? snapshots()[name] ?? null : null;
321
+ /** The token a World is opened with: a snapshot this tab made opens with its read token. */
322
+ const tokenOf = (n: string): string => snapshotToken(n) || worldToken(n);
323
+ const returnToLive = async (s: Snapshot): Promise<void> => {
324
+ // the parent's pages (and its token) live at its own origin: that page removes the branch
325
+ const back = worldOriginOf(s.parentOrigin);
326
+ if (back && back !== ORIGIN) { forgetSnapshot(s.name); window.location.assign(`${back}${BASE}/${s.parent}#close=${encodeURIComponent(s.name)}`); return; }
327
+ // the branch goes with the way back: a snapshot is a throwaway World, not something to leave running (one this
328
+ // tab made; a carried one is only left)
329
+ if (s.made) await door(worldToken(s.parent), `/-/${s.parent}/branches/${s.name}`, { method: 'DELETE' }).catch(() => null);
330
+ forgetSnapshot(s.name); go(s.parent.split('/'));
331
+ };
113
332
  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}
333
+ <main className={name ? 'app' : undefined}>
116
334
  <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}
335
+ {name ? <DrawerButton /> : null}
336
+ <Brand href={`${home() && home() !== ORIGIN ? home() : ''}${BASE}/`} onClick={(e) => { if (home() && home() !== ORIGIN) return; e.preventDefault(); go([]); }} />
337
+ <nav aria-label="breadcrumb" className="crumbs">
338
+ {name ? <><span className="crumb-sep" aria-hidden="true">/</span><span>{name.split('/')[0]}</span><span className="crumb-sep" aria-hidden="true">/</span><a href={`${BASE}/${name}`} className="crumb-world" onClick={(e) => { e.preventDefault(); go(name.split('/')); }}>{name.split('/')[1]}</a></> : null}
120
339
  {twin ? <span> / {twin}</span> : null}
340
+ {tabbed && tabPart === 'trace' ? <span> / trace</span> : null}
121
341
  </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>
342
+ {/* the way back is the org's Worlds on the platform that signed the pass: the issuer is verified, the org is this World's own */}
343
+ {portal() ? <a href={`${portal()}/${name ? encodeURIComponent(name.split('/')[0]!) : ''}`} title={`Back to the platform at ${new URL(portal()).host}`} className="back-link">← <span className="back-text">{name ? `${name.split('/')[0]}'s Worlds` : new URL(portal()).host}</span></a> : null}
344
+ <span className="mode">{local ? 'on this machine' : snapshot ? 'a snapshot, read only' : who ? `signed in as ${who}` : mode.kind === 'admin' ? 'host: admin' : 'opened with a link or key'}</span>
345
+ {local ? null : <button onClick={signOut}>Sign out</button>}
128
346
  </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([]); }} />
347
+ {name
348
+ ? <WorldView key={name} name={name} token={tokenOf(name)} twin={twin} tab={tabbed ? tabPart ?? 'overview' : 'overview'} arg={tabbed ? tabArg : undefined} admin={snapshot ? null : mode.kind === 'admin' ? token : null} snapshot={snapshot} onReturn={returnToLive} opens={(n) => !!snapshots()[n] || (mode.kind === 'admin' && mode.worlds.some((w) => w.name === n))} onRemoved={() => { void load(token); go([]); }} onWho={setWho} />
131
349
  : <Worlds mode={mode} admin={mode.kind === 'admin' ? token : null} reload={() => load(token)} />}
132
350
  </main>
133
351
  );
134
352
  }
135
353
 
354
+ /** A HOST's home (self-hosted, or a World token's list): the Worlds this origin serves, each opened in the page; a host's
355
+ * admin rotates, removes and makes them here, where the admin token lives. People and orgs are the platform's pages. */
136
356
  function Worlds({ mode, admin, reload }: { mode: Mode; admin: string | null; reload: () => Promise<void> }): React.JSX.Element {
137
357
  const [org, setOrg] = useState(''); const [world, setWorld] = useState(''); const [vendors, setVendors] = useState('');
138
358
  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);
359
+ const rotateWorld = async (name: string): Promise<void> => {
360
+ if (!(await confirmAct({ title: `Rotate the tokens of ${name}?`, body: 'Every app, script and person holding the current tokens loses access at once. The new ones are shown once.', action: 'Rotate tokens', danger: true }))) return;
361
+ const r = await door(admin!, `/-/worlds/${name}/rotate`, { method: 'POST' });
362
+ const b = (await r.json().catch(() => ({}))) as { token?: string; readToken?: string; error?: string };
363
+ setNote(r.ok ? `rotated ${name}: the new tokens are shown once — token ${b.token} · read token ${b.readToken}` : b.error ?? `answered ${r.status}`);
364
+ await reload();
365
+ };
366
+ const removeWorld = async (name: string): Promise<void> => {
159
367
  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);
368
+ const b = (await r.json().catch(() => ({}))) as { error?: string };
369
+ setNote(r.ok ? `removed ${name}` : b.error ?? `answered ${r.status}`);
370
+ await reload();
163
371
  };
164
372
  const [provisioning, setProvisioning] = useState(false);
165
373
  const provision = async (e: React.FormEvent): Promise<void> => {
166
374
  e.preventDefault(); setNote(null); setProvisioning(true);
167
375
  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
376
+ const r = await door(admin!, '/-/worlds', { method: 'POST', body: JSON.stringify({ org, world, vendors: vendors.split(',').map((v) => v.trim()).filter(Boolean) }) });
377
+ const body = (await r.json()) as { error?: string; name?: string };
378
+ if (!r.ok) { setNote(body.error ?? `the host answered ${r.status}`); return; }
379
+ setNote(`provisioned ${body.name}`); setWorld(''); setVendors(''); await reload(); // the org stays: the next world usually goes to the same org
172
380
  } finally { setProvisioning(false); }
173
381
  };
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
382
  return (
177
383
  <section aria-labelledby="worlds">
178
- {MANAGED && admin ? <GettingStarted token={admin} orgs={myOrgs} worlds={allRows.length} firstWorld={rows[0]?.name} version={worldsVersion} /> : null}
179
384
  <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}
385
+ <p className="lede">A World is a copy of the services your app uses (Stripe, Slack, GitHub…), with none of the real consequences: your app talks to it, and you can see everything it did on each vendor's own screens.</p>
181
386
  <table>
182
- <thead><tr><th>world</th><th>address</th><th>twins</th>{MANAGED ? <th>actions</th> : null}</tr></thead>
387
+ <thead><tr><th>world</th><th>address</th><th>stand-ins for</th>{admin ? <th>actions</th> : null}</tr></thead>
183
388
  <tbody>
184
- {rows.map((w) => (
389
+ {mode.worlds.map((w) => (
185
390
  <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>
391
+ <td><a href={`${BASE}/${w.name}`} onClick={(e) => { e.preventDefault(); go(w.name.split('/')); }}>{w.name}</a></td>
190
392
  <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}
393
+ <td>{w.twins.map((t) => vendorName(t.vendor)).join(', ') || '—'}</td>
394
+ {/* a host's admin manages a World here, on the host's origin, where the admin token lives: a World's own
395
+ pages (its origin) never hold it */}
396
+ {admin ? <td>
397
+ <button type="button" aria-label={`Rotate the tokens of ${w.name}`} onClick={() => void rotateWorld(w.name)}>Rotate tokens</button>{' '}
398
+ <button type="button" aria-label={`Remove ${w.name}`} onClick={() => void confirmAct({ title: `Remove ${w.name}?`, body: 'The host stops serving it and its whole history goes with it. This cannot be undone.', action: 'Remove World', danger: true, typed: w.name }).then((ok) => { if (ok) void removeWorld(w.name); })}>Remove world</button>
399
+ </td> : null}
193
400
  </tr>
194
401
  ))}
195
402
  </tbody>
196
403
  </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
404
  {admin ? (
201
405
  <form onSubmit={provision} aria-labelledby="provision">
202
406
  <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>}
407
+ <label>Org <input name="org" value={org} onChange={(e) => setOrg(e.target.value)} required /></label>
206
408
  <label>World <input name="world" value={world} onChange={(e) => setWorld(e.target.value)} required /></label>
207
409
  <label>Twins <input name="vendors" value={vendors} onChange={(e) => setVendors(e.target.value)} placeholder="github, slack" required /></label>
208
410
  <button type="submit" disabled={provisioning}>{provisioning ? 'Provisioning… (a host boots the world; ten seconds or so)' : 'Provision'}</button>
209
411
  {note ? <p role="status">{note}</p> : null}
210
412
  </form>
211
- ) : null}
413
+ ) : note ? <p role="status">{note}</p> : null}
212
414
  </section>
213
415
  );
214
416
  }
215
417
 
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
- );
418
+ type Clock = { at: string; frozen: boolean };
419
+ type WorldMap = { world: string; twins: Array<{ twin: string; position: number; mirror: boolean; root: { url: string } | null; resources: Array<{ type: string; count: number }> }> };
420
+ type Placed = { twin: string; position: number; entry: Entry };
421
+ type Timeline = { entries: Placed[]; next: string | null };
422
+ type Diff = { base: { id: string; at: string | null }; changes: Array<{ twin: string; entry: Entry }> };
423
+ type Branch = { name: string; at?: { instant?: string }; createdAt: string; expiresAt: string | null };
424
+ /** What the World's session answered: its scope; `unknown` when the host has no session door (a write then says). */
425
+ type Scope = 'pending' | 'read' | 'write' | 'unknown';
426
+
427
+ // A World's page is laid out as Neon's console is (Volter is Neon for every SaaS, docs/concepts/the-model.md): the
428
+ // World's own pages, the branch picker, then the branch's pages and its services, which here are its vendors, each a
429
+ // sibling. All screens is a canvas app's page, the whole window. The older page names still open their page.
430
+ const TABS = [['overview', 'Dashboard'], ['branches', 'Branches'], ['settings', 'Settings'], ['branch', 'Overview'], ['changes', 'Changes'], ['activity', 'Activity'], ['vendors', 'Vendors'], ['canvas', 'All screens']] as const;
431
+ type Tab = (typeof TABS)[number][0];
432
+ const TAB_ALIASES: Record<string, Tab> = { dashboard: 'overview', mirrors: 'vendors', screens: 'canvas', timeline: 'activity', monitoring: 'activity', trace: 'activity', operations: 'settings' };
433
+ /** A vendor's own screens, at the twin's place in the World: the vendor's site (served-world.ts, the wire's page fallback). */
434
+ const mirrorUrl = (name: string, vendor: string): string => `${WORLDS}/${name}/${vendor}/`;
435
+ /** What an entry did, in words: the thing's kind and the vendor's verb, the thing's own label where it has one. */
436
+ const DONE: Record<string, string> = { verify: 'verified', use: 'used', issue: 'issued', revoke: 'revoked', rotate: 'rotated', seed: 'seeded', upsert: 'saved', set: 'set', start: 'started', finish: 'finished', fail: 'failed', schedule: 'scheduled', deliver: 'delivered', like: 'liked', follow: 'followed', reply: 'replied', create: 'created', update: 'updated', delete: 'deleted', record: 'recorded', remove: 'removed', add: 'added', send: 'sent', post: 'posted', postmessage: 'posted', cancel: 'canceled', close: 'closed', open: 'opened', publish: 'published', upload: 'uploaded', invite: 'invited', archive: 'archived', merge: 'merged', refund: 'refunded', capture: 'captured', confirm: 'confirmed', pay: 'paid' };
437
+ function said(e: Entry): { what: string; label: string | null } {
438
+ const op = e.operation ?? e.op;
439
+ const verb = (op.includes('.') ? op.slice(op.lastIndexOf('.') + 1) : op).toLowerCase();
440
+ const kind = e.subject.type.replace(/[_-]+/g, ' ').trim();
441
+ const Kind = `${kind.charAt(0).toUpperCase()}${kind.slice(1)}`;
442
+ // `<vendor>.twin.<thing>` and `seed_<thing>`: the World itself set it up (a seed), not the app
443
+ const seeded = /(^|\.)twin\./.test(op) || /^seed_/.test(verb);
444
+ // an operation with no past tense here reads as words (`channel.mark` → Channel mark), never as its identifier
445
+ const words = op.replace(/[._]+/g, ' ').trim();
446
+ const what = seeded ? `${Kind} seeded` : DONE[verb] ? `${Kind} ${DONE[verb]}` : `${words.charAt(0).toUpperCase()}${words.slice(1)}`;
447
+ const f = e.fields ?? {};
448
+ const label = ['title', 'name', 'email', 'text', 'subject', 'summary', 'description'].map((k) => f[k]).find((v): v is string => typeof v === 'string' && v.trim() !== '') ?? null;
449
+ // a vendor's own markup (Slack's <@U1|ada>, <#C1|general>, <https://…|text>) reads as it does on the vendor's screen
450
+ const plain = label === null ? null : label.replace(/<([@#!])[^|>]*\|([^>]+)>/g, (_m, sigil: string, name: string) => `${sigil === '!' ? '@' : sigil}${name}`).replace(/<(?:https?:[^|>]+)\|([^>]+)>/g, '$1').replace(/<(https?:[^>]+)>/g, '$1');
451
+ return { what, label: plain === null ? null : plain.length > 80 ? `${plain.slice(0, 79)}…` : plain };
245
452
  }
453
+ /** A secret's id is never shown whole (a token, a password, a key): its last four characters. */
454
+ const shown = (e: Entry): string => (/token|password|secret|key|credential/i.test(e.subject.type) && e.subject.id.length > 6 ? `••••${e.subject.id.slice(-4)}` : e.subject.id);
455
+ /** Who did it: an app or agent by its id; the vendor itself for what it does on its own (an event it records). */
456
+ const byWhom = (e: Entry, vendor: string): string => (!e.actor ? '' : e.actor.kind === 'system' ? `by ${vendorName(vendor)}` : e.actor.id ? `by ${e.actor.id}` : e.actor.kind === 'agent' || e.actor.kind === 'app' ? 'by your app' : `by ${e.actor.kind}`);
457
+ /** An entry's standing worth saying: where it came from, or what its push did. Not yet pushed is the ordinary case. */
458
+ const notable = (e: Entry): string | null => (e.operation === 'observed' || e.receipt || e.provenance === 'placeholder' ? standing(e) : null);
246
459
 
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 };
460
+ function WorldView({ name, token, twin, tab, arg, admin, snapshot, onReturn, opens, onRemoved, onWho }: { name: string; token: string; twin?: string; tab: string; arg?: string; admin: string | null; snapshot: Snapshot | null; onReturn: (s: Snapshot) => Promise<void>; opens: (name: string) => boolean; onRemoved: () => void; onWho?: (who: string) => void }): React.JSX.Element {
461
+ // the session: its scope says which controls this token may use, and its cookie is what a mirror reads with
462
+ const [scope, setScope] = useState<Scope>('pending');
463
+ // why the session was not opened (the wall says it, with a way to try again), and that try
464
+ const [sessionError, setSessionError] = useState<string | null>(null); const [attempt, setAttempt] = useState(0);
465
+ // biome-ignore lint/correctness/useExhaustiveDependencies: `attempt` is the wall's "Try again"
466
+ useEffect(() => {
467
+ let live = true; setSessionError(null);
468
+ void door(token, `/-/${name}/session`, { method: 'POST' }).then(async (r) => {
469
+ const b = (await r.json().catch(() => ({}))) as { scope?: string; origin?: string; error?: string; who?: string; issuer?: string };
470
+ // the person a platform's pass opened this session for: the page says who is signed in, and the way back to it
471
+ keepPortal(b.issuer);
472
+ if (b.who) onWho?.(b.who);
473
+ // the World's session lives at its own origin: its pages go there
474
+ // the host's own answer names the World's origin (a console apart is on another host and port): an http(s) origin only
475
+ const own = httpOrigin(b.origin)?.origin;
476
+ if (r.status === 409 && own && own !== ORIGIN) { moveTo(own, token, name); return; }
477
+ if (!live) return;
478
+ setScope(!r.ok ? 'unknown' : b.scope === 'read' ? 'read' : b.scope === 'write' ? 'write' : 'unknown');
479
+ if (!r.ok) setSessionError(b.error ?? `answered ${r.status}`);
480
+ }).catch((e: unknown) => { if (live) { setScope('unknown'); setSessionError(`the World did not answer: ${e instanceof Error ? e.message : String(e)}`); } });
481
+ return () => { live = false; };
482
+ }, [name, token, attempt]);
483
+ /** The session as the wall shows it: opening, opened (with its scope), or refused. */
484
+ const session: MirrorSession = sessionError ? { state: 'error', error: sessionError } : scope === 'pending' ? { state: 'pending' } : { state: 'ok', scope };
485
+ // fail closed: a token whose scope is not known (the session request failed) is shown no control it might not
486
+ // hold; the server refuses such writes anyway, and a read-only viewer should not be offered them
487
+ const canWrite = scope === 'write';
488
+ /** A write the door refused as out of scope: this token reads only, whatever the session said. */
489
+ const refused = (r: Response): void => { if (r.status === 403) setScope('read'); };
249
490
 
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);
491
+ const [tick, setTick] = useState(0);
492
+ useEffect(() => { const id = setInterval(() => { if (!document.hidden) setTick((t) => t + 1); }, 5000); return () => clearInterval(id); }, []);
493
+ const [clockVersion, setClockVersion] = useState(0);
494
+ const clock = useDoor<Clock>(token, `/-/${name}/clock`, tick + clockVersion);
495
+ const twins = useDoor<{ twins: Twin[] }>(token, `/-/${name}/twins`);
496
+ const branches = useDoor<{ branches: Branch[] }>(token, `/-/${name}/branches`, clockVersion);
497
+ const map = useDoor<WorldMap>(token, `/-/${name}/map`, Math.floor(tick / 2));
498
+ // branches are the host's: a host that does not answer the door has no "as of", and the console shows none
499
+ const hasBranches = !(branches.state === 'error' && (branches.status === 404 || branches.status === 405));
500
+
501
+ if (twin) return <TwinView name={name} token={token} twin={twin} />;
502
+ const current: Tab = TABS.some(([k]) => k === tab) ? (tab as Tab) : TAB_ALIASES[tab] ?? 'overview';
503
+ const vendors = twins.state === 'ok' ? twins.data.twins.map((t) => t.twin) : [];
504
+ const held = (v: string): number => (map.state === 'ok' ? (map.data.twins.find((x) => x.twin === v)?.resources ?? []).reduce((n, r) => n + r.count, 0) : 0);
505
+ const chosen = current === 'vendors' && arg ? arg : undefined;
506
+ const retry = (): void => { setScope('pending'); setAttempt((a) => a + 1); };
507
+ // ALL SCREENS is a canvas app's own page: the whole window, the World's pages a click away (Figma's, tldraw's)
508
+ if (current === 'canvas' || (current === 'vendors' && !chosen)) {
509
+ return <Canvas name={name} twins={twins} session={session} retry={retry} snapshot={snapshot} back={() => go(name.split('/'))} />;
510
+ }
511
+ const title = chosen ? vendorName(chosen) : TABS.find(([k]) => k === current)?.[1] ?? '';
512
+ const open = (parts: string[]) => (e: React.MouseEvent): void => { e.preventDefault(); go([...name.split('/'), ...parts]); };
513
+ const item = (k: Tab, label: string = TABS.find(([t]) => t === k)?.[1] ?? k): React.JSX.Element => {
514
+ const on = current === k && !chosen;
515
+ return <a href={`${BASE}/${name}/~/${k}`} aria-current={on ? 'page' : undefined} className={on ? 'on' : undefined} onClick={open(['~', k])}>{label}</a>;
278
516
  };
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
517
  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}
518
+ <div className="shell">
519
+ <aside className="side">
520
+ <nav aria-label={`${name} pages`} className="side-nav">
521
+ <Connect name={name} />
522
+ <p className="side-group">World</p>
523
+ {item('overview')}
524
+ {hasBranches ? item('branches') : null}
525
+ {item('settings')}
526
+ <BranchPicker name={name} snapshot={snapshot} branches={hasBranches ? branches : null} opens={opens} />
527
+ <p className="side-group">Branch</p>
528
+ {item('branch')}
529
+ {item('changes')}
530
+ {item('activity')}
531
+ <p className="side-group">Vendors</p>
532
+ <a href={`${BASE}/${name}/~/canvas`} className="all-screens" onClick={open(['~', 'canvas'])}><span className="mark" aria-hidden="true">▦</span><span>All screens</span></a>
533
+ <ul className="side-vendors">
534
+ {vendors.map((v) => (
535
+ <li key={v}><a href={`${BASE}/${name}/~/vendors/${v}`} aria-current={chosen === v ? 'page' : undefined} className={chosen === v ? 'on' : undefined} onClick={open(['~', 'vendors', v])}><Mark vendor={v} /><span>{vendorName(v)}</span><span className="count">{held(v) || ''}</span></a></li>
536
+ ))}
537
+ </ul>
538
+ </nav>
539
+ <div className="side-foot"><ThemeChoice /></div>
540
+ </aside>
541
+ <div className="content">
542
+ {snapshot ? (
543
+ <div className="snapshot" role="region" aria-label="Snapshot">
544
+ <span>You are looking at <strong>{snapshot.parent}</strong> as it was at <At iso={snapshot.at} /> · a read-only copy (read-only snapshot) · {snapshot.expiresAt ? (Date.parse(snapshot.expiresAt) <= Date.now() ? <>gone since <At iso={snapshot.expiresAt} /></> : <>goes away <At iso={snapshot.expiresAt} /></>) : 'kept until you go back'}</span>
545
+ <button type="button" onClick={() => void onReturn(snapshot)}>Return to live</button>
546
+ </div>
547
+ ) : null}
548
+ <div className="content-head">
549
+ <h2 id="world">{chosen ? <span className="vendor-cell"><Mark vendor={chosen} />{title}</span> : title}{scope === 'read' ? <> <span className="badge read-only" title="This token reads the World; it changes nothing">Read only</span></> : null}</h2>
550
+ <div className="head-actions">
551
+ <ClockBar name={name} token={token} clock={clock} canWrite={canWrite && !snapshot} onChanged={() => setClockVersion((v) => v + 1)} refused={refused} />
552
+ {!snapshot && canWrite && hasBranches && branches.state !== 'loading' ? <AsOf name={name} token={token} clock={clock} refused={refused} /> : null}
553
+ </div>
554
+ </div>
555
+ <div role="region" aria-labelledby="world" className="panel">
556
+ {current === 'overview' ? <WorldOverview name={name} token={token} map={map} vendors={vendors} twins={twins} session={session} branches={hasBranches ? branches : null} canWrite={canWrite && !snapshot} tick={tick} {...(snapshot ? { snapshotOf: snapshot.parent } : {})} /> : null}
557
+ {current === 'branch' ? <BranchOverview name={name} token={token} map={map} vendors={vendors} tick={tick} /> : null}
558
+ {current === 'vendors' ? <Vendors name={name} token={token} twins={twins} session={session} retry={retry} selected={chosen} /> : null}
559
+ {current === 'activity' && tab === 'trace' ? <Trace name={name} token={token} id={arg ?? ''} /> : null}
560
+ {current === 'activity' && tab !== 'trace' ? <TimelineTab name={name} token={token} vendors={vendors} /> : null}
561
+ {current === 'changes' ? <Changes name={name} token={token} /> : null}
562
+ {current === 'branches' ? <Overview name={name} token={token} canWrite={canWrite} branches={hasBranches ? branches : null} opens={opens} onBranchesChanged={() => setClockVersion((v) => v + 1)} refused={refused} /> : null}
563
+ {current === 'settings' ? <Operations name={name} token={token} admin={admin} canWrite={canWrite} refused={refused} onRemoved={onRemoved} /> : null}
564
+ </div>
565
+ </div>
303
566
  </div>
304
567
  );
305
568
  }
306
569
 
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.`;
570
+ /** Connect's last line: the setup as a prompt for the person's coding agent (packages/cli/src/agent-prompt.ts, the one
571
+ * text), copied whole. */
572
+ function CopyAgentPrompt({ text }: { text: string }): React.JSX.Element {
573
+ const copy = (): void => { void navigator.clipboard?.writeText(text).then(() => toast('Copied the prompt: paste it into your coding agent'), () => toast('Could not copy the prompt', 'error')); };
574
+ return <button type="button" className="agent-prompt-button" onClick={copy}>Copy a prompt for your coding agent</button>;
321
575
  }
322
576
 
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
- };
577
+ /** "Connect", heading the sidebar as Neon's does: how an app's calls to these vendors land in this World. */
578
+ function Connect({ name }: { name: string }): React.JSX.Element {
335
579
  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>
580
+ <details className="menu connect-side">
581
+ <summary className="button connect-button">Connect</summary>
582
+ {portal() ? (
583
+ // a World a platform opened lives on a host: an app reaches it with a key, a local World pushes to it
584
+ <div className="popover">
585
+ <p>Point your app's SDKs here, with a key made on the platform; its calls to these vendors land in this World:</p>
586
+ <pre>{WORLDS}/{name}/&lt;vendor&gt;</pre>
587
+ <a className="button" href={`${portal()}/${encodeURIComponent(name.split('/')[0] ?? '')}`}>Make a key on the platform</a>
588
+ <p className="mode">Or push a World from your machine: <code>volter remote add origin {name}</code>, then <code>volter world push</code>.</p>
589
+ <CopyAgentPrompt text={agentPrompt({ platform: portal(), world: name })} />
590
+ </div>
591
+ ) : (
592
+ <div className="popover">
593
+ <p>Run your app in this World; its calls to these vendors land here:</p>
594
+ <pre>volter world run -- npm start</pre>
595
+ <p className="mode">Or point an SDK at <code>{WORLDS}/{name}/&lt;vendor&gt;</code> with the token in <code>.volter/token</code>.</p>
596
+ <CopyAgentPrompt text={agentPrompt()} />
597
+ </div>
342
598
  )}
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>
599
+ </details>
351
600
  );
352
601
  }
353
602
 
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
- };
603
+ /** The branch picker, between the World's pages and the branch's (where Neon's sits): this World live, or one of the
604
+ * host's branches of it; a snapshot names its parent as the way back. */
605
+ function BranchPicker({ name, snapshot, branches, opens }: { name: string; snapshot: Snapshot | null; branches: Load<{ branches: Branch[] }> | null; opens: (name: string) => boolean }): React.JSX.Element {
606
+ const short = (n: string): string => n.split('/').pop() ?? n;
607
+ const live = snapshot?.parent ?? name;
608
+ const others = branches?.state === 'ok' ? branches.data.branches.filter((b) => b.name !== name) : [];
377
609
  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>
610
+ <details className="menu branch-picker">
611
+ <summary><svg className="branch-glyph" viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M5 3.25a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Zm0 2.122a2.25 2.25 0 1 0-1.5 0v5.256a2.25 2.25 0 1 0 1.5 0V9.5h4.25A2.25 2.25 0 0 0 11.5 7.25v-.878a2.25 2.25 0 1 0-1.5 0v.878a.75.75 0 0 1-.75.75H5V5.372ZM4.25 12a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm6.5-9.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Z" /></svg><span className="branch-name">{snapshot ? short(name) : short(live)}</span><span className="badge">{snapshot ? 'snapshot' : 'live'}</span></summary>
612
+ <div className="popover">
613
+ <p className="mode">Branch</p>
614
+ <ul className="branch-list">
615
+ <li><a href={`${BASE}/${live}`} aria-current={!snapshot ? 'page' : undefined} onClick={(e) => { e.preventDefault(); go(live.split('/')); }}>{short(live)} <span className="mode">live</span></a></li>
616
+ {snapshot ? <li><a href={`${BASE}/${name}`} aria-current="page" onClick={(e) => e.preventDefault()}>{short(name)} <span className="mode">as of <At iso={snapshot.at} /></span></a></li> : null}
617
+ {others.map((b) => (
618
+ <li key={b.name}>{opens(b.name)
619
+ ? <a href={`${BASE}/${b.name}`} onClick={(e) => { e.preventDefault(); go(b.name.split('/')); }}>{short(b.name)} <span className="mode">{b.at?.instant ? <>as of <At iso={b.at.instant} /></> : 'copy'}</span></a>
620
+ : <span className="mode" title="Its token was handed to whoever made it">{short(b.name)}</span>}</li>
621
+ ))}
622
+ </ul>
623
+ {branches?.state === 'ok' && others.length === 0 && !snapshot ? <p className="mode">No branches yet. “View an earlier moment…” opens one.</p> : null}
391
624
  </div>
392
- {note ? <p role="status">{note}</p> : null}
393
625
  </details>
394
626
  );
395
627
  }
396
628
 
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 {
629
+ /** THE BRANCH'S OVERVIEW, as Neon's branch Overview is: its services in a table (each vendor with its state and what it
630
+ * holds), then its monitoring: here the writes the World recorded, hour by hour over the last day, by vendor. */
631
+ function BranchOverview({ name, token, map, vendors, tick }: { name: string; token: string; map: Load<WorldMap>; vendors: string[]; tick: number }): React.JSX.Element {
632
+ const recent = useDoor<Timeline>(token, `/-/${name}/timeline?limit=500`, Math.floor(tick / 3));
633
+ const rows = map.state === 'ok' ? map.data.twins : [];
634
+ const open = (parts: string[]) => (e: React.MouseEvent): void => { e.preventDefault(); go([...name.split('/'), ...parts]); };
635
+ // 24 hourly buckets ending now, each counting the entries that happened in it, per vendor
636
+ const now = Date.now(); const hours = 24; const bucket = 3_600_000;
637
+ const entries = recent.state === 'ok' ? recent.data.entries : [];
638
+ const series = vendors.map((v) => {
639
+ const counts = new Array<number>(hours).fill(0);
640
+ for (const p of entries) { if (p.twin !== v) continue; const t = Date.parse(p.entry.occurredAt); const i = hours - 1 - Math.floor((now - t) / bucket); if (i >= 0 && i < hours) counts[i]! += 1; }
641
+ return { vendor: v, counts };
642
+ });
643
+ const totals = new Array<number>(hours).fill(0).map((_, i) => series.reduce((n, s) => n + s.counts[i]!, 0));
644
+ const peak = Math.max(1, ...totals);
645
+ const W = 720; const H = 160; const bw = W / hours;
646
+ const hues = ['var(--volter-data-base)', '#7c5cff', '#e3572b', '#1f9d6b', '#d9a400', '#2b7de3', '#c2417e', '#6b7280', '#0ea5a4'];
411
647
  return (
412
648
  <>
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}
649
+ <div className="block">
650
+ <table className="dense services" aria-label="Vendors on this branch">
651
+ <thead><tr><th>Vendor</th><th>Details</th><th /></tr></thead>
652
+ <tbody>{vendors.map((v) => {
653
+ const t = rows.find((x) => x.twin === v);
654
+ const top = [...(t?.resources ?? [])].filter((r) => r.count > 0).sort((a, b) => b.count - a.count);
655
+ return (
656
+ <tr key={v}>
657
+ <td><span className="vendor-cell"><Mark vendor={v} /><a href={`${BASE}/${name}/~/vendors/${v}`} onClick={open(['~', 'vendors', v])}>{vendorName(v)}</a> <span className={t?.root ? 'badge rooted' : 'badge'}>{t?.root ? 'connected' : 'stand-in'}</span></span></td>
658
+ <td className="mode">{top.length ? top.slice(0, 4).map((r) => counted(r.count, r.type)).join(' · ') : 'nothing yet'}</td>
659
+ <td className="num">{t?.mirror ? <a href={`${BASE}/${name}/~/vendors/${v}`} onClick={open(['~', 'vendors', v])}>Screens</a> : null}</td>
660
+ </tr>
661
+ );
662
+ })}</tbody>
663
+ </table>
664
+ </div>
665
+ <div className="block chart-block">
666
+ <div className="block-head"><h3>Activity, last 24 hours</h3><a href={`${BASE}/${name}/~/activity`} onClick={open(['~', 'activity'])}>All activity</a></div>
667
+ <Pending load={recent} what="the activity" />
668
+ {recent.state === 'ok' ? (
669
+ <div className="pad">
670
+ <svg viewBox={`0 0 ${W} ${H + 22}`} className="chart" role="img" aria-label={`Writes per hour over the last day: ${totals.reduce((a, b) => a + b, 0)} in all`}>
671
+ {[0.5, 1].map((f) => <line key={f} x1={0} x2={W} y1={H - f * H} y2={H - f * H} className="grid" />)}
672
+ <line x1={0} x2={W} y1={H} y2={H} className="axis" />
673
+ {totals.map((_, i) => {
674
+ let y = H;
675
+ return <g key={i}>{series.map((s, si) => { const h = (s.counts[i]! / peak) * (H - 8); if (h === 0) return null; y -= h; return <rect key={s.vendor} x={i * bw + 2} y={y} width={bw - 4} height={h} fill={hues[si % hues.length]} rx={2}><title>{`${vendorName(s.vendor)}: ${s.counts[i]} in the hour ${hours - 1 - i === 0 ? 'now' : `${hours - 1 - i}h ago`}`}</title></rect>; })}</g>;
676
+ })}
677
+ {[0, 6, 12, 18, 23].map((i) => <text key={i} x={i * bw + bw / 2} y={H + 16} className="tick">{hours - 1 - i === 0 ? 'now' : `-${hours - 1 - i}h`}</text>)}
678
+ </svg>
679
+ <ul className="legend">{series.filter((s) => s.counts.some((c) => c > 0)).map((s) => <li key={s.vendor}><span className="swatch" style={{ background: hues[vendors.indexOf(s.vendor) % hues.length] }} />{vendorName(s.vendor)} <span className="mode">{s.counts.reduce((a, b) => a + b, 0)}</span></li>)}</ul>
680
+ {totals.every((n) => n === 0) ? <p className="mode">Nothing was written in the last day. Run your app in this World and its calls show up here.</p> : null}
681
+ </div>
682
+ ) : null}
683
+ </div>
416
684
  </>
417
685
  );
418
686
  }
419
687
 
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(); };
688
+ /** THE DASHBOARD, as the references open a project: Vercel leads with the live deployment (its preview, its address),
689
+ * Neon with how to connect and the branches. A World's live preview is its vendors' own screens; beside them, how an
690
+ * app connects and the branches; under them, what just happened. Counts sit with what they count, not in a row. */
691
+ const THUMB_SCALE = 0.2;
692
+ function WorldOverview({ name, token, map, vendors, twins, session, branches, canWrite, tick, snapshotOf }: { name: string; token: string; map: Load<WorldMap>; vendors: string[]; twins: Load<{ twins: Twin[] }>; session: MirrorSession; branches: Load<{ branches: Branch[] }> | null; canWrite: boolean; tick: number; /** the World this one is a snapshot of, when it is one */ snapshotOf?: string }): React.JSX.Element {
693
+ const diff = useDoor<Diff>(token, `/-/${name}/diff`, Math.floor(tick / 2));
694
+ const recent = useDoor<Timeline>(token, `/-/${name}/timeline?limit=8`, tick);
695
+ const rows = map.state === 'ok' ? map.data.twins : [];
696
+ const latest = recent.state === 'ok' ? recent.data.entries : [];
697
+ const screens = twins.state === 'ok' ? twins.data.twins.filter((t) => t.mirror).map((t) => t.twin) : [];
698
+ const open = (parts: string[]) => (e: React.MouseEvent): void => { e.preventDefault(); go([...name.split('/'), ...parts]); };
699
+ const heldOf = (v: string): string => { const top = [...(rows.find((x) => x.twin === v)?.resources ?? [])].filter((r) => r.count > 0).sort((a, b) => b.count - a.count); return top.length ? top.slice(0, 2).map((r) => counted(r.count, r.type)).join(' · ') : 'empty'; };
700
+ const changes = diff.state === 'ok' ? diff.data.changes.length : null;
701
+ const list = branches?.state === 'ok' ? branches.data.branches : [];
427
702
  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}
703
+ <div className="dash">
704
+ <section className="block dash-screens" aria-labelledby="screens-head">
705
+ <div className="block-head">
706
+ <h3 id="screens-head">Screens <span className="badge live">live</span></h3>
707
+ <span className="mode">{vendors.length} vendor{vendors.length === 1 ? '' : 's'}{changes !== null ? <> · <a href={`${BASE}/${name}/~/changes`} onClick={open(['~', 'changes'])}>{changes} change{changes === 1 ? '' : 's'} since start</a></> : null}</span>
708
+ <a className="button" href={`${BASE}/${name}/~/canvas`} onClick={open(['~', 'canvas'])}>Open all screens</a>
709
+ </div>
710
+ {session.state === 'ok' ? (
711
+ <ul className="thumbs">
712
+ {vendors.map((v) => (
713
+ <li key={v}>
714
+ <a href={`${BASE}/${name}/~/vendors/${v}`} onClick={open(['~', 'vendors', v])} aria-label={`${vendorName(v)}'s screens`}>
715
+ <span className="thumb">{screens.includes(v)
716
+ ? <iframe title={`${vendorName(v)} preview`} src={mirrorUrl(name, v)} loading="lazy" tabIndex={-1} style={{ width: `${100 / THUMB_SCALE}%`, height: `${100 / THUMB_SCALE}%`, transform: `scale(${THUMB_SCALE})` }} />
717
+ : <span className="thumb-none">No screens</span>}<span className={rows.find((x) => x.twin === v)?.root ? 'badge rooted thumb-status' : 'badge thumb-status'}>{rows.find((x) => x.twin === v)?.root ? 'connected' : 'stand-in'}</span></span>
718
+ <span className="thumb-foot"><Mark vendor={v} /><b>{vendorName(v)}</b><span className="mode">{heldOf(v)}</span></span>
719
+ </a>
720
+ </li>
721
+ ))}
722
+ </ul>
723
+ ) : <p className="mode pad">{session.state === 'error' ? `The World did not open its screens: ${session.error}` : 'Opening…'}</p>}
724
+ </section>
725
+ <div className="dash-side">
726
+ {branches ? <section className="block" aria-labelledby="branches-head">
727
+ <div className="block-head"><h3 id="branches-head">Branches</h3><a href={`${BASE}/${name}/~/branches`} onClick={open(['~', 'branches'])}>All</a></div>
728
+ <ul className="mini-feed">
729
+ <li><svg className="branch-glyph" viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M5 3.25a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Zm0 2.122a2.25 2.25 0 1 0-1.5 0v5.256a2.25 2.25 0 1 0 1.5 0V9.5h4.25A2.25 2.25 0 0 0 11.5 7.25v-.878a2.25 2.25 0 1 0-1.5 0v.878a.75.75 0 0 1-.75.75H5V5.372ZM4.25 12a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm6.5-9.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Z" /></svg><span className="mini-what">{snapshotOf ? <><b>{name.split('/')[1]}</b> <span className="badge">snapshot</span> <span className="mode">of {snapshotOf.split('/')[1]}</span></> : <><b>{name.split('/')[1]}</b> <span className="badge">live</span></>}</span></li>
730
+ {list.slice(0, 3).map((b) => <li key={b.name}><svg className="branch-glyph" viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M5 3.25a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Zm0 2.122a2.25 2.25 0 1 0-1.5 0v5.256a2.25 2.25 0 1 0 1.5 0V9.5h4.25A2.25 2.25 0 0 0 11.5 7.25v-.878a2.25 2.25 0 1 0-1.5 0v.878a.75.75 0 0 1-.75.75H5V5.372ZM4.25 12a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm6.5-9.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Z" /></svg><span className="mini-what">{b.name.split('/').pop()} <span className="mode">{b.at?.instant ? `as of ${new Date(b.at.instant).toLocaleString()}` : 'copy'}</span></span></li>)}
731
+ </ul>
732
+ {canWrite ? <p className="mode pad">“View an earlier moment…” above opens a branch as the World was then.</p> : null}
733
+ </section> : null}
734
+ </div>
735
+ <section className="block dash-activity" aria-labelledby="activity-head">
736
+ <div className="block-head"><h3 id="activity-head">Recent activity</h3><a href={`${BASE}/${name}/~/activity`} onClick={open(['~', 'activity'])}>View all</a></div>
737
+ <Pending load={recent} what="the activity" />
738
+ {recent.state === 'ok' && latest.length === 0 ? (
739
+ <div className="empty-block"><strong>No activity yet</strong><span className="mode">Run your app in this World and its calls show up here.</span></div>
740
+ ) : (
741
+ <ul className="mini-feed">{latest.map((p) => {
742
+ const s = said(p.entry);
743
+ return <li key={keyOf(p)}><Mark vendor={p.twin} /><span className="mini-what"><strong>{s.what}</strong>{s.label ? <span className="mode"> {s.label}</span> : null}</span><span className="mode num" title={p.entry.occurredAt}>{ago(p.entry.occurredAt)}</span></li>;
744
+ })}</ul>
745
+ )}
746
+ </section>
434
747
  </div>
435
748
  );
436
749
  }
437
750
 
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();
751
+
752
+ /** The World's clock, like a test clock's simulation banner: its time, and for a write token the controls that move it. */
753
+ function ClockBar({ name, token, clock, canWrite, onChanged, refused }: { name: string; token: string; clock: Load<Clock>; canWrite: boolean; onChanged: () => void; refused: (r: Response) => void }): React.JSX.Element {
754
+ const [setting, setSetting] = useState(false); const [value, setValue] = useState(''); const [by, setBy] = useState('');
755
+ const [note, setNote] = useState<{ ok: boolean; text: string } | null>(null); const [busy, setBusy] = useState(false);
756
+ const now = clock.state === 'ok' ? clock.data : null;
757
+ const send = async (label: string, path: string, body: unknown): Promise<void> => {
758
+ setBusy(true); setNote(null);
759
+ try {
760
+ const r = await door(token, `/-/${name}/${path}`, { method: path === 'clock' ? 'PUT' : 'POST', body: JSON.stringify(body) });
761
+ if (!r.ok) { refused(r); setNote({ ok: false, text: await failure(r) }); return; }
762
+ const c = (await r.json()) as Clock;
763
+ setNote({ ok: true, text: `${label} — the World is at ${new Date(c.at).toLocaleString()}` }); setSetting(false); onChanged();
764
+ } catch (e) { setNote({ ok: false, text: `the World did not answer: ${e instanceof Error ? e.message : String(e)}` }); } finally { setBusy(false); }
765
+ };
766
+ const setTime = (e: React.FormEvent): void => {
767
+ e.preventDefault();
768
+ const at = new Date(value);
769
+ if (Number.isNaN(at.getTime())) { setNote({ ok: false, text: 'Pick a date and time.' }); return; }
770
+ if (now && at.getTime() < Date.parse(now.at)) { setNote({ ok: false, text: `The World's clock only moves forward: pick a time after ${new Date(now.at).toLocaleString()}.` }); return; }
771
+ void send('Clock set', 'clock', { at: at.toISOString() });
454
772
  };
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';
773
+ const advance = (amount: string, label: string): void => { void send(`Advanced ${label}`, 'clock/advance', { by: amount }); };
774
+ const presets: Array<[string, string]> = [['1h', '1 hour'], ['1d', '1 day'], ['7d', '1 week'], ['30d', '30 days']];
457
775
  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}
776
+ <div className={`clock${now?.frozen ? ' frozen' : ''}`} role="region" aria-label="World clock">
777
+ <p className="clock-now">
778
+ {clock.state === 'loading' ? <span className="mode">clock…</span> : clock.state === 'error' ? <span className="mode">Clock: {clock.error}</span> : now!.frozen
779
+ ? <><span className="badge">Simulation</span> World time <strong><At iso={now!.at} /></strong></>
780
+ : <>World time <strong><At iso={now!.at} /></strong> <span className="mode">· real time</span></>}
781
+ </p>
782
+ {canWrite && now ? (
783
+ <details className="clock-more"><summary>Simulate time…</summary><div className="popover">
784
+ <p className="mode">Move this World's clock forward to see what happens later: renewals, expiries, scheduled sends. Like a test clock, it only moves forward; to look back, use “View an earlier moment…”.</p>
785
+ <div className="clock-controls">
786
+ <button type="button" aria-expanded={setting} onClick={() => { setSetting(!setting); setValue(localInput(nextMinute(now.at))); setNote(null); }}>Set time…</button>
787
+ <div role="group" aria-label="Advance the clock" className="advance">
788
+ <span className="mode">Advance</span>
789
+ {presets.map(([amount, label]) => <button key={amount} type="button" aria-label={`Advance ${label}`} disabled={busy || !now.frozen} title={now.frozen ? undefined : 'Set the time first: advancing moves a set clock'} onClick={() => advance(amount, label)}>{label}</button>)}
790
+ <form className="inline" onSubmit={(e) => { e.preventDefault(); if (!/^\d+[smhd]$/.test(by.trim())) { setNote({ ok: false, text: 'Advance by a number and a unit: 90m, 12h, 3d.' }); return; } advance(by.trim(), `by ${by.trim()}`); }}>
791
+ <label className="sr">Advance by <input name="advanceBy" value={by} onChange={(e) => setBy(e.target.value)} placeholder="90m, 12h, 3d" size={9} disabled={!now.frozen} /></label>
792
+ <button type="submit" disabled={busy || !now.frozen}>Advance</button>
793
+ </form>
794
+ </div>
795
+ {!now.frozen ? <p className="mode">The World runs on real time. Set a time to start simulating; then advance it.</p> : null}
796
+ {setting ? (
797
+ <form className="inline" aria-label="Set the World time" onSubmit={setTime} noValidate>
798
+ <label>World time <input name="worldTime" type="datetime-local" value={value} min={localInput(nextMinute(now.at))} onChange={(e) => setValue(e.target.value)} required /></label>
799
+ <span className="mode">{Intl.DateTimeFormat().resolvedOptions().timeZone}</span>
800
+ <button type="submit" disabled={busy}>Set time</button>
801
+ <button type="button" onClick={() => setSetting(false)}>Cancel</button>
802
+ </form>
803
+ ) : null}
804
+ </div>
805
+ </div></details>
806
+ ) : null}
807
+ {note ? <p role={note.ok ? 'status' : 'alert'}>{note.text}</p> : null}
473
808
  </div>
474
809
  );
475
810
  }
476
811
 
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;
812
+ /** "View as of…": a branch of this World at a past instant, opened read only, gone within the hour. */
813
+ function AsOf({ name, token, clock, refused }: { name: string; token: string; clock: Load<Clock>; refused: (r: Response) => void }): React.JSX.Element {
814
+ const [open, setOpen] = useState(false); const [value, setValue] = useState(''); const [note, setNote] = useState<string | null>(null); const [busy, setBusy] = useState(false);
815
+ const now = clock.state === 'ok' ? clock.data.at : new Date().toISOString();
816
+ // closes as a popover does: Esc (focus back on its button) or a click outside it
817
+ const box = useRef<HTMLDivElement>(null);
818
+ useEffect(() => {
819
+ if (!open) return;
820
+ const esc = (e: KeyboardEvent): void => { if (e.key === 'Escape' && !document.querySelector('dialog[open]')) { setOpen(false); box.current?.querySelector<HTMLButtonElement>('button')?.focus(); } };
821
+ const outside = (e: MouseEvent): void => { if (!box.current?.contains(e.target as globalThis.Node)) setOpen(false); };
822
+ document.addEventListener('keydown', esc); document.addEventListener('click', outside);
823
+ return () => { document.removeEventListener('keydown', esc); document.removeEventListener('click', outside); };
824
+ }, [open]);
825
+ const create = async (e: React.FormEvent): Promise<void> => {
826
+ e.preventDefault(); setNote(null);
827
+ const at = new Date(value);
828
+ if (Number.isNaN(at.getTime())) { setNote('Pick a date and time.'); return; }
829
+ if (at.getTime() > Date.parse(now)) { setNote(`That is after the World's present (${new Date(now).toLocaleString()}); a snapshot looks back.`); return; }
830
+ setBusy(true);
831
+ try {
832
+ const r = await door(token, `/-/${name}/branches`, { method: 'POST', body: JSON.stringify({ at: { instant: at.toISOString() }, ttl: 3600 }) });
833
+ if (!r.ok) { refused(r); setNote(await failure(r)); return; }
834
+ const b = (await r.json()) as { name: string; token: string; readToken: string; expiresAt: string | null; origin?: string };
835
+ keepSnapshot({ name: b.name, parent: name, token: b.token, readToken: b.readToken, at: at.toISOString(), expiresAt: b.expiresAt ?? null, parentOrigin: ORIGIN, made: true });
836
+ // a branch with an origin of its own is viewed there (this origin serves this World only): the page moves, carrying
837
+ // the branch's read token and the snapshot in the fragment, and "Return to live" comes back here to remove it
838
+ const there = httpOrigin(b.origin)?.origin;
839
+ if (there && there !== ORIGIN) { moveTo(there, b.readToken, b.name, `${BASE}/${b.name}`); return; }
840
+ go(b.name.split('/'));
841
+ } catch (e) { setNote(`the World did not answer: ${e instanceof Error ? e.message : String(e)}`); } finally { setBusy(false); }
842
+ };
483
843
  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}
844
+ <div className="as-of" ref={box}>
845
+ <button type="button" aria-expanded={open} title="Open a read-only copy of this World as it was at a moment you pick" onClick={() => { setOpen(!open); setValue(localInput(now)); setNote(null); }}>View an earlier moment…</button>
846
+ {open ? (
847
+ <form className="inline" aria-label="View the World as of a past moment" onSubmit={create} noValidate>
848
+ <label>As of <input name="asOf" type="datetime-local" value={value} max={localInput(now)} onChange={(e) => setValue(e.target.value)} required /></label>
849
+ <button type="submit" disabled={busy}>{busy ? 'Opening…' : 'Open snapshot'}</button>
850
+ <span className="mode">Opens a read-only copy of this World as it was then; the copy goes away within the hour.</span>
851
+ </form>
852
+ ) : null}
853
+ {note ? <p role="alert">{note}</p> : null}
488
854
  </div>
489
855
  );
490
856
  }
491
857
 
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?.();
858
+ /** Overview: what the World holds, one card per twin (a resource explorer), and the branches cut from it. */
859
+ function Overview({ name, token, canWrite, branches, opens, onBranchesChanged, refused }: { name: string; token: string; canWrite: boolean; branches: Load<{ branches: Branch[] }> | null; opens: (name: string) => boolean; onBranchesChanged: () => void; refused: (r: Response) => void }): React.JSX.Element {
860
+ const map = useDoor<WorldMap>(token, `/-/${name}/map`);
861
+ const [note, setNote] = useState<string | null>(null);
862
+ const remove = async (b: string): Promise<void> => {
863
+ if (!(await confirmAct({ title: `Delete the branch ${b}?`, body: 'Whoever holds its tokens loses it, and its history goes with it.', action: 'Delete branch', danger: true }))) return;
864
+ const r = await door(token, `/-/${name}/branches/${b}`, { method: 'DELETE' }).catch((e: unknown) => e as Error);
865
+ if (r instanceof Error) { setNote(`delete ${b}: the World did not answer (${r.message})`); return; }
866
+ if (!r.ok) { refused(r); setNote(`delete ${b}: ${await failure(r)}`); return; }
867
+ forgetSnapshot(b); setNote(`deleted ${b}`); onBranchesChanged();
504
868
  };
869
+ const total = map.state === 'ok' ? map.data.twins.reduce((n, t) => n + t.resources.reduce((m, x) => m + x.count, 0), 0) : 0;
870
+ // the Branches tab: this World's copies; what it holds per vendor is the Vendors tab's list (kept here for a host
871
+ // without branches, where this tab is not shown)
872
+ if (branches) return <BranchList name={name} token={token} canWrite={canWrite} branches={branches} opens={opens} note={note} remove={remove} />;
873
+ return (
874
+ <>
875
+ <h3>Map</h3>
876
+ <Pending load={map} what="the World's map" />
877
+ {map.state === 'ok' ? (map.data.twins.length === 0 ? <p>This World holds no twins.</p> : (
878
+ <>
879
+ <p className="mode">{map.data.twins.length} twin{map.data.twins.length === 1 ? '' : 's'} · {total} resource{total === 1 ? '' : 's'}</p>
880
+ <ul className="map" aria-label="Twins in this World">
881
+ {map.data.twins.map((t) => {
882
+ const top = [...t.resources].sort((a, b) => b.count - a.count);
883
+ return (
884
+ <li key={t.twin} className="card" aria-labelledby={`map-${t.twin}`}>
885
+ <div className="card-head">
886
+ <h4 id={`map-${t.twin}`} className="vendor">{t.twin}</h4>
887
+ <span className={t.root ? 'badge rooted' : 'badge'}>{t.root ? 'rooted' : 'simulated'}</span>
888
+ </div>
889
+ <p className="mode">{t.root ? <>rooted at <code>{t.root.url}</code></> : 'simulated — no vendor behind it'} · position {t.position}</p>
890
+ {top.length === 0 ? <p className="mode">Holds nothing yet.</p> : (
891
+ <ul className="resources">{top.slice(0, 6).map((r) => <li key={r.type}><span>{r.type}</span> <strong>{r.count}</strong></li>)}{top.length > 6 ? <li className="mode">+{top.length - 6} more types</li> : null}</ul>
892
+ )}
893
+ <p className="card-actions">
894
+ {t.mirror ? <a className="button" href={mirrorUrl(name, t.twin)} target="_blank" rel="noopener noreferrer">Open {t.twin}</a> : <span className="mode">no mirror</span>}
895
+ <a href={`${BASE}/${name}/${t.twin}`} aria-label={`Log and tree of ${t.twin}`} onClick={(e) => { e.preventDefault(); go([...name.split('/'), t.twin]); }}>Log &amp; tree</a>
896
+ </p>
897
+ </li>
898
+ );
899
+ })}
900
+ </ul>
901
+ </>
902
+ )) : null}
903
+ </>
904
+ );
905
+ }
906
+
907
+ /** A World's branches: copies of it, each as of a moment (a snapshot "as of" is one, short-lived), openable and removable. */
908
+ function BranchList({ name, canWrite, branches, opens, note, remove }: { name: string; token: string; canWrite: boolean; branches: Load<{ branches: Branch[] }>; opens: (name: string) => boolean; note: string | null; remove: (b: string) => Promise<void> }): React.JSX.Element {
909
+ // as Neon's Branches page: what a branch is for, then every branch with its parent, the moment it copies and its life
910
+ const short = (n: string): string => n.split('/').pop() ?? n;
911
+ const list = branches.state === 'ok' ? branches.data.branches : [];
505
912
  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>
913
+ <div aria-labelledby="branches">
914
+ <p className="lede">Branch this World to look at it as it was, or to change it without touching <b>{short(name)}</b>. {canWrite ? '“View an earlier moment…” above makes one as the World was then.' : ''}</p>
519
915
  {note ? <p role="status">{note}</p> : null}
520
- </section>
916
+ <Pending load={branches} what="the branches" />
917
+ {branches.state === 'ok' ? (
918
+ <div className="block">
919
+ <table className="dense" aria-label="Branches">
920
+ <thead><tr><th id="branches">Branch</th><th>Parent</th><th>As of</th><th>Made</th><th>Goes away</th><th /></tr></thead>
921
+ <tbody>
922
+ <tr><td><b>{short(name)}</b> <span className="badge">default</span></td><td className="mode">—</td><td className="mode">live</td><td className="mode">—</td><td className="mode">kept</td><td /></tr>
923
+ {list.map((b) => (
924
+ <tr key={b.name}>
925
+ <td><b>{short(b.name)}</b></td>
926
+ <td>{short(name)}</td>
927
+ <td>{b.at?.instant ? <At iso={b.at.instant} /> : 'when it was made'}</td>
928
+ <td><At iso={b.createdAt} /></td>
929
+ <td>{b.expiresAt ? <At iso={b.expiresAt} /> : 'kept'}</td>
930
+ <td className="num">
931
+ {opens(b.name) ? <button type="button" aria-label={`Open ${b.name}`} onClick={() => go(b.name.split('/'))}>Open</button> : <span className="mode" title="Its token was handed to whoever made it">opened by whoever made it</span>}{' '}
932
+ {canWrite ? <button type="button" aria-label={`Delete ${b.name}`} onClick={() => void remove(b.name)}>Delete</button> : null}
933
+ </td>
934
+ </tr>
935
+ ))}
936
+ </tbody>
937
+ </table>
938
+ {list.length === 0 ? <p className="mode pad">No branches yet.</p> : null}
939
+ </div>
940
+ ) : null}
941
+ </div>
521
942
  );
522
943
  }
523
944
 
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();
945
+ /** THE VENDORS: each vendor's own screens (its mirror), the World's live preview. A list of the vendors with what each
946
+ * holds, and the chosen one's screens at full size, at their own address; "Side by side" shows them all at once. */
947
+ function Vendors({ name, token, twins, session, retry, selected }: { name: string; token: string; twins: Load<{ twins: Twin[] }>; session: MirrorSession; retry: () => void; selected?: string }): React.JSX.Element {
948
+ const map = useDoor<WorldMap>(token, `/-/${name}/map`);
949
+ const list = twins.state === 'ok' ? twins.data.twins : [];
950
+ const held = (v: string): string => {
951
+ const t = map.state === 'ok' ? map.data.twins.find((x) => x.twin === v) : undefined;
952
+ if (!t) return '';
953
+ const top = [...t.resources].filter((r) => r.count > 0).sort((a, b) => b.count - a.count);
954
+ return top.length === 0 ? 'nothing yet' : top.slice(0, 3).map((r) => counted(r.count, r.type)).join(' · ') + (top.length > 3 ? ' · …' : '');
535
955
  };
536
- if (!sec) return <p className="mode">loading…</p>;
956
+ const rootOf = (v: string): string | null => (map.state === 'ok' ? map.data.twins.find((x) => x.twin === v)?.root?.url ?? null : null);
957
+ const chosen = list.find((t) => t.twin === selected);
958
+ if (!chosen) return twins.state === 'ok' ? <p className="mode">This World has no vendor named {selected}. <a href={`${BASE}/${name}/~/canvas`} onClick={(e) => { e.preventDefault(); go([...name.split('/'), '~', 'canvas']); }}>All screens</a></p> : <Pending load={twins} what="the vendors" />;
537
959
  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>
960
+ <>
961
+ <Pending load={twins} what="the vendors" />
962
+ {twins.state === 'ok' && list.length === 0 ? <p>No vendors in this World yet.</p> : null}
963
+ {chosen ? (
964
+ <div className="vendors">
965
+ <div className="stage">
966
+ <div className="stage-bar">
967
+ <Mark vendor={chosen.twin} />
968
+ <span className="mode">{held(chosen.twin) || 'empty'}{rootOf(chosen.twin) ? ` · connected to ${rootOf(chosen.twin)}` : ' · stand-in'}</span>
969
+ <span className="stage-actions">
970
+ {list.filter((t) => t.mirror).length > 1 ? <a className="button" href={`${BASE}/${name}/~/vendors`} onClick={(e) => { e.preventDefault(); go([...name.split('/'), '~', 'vendors']); }}>All vendors</a> : null}
971
+ {chosen.mirror ? <a className="button" href={mirrorUrl(name, chosen.twin)} target="_blank" rel="noopener noreferrer" aria-label={`Open ${vendorName(chosen.twin)} in a new tab`}>Open in new tab ↗</a> : null}
972
+ </span>
973
+ </div>
974
+ {session.state === 'ok' && session.scope === 'read' ? <p className="mode stage-note">You are looking read only: the screens browse, and refuse changes.</p> : null}
975
+ {session.state === 'pending' ? <p className="mode" role="status">Opening…</p> : null}
976
+ {session.state === 'error' ? <p role="alert">The World did not open its screens: {session.error} <button type="button" onClick={retry}>Try again</button></p> : null}
977
+ {session.state === 'ok' ? (chosen.mirror
978
+ ? <iframe key={chosen.twin} title={`${chosen.twin} mirror`} src={mirrorUrl(name, chosen.twin)} />
979
+ : <p className="empty">{vendorName(chosen.twin)} has no screens here yet. What your app did there is in <a href={`${BASE}/${name}/~/activity`} onClick={(e) => { e.preventDefault(); go([...name.split('/'), '~', 'activity']); }}>Activity</a>.</p>) : null}
980
+ </div>
981
+ </div>
982
+ ) : null}
983
+ </>
546
984
  );
547
985
  }
548
986
 
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();
987
+ /** Consecutive changes that are one action on one kind of thing, as runs (a single change is a run of one). */
988
+ function runsOf(entries: Entry[]): Entry[][] {
989
+ const out: Entry[][] = [];
990
+ for (const e of entries) { const last = out[out.length - 1]; if (last && (last[0]!.operation ?? last[0]!.op) === (e.operation ?? e.op)) last.push(e); else out.push([e]); }
991
+ return out;
992
+ }
993
+
994
+ /** Rows that say the same thing (nine labels created at one instant) as one line that opens to them. */
995
+ function collapsed(items: Placed[]): Array<Placed | Placed[]> {
996
+ const out: Array<Placed | Placed[]> = [];
997
+ for (const p of items) {
998
+ const last = out[out.length - 1];
999
+ const head = Array.isArray(last) ? last[0]! : last;
1000
+ if (head && head.twin === p.twin && (head.entry.operation ?? head.entry.op) === (p.entry.operation ?? p.entry.op)) {
1001
+ if (Array.isArray(last)) last.push(p); else out[out.length - 1] = [head, p];
1002
+ } else out.push(p);
1003
+ }
1004
+ // a run of two stays two rows; three or more is one line
1005
+ return out.flatMap((x) => (Array.isArray(x) && x.length < 3 ? x : [x]));
1006
+ }
1007
+
1008
+ const PAGE = 50;
1009
+ const keyOf = (p: Placed): string => `${p.twin}:${p.position}`;
1010
+ /** Newest first — the door's order: the instant, then the twin, then the position. */
1011
+ const newestFirst = (a: Placed, b: Placed): number => Date.parse(b.entry.occurredAt) - Date.parse(a.entry.occurredAt) || a.twin.localeCompare(b.twin) || b.position - a.position;
1012
+ /** The feed in days, each day in instants: entries at one instant stay together, in no finer order than the World knows. */
1013
+ function grouped(items: Placed[]): Array<{ day: string; label: string; instants: Array<{ at: string; items: Placed[] }> }> {
1014
+ const days: Array<{ day: string; label: string; instants: Array<{ at: string; items: Placed[] }> }> = [];
1015
+ for (const p of items) {
1016
+ const d = new Date(p.entry.occurredAt); const key = d.toDateString();
1017
+ let day = days[days.length - 1];
1018
+ if (!day || day.day !== key) { day = { day: key, label: d.toLocaleDateString(undefined, { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' }), instants: [] }; days.push(day); }
1019
+ const last = day.instants[day.instants.length - 1];
1020
+ if (last && Date.parse(last.at) === d.getTime()) last.items.push(p); else day.instants.push({ at: p.entry.occurredAt, items: [p] });
1021
+ }
1022
+ return days;
1023
+ }
1024
+
1025
+ function EntryRow({ name, p }: { name: string; p: Placed }): React.JSX.Element {
1026
+ const e = p.entry; const t = traceOf(e.traceparent); const s = said(e); const n = notable(e);
1027
+ return (
1028
+ <li className="entry" title={`${e.operation ?? e.op} ${e.subject.type}:${e.subject.id} · ${actorOf(e)} · ${standing(e)}`}>
1029
+ <span className="twin-chip"><Mark vendor={p.twin} />{vendorName(p.twin)}</span>
1030
+ <span className="op">{s.what}</span>
1031
+ {s.label ? <span className="label">“{s.label}”</span> : <code className="subject">{shown(e)}</code>}
1032
+ <span className="actor mode">{byWhom(e, p.twin)}</span>
1033
+ {n ? <span className="standing">{n}</span> : null}
1034
+ {t ? <a className="trace-link" href={`${BASE}/${name}/~/trace/${t.trace}`} aria-label={`View trace ${t.trace.slice(0, 8)}`} onClick={(ev) => { ev.preventDefault(); go([...name.split('/'), '~', 'trace', t.trace]); }}>trace</a> : null}
1035
+ </li>
1036
+ );
1037
+ }
1038
+
1039
+ /** The feed of everything that happened, across twins — an activity feed: newest first, by day, newer changes held behind a pill. */
1040
+ function TimelineTab({ name, token, vendors }: { name: string; token: string; vendors: string[] }): React.JSX.Element {
1041
+ const [only, setOnly] = useState<string | null>(null);
1042
+ const [items, setItems] = useState<Placed[] | null>(null); const [next, setNext] = useState<string | null>(null);
1043
+ const [error, setError] = useState<string | null>(null); const [older, setOlder] = useState(false); const [olderError, setOlderError] = useState<string | null>(null);
1044
+ const [fresh, setFresh] = useState<{ items: Placed[]; gap: Timeline | null }>({ items: [], gap: null });
1045
+ const path = useCallback((before?: string): string => `/-/${name}/timeline?limit=${PAGE}${only ? `&twin=${encodeURIComponent(only)}` : ''}${before ? `&before=${encodeURIComponent(before)}` : ''}`, [name, only]);
1046
+ const itemsRef = useRef<Placed[] | null>(null); itemsRef.current = items;
1047
+ useEffect(() => {
1048
+ let live = true;
1049
+ setItems(null); setError(null); setFresh({ items: [], gap: null });
1050
+ void (async () => {
1051
+ const r = await door(token, path());
1052
+ if (!live) return;
1053
+ if (!r.ok) { setError(await failure(r)); return; }
1054
+ const b = (await r.json()) as Timeline; if (!live) return;
1055
+ setItems(b.entries); setNext(b.next);
1056
+ })().catch((e: unknown) => { if (live) setError(`the World did not answer: ${e instanceof Error ? e.message : String(e)}`); });
1057
+ // live: newer entries are counted, not inserted — the list never moves under the reader
1058
+ const id = setInterval(() => {
1059
+ const cur = itemsRef.current; if (cur === null || document.hidden) return;
1060
+ void door(token, path()).then(async (r) => {
1061
+ if (!r.ok || !live) return;
1062
+ const b = (await r.json()) as Timeline; if (!live) return;
1063
+ if (cur.length === 0) { if (b.entries.length) { setItems(b.entries); setNext(b.next); } return; }
1064
+ const known = new Set((itemsRef.current ?? cur).map(keyOf));
1065
+ const unseen = b.entries.filter((p) => !known.has(keyOf(p)));
1066
+ // a full page with nothing known in it: more arrived than one page holds, and showing them starts over
1067
+ setFresh({ items: unseen, gap: unseen.length === b.entries.length && b.next ? b : null });
1068
+ }).catch(() => null);
1069
+ }, 3000);
1070
+ return () => { live = false; clearInterval(id); };
1071
+ }, [token, path]);
1072
+ const show = (): void => {
1073
+ if (fresh.gap) { setItems(fresh.gap.entries); setNext(fresh.gap.next); } else setItems((cur) => [...fresh.items, ...(cur ?? [])].sort(newestFirst));
1074
+ setFresh({ items: [], gap: null });
562
1075
  };
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(); };
1076
+ const loadOlder = async (): Promise<void> => {
1077
+ if (!next) return; setOlder(true); setOlderError(null);
1078
+ try {
1079
+ const r = await door(token, path(next));
1080
+ // what is shown stays shown: only the older page failed
1081
+ if (!r.ok) { setOlderError(await failure(r)); return; }
1082
+ const b = (await r.json()) as Timeline;
1083
+ setItems((cur) => { const known = new Set((cur ?? []).map(keyOf)); return [...(cur ?? []), ...b.entries.filter((p) => !known.has(keyOf(p)))]; }); setNext(b.next);
1084
+ } catch (e) { setOlderError(`the World did not answer: ${e instanceof Error ? e.message : String(e)}`); } finally { setOlder(false); }
1085
+ };
1086
+ const days = useMemo(() => grouped(items ?? []), [items]);
1087
+ const n = fresh.items.length;
564
1088
  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>
1089
+ <>
1090
+ <div role="group" aria-label="Filter by twin" className="chips">
1091
+ <button type="button" aria-pressed={only === null} onClick={() => setOnly(null)}>All vendors</button>
1092
+ {vendors.map((v) => <button key={v} type="button" aria-pressed={only === v} aria-label={v} onClick={() => setOnly(only === v ? null : v)}>{vendorName(v)}</button>)}
1093
+ </div>
1094
+ <div aria-live="polite" className="pill-slot">{n > 0 ? <button type="button" className="pill" onClick={show}>{fresh.gap ? `${n}+` : n} new change{n === 1 ? '' : 's'} — show</button> : null}</div>
1095
+ {error ? <p role="alert">Could not read the timeline: {error}</p> : items === null ? <p className="mode" role="status">Loading the timeline…</p> : items.length === 0 ? (
1096
+ only ? <p>Nothing has happened in {only} yet.</p> : (
1097
+ <div className="empty">
1098
+ <p><strong>Nothing has happened in this World yet.</strong></p>
1099
+ <p className="mode">An entry arrives each time something changes a twin: an app (or a seed script) calling a twin's API at <code>{WORLDS}/{name}/&lt;twin&gt;</code>, a mirror's form, a changeset landing. They show here within a few seconds.</p>
1100
+ </div>
1101
+ )
1102
+ ) : (
1103
+ <div className="feed" aria-label="Timeline">
1104
+ {days.map((d) => (
1105
+ <div key={d.day} role="group" aria-label={d.label} className="day">
1106
+ <h3 className="day-head">{d.label}</h3>
1107
+ <ol className="instants">
1108
+ {d.instants.map((g) => (
1109
+ <li key={`${g.at}:${keyOf(g.items[0]!)}`} className="instant">
1110
+ <div className="instant-time" title={g.items.length > 1 ? "These happened at the same moment of the World's clock: their order across vendors is not known." : undefined}><At iso={g.at} time /></div>
1111
+ <ul className="entries">{collapsed(g.items).map((x) => (Array.isArray(x)
1112
+ ? <li key={keyOf(x[0]!)} className="entry run"><details><summary><span className="twin-chip"><Mark vendor={x[0]!.twin} />{vendorName(x[0]!.twin)}</span> <span className="op">{counted(x.length, x[0]!.entry.subject.type)} {said(x[0]!.entry).what.split(' ').slice(-1)[0]}</span> <span className="actor mode">{byWhom(x[0]!.entry, x[0]!.twin)}</span></summary><ul className="entries">{x.map((p) => <EntryRow key={keyOf(p)} name={name} p={p} />)}</ul></details></li>
1113
+ : <EntryRow key={keyOf(x)} name={name} p={x} />))}</ul>
1114
+ </li>
1115
+ ))}
1116
+ </ol>
1117
+ </div>
1118
+ ))}
1119
+ {next ? <button type="button" onClick={() => void loadOlder()} disabled={older} aria-busy={older}>Load older</button> : <p className="mode">The beginning of this World{only ? `'s ${only}` : ''}.</p>}
1120
+ {olderError ? <p role="alert">Could not load older changes: {olderError}</p> : null}
1121
+ </div>
1122
+ )}
1123
+ </>
573
1124
  );
574
1125
  }
575
1126
 
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?.();
1127
+ /** A cause followed across vendors — a waterfall with a lane per twin, each step openable to its fields. */
1128
+ function Trace({ name, token, id }: { name: string; token: string; id: string }): React.JSX.Element {
1129
+ const valid = /^[0-9a-f]{32}$/.test(id);
1130
+ const load = useDoor<Timeline>(token, valid ? `/-/${name}/timeline?trace=${id}&limit=500` : null);
1131
+ const steps = load.state === 'ok' ? [...load.data.entries].sort((a, b) => -newestFirst(a, b)) : [];
1132
+ const lanes = [...new Set(steps.map((s) => s.twin))];
1133
+ const t0 = steps[0] ? Date.parse(steps[0].entry.occurredAt) : 0;
1134
+ const back = <a href={`${BASE}/${name}/~/timeline`} onClick={(e) => { e.preventDefault(); go([...name.split('/'), '~', 'timeline']); }}>← Timeline</a>;
1135
+ return (
1136
+ <>
1137
+ <p>{back}</p>
1138
+ <h3>Trace <code>{id}</code></h3>
1139
+ {!valid ? <p role="alert">{id ? `${id} is not a trace id` : 'No trace named'}: a W3C trace id is 32 hex characters.</p> : <Pending load={load} what="the trace" />}
1140
+ {load.state === 'ok' && steps.length === 0 ? (
1141
+ <div className="empty">
1142
+ <p><strong>Nothing in this World carries this trace.</strong></p>
1143
+ <p className="mode">Causes are followed through the W3C <code>traceparent</code> header: an app instrumented with OpenTelemetry sends it with each call, and every entry that call writes keeps it — so one click in the app can be read across Stripe, Slack and the rest. Without it the World knows the order of what happened, not what caused what.</p>
1144
+ </div>
1145
+ ) : null}
1146
+ {steps.length > 0 ? (
1147
+ <>
1148
+ <p className="mode">{steps.length} step{steps.length === 1 ? '' : 's'} across {lanes.length} twin{lanes.length === 1 ? '' : 's'}{load.state === 'ok' && load.data.next ? ' · the first 500 shown' : ''}</p>
1149
+ <div role="table" aria-label="Trace waterfall" className="waterfall" style={{ gridTemplateColumns: `minmax(130px, auto) repeat(${lanes.length}, minmax(140px, 1fr))` }}>
1150
+ <div role="row" className="wf-row wf-head">
1151
+ <span role="columnheader">time</span>
1152
+ {lanes.map((l) => <span role="columnheader" key={l}>{l}</span>)}
1153
+ </div>
1154
+ {steps.map((s) => {
1155
+ const ms = Date.parse(s.entry.occurredAt) - t0; const span = traceOf(s.entry.traceparent)?.span;
1156
+ return (
1157
+ <div role="row" className="wf-row" key={keyOf(s)}>
1158
+ <span role="cell" className="wf-time"><At iso={s.entry.occurredAt} time /> <span className="mode">+{ms} ms</span></span>
1159
+ {lanes.map((l) => (
1160
+ <span role="cell" key={l} className={l === s.twin ? 'wf-cell on' : 'wf-cell'}>
1161
+ {l === s.twin ? (
1162
+ <details>
1163
+ <summary>{s.entry.operation ?? s.entry.op} <code>{s.entry.subject.type}:{s.entry.subject.id}</code></summary>
1164
+ <p className="mode">{actorOf(s.entry)} · {standing(s.entry)} · position {s.position}{span ? <> · span <code>{span}</code></> : null}</p>
1165
+ <Fields fields={s.entry.fields} />
1166
+ </details>
1167
+ ) : null}
1168
+ </span>
1169
+ ))}
1170
+ </div>
1171
+ );
1172
+ })}
1173
+ </div>
1174
+ </>
1175
+ ) : null}
1176
+ </>
1177
+ );
1178
+ }
1179
+
1180
+ function Fields({ fields }: { fields?: Record<string, unknown> }): React.JSX.Element {
1181
+ const rows = Object.entries(fields ?? {});
1182
+ if (rows.length === 0) return <p className="mode">No fields.</p>;
1183
+ return <dl className="fields">{rows.map(([k, v]) => <React.Fragment key={k}><dt>{k}</dt><dd><code>{typeof v === 'string' ? v : JSON.stringify(v)}</code></dd></React.Fragment>)}</dl>;
1184
+ }
1185
+
1186
+ type MirrorSession = { state: 'pending' } | { state: 'ok'; scope: string } | { state: 'error'; error: string };
1187
+ /** THE CANVAS: every vendor's own screens as frames on one surface, the way a canvas app holds its frames (React
1188
+ * Flow; Figma's and tldraw's feel). Pan by dragging the background, zoom with the wheel or the controls, move a frame
1189
+ * by its title bar (kept in this browser), click a frame to use that vendor's screens and the background or Esc to
1190
+ * leave it, so a frame never captures the pan. A frame is its vendor's site at a desktop's size. */
1191
+ const FRAME_W = 1280; const FRAME_H = 820; const FRAME_GAP = 160;
1192
+ type Screen = { world: string; vendor: string; active: boolean; open: (vendor: string) => void };
1193
+ function ScreenFrame({ data }: NodeProps<Node<Screen>>): React.JSX.Element {
1194
+ const { world, vendor, active, open } = data;
1195
+ return (
1196
+ <>
1197
+ <NodeToolbar isVisible position={Position.Top} align="start" className="frame-label">
1198
+ <Mark vendor={vendor} /><span>{vendorName(vendor)}</span>
1199
+ </NodeToolbar>
1200
+ <div className={active ? 'frame on' : 'frame'} style={{ width: FRAME_W, height: FRAME_H }}>
1201
+ <div className="frame-bar" title="Drag to move">
1202
+ <Mark vendor={vendor} /><strong>{vendorName(vendor)}</strong>
1203
+ <span className="frame-url">{mirrorUrl(world, vendor).replace(/^https?:\/\//, '')}</span>
1204
+ <a className="nodrag" href={`${BASE}/${world}/~/vendors/${vendor}`} onClick={(e) => { e.preventDefault(); open(vendor); }}>Open</a>
1205
+ <a className="nodrag" href={mirrorUrl(world, vendor)} target="_blank" rel="noopener noreferrer" aria-label={`Open ${vendorName(vendor)} in a new tab`}>↗</a>
1206
+ </div>
1207
+ <div className={active ? 'frame-body nodrag nopan nowheel' : 'frame-body'}>
1208
+ <iframe title={`${vendor} mirror`} src={mirrorUrl(world, vendor)} loading="lazy" style={{ pointerEvents: active ? 'auto' : 'none' }} />
1209
+ </div>
1210
+ </div>
1211
+ </>
1212
+ );
1213
+ }
1214
+ const FRAME_TYPES = { screen: ScreenFrame };
1215
+ function Canvas(props: { name: string; twins: Load<{ twins: Twin[] }>; session: MirrorSession; retry: () => void; snapshot: Snapshot | null; back: () => void }): React.JSX.Element {
1216
+ return <ReactFlowProvider><CanvasSurface {...props} /></ReactFlowProvider>;
1217
+ }
1218
+ /** The canvas fills the window, as Figma's, tldraw's and Miro's do: the way back and the frames' list float at the top
1219
+ * left (a canvas app's layers), zoom at the bottom left, the minimap at the bottom right. */
1220
+ function CanvasSurface({ name, twins, session, retry, snapshot, back }: { name: string; twins: Load<{ twins: Twin[] }>; session: MirrorSession; retry: () => void; snapshot: Snapshot | null; back: () => void }): React.JSX.Element {
1221
+ const frames = twins.state === 'ok' ? twins.data.twins.filter((t) => t.mirror).map((t) => t.twin) : [];
1222
+ const saved = `volter.canvas.${name}`;
1223
+ const [active, setActive] = useState<string | null>(null);
1224
+ const [layers, setLayers] = useState(true);
1225
+ const flow = useReactFlow();
1226
+ const open = useCallback((v: string): void => go([...name.split('/'), '~', 'vendors', v]), [name]);
1227
+ const cols = Math.max(1, Math.ceil(Math.sqrt(frames.length)));
1228
+ const placed = useCallback((): Array<Node<Screen>> => {
1229
+ let kept: Record<string, { x: number; y: number }> = {};
1230
+ try { kept = JSON.parse(localStorage.getItem(saved) ?? '{}') as Record<string, { x: number; y: number }>; } catch { /* a convenience */ }
1231
+ return frames.map((v, i) => ({ id: v, type: 'screen', dragHandle: '.frame-bar', position: kept[v] ?? { x: (i % cols) * (FRAME_W + FRAME_GAP), y: Math.floor(i / cols) * (FRAME_H + FRAME_GAP) }, data: { world: name, vendor: v, active: false, open } }));
1232
+ // biome-ignore lint/correctness/useExhaustiveDependencies: the frames' names are what change it
1233
+ }, [frames.join(','), name, cols, open]);
1234
+ const [nodes, setNodes, onNodesChange] = useNodesState<Node<Screen>>([]);
1235
+ useEffect(() => { setNodes(placed()); }, [placed, setNodes]);
1236
+ useEffect(() => { setNodes((all) => all.map((n) => (n.data.active === (n.id === active) ? n : { ...n, data: { ...n.data, active: n.id === active } }))); }, [active, setNodes]);
1237
+ useEffect(() => { const on = (e: KeyboardEvent): void => { if (e.key === 'Escape') setActive(null); }; document.addEventListener('keydown', on); return () => document.removeEventListener('keydown', on); }, []);
1238
+ const keep = (): void => { try { localStorage.setItem(saved, JSON.stringify(Object.fromEntries(nodes.map((n) => [n.id, n.position])))); } catch { /* a convenience */ } };
1239
+ /** A layer's row: bring that frame to the middle of the window, and use it. */
1240
+ const focus = (v: string): void => {
1241
+ setActive(v);
1242
+ const n = flow.getNode(v); if (!n) return;
1243
+ // the frame in the room the floating panels leave, at the largest zoom that shows it whole
1244
+ const zoom = Math.min((window.innerWidth - 300) / FRAME_W, (window.innerHeight - 120) / FRAME_H, 1);
1245
+ void flow.setCenter(n.position.x + FRAME_W / 2 - 130 / zoom, n.position.y + FRAME_H / 2, { zoom, duration: 400 });
589
1246
  };
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(); };
1247
+ const dark = typeof window !== 'undefined' && window.matchMedia?.('(prefers-color-scheme: dark)').matches;
1248
+ const ready = session.state === 'ok' && twins.state === 'ok';
591
1249
  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>
1250
+ <div className="canvas full" role="region" aria-label="Every vendor's screens">
1251
+ <ReactFlow nodes={ready ? nodes : []} onNodesChange={onNodesChange} nodeTypes={FRAME_TYPES} onNodeDragStop={keep}
1252
+ onNodeClick={(_e, n) => setActive(n.id)} onPaneClick={() => setActive(null)}
1253
+ fitView fitViewOptions={{ padding: { top: '64px', left: '272px', right: '32px', bottom: '32px' } }} minZoom={0.05} maxZoom={1.5} nodesConnectable={false} elementsSelectable={false}
1254
+ panOnScroll={false} zoomOnDoubleClick={false} colorMode={dark ? 'dark' : 'light'} proOptions={{ hideAttribution: true }}>
1255
+ <Background variant={BackgroundVariant.Dots} gap={28} size={1.4} />
1256
+ <Panel position="top-left" className="float layers">
1257
+ <div className="float-head">
1258
+ <button type="button" className="back" onClick={back} aria-label={`Back to ${name}`}>←</button>
1259
+ <span className="float-title"><strong>{name.split('/')[1]}</strong><span className="mode">{name.split('/')[0]}{snapshot ? ' · snapshot' : ''}</span></span>
1260
+ <button type="button" className="ghost" aria-expanded={layers} aria-label={layers ? 'Hide the screens list' : 'Show the screens list'} onClick={() => setLayers(!layers)}>{layers ? '▾' : '▸'}</button>
1261
+ </div>
1262
+ {layers ? (
1263
+ <ul className="layer-list" aria-label="Screens">
1264
+ {frames.map((v) => <li key={v}><button type="button" className={active === v ? 'on' : undefined} onClick={() => focus(v)}><Mark vendor={v} /><span>{vendorName(v)}</span></button></li>)}
1265
+ {twins.state === 'ok' && frames.length === 0 ? <li className="mode">No vendor in this World has screens.</li> : null}
1266
+ </ul>
1267
+ ) : null}
1268
+ <Pending load={twins} what="the vendors" />
1269
+ {session.state === 'pending' ? <p className="mode" role="status">Opening…</p> : null}
1270
+ {session.state === 'error' ? <p role="alert">The World did not open its screens: {session.error} <button type="button" onClick={retry}>Try again</button></p> : null}
1271
+ </Panel>
1272
+ <Panel position="top-center" className="float hint mode">{active ? `Using ${vendorName(active)} · Esc to leave` : 'Drag to pan · scroll to zoom · click a screen to use it'}</Panel>
1273
+ <Controls showInteractive={false} position="bottom-left" />
1274
+ <MiniMap pannable zoomable nodeBorderRadius={8} position="bottom-right" />
1275
+ </ReactFlow>
1276
+ </div>
601
1277
  );
602
1278
  }
603
1279
 
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>;
1280
+ /** What this World changed since its base — what a changeset would cut — in a compare view, by twin. */
1281
+ function Changes({ name, token }: { name: string; token: string }): React.JSX.Element {
1282
+ const diff = useDoor<Diff>(token, `/-/${name}/diff`);
1283
+ const byTwin = useMemo(() => {
1284
+ const m = new Map<string, Entry[]>();
1285
+ if (diff.state === 'ok') for (const c of diff.data.changes) m.set(c.twin, [...(m.get(c.twin) ?? []), c.entry]);
1286
+ return [...m.entries()];
1287
+ }, [diff]);
612
1288
  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>
1289
+ <>
1290
+ <Pending load={diff} what="the changes" />
1291
+ {diff.state === 'ok' ? (
1292
+ <>
1293
+ <p className="mode">What this World changed since it started{diff.data.base.at ? <> (<At iso={diff.data.base.at} />, <code>{diff.data.base.id}</code>)</> : <> (<code>{diff.data.base.id}</code>)</>}: {diff.data.changes.length} change{diff.data.changes.length === 1 ? '' : 's'} at {byTwin.length} vendor{byTwin.length === 1 ? '' : 's'}. A changeset takes these to the real services.</p>
1294
+ {byTwin.length === 0 ? <p className="empty">No changes since {diff.data.base.id}{diff.data.base.at ? <> (<At iso={diff.data.base.at} />)</> : null}.</p> : byTwin.map(([t, entries]) => (
1295
+ <div key={t} className="compare" role="group" aria-labelledby={`changes-${t}`}>
1296
+ <h3 id={`changes-${t}`} className="vendor-cell"><Mark vendor={t} />{vendorName(t)} <span className="mode">· {entries.length} change{entries.length === 1 ? '' : 's'}</span></h3>
1297
+ <ul>{runsOf(entries).map((run) => run.length >= 3 ? (
1298
+ <li key={run[0]!.id}>
1299
+ <details>
1300
+ <summary><span className="op">{counted(run.length, run[0]!.subject.type)} {said(run[0]!).what.split(' ').slice(-1)[0]}</span> <span className="mode">{run.map((e) => said(e).label ?? shown(e)).slice(0, 4).join(', ')}{run.length > 4 ? ', …' : ''}</span></summary>
1301
+ <ul>{run.map((e) => <li key={e.id}><span className="op">{said(e).what}</span> {said(e).label ? <span className="label">“{said(e).label}”</span> : <code>{shown(e)}</code>} <span className="mode"><At iso={e.occurredAt} /> · {byWhom(e, t)}</span></li>)}</ul>
1302
+ </details>
1303
+ </li>
1304
+ ) : run.map((e) => (
1305
+ <li key={e.id}>
1306
+ <details>
1307
+ <summary><span className="op">{said(e).what}</span> {said(e).label ? <span className="label">“{said(e).label}”</span> : <code>{shown(e)}</code>} <span className="mode"><At iso={e.occurredAt} /> · {byWhom(e, t)}</span></summary>
1308
+ <Fields fields={e.fields} />
1309
+ </details>
1310
+ </li>
1311
+ )))}</ul>
1312
+ </div>
1313
+ ))}
1314
+ </>
1315
+ ) : null}
1316
+ </>
623
1317
  );
624
1318
  }
625
1319
 
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 ?? '?';
1320
+ /** The operator's tables: each twin's standing and root, the requests, the changesets — and the host admin's rotate and remove. */
1321
+ /** A World's NAMED KEYS, as a service's API keys page is (Neon's, Resend's): each named for what holds it, shown once when
1322
+ * made, listed with when it was last used, and revoked alone; `volter remote add` makes one through the platform. */
1323
+ function Keys({ name, token }: { name: string; token: string }): React.JSX.Element {
1324
+ type Key = { id: string; name: string; scope: string; createdAt: string; lastUsedAt: string | null };
1325
+ const [version, setVersion] = useState(0);
1326
+ const keys = useDoor<{ keys: Key[] }>(token, `/-/${name}/keys`, version);
1327
+ const [making, setMaking] = useState(false); const [keyName, setKeyName] = useState(''); const [scope, setScope] = useState<'write' | 'read'>('write'); const [busy, setBusy] = useState(false); const [error, setError] = useState<string | null>(null);
1328
+ const [made, setMade] = useState<{ key: string; name: string } | null>(null);
1329
+ // a read-only link: a read key carried in the dashboard's address (the fragment, which no server sees); whoever opens
1330
+ // it reads this World and changes nothing, until the key is revoked here
1331
+ const [link, setLink] = useState<string | null>(null);
1332
+ const share = async (): Promise<void> => {
1333
+ const label = `Read-only link · ${new Date().toLocaleDateString(undefined, { month: 'short', day: 'numeric' })}`;
1334
+ const r = await door(token, `/-/${name}/keys`, { method: 'POST', body: JSON.stringify({ name: label, scope: 'read' }) }).catch(() => null);
1335
+ const b = r ? ((await r.json().catch(() => ({}))) as { key?: string; error?: string }) : { error: `${ORIGIN} did not answer` };
1336
+ if (!r?.ok || !b.key) { toast(b.error ?? 'refused', 'error'); return; }
1337
+ setLink(`${window.location.origin}${BASE}/${name}#${new URLSearchParams({ token: b.key }).toString()}`); setVersion((v) => v + 1);
1338
+ };
1339
+ const make = async (e: React.FormEvent): Promise<void> => {
1340
+ e.preventDefault(); setBusy(true); setError(null);
1341
+ const r = await door(token, `/-/${name}/keys`, { method: 'POST', body: JSON.stringify({ name: keyName.trim(), scope }) }).catch(() => null);
1342
+ setBusy(false);
1343
+ const b = r ? ((await r.json().catch(() => ({}))) as { key?: string; error?: string }) : { error: `${ORIGIN} did not answer` };
1344
+ if (!r?.ok || !b.key) { setError(b.error ?? 'refused'); return; }
1345
+ setMaking(false); setMade({ key: b.key, name: keyName.trim() }); setKeyName(''); setVersion((v) => v + 1);
1346
+ };
1347
+ const revoke = async (k: Key): Promise<void> => {
1348
+ if (!(await confirmAct({ title: `Revoke ${k.name}?`, body: 'Whatever holds it loses this World at once.', action: 'Revoke key', danger: true }))) return;
1349
+ const r = await door(token, `/-/${name}/keys/${k.id}`, { method: 'DELETE' });
1350
+ if (r.ok) toast(`Revoked ${k.name}`); else toast(await failure(r), 'error');
1351
+ setVersion((v) => v + 1);
1352
+ };
632
1353
  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>
1354
+ <section aria-labelledby="keys-head" className="keys">
1355
+ <div className="keys-head"><div><h3 id="keys-head">Keys</h3><p className="mode">What an app, a script or a CI job holds to reach this World: each named for what holds it and revoked alone. People open the World as themselves; keys are for software.</p></div>{/* opened from a platform, keys are made there (a session here makes none): its Worlds page has "Make a key…" */}
1356
+ {portal() ? <a className="button primary" href={`${portal()}/${encodeURIComponent(name.split('/')[0] ?? '')}`}>Make a key on the platform</a> : <div className="head-actions"><button type="button" onClick={() => void share()}>Share a read-only link</button><button type="button" className="primary" onClick={() => { setError(null); setMaking(true); }}>Make a key</button></div>}</div>
1357
+ <Pending load={keys} what="the keys" retry={() => setVersion((v) => v + 1)} />
1358
+ {keys.state === 'ok' ? (keys.data.keys.length === 0 ? <p className="mode">No keys yet. The World's own token opens it too.</p> : (
1359
+ <table className="dense" aria-label="Keys">
1360
+ <thead><tr><th>Name</th><th>Access</th><th>Made</th><th>Last used</th><th className="num"><span className="sr">Actions</span></th></tr></thead>
1361
+ <tbody>{keys.data.keys.map((k) => (
1362
+ <tr key={k.id}><td>{k.name}</td><td>{k.scope === 'write' ? 'Read and write' : 'Read only'}</td><td className="mode"><span title={when(k.createdAt)}>{ago(k.createdAt)}</span></td><td className="mode">{k.lastUsedAt ? ago(k.lastUsedAt) : 'never'}</td><td className="num"><Menu label={`More for ${k.name}`} items={[{ label: 'Revoke…', danger: true, onSelect: () => void revoke(k) }]} /></td></tr>
1363
+ ))}</tbody>
1364
+ </table>
1365
+ )) : null}
1366
+ <Dialog open={making} onClose={() => { if (!busy) setMaking(false); }} title="Make a key" footer={<><button type="button" onClick={() => setMaking(false)} disabled={busy}>Cancel</button><button type="submit" form="make-key" disabled={busy || !keyName.trim()}>{busy ? 'Making…' : 'Make key'}</button></>}>
1367
+ <form id="make-key" onSubmit={make} aria-label="Make a key">
1368
+ <label>Name <input autoFocus name="keyName" value={keyName} onChange={(e) => setKeyName(e.target.value)} placeholder="ci, staging app…" maxLength={80} required /></label>
1369
+ <label>Access <select value={scope} onChange={(e) => setScope(e.target.value === 'read' ? 'read' : 'write')}><option value="write">Read and write</option><option value="read">Read only</option></select></label>
1370
+ {error ? <p role="alert">{error}</p> : null}
1371
+ </form>
1372
+ </Dialog>
1373
+ <Dialog open={Boolean(made)} onClose={() => setMade(null)} title={`${made?.name ?? 'The key'} is ready`} footer={<button type="button" className="primary" onClick={() => setMade(null)}>Done</button>}>
1374
+ <p className="mode">Copy it now: it is shown this once. Put it where the app reads its secrets.</p>
1375
+ {made ? <CopyField label="Key" value={made.key} secret /> : null}
1376
+ </Dialog>
1377
+ <Dialog open={Boolean(link)} onClose={() => setLink(null)} title="A read-only link" footer={<button type="button" className="primary" onClick={() => setLink(null)}>Done</button>}>
1378
+ <p className="mode">Whoever opens it sees this World's pages and changes nothing. Copy it now: it is shown this once. It is listed under Keys, where revoking it closes the link at once.</p>
1379
+ {link ? <CopyField label="Link" value={link} secret /> : null}
1380
+ </Dialog>
1381
+ </section>
640
1382
  );
641
1383
  }
642
1384
 
643
- function WorldView({ name, token, twin, admin, onRemoved }: { name: string; token: string; twin?: string; admin: string | null; onRemoved: () => void }): React.JSX.Element {
1385
+ function Operations({ name, token, admin, canWrite, refused, onRemoved }: { name: string; token: string; admin: string | null; canWrite: boolean; refused: (r: Response) => void; onRemoved: () => void }): React.JSX.Element {
644
1386
  const [twins, setTwins] = useState<Twin[] | null>(null);
645
1387
  const [changesets, setChangesets] = useState<Changeset[] | null>(null);
646
1388
  const [note, setNote] = useState<string | null>(null);
@@ -658,85 +1400,89 @@ function WorldView({ name, token, twin, admin, onRemoved }: { name: string; toke
658
1400
  const act = async (label: string, path: string, init: RequestInit): Promise<void> => {
659
1401
  setNote(null);
660
1402
  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}`}`);
1403
+ if (!r.ok) refused(r);
1404
+ setNote(r.ok ? `${label}: done` : `${label}: ${await failure(r)}`);
663
1405
  await refresh();
664
1406
  };
665
1407
  const rotate = async (): Promise<void> => {
666
- if (!window.confirm(`Rotate the tokens of ${name}? Every holder of the current ones loses access.`)) return;
1408
+ if (!(await confirmAct({ title: `Rotate the tokens of ${name}?`, body: 'Every app, script and person holding the current tokens loses access at once. The new ones are shown once.', action: 'Rotate tokens', danger: true }))) return;
667
1409
  const r = await door(admin!, `/-/worlds/${name}/rotate`, { method: 'POST' });
668
1410
  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
1411
  };
670
1412
  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' });
1413
+ if (!(await confirmAct({ title: `Remove ${name}?`, body: 'The host stops serving it and its whole history goes with it. This cannot be undone.', action: 'Remove World', danger: true, typed: name }))) return;
1414
+ const r = await door(admin!, `/-/worlds/${name}`, { method: 'DELETE' }).catch((e: unknown) => e as Error);
1415
+ if (r instanceof Error) { setNote(`remove: the host did not answer (${r.message})`); return; }
673
1416
  if (r.ok) onRemoved(); else setNote(`remove: answered ${r.status}`);
674
1417
  };
675
- if (twin) return <TwinView name={name} token={token} twin={twin} />;
676
1418
  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>
1419
+ <>
680
1420
  {note ? <p role="status">{note}</p> : null}
681
1421
  {tokens ? <p role="status">token <code>{tokens.token}</code> · read token <code>{tokens.readToken}</code></p> : null}
682
- <h3>Twins</h3>
1422
+ {canWrite ? <Keys name={name} token={token} /> : null}
1423
+ <h3>Real services</h3>
1424
+ <p className="mode">Every vendor here is a stand-in. Connect one to its real service to read the real state into this World; give it the real key to send this World's changes there (a changeset's Deploy, below).</p>
683
1425
  {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>
1426
+ <table aria-label="Real services">
1427
+ <thead><tr><th>vendor</th><th>real service</th><th>real key</th><th>last read from it</th><th>last sent</th>{canWrite ? <th></th> : null}</tr></thead>
686
1428
  <tbody>
687
1429
  {twins.map((t) => (
688
1430
  <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>
1431
+ <td><a href={`${BASE}/${name}/${t.twin}`} title={`Its log and stored state · ${t.position ?? 0} changes held${t.protocol?.major !== undefined ? ` · protocol ${t.protocol.major}${t.protocol.standing && t.protocol.standing !== 'current' ? ` (${t.protocol.standing})` : ''}` : ''}`} onClick={(e) => { e.preventDefault(); go([...name.split('/'), t.twin]); }}>{vendorName(t.twin)}</a></td>
1432
+ <td>{t.root?.url ? <code>{t.root.url}</code> : <span className="mode">not connected</span>}</td>
1433
+ <td>{t.credential ? `sealed ${t.credential.fingerprint ?? ''} · ${when(t.credential.placedAt)}` : <span className="mode">none</span>}</td>
1434
+ <td>{t.refresh?.failed ? <span role="alert">failed ×{t.refresh.failed.count ?? 1}: {t.refresh.failed.reason ?? ''}</span> : t.root ? when(t.refresh?.observedAt ?? t.refresh?.at) : <span className="mode">—</span>}</td>
1435
+ <td>{t.lastReceipt ? `${t.lastReceipt.status ?? ''} ${t.lastReceipt.externalId ?? ''} ${t.lastReceipt.reason ?? ''}`.trim() : <span className="mode">—</span>}</td>
1436
+ {canWrite ? (
1437
+ <td>
1438
+ <button onClick={() => void confirmAct({ title: `Read ${vendorName(t.twin)} now?`, body: `The real ${vendorName(t.twin)}'s current state is read into this World. Nothing is written to ${vendorName(t.twin)}.`, action: 'Read from it' }).then((ok) => { if (ok) void act(`refresh ${t.twin}`, `/-/${name}/twins/${t.twin}/refresh`, { method: 'POST' }); })} disabled={!t.root} title="Read the real service's current state into this World">Read from it</button>{' '}
1439
+ <button onClick={() => { const url = window.prompt(`The real ${vendorName(t.twin)}'s address (its API's URL); empty disconnects 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) }); }} title="Connect this vendor to the real service's URL (empty disconnects it)">Connect…</button>{' '}
1440
+ <button onClick={() => { const secret = window.prompt(`The real ${vendorName(t.twin)} key: it is sealed beside this World and 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' } }); }} title="Give this World the real service's key; it is sealed and never shown again">Real key…</button>
1441
+ </td>
1442
+ ) : null}
701
1443
  </tr>
702
1444
  ))}
703
1445
  </tbody>
704
1446
  </table>
705
1447
  )}
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> : (
1448
+ <h3>Traffic (last 24 hours)</h3>
1449
+ {report === null ? <p className="mode">No request report for this world.</p> : report.requests === 0 ? <p className="mode">No requests recorded in the last 24 hours. Recording is off unless the World runs with <code>VOLTER_TWIN_REQUEST_JOURNAL=1</code>; what the requests changed is always in Activity.</p> : (
708
1450
  <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>
1451
+ <thead><tr><th>vendor</th><th>requests</th><th>by status</th><th>p50</th><th>p95</th><th>top routes</th></tr></thead>
710
1452
  <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
1453
  </table>
712
1454
  )}
713
- <h3>Changesets</h3>
714
- {changesets === null ? <p>Loading…</p> : changesets.length === 0 ? <p>No changeset has landed.</p> : (
1455
+ <h3>Sending changes to the real services</h3>
1456
+ <p className="mode">A changeset is a set of this World's changes (<code>volter world changeset -m "…"</code>), verified and approved, then deployed to each connected real service.</p>
1457
+ {changesets === null ? <p>Loading…</p> : changesets.length === 0 ? <p className="mode">No changesets yet.</p> : (
715
1458
  <table>
716
- <thead><tr><th>name</th><th>changes</th><th>approvals</th><th>actions</th></tr></thead>
1459
+ <thead><tr><th>name</th><th>changes</th><th>approvals</th>{canWrite ? <th>actions</th> : null}</tr></thead>
717
1460
  <tbody>
718
1461
  {changesets.map((c) => (
719
1462
  <tr key={c.name}>
720
1463
  <td>{c.name}</td>
721
1464
  <td>{Array.isArray(c.actions) ? c.actions.length : '—'}</td>
722
1465
  <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>
1466
+ {canWrite ? (
1467
+ <td>
1468
+ <button onClick={() => void act(`verify ${c.name}`, `/-/${name}/changesets/${encodeURIComponent(c.name)}/verify`, { method: 'POST' })}>Verify</button>{' '}
1469
+ <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>{' '}
1470
+ <button onClick={() => void confirmAct({ title: `Deploy ${c.name}?`, body: 'Its changes are sent to the real services now. That is what the World exists to keep you from doing by accident.', action: 'Deploy to the real services', danger: true, typed: c.name }).then((ok) => { if (ok) void act(`deploy ${c.name}`, `/-/${name}/deploy/${encodeURIComponent(c.name)}`, { method: 'POST', body: JSON.stringify({ confirm: c.name }) }); })}>Deploy</button>
1471
+ </td>
1472
+ ) : null}
728
1473
  </tr>
729
1474
  ))}
730
1475
  </tbody>
731
1476
  </table>
732
1477
  )}
733
1478
  {admin ? (
734
- <p className="admin">
735
- <button onClick={() => void rotate()}>Rotate tokens</button>{' '}
736
- <button onClick={() => void remove()}>Remove world</button>
737
- </p>
1479
+ <div className="danger" role="region" aria-label="Danger zone">
1480
+ <h3>Danger zone</h3>
1481
+ <p><button onClick={() => void rotate()}>Rotate tokens</button> <span className="mode">New tokens for this World; every app, script and browser using the old ones is signed out.</span></p>
1482
+ <p><button onClick={() => void remove()}>Remove world</button> <span className="mode">Deletes this World and everything in it.</span></p>
1483
+ </div>
738
1484
  ) : null}
739
- </section>
1485
+ </>
740
1486
  );
741
1487
  }
742
1488
 
@@ -789,7 +1535,7 @@ function TwinView({ name, token, twin }: { name: string; token: string; twin: st
789
1535
  <td>{e.operation ?? e.op}</td>
790
1536
  <td><code>{e.subject.type}:{e.subject.id}</code></td>
791
1537
  <td>{e.actor ? `${e.actor.kind}${e.actor.id ? ` ${e.actor.id}` : ''}` : '—'}</td>
792
- <td>{receipt(e)}</td>
1538
+ <td>{standing(e)}</td>
793
1539
  </tr>
794
1540
  ))}
795
1541
  </tbody>
@@ -812,4 +1558,5 @@ function TwinView({ name, token, twin }: { name: string; token: string; twin: st
812
1558
  );
813
1559
  }
814
1560
 
815
- createRoot(ROOT).render(<App />);
1561
+ applyTheme(); // the person's theme before the first paint
1562
+ createRoot(ROOT).render(<><App /><Toaster /><ConfirmHost /><MenuDismiss /></>);