@volter/world-console 2.0.13 → 2.0.15

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.
@@ -4,14 +4,13 @@
4
4
  // answer, and offers nothing `volter world` or `curl` could not do. Locators are roles and text.
5
5
  import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
6
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, Empty, failure, listed, Mark, Menu, Pending, Toaster, toast, useDoor as useKitDoor, vendorName, when, ago, type Load } from './kit/kit.tsx';
8
- import { createRoot } from 'react-dom/client';
7
+ import { applyTheme, storedTheme, type Theme, At, Brand, ConfirmHost, MenuDismiss, confirmAct, CopyField, counted, Dialog, DrawerButton, Empty, failure as doorWords, listed, Mark, Menu, Pending, Skeleton, Toaster, toast, useDoor as useKitDoor, vendorName, when, ago, type Load } from './kit/kit.tsx';
9
8
  import { agentPrompt } from '../../../packages/cli/src/agent-prompt.ts';
9
+ import { ICONS } from './brand/icons.ts';
10
10
 
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 };
11
+ type Twin = { twin: string; protocol?: { major?: number; standing?: string } | null; root?: { url?: string; deploy?: string } | null; credential?: { placedAt?: string; fingerprint?: string; sealedBy?: 'vault' | 'key'; boundTo?: 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 };
12
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 }> };
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 };
14
- type Changeset = { name: string; actions?: unknown[]; receipts?: unknown; verification?: unknown; approvals?: 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; at?: string; externalId?: string; reason?: string }; landsId?: string; /** a landed copy under the vendor's id: the local subject id it stands for */ aliasOf?: string; fields?: Record<string, unknown>; traceparent?: string; input?: unknown; /** the credential that called, as the World names it: `key:<name>`, `key`, `person:<who>`, `person`, `token`, `token:read` */ caller?: string };
15
14
  type Mode = { kind: 'admin'; worlds: Row[] } | { kind: 'world'; worlds: Array<{ name: string; base: string; twins: Row['twins'] }> };
16
15
 
17
16
  const TOKEN_KEY = 'volter.console.token';
@@ -65,13 +64,60 @@ const held = (): string => {
65
64
  try { return sessionStorage.getItem(TOKEN_KEY) ?? ''; } catch { return ''; }
66
65
  };
67
66
 
67
+ /** A page's own act a refusal points at: a button on the page that has it, or the page that owns the fix. */
68
+ type Act = 'verify' | 'approve' | 'vault' | 'remote' | 'settings';
69
+ /** A door's refusal in the page's words: the door speaks to its CLI and its API (`run …`, `PUT /-/…`), a page never
70
+ * tells a person to run a command, so each refusal a page can answer becomes that answer (an act here, or the page
71
+ * that has it), and what is left loses its command hints. */
72
+ function plainly(words: string): { text: string; acts: Act[] } {
73
+ const w = words.trim();
74
+ if (/not ready/i.test(w) && /(never verified|not verified|no approvals)/i.test(w)) {
75
+ const parts: string[] = []; const acts: Act[] = [];
76
+ if (/(never verified|not verified)/i.test(w)) { parts.push('it is not verified yet'); acts.push('verify'); }
77
+ if (/no approvals/i.test(w)) { parts.push('no one has approved it'); acts.push('approve'); }
78
+ return { text: `Not ready to deploy: ${listed(parts)}.`, acts };
79
+ }
80
+ const sealed = /no credential (?:is )?sealed for ([a-z0-9_-]+)/i.exec(w);
81
+ if (sealed) return { text: `No credential is sealed for ${vendorName(sealed[1]!)}: seal one in Settings › Vault.`, acts: ['vault'] };
82
+ if (/has no origin/i.test(w)) return { text: 'This World has no remote: name one in Settings › Remote.', acts: ['remote'] };
83
+ const rootless = /the ([a-z0-9_-]+) twin has no root/i.exec(w) ?? /:\s*([a-z0-9_, -]+?) (?:has|have) no root/i.exec(w);
84
+ if (rootless) { const vs = rootless[1]!.split(/,\s*/).filter(Boolean).map(vendorName); return { text: `${listed(vs)} ${vs.length === 1 ? 'is' : 'are'} not connected to ${vs.length === 1 ? 'its' : 'their'} real service: Settings › Real services.`, acts: ['settings'] }; }
85
+ if (/opening a seal is/i.test(w)) return { text: 'A key cannot seal a credential: open the World as yourself.', acts: [] };
86
+ if (/a key manages no keys/i.test(w)) return { text: 'A key cannot manage keys: open the World as yourself.', acts: [] };
87
+ if (/^PUT origin:/i.test(w)) return { text: "That is not a World's address: https://<host>/<org>/<world>", acts: [] };
88
+ if (/^clock: \{/i.test(w)) return { text: 'Pick a date and time.', acts: [] };
89
+ if (/^advance: \{/i.test(w)) return { text: 'Advance by a number and a unit: 90m, 12h, 3d.', acts: [] };
90
+ if (/^branches: \{/i.test(w)) return { text: 'That branch could not be made: pick now, or an instant before it.', acts: [] };
91
+ if (/^approve: \{/i.test(w)) return { text: 'Name who approves.', acts: [] };
92
+ if (/a browser deploys only what it names/i.test(w)) return { text: 'A deploy is confirmed by its name: use Deploy again.', acts: [] };
93
+ if (/^no such door/i.test(w)) return { text: 'This World does not answer that here.', acts: [] };
94
+ const text = w
95
+ .replace(/\s*[—–-]+\s*(?:run\s+)?`[^`]*`/g, '') // "— run `volter …`", "— `printf … | volter …`"
96
+ .replace(/\s*[—–-]+\s*(?:PUT|POST|GET|DELETE)\s+\/-\/\S+(?:\s*\{[^}]*\})?/g, '') // "— PUT /-/<w>/origin { url, token }"
97
+ .replace(/\s*\(\s*`[^`]*`\s*\)/g, '') // "(`volter twin <vendor> root <url>`)"
98
+ .replace(/`(?:volter|printf|curl|npm|bun)[^`]*`/g, '')
99
+ .replace(/:\s*\{[^}]*\}/g, '') // a body's shape, `name: { … }`
100
+ .replace(/\(\s*\)/g, '')
101
+ .replace(/\s*;\s*(?=[;.]|$)/g, '').replace(/\s+([,.;:])/g, '$1').trim();
102
+ return { text: text || words, acts: [] };
103
+ }
104
+ /** The door's refusal, in the page's words. */
105
+ const failure = async (r: Response): Promise<string> => plainly(await doorWords(r)).text;
106
+ /** Where an act a refusal points at lives, when it is a page. */
107
+ const ACT_PAGE: Partial<Record<Act, [string, string]>> = { vault: ['vault', 'Open Vault'], remote: ['remote', 'Open Remote'], settings: ['settings/services', 'Open Real services'] };
108
+ function ActLinks({ name, acts }: { name: string; acts: Act[] }): React.JSX.Element | null {
109
+ const pages = acts.map((a) => ACT_PAGE[a]).filter((x): x is [string, string] => Boolean(x));
110
+ if (pages.length === 0) return null;
111
+ return <>{pages.map(([page, label]) => <React.Fragment key={page}> <a href={`${BASE}/${name}/~/${page}`} onClick={(e) => { e.preventDefault(); go([...name.split('/'), '~', ...page.split('/')]); }}>{label}</a></React.Fragment>)}</>;
112
+ }
113
+
68
114
  async function door(token: string, path: string, init: RequestInit = {}): Promise<Response> {
69
115
  // SESSION: no token header; the World's session cookie (same-origin) is the credential
70
116
  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' } : {}) } });
71
117
  }
72
118
 
73
119
  /** 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); }
120
+ function useDoor<T>(token: string, path: string | null, version = 0): Load<T> { const l = useKitDoor<T>((p) => door(token, p), path, version, token); return l.state === 'error' ? { ...l, error: plainly(l.error).text } : l; }
75
121
 
76
122
  /** A datetime-local input's value for an instant, in the viewer's zone. */
77
123
  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())}`; };
@@ -81,7 +127,6 @@ const nextMinute = (iso: string): string => new Date(Math.ceil(Date.parse(iso) /
81
127
 
82
128
  /** An entry's standing: observed from the vendor, a receipt, default data, or not yet pushed. */
83
129
  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
130
  /** The trace id of a W3C traceparent (`00-<32 hex>-<16 hex>-<2 hex>`), or null. */
86
131
  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
132
 
@@ -161,13 +206,18 @@ function moveTo(origin: string, token: string, name: string, path?: string): voi
161
206
  }
162
207
 
163
208
  /** 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 ''; } }); }
209
+ // where the path is kept: the address bar, or (a page whose root says `data-route="memory"`, as a story's frame is) this
210
+ // page's memory, so a page that is not served at its own addresses still moves between its pages
211
+ let memoryPath: string | null = ROOT.dataset.route === 'memory' ? '' : null;
212
+ /** Move a memory-routed page to `path` (under the base), as following a link there would. */
213
+ export function openAt(path: string): void { memoryPath = path; window.dispatchEvent(new PopStateEvent('popstate')); }
214
+ function route(): string[] { return (memoryPath ?? window.location.pathname.slice(BASE.length)).split('/').filter(Boolean).map((p) => { try { return decodeURIComponent(p); } catch { return ''; } }); }
165
215
  /** An org's or a World's name: letters, digits and single hyphens (world-origins.ts), never a separator or a dot. */
166
216
  const WORLD_PART = /^[a-z0-9](?:[a-z0-9]|-(?!-))*$/i;
167
217
  /** The World a path names, `<org>/<world>`, or '' when its segments are not names: nothing decoded from a path (an
168
218
  * encoded `/` or `..`) ever reaches a door's path. */
169
219
  const worldOf = (o?: string, w?: string): string => (o && w && WORLD_PART.test(o) && WORLD_PART.test(w) ? `${o}/${w}` : '');
170
- function go(parts: string[]): void { window.history.pushState(null, '', `${BASE}/${parts.map(encodeURIComponent).join('/')}`); window.dispatchEvent(new PopStateEvent('popstate')); }
220
+ function go(parts: string[]): void { const path = `/${parts.map(encodeURIComponent).join('/')}`; if (memoryPath !== null) memoryPath = path; else window.history.pushState(null, '', `${BASE}${path}`); window.dispatchEvent(new PopStateEvent('popstate')); }
171
221
  /** A page of a branch this tab holds: at the branch's own origin when it has one (this origin serves its World only), carrying its read token. */
172
222
  function goBranch(branch: string, parts: string[]): void {
173
223
  const there = worldOriginOf(snapshots()[branch]?.origin);
@@ -202,6 +252,20 @@ async function localSession(): Promise<{ token: string } | 'moved' | null> {
202
252
  return null;
203
253
  }
204
254
 
255
+ /** Appearance: the system's, light or dark, kept in this browser. */
256
+ function ThemeChoice(): React.JSX.Element {
257
+ const [theme, setTheme] = useState<Theme>(storedTheme);
258
+ const pick = (t: Theme) => (): void => { try { if (t === 'system') localStorage.removeItem('volter.theme'); else localStorage.setItem('volter.theme', t); } catch { /* a private window keeps it for this page */ } applyTheme(t); setTheme(t); };
259
+ return (
260
+ <>
261
+ <p className="menu-label">Appearance</p>
262
+ <div className="theme-choice" role="radiogroup" aria-label="Appearance">
263
+ {(['system', 'light', 'dark'] as const).map((t) => <button key={t} type="button" role="radio" aria-checked={theme === t} onClick={pick(t)}>{t === 'system' ? 'System' : t === 'light' ? 'Light' : 'Dark'}</button>)}
264
+ </div>
265
+ </>
266
+ );
267
+ }
268
+
205
269
  function App(): React.JSX.Element {
206
270
  const [token, setToken] = useState<string>(held());
207
271
  // a World this machine serves (localSession): no sign-in, no sign-out; `probed`: that was asked, so a sign-in form
@@ -269,7 +333,7 @@ function App(): React.JSX.Element {
269
333
  const ping = await nativeFetch(`${ORIGIN}/-/ping`); // not a platform answer: perhaps this origin is a bare host
270
334
  if (!ping.ok) { setError(`${ORIGIN} does not answer as a host`); setMode(null); return; }
271
335
  worlds = ((await ping.json()) as { worlds?: string[] }).worlds ?? [];
272
- 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; }
336
+ if (worlds.length === 0) { setError(`${ORIGIN} does not list its Worlds: open a World at its own address, ${ORIGIN}${BASE}/<org>/<world>`); setMode(null); return; }
273
337
  }
274
338
  const opened: Array<{ name: string; base: string; twins: Row['twins'] }> = [];
275
339
  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 })) }); } }
@@ -282,16 +346,20 @@ function App(): React.JSX.Element {
282
346
  const [who, setWho] = useState<string>('');
283
347
 
284
348
  if (!token || (!mode && !error)) {
349
+ // as GitHub Desktop asks to sign in: one window, what opening does, the one field and the one act
285
350
  return (
286
- <main>
287
- <Brand href={`${BASE}/`} />
288
- {(token && !mode && !error) || (!token && !probed) ? <p>Opening…</p> : (
289
- <form onSubmit={(e) => { e.preventDefault(); keep(draft.trim()); setToken(draft.trim()); }}>
290
- <p>A host's admin token opens every world it serves; a world's token opens that world.</p>
291
- <label>Token <input name="token" type="password" value={draft} onChange={(e) => setDraft(e.target.value)} autoFocus /></label>
292
- <button type="submit">Open</button>
293
- </form>
294
- )}
351
+ <main className="app gate">
352
+ <div className="gate-window">
353
+ <Brand href={`${BASE}/`} />
354
+ {(token && !mode && !error) || (!token && !probed) ? <p className="mode">Opening…</p> : (
355
+ <form onSubmit={(e) => { e.preventDefault(); keep(draft.trim()); setToken(draft.trim()); }} className="stack">
356
+ <h2>Open a World</h2>
357
+ <p className="mode">A World's token opens that World. A host's admin token opens every World it serves.</p>
358
+ <label><span>Token</span><input name="token" type="password" value={draft} onChange={(e) => setDraft(e.target.value)} autoFocus autoComplete="off" /></label>
359
+ <button type="submit" className="primary">Open</button>
360
+ </form>
361
+ )}
362
+ </div>
295
363
  </main>
296
364
  );
297
365
  }
@@ -311,21 +379,32 @@ function App(): React.JSX.Element {
311
379
  if (mode) for (const w of mode.worlds) navigator.sendBeacon(`${w.base.replace(/\/[^/]+\/[^/]+$/, '')}/-/${w.name}/session/end`); keep(''); setToken(''); setDraft(''); setMode(null); go([]);
312
380
  };
313
381
  if (error || !mode) {
314
- 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>;
382
+ return <main className="app gate"><div className="gate-window"><Brand href={`${BASE}/`} /><h2>Could not open it</h2><p role="alert">{error ?? 'nothing answered'}</p>{portal() ? <a className="button primary" href={`${portal()}/`}>Back to {new URL(portal()).host}</a> : <button type="button" className="primary" onClick={signOut}>{local ? 'Try again' : 'Use another token'}</button>}</div></main>;
315
383
  }
316
384
  // `/<org>/<world>/<twin>` is a twin; `/<org>/<world>/~/<tab>[/<arg>]` is a tab of the World — `~`
317
385
  // is never a vendor name, so a tab can never shadow a twin
318
- const [org, world, third, tabPart, argPart] = parts;
386
+ const [org, world, third, tabPart, argPart, argMore, argLast] = parts;
319
387
  const tabbed = third === '~';
320
388
  // 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
321
389
  const PART = /^[A-Za-z0-9_-]+$/;
390
+ const RECORD_ID = /^[^\u0000-\u001f\u007f]{1,200}$/;
322
391
  const twin = tabbed || !third || !PART.test(third) ? undefined : third;
323
- const tabArg = argPart && PART.test(argPart) ? argPart : undefined;
392
+ // a tab's argument is one name, or a World's full name `<org>/<world>` (a host's branch, on Branches)
393
+ const tabArg = argPart && PART.test(argPart) ? (argMore !== undefined && tabPart !== 'changes' && tabPart !== 'screens' ? (worldOf(argPart, argMore) || undefined) : argPart) : undefined;
394
+ // Changes' tab and a changeset's page under it: `~/changes/changesets/<name>`
395
+ const tabSub = tabPart === 'changes' && argPart === 'changesets' && argMore && PART.test(argMore) ? argMore
396
+ // a record on a vendor's screens: `~/screens/<vendor>/<type>/<id>`
397
+ // (the type is the vendor's, dotted where it is namespaced — `treasury.inbound_transfer`; the id is the vendor's own — a
398
+ // Slack message's is its timestamp; both only ever reach the screens' `?show=`, encoded)
399
+ : tabPart === 'screens' && argMore && argLast && /^[A-Za-z0-9_.-]+$/.test(argMore) && RECORD_ID.test(argLast) ? `${argMore}:${argLast}` : undefined;
324
400
  // only a World's name opens a World page (and so reaches its doors); any other path is the list of Worlds
325
401
  const name = worldOf(org, world);
326
402
  const snapshot = name ? snapshots()[name] ?? null : null;
327
403
  /** The token a World is opened with: a snapshot this tab made opens with its read token. */
328
404
  const tokenOf = (n: string): string => snapshotToken(n) || worldToken(n);
405
+ // with a snapshot in view the World's own pages read the World it is a snapshot of: with the parent's credential when this
406
+ // page holds one (the admin's row for it, or a World token whose doors answered for it), else none
407
+ const parentToken = snapshot ? (mode.kind === 'admin' ? mode.worlds.find((w) => w.name === snapshot.parent)?.token ?? null : mode.worlds.some((w) => w.name === snapshot.parent) ? token : null) : null;
329
408
  const returnToLive = async (s: Snapshot): Promise<void> => {
330
409
  // the parent's pages (and its token) live at its own origin: that page removes the branch
331
410
  const back = worldOriginOf(s.parentOrigin);
@@ -336,8 +415,9 @@ function App(): React.JSX.Element {
336
415
  go(s.parent.split('/'));
337
416
  };
338
417
  return (
339
- <main className={name ? 'app' : undefined}>
340
- <header>
418
+ <main className={name ? 'app workspace' : 'app home'}>
419
+ {/* a World's workspace has its own toolbar (Workspace): no header over it */}
420
+ {name ? null : <header>
341
421
  {name ? <DrawerButton /> : null}
342
422
  <Brand href={`${home() && home() !== ORIGIN ? home() : ''}${BASE}/`} onClick={(e) => { if (home() && home() !== ORIGIN) return; e.preventDefault(); go([]); }} />
343
423
  <nav aria-label="breadcrumb" className="crumbs">
@@ -347,11 +427,12 @@ function App(): React.JSX.Element {
347
427
  </nav>
348
428
  {/* 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 */}
349
429
  {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}
350
- <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>
351
- {local ? null : <button onClick={signOut}>Sign out</button>}
352
- </header>
430
+ {/* a World's page says who is here in its sidebar's account block; the host's list, which has no sidebar, here */}
431
+ {name ? null : <span className="mode">{local ? 'on this machine' : who ? `signed in as ${who}` : mode.kind === 'admin' ? 'host: admin' : 'opened with a link or key'}</span>}
432
+ {name || local ? null : <button onClick={signOut}>Sign out</button>}
433
+ </header>}
353
434
  {name
354
- ? <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} />
435
+ ? <WorldView key={name} name={name} token={tokenOf(name)} twin={twin} tab={tabbed ? tabPart ?? 'overview' : 'overview'} arg={tabbed ? tabArg : undefined} {...(tabbed && tabSub ? { sub: tabSub } : {})} admin={snapshot ? null : mode.kind === 'admin' ? token : null} snapshot={snapshot} parentToken={parentToken} who={{ local, person: who || null, admin: mode.kind === 'admin' }} onSignOut={signOut} onReturn={returnToLive} opens={(n) => !!snapshots()[n] || (mode.kind === 'admin' && mode.worlds.some((w) => w.name === n))} onRemoved={() => { void load(token); go([]); }} onWho={setWho} worlds={mode.worlds.map((w) => w.name)} />
355
436
  : <Worlds mode={mode} admin={mode.kind === 'admin' ? token : null} reload={() => load(token)} />}
356
437
  </main>
357
438
  );
@@ -366,79 +447,91 @@ function Worlds({ mode, admin, reload }: { mode: Mode; admin: string | null; rel
366
447
  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;
367
448
  const r = await door(admin!, `/-/worlds/${name}/rotate`, { method: 'POST' });
368
449
  const b = (await r.json().catch(() => ({}))) as { token?: string; readToken?: string; error?: string };
369
- setNote(r.ok ? `rotated ${name}: the new tokens are shown once — token ${b.token} · read token ${b.readToken}` : b.error ?? `answered ${r.status}`);
450
+ setNote(r.ok ? `rotated ${name}: the new tokens are shown once — token ${b.token} · read token ${b.readToken}` : b.error ? plainly(b.error).text : `answered ${r.status}`);
370
451
  await reload();
371
452
  };
372
453
  const removeWorld = async (name: string): Promise<void> => {
373
454
  const r = await door(admin!, `/-/worlds/${name}`, { method: 'DELETE' });
374
455
  const b = (await r.json().catch(() => ({}))) as { error?: string };
375
- setNote(r.ok ? `removed ${name}` : b.error ?? `answered ${r.status}`);
456
+ setNote(r.ok ? `removed ${name}` : b.error ? plainly(b.error).text : `answered ${r.status}`);
376
457
  await reload();
377
458
  };
378
- const [provisioning, setProvisioning] = useState(false);
459
+ const [provisioning, setProvisioning] = useState(false); const [making, setMaking] = useState(false); const [filter, setFilter] = useState('');
379
460
  const provision = async (e: React.FormEvent): Promise<void> => {
380
461
  e.preventDefault(); setNote(null); setProvisioning(true);
381
462
  try {
382
463
  const r = await door(admin!, '/-/worlds', { method: 'POST', body: JSON.stringify({ org, world, vendors: vendors.split(',').map((v) => v.trim()).filter(Boolean) }) });
383
464
  const body = (await r.json()) as { error?: string; name?: string };
384
- if (!r.ok) { setNote(body.error ?? `the host answered ${r.status}`); return; }
385
- setNote(`provisioned ${body.name}`); setWorld(''); setVendors(''); await reload(); // the org stays: the next world usually goes to the same org
465
+ if (!r.ok) { setNote(body.error ? plainly(body.error).text : `the host answered ${r.status}`); return; }
466
+ setNote(`provisioned ${body.name}`); setWorld(''); setVendors(''); setMaking(false); await reload(); // the org stays: the next world usually goes to the same org
386
467
  } finally { setProvisioning(false); }
387
468
  };
469
+ const f = filter.trim().toLowerCase();
470
+ const rows = mode.worlds.filter((w) => !f || w.name.toLowerCase().includes(f) || w.twins.some((t) => t.vendor.includes(f)));
471
+ const orgs = [...new Set(rows.map((w) => w.name.split('/')[0]!))];
388
472
  return (
389
- <section aria-labelledby="worlds">
390
- <h2 id="worlds">Worlds</h2>
391
- <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>
392
- <table>
393
- <thead><tr><th>world</th><th>address</th><th>stand-ins for</th>{admin ? <th>actions</th> : null}</tr></thead>
394
- <tbody>
395
- {mode.worlds.map((w) => (
396
- <tr key={w.name}>
397
- <td><a href={`${BASE}/${w.name}`} onClick={(e) => { e.preventDefault(); go(w.name.split('/')); }}>{w.name}</a></td>
398
- <td><code>{w.base}</code></td>
399
- <td>{w.twins.map((t) => vendorName(t.vendor)).join(', ') || '—'}</td>
400
- {/* a host's admin manages a World here, on the host's origin, where the admin token lives: a World's own
401
- pages (its origin) never hold it */}
402
- {admin ? <td>
403
- <button type="button" aria-label={`Rotate the tokens of ${w.name}`} onClick={() => void rotateWorld(w.name)}>Rotate tokens</button>{' '}
404
- <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>
405
- </td> : null}
406
- </tr>
407
- ))}
408
- </tbody>
409
- </table>
410
- {admin ? (
411
- <form onSubmit={provision} aria-labelledby="provision">
412
- <h3 id="provision">Provision a world</h3>
413
- <label>Org <input name="org" value={org} onChange={(e) => setOrg(e.target.value)} required /></label>
414
- <label>World <input name="world" value={world} onChange={(e) => setWorld(e.target.value)} required /></label>
415
- <label>Twins <input name="vendors" value={vendors} onChange={(e) => setVendors(e.target.value)} placeholder="github, slack" required /></label>
416
- <button type="submit" disabled={provisioning}>{provisioning ? 'Provisioning… (a host boots the world; ten seconds or so)' : 'Provision'}</button>
473
+ <section className="home-window" aria-labelledby="worlds">
474
+ <div className="home-head">
475
+ <h2 id="worlds">Worlds</h2>
476
+ <span className="mode">{counted(mode.worlds.length, 'World')} {admin ? 'this host serves' : 'this key opens'}</span>
477
+ {admin ? <button type="button" className="primary" onClick={() => { setNote(null); setMaking(true); }}><Icon name="plus" size={16} />New World</button> : null}
478
+ </div>
479
+ <div className="home-find"><input type="search" placeholder="Filter by World or vendor" aria-label="Filter Worlds" value={filter} onChange={(e) => setFilter(e.target.value)} /></div>
480
+ {note && !making ? <p role="status" className="home-note">{note}</p> : null}
481
+ {orgs.map((org) => (
482
+ <React.Fragment key={org}>
483
+ <p className="menu-label home-org">{org}</p>
484
+ <ul className="home-list">{rows.filter((w) => w.name.split('/')[0] === org).map((w) => (
485
+ <li key={w.name}>
486
+ <a href={`${BASE}/${w.name}`} onClick={(e) => { e.preventDefault(); go(w.name.split('/')); }}>
487
+ <Icon name="globe" size={20} />
488
+ <span className="ws-row-text"><b>{w.name.split('/')[1]}</b><small><code>{w.base}</code></small></span>
489
+ <span className="home-vendors">{w.twins.map((t) => <span key={t.vendor} title={vendorName(t.vendor)}><Mark vendor={t.vendor} /></span>)}</span>
490
+ </a>
491
+ {/* a host's admin manages a World here, on the host's origin, where the admin token lives: a World's own pages never hold it */}
492
+ {admin ? (
493
+ <details className="menu home-more">
494
+ <summary aria-label={`More for ${w.name}`}>⋯</summary>
495
+ <div className="popover menu-list">
496
+ <button type="button" onClick={(e) => { (e.currentTarget as HTMLElement).closest('details')?.removeAttribute('open'); void rotateWorld(w.name); }}>Rotate tokens…</button>
497
+ <button type="button" className="danger-item" onClick={(e) => { (e.currentTarget as HTMLElement).closest('details')?.removeAttribute('open'); 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>
498
+ </div>
499
+ </details>
500
+ ) : null}
501
+ </li>
502
+ ))}</ul>
503
+ </React.Fragment>
504
+ ))}
505
+ {rows.length === 0 ? <p className="mode home-note">{f ? `No World matches ${filter}.` : 'No Worlds here yet.'}</p> : null}
506
+ <Dialog open={making} onClose={() => { if (!provisioning) setMaking(false); }} title="New World" footer={<><button type="button" onClick={() => setMaking(false)} disabled={provisioning}>Cancel</button><button type="submit" form="provision" className="primary" disabled={provisioning}>{provisioning ? 'Making… (ten seconds or so)' : 'Make World'}</button></>}>
507
+ <form id="provision" onSubmit={provision} className="stack">
508
+ <label><span>Org</span><input name="org" value={org} onChange={(e) => setOrg(e.target.value)} required /></label>
509
+ <label><span>World</span><input name="world" value={world} onChange={(e) => setWorld(e.target.value)} required /></label>
510
+ <label><span>Vendors</span><input name="vendors" value={vendors} onChange={(e) => setVendors(e.target.value)} placeholder="stripe, slack" required /><span className="mode">A twin of each, with its default data.</span></label>
417
511
  {note ? <p role="status">{note}</p> : null}
418
512
  </form>
419
- ) : note ? <p role="status">{note}</p> : null}
513
+ </Dialog>
420
514
  </section>
421
515
  );
422
516
  }
423
517
 
424
518
  type Clock = { at: string; frozen: boolean };
425
519
  type WorldMap = { world: string; twins: Array<{ twin: string; position: number; mirror: boolean; root: { url: string } | null; resources: Array<{ type: string; count: number }> }> };
426
- type Placed = { twin: string; position: number; entry: Entry };
520
+ type Placed = { twin: string; position: number; entry: Entry; /** the credential that called, as the World names it from its request journal (the timeline door): `key:<name>`, `person:<who>`, `person`, `token` */ caller?: string };
427
521
  type Timeline = { entries: Placed[]; next: string | null };
428
522
  type Diff = { base: { id: string; at: string | null }; changes: Array<{ twin: string; entry: Entry }> };
429
- type Branch = { name: string; at?: { instant?: string }; createdAt: string; expiresAt: string | null };
523
+ type Branch = { name: string; from?: string; at?: { instant?: string } | null; createdAt: string; expiresAt: string | null };
430
524
  /** The branches door: the host's snapshots of this World, and the World's own named branches (`volter world branch`),
431
525
  * one of which, `checkedOut`, is the one this serve holds. */
432
526
  type BranchesDoor = { branches: Branch[]; checkedOut?: string; named?: Array<{ name: string; running: boolean }>; canServe?: boolean };
433
527
  /** What the World's session answered: its scope; `unknown` when the host has no session door (a write then says). */
434
528
  type Scope = 'pending' | 'read' | 'write' | 'unknown';
435
529
 
436
- // A World's page is laid out as Neon's console is (Volter is Neon for every SaaS, docs/concepts/the-model.md): the
437
- // World's own pages, the branch picker, then the branch's pages and its services, which here are its vendors, each a
438
- // sibling. All screens is a canvas app's page, the whole window. The older page names still open their page.
439
- const TABS = [['overview', 'Dashboard'], ['branches', 'Branches'], ['settings', 'Settings'], ['branch', 'Overview'], ['changes', 'Changes'], ['changesets', 'Changesets'], ['activity', 'Activity'], ['vendors', 'Vendors'], ['canvas', 'All screens']] as const;
440
- type Tab = (typeof TABS)[number][0];
441
- const TAB_ALIASES: Record<string, Tab> = { dashboard: 'overview', mirrors: 'vendors', screens: 'canvas', timeline: 'activity', monitoring: 'activity', trace: 'activity', operations: 'settings' };
530
+ /** The addresses under a World's `~/` (docs/contributing/console.md): its places (changes and its changesets, events and a
531
+ * trace, a vendor's records, its screens and the canvas of them all) and its windows (branches, settings and the two
532
+ * settings tabs addressed on their own, remote and vault). Anything else opens Changes. */
533
+ const TABS = ['changes', 'events', 'trace', 'vendors', 'screens', 'canvas', 'branches', 'settings', 'remote', 'vault'] as const;
534
+ type Tab = (typeof TABS)[number] | 'overview';
442
535
  /** 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). */
443
536
  const mirrorUrl = (name: string, vendor: string): string => `${WORLDS}/${name}/${vendor}/`;
444
537
  /** What an entry did, in words: the thing's kind and the vendor's verb, the thing's own label where it has one. */
@@ -461,12 +554,8 @@ function said(e: Entry): { what: string; label: string | null } {
461
554
  }
462
555
  /** A secret's id is never shown whole (a token, a password, a key): its last four characters. */
463
556
  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);
464
- /** Who did it: an app or agent by its id; the vendor itself for what it does on its own (an event it records). */
465
- 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}`);
466
- /** An entry's standing worth saying: where it came from, or what its push did. Not yet pushed is the ordinary case. */
467
- const notable = (e: Entry): string | null => (e.operation === 'observed' || e.receipt || e.provenance === 'placeholder' ? standing(e) : null);
468
557
 
469
- 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 {
558
+ function WorldView({ name, token, twin, tab, arg, sub, admin, snapshot, parentToken, who, onSignOut, onReturn, opens, onRemoved, onWho, worlds = [] }: { /** the Worlds this page opens, for the World menu's switcher */ worlds?: string[]; /** who is at this page (the account block) */ who: { local: boolean; person: string | null; admin: boolean }; onSignOut: () => void; name: string; token: string; twin?: string; tab: string; arg?: string; /** with a snapshot in view: the credential this page holds for the World it is a snapshot of, or null */ parentToken?: string | null; /** a page under a tab's argument: a changeset under Changes › Changesets */ sub?: 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 {
470
559
  // the session: its scope says which controls this token may use, and its cookie is what a mirror reads with
471
560
  const [scope, setScope] = useState<Scope>('pending');
472
561
  // why the session was not opened (the wall says it, with a way to try again), and that try
@@ -488,7 +577,7 @@ function WorldView({ name, token, twin, tab, arg, admin, snapshot, onReturn, ope
488
577
  if (r.status === 409 && own && own !== ORIGIN) { moveTo(own, token, name); return; }
489
578
  if (!live) return;
490
579
  setScope(!r.ok ? 'unknown' : b.scope === 'read' ? 'read' : b.scope === 'write' ? 'write' : 'unknown');
491
- if (!r.ok) setSessionError(b.error ?? `answered ${r.status}`);
580
+ if (!r.ok) setSessionError(b.error ? plainly(b.error).text : `answered ${r.status}`);
492
581
  }).catch((e: unknown) => { if (live) { setScope('unknown'); setSessionError(`the World did not answer: ${e instanceof Error ? e.message : String(e)}`); } });
493
582
  return () => { live = false; };
494
583
  }, [name, token, attempt]);
@@ -498,7 +587,9 @@ function WorldView({ name, token, twin, tab, arg, admin, snapshot, onReturn, ope
498
587
  // hold; the server refuses such writes anyway, and a read-only viewer should not be offered them
499
588
  const canWrite = scope === 'write';
500
589
  /** A write the door refused as out of scope: this token reads only, whatever the session said. */
501
- const refused = (r: Response): void => { if (r.status === 403) setScope('read'); };
590
+ // only the World saying this credential reads (`the read token opens only reads`, write access needed) makes the page
591
+ // read only: a 403 for another reason (a key opening a seal) is said where it happened and changes no scope
592
+ const refused = (r: Response): void => { if (r.status !== 403) return; void r.clone().json().then((b: { error?: string }) => { if (/read token|opens only reads|write access|reads only/i.test(b?.error ?? '')) setScope('read'); }, () => null); };
502
593
 
503
594
  const [tick, setTick] = useState(0);
504
595
  useEffect(() => { const id = setInterval(() => { if (!document.hidden) setTick((t) => t + 1); }, 5000); return () => clearInterval(id); }, []);
@@ -510,271 +601,674 @@ function WorldView({ name, token, twin, tab, arg, admin, snapshot, onReturn, ope
510
601
  // branches are the host's: a host that does not answer the door has no "as of", and the console shows none
511
602
  const hasBranches = !(branches.state === 'error' && (branches.status === 404 || branches.status === 405));
512
603
 
513
- if (twin) return <TwinView name={name} token={token} twin={twin} />;
514
- const current: Tab = TABS.some(([k]) => k === tab) ? (tab as Tab) : TAB_ALIASES[tab] ?? 'overview';
604
+
605
+ const current: Tab = (TABS as readonly string[]).includes(tab) ? (tab as Tab) : 'overview';
515
606
  const vendors = twins.state === 'ok' ? twins.data.twins.map((t) => t.twin) : [];
516
- 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);
517
607
  const chosen = current === 'vendors' && arg ? arg : undefined;
518
608
  const retry = (): void => { setScope('pending'); setAttempt((a) => a + 1); };
519
609
  // ALL SCREENS is a canvas app's own page: the whole window, the World's pages a click away (Figma's, tldraw's)
520
- if (current === 'canvas' || (current === 'vendors' && !chosen)) {
521
- return <Canvas name={name} twins={twins} session={session} retry={retry} snapshot={snapshot} back={() => go(name.split('/'))} />;
610
+ if (current === 'canvas' || (current === 'vendors' && !chosen) || (tab === 'screens' && !arg)) {
611
+ return <Canvas name={name} token={token} twins={twins} session={session} retry={retry} snapshot={snapshot} back={() => go(name.split('/'))} />;
522
612
  }
523
- const title = chosen ? vendorName(chosen) : TABS.find(([k]) => k === current)?.[1] ?? '';
524
- const open = (parts: string[]) => (e: React.MouseEvent): void => { e.preventDefault(); go([...name.split('/'), ...parts]); };
525
- const item = (k: Tab, label: string = TABS.find(([t]) => t === k)?.[1] ?? k): React.JSX.Element => {
526
- const on = current === k && !chosen;
527
- return <a href={`${BASE}/${name}/~/${k}`} aria-current={on ? 'page' : undefined} className={on ? 'on' : undefined} onClick={open(['~', k])}>{label}</a>;
528
- };
613
+ // EVERY World opens as its workspace: on this machine, on a host, and a snapshot of one (read only, a way back to live)
614
+ const tabOf = (t?: string): SettingsTab => (SETTINGS_TABS.find(([k]) => k === t)?.[0] ?? 'account');
615
+ const win: WorkspaceWindow = tab === 'settings' ? { kind: 'settings', tab: tabOf(arg) } : tab === 'remote' ? { kind: 'settings', tab: 'remote' } : tab === 'vault' ? { kind: 'settings', tab: 'vault' } : tab === 'branches' ? { kind: 'branches', ...(arg ? { open: arg } : {}) } : null;
616
+ // a vendor's own address (`/<org>/<world>/<vendor>`) is its place: what it stores and its log
617
+ const view: WorkspaceView = twin ? 'vendor' : tab === 'screens' && arg ? 'screen' : current === 'vendors' && chosen ? 'vendor' : tab === 'trace' ? 'trace' : tab === 'events' ? 'events' : tab === 'changes' && arg === 'changesets' ? 'history' : 'changes';
618
+ return <Workspace name={name} token={token} view={view} win={win} who={who} snapshot={snapshot} onReturn={onReturn} onSignOut={onSignOut} worlds={worlds} session={session} retry={retry} {...(view === 'vendor' ? { arg: twin ?? chosen } : view === 'events' || view === 'trace' || view === 'screen' ? (arg ? { arg } : {}) : {})} {...(sub ? { sub } : {})} twins={twins} map={map} branches={hasBranches ? branches : null} clock={clock} canWrite={canWrite && !snapshot} refused={refused} signsAs={signsAs} opens={opens} tick={tick} admin={admin} onRemoved={onRemoved} onClock={() => setClockVersion((v) => v + 1)} scope={snapshot ? 'read' : scope} />;
619
+ }
620
+
621
+ /** A brand icon by its Lucide name (scripts/brand-tokens.ts writes the set), in the text's colour. */
622
+ function Icon({ name, size = 20 }: { name: string; size?: number }): React.JSX.Element {
623
+ return <svg className="icon" viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" strokeWidth={size <= 16 ? 1.75 : 1.5} strokeLinecap="square" strokeLinejoin="miter" aria-hidden="true">{(ICONS[name] ?? []).map(([tag, attrs], i) => React.createElement(tag, { key: i, ...attrs }))}</svg>;
624
+ }
625
+ /** What a record's writes did to it, as a working tree marks a file: made here, removed, or changed. */
626
+ function changeKind(writes: Entry[]): { icon: string; word: string; tone: string } {
627
+ const verb = (e: Entry): string => { const op = (e.operation ?? e.op).toLowerCase(); return op.includes('.') ? op.slice(op.lastIndexOf('.') + 1) : op; };
628
+ if (writes.some((e) => /^(delete|remove|archive|revoke)/.test(verb(e)) || e.fields?.deleted === true)) return { icon: 'square-minus', word: 'removed', tone: 'removed' };
629
+ if (/^(create|add|insert|issue|send|post|upload|invite|record|seed)/.test(verb(writes[0]!))) return { icon: 'square-plus', word: 'made', tone: 'added' };
630
+ return { icon: 'square-dot', word: 'changed', tone: 'changed' };
631
+ }
632
+ type WorkspaceView = 'changes' | 'history' | 'events' | 'trace' | 'vendor' | 'screen';
633
+ type ChangedRecord = { key: string; twin: string; writes: Entry[] };
634
+ /** The workspace's windows over its view: Settings (GitHub Desktop's, its tabs beside its pane) and Branches. */
635
+ type WorkspaceWindow = { kind: 'settings'; tab: SettingsTab } | { kind: 'branches'; open?: string } | null;
636
+ const SETTINGS_TABS = [['account', 'Account', 'user'], ['changesets', 'Changesets', 'git-commit-horizontal'], ['remote', 'Remote', 'cloud'], ['access', 'Access', 'key-round'], ['services', 'Real services', 'plug'], ['vault', 'Vault', 'vault'], ['checks', 'Checks', 'list-checks'], ['clock', 'Clock', 'clock'], ['appearance', 'Appearance', 'palette']] as const;
637
+ type SettingsTab = (typeof SETTINGS_TABS)[number][0];
638
+ const shortBranch = (world: string, b: string): string => { const last = b.split('/').pop() ?? b; const base = world.split('/').pop() ?? ''; return last.startsWith(`${base}-`) ? last.slice(base.length + 1) : last; };
639
+ const kindOf = (t: string): string => t.replace(/^_+/, '').replace(/[_-]+/g, ' ');
640
+
641
+ /** One changed record as a diff reads: its fields as they stood before this branch's changes (folded from its history
642
+ * ahead of them) and as they stand now, only those that moved; the writes that moved them listed above. */
643
+ function RecordDiff({ record, history, screens }: { record: ChangedRecord; history: Entry[] | null; /** the World, when this vendor has screens: the record's change is seen there */ screens?: string }): React.JSX.Element {
644
+ const head = record.writes[record.writes.length - 1]!; const s = said(head); const k = changeKind(record.writes);
645
+ const mine = new Set(record.writes.map((e) => e.id));
646
+ const first = record.writes[0]!.occurredAt;
647
+ const before: Record<string, unknown> = {};
648
+ // a landed copy (a push's receipt of one of these writes, under the vendor's id) is the same write, never what it held before
649
+ if (history) for (const e of history) if (!mine.has(e.id) && !e.landsId && !e.aliasOf && e.subject.type === head.subject.type && e.subject.id === head.subject.id && e.occurredAt <= first) Object.assign(before, e.fields ?? {});
650
+ const after: Record<string, unknown> = { ...before }; for (const e of record.writes) Object.assign(after, e.fields ?? {});
651
+ const made = k.tone === 'added' && Object.keys(before).length === 0;
652
+ const rows = [...new Set([...Object.keys(before), ...Object.keys(after)])].filter((f) => !same(before[f], after[f]));
529
653
  return (
530
- <div className="shell">
531
- <aside className="side">
532
- <nav aria-label={`${name} pages`} className="side-nav">
533
- <Connect name={name} />
534
- <p className="side-group">World</p>
535
- {item('overview')}
536
- {hasBranches ? item('branches') : null}
537
- {item('settings')}
538
- <BranchPicker name={name} snapshot={snapshot} branches={hasBranches ? branches : null} opens={opens} />
539
- <p className="side-group">Branch</p>
540
- {item('branch')}
541
- {item('changes')}
542
- {item('changesets')}
543
- {item('activity')}
544
- <p className="side-group">Vendors</p>
545
- <a href={`${BASE}/${name}/~/canvas`} className="all-screens" onClick={open(['~', 'canvas'])}><span className="mark" aria-hidden="true">▦</span><span>All screens</span></a>
546
- <ul className="side-vendors">
547
- {vendors.map((v) => (
548
- <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>
549
- ))}
550
- </ul>
551
- </nav>
552
- <div className="side-foot"><ThemeChoice /></div>
553
- </aside>
554
- <div className="content">
555
- {snapshot ? (
556
- <div className="snapshot" role="region" aria-label="Snapshot">
557
- <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>
558
- <button type="button" onClick={() => void onReturn(snapshot)}>Return to live</button>
559
- </div>
560
- ) : null}
561
- <div className="content-head">
562
- <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>
563
- <div className="head-actions">
564
- <ClockBar name={name} token={token} clock={clock} canWrite={canWrite && !snapshot} onChanged={() => setClockVersion((v) => v + 1)} refused={refused} />
565
- {!snapshot && canWrite && hasBranches && branches.state !== 'loading' ? <AsOf name={name} token={token} clock={clock} refused={refused} /> : null}
566
- </div>
567
- </div>
568
- <div role="region" aria-labelledby="world" className="panel">
569
- {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}
570
- {current === 'branch' ? <BranchOverview name={name} token={token} map={map} vendors={vendors} tick={tick} /> : null}
571
- {current === 'vendors' ? <Vendors name={name} token={token} twins={twins} session={session} retry={retry} selected={chosen} /> : null}
572
- {current === 'activity' && tab === 'trace' ? <Trace name={name} token={token} id={arg ?? ''} /> : null}
573
- {current === 'activity' && tab !== 'trace' ? <TimelineTab name={name} token={token} vendors={vendors} /> : null}
574
- {current === 'changes' ? <Changes name={name} token={token} /> : null}
575
- {current === 'changesets' ? <Changesets name={name} token={token} signsAs={signsAs} canWrite={canWrite && !snapshot} rooted={map.state === 'ok' ? map.data.twins.filter((t) => t.root).map((t) => t.twin) : []} {...(arg ? { open: arg } : {})} refused={refused} /> : null}
576
- {current === 'branches' ? <Overview name={name} token={token} canWrite={canWrite} branches={hasBranches ? branches : null} opens={opens} onBranchesChanged={() => setClockVersion((v) => v + 1)} refused={refused} /> : null}
577
- {current === 'settings' ? <Operations name={name} token={token} admin={admin} canWrite={canWrite} refused={refused} onRemoved={onRemoved} /> : null}
578
- </div>
654
+ <div className="ws-diff" aria-label={`${s.label ?? shown(head)}: its changes`}>
655
+ <div className="ws-diff-head">
656
+ <span className={`ws-kind ${k.tone}`} title={k.word}><Icon name={k.icon} size={16} /></span>
657
+ <Mark vendor={record.twin} />
658
+ <div className="cell-label"><b>{s.label ?? shown(head)}</b><small>{vendorName(record.twin)} · {head.subject.type.replace(/^_+/, '').replace(/[_-]+/g, ' ')} <code>{shown(head)}</code> · {k.word} here</small></div>
659
+ {screens && k.tone !== 'removed' ? <a className="ws-on-screens" href={`${BASE}/${screens}/~/screens/${record.twin}/${head.subject.type}/${encodeURIComponent(head.subject.id)}`} onClick={(e) => { e.preventDefault(); go([...screens.split('/'), '~', 'screens', record.twin, head.subject.type, head.subject.id]); }}><Icon name="monitor" size={16} />On {vendorName(record.twin)}'s screens</a> : null}
579
660
  </div>
661
+ <ol className="ws-writes">{record.writes.map((e) => <li key={e.id}><b>{said(e).what}</b> <span className="mode"><At iso={e.occurredAt} /> · {whoDid(e)}</span></li>)}</ol>
662
+ {history === null ? <p className="mode pad">Reading its history…</p> : rows.length === 0 ? <p className="mode pad">No field holds a new value.</p> : (
663
+ <table className="ws-fields" aria-label="Fields before and after">
664
+ <thead><tr><th>Field</th>{made ? null : <th>Before</th>}<th>{k.tone === 'removed' ? 'After' : 'Now'}</th></tr></thead>
665
+ <tbody>{rows.map((f) => (
666
+ <tr key={f}>
667
+ <th scope="row"><code>{f}</code></th>
668
+ {made ? null : <td className={f in before ? 'was' : 'none'}>{f in before ? <Value v={before[f]} /> : <span className="mode">—</span>}</td>}
669
+ <td className={f in after ? 'now' : 'none'}>{f in after ? <Value v={after[f]} /> : <span className="mode">—</span>}</td>
670
+ </tr>
671
+ ))}</tbody>
672
+ </table>
673
+ )}
580
674
  </div>
581
675
  );
582
676
  }
583
677
 
584
- /** Connect's last line: the setup as a prompt for the person's coding agent (packages/cli/src/agent-prompt.ts, the one
585
- * text), copied whole. */
586
- function CopyAgentPrompt({ text }: { text: string }): React.JSX.Element {
587
- 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')); };
588
- return <button type="button" className="agent-prompt-button" onClick={copy}>Copy a prompt for your coding agent</button>;
678
+ /** The changed records, as GitKraken lists changed files: Path (one flat list, each row marked with its vendor) or Tree
679
+ * (the records under their vendor), chosen above the list and kept in this browser. */
680
+ function RecordList({ records, selected, onPick, label }: { records: ChangedRecord[]; selected?: string; onPick: (key: string) => void; label: string }): React.JSX.Element {
681
+ const [tree, setTree] = useState<boolean>(() => { try { return localStorage.getItem('volter.console.records') === 'tree'; } catch { return false; } });
682
+ const pick = (t: boolean): void => { setTree(t); try { localStorage.setItem('volter.console.records', t ? 'tree' : 'path'); } catch { /* a private window */ } };
683
+ const row = (r: ChangedRecord, nested: boolean): React.JSX.Element => {
684
+ const h = r.writes[r.writes.length - 1]!; const s = said(h); const k = changeKind(r.writes);
685
+ return <li key={r.key}><button type="button" className={[selected === r.key ? 'on' : '', nested ? 'nested' : ''].join(' ').trim() || undefined} aria-pressed={selected === r.key} onClick={() => onPick(r.key)}>{nested ? null : <Mark vendor={r.twin} />}<span className="ws-row-text"><b>{s.label ?? shown(h)}</b><small>{kindOf(h.subject.type)} · {s.what.split(' ').slice(-1)[0]}{r.writes.length > 1 ? ` · ${counted(r.writes.length, 'write')}` : ''}</small></span><span className={`ws-kind ${k.tone}`} title={k.word} aria-label={k.word}><Icon name={k.icon} size={16} /></span></button></li>;
686
+ };
687
+ const vendors = [...new Set(records.map((r) => r.twin))];
688
+ return (
689
+ <>
690
+ <div className="ws-view-switch" role="group" aria-label="List the records by">
691
+ <button type="button" aria-pressed={!tree} className={tree ? undefined : 'on'} onClick={() => pick(false)}>Path</button>
692
+ <button type="button" aria-pressed={tree} className={tree ? 'on' : undefined} onClick={() => pick(true)}>Tree</button>
693
+ </div>
694
+ <ul className="ws-list" aria-label={label}>{tree
695
+ ? vendors.map((v) => {
696
+ const mine = records.filter((r) => r.twin === v);
697
+ return <li key={v} className="ws-group"><p className="ws-group-head"><Mark vendor={v} /><b>{vendorName(v)}</b><span className="mode">{mine.length}</span></p><ul>{mine.map((r) => row(r, true))}</ul></li>;
698
+ })
699
+ : records.map((r) => row(r, false))}</ul>
700
+ </>
701
+ );
589
702
  }
590
703
 
591
- /** "Connect", heading the sidebar as Neon's does: how an app's calls to these vendors land in this World. */
592
- function Connect({ name }: { name: string }): React.JSX.Element {
704
+ /** The branch picker, as GitHub Desktop's Current Branch opens: a filter and New branch, this World's branches with the
705
+ * one served here ticked, then the branches made of it (live, or as of an instant) and when each goes away. */
706
+ function BranchMenu({ name, token, branches, opens, refused, onChanged }: { name: string; token: string; branches: Load<BranchesDoor> | null; opens: (name: string) => boolean; refused: (r: Response) => void; onChanged: () => void }): React.JSX.Element {
707
+ const [filter, setFilter] = useState(''); const [making, setMaking] = useState(false);
708
+ const served = (branches?.state === 'ok' && branches.data.checkedOut) || name.split('/')[1]!;
709
+ const named = branches?.state === 'ok' && branches.data.named?.length ? branches.data.named : [{ name: served, running: true }];
710
+ const copies = branches?.state === 'ok' ? branches.data.branches : [];
711
+ const f = filter.trim().toLowerCase(); const hit = (n: string): boolean => !f || n.toLowerCase().includes(f);
712
+ const close = (e: React.MouseEvent): void => { (e.currentTarget as HTMLElement).closest('details')?.removeAttribute('open'); };
593
713
  return (
594
- <details className="menu connect-side">
595
- <summary className="button connect-button">Connect</summary>
596
- {portal() ? (
597
- // a World a platform opened lives on a host: an app reaches it with a key, a local World pushes to it
598
- <div className="popover">
599
- <p>Point your app's SDKs here, with a key made on the platform; its calls to these vendors land in this World:</p>
600
- <pre>{WORLDS}/{name}/&lt;vendor&gt;</pre>
601
- <a className="button" href={`${portal()}/${encodeURIComponent(name.split('/')[0] ?? '')}`}>Make a key on the platform</a>
602
- <p className="mode">Or push a World from your machine: <code>volter remote add origin {name}</code>, then <code>volter world push</code>.</p>
603
- <CopyAgentPrompt text={agentPrompt({ platform: portal(), world: name })} />
604
- </div>
605
- ) : (
606
- <div className="popover">
607
- <p>Run your app in this World; its calls to these vendors land here:</p>
608
- <pre>volter world run -- npm start</pre>
609
- <p className="mode">Or point an SDK at <code>{WORLDS}/{name}/&lt;vendor&gt;</code> with the token in <code>.volter/token</code>.</p>
610
- <CopyAgentPrompt text={agentPrompt()} />
611
- </div>
612
- )}
714
+ <details className="menu ws-pick">
715
+ <summary title="The branch served here"><Icon name="git-branch" size={16} /><span className="ws-pick-label"><small>Branch</small><b>{served}</b></span><Icon name="chevron-down" size={16} /></summary>
716
+ <div className="popover ws-branch-menu">
717
+ <div className="ws-branch-find"><input type="search" placeholder="Filter" aria-label="Filter branches" value={filter} onChange={(e) => setFilter(e.target.value)} /><button type="button" onClick={(e) => { close(e); setMaking(true); }} disabled={!branches}>New branch</button></div>
718
+ <p className="menu-label">This World</p>
719
+ <ul className="ws-branch-list">{named.filter((b) => hit(b.name)).map((b) => (
720
+ <li key={b.name} className={b.name === served ? 'current' : undefined}><span className="tick">{b.name === served ? <Icon name="check" size={16} /> : <Icon name="git-branch" size={16} />}</span><span className="name">{b.name}</span><span className="mode">{b.name === served ? 'served here' : b.running ? 'running' : 'stopped'}</span></li>
721
+ ))}</ul>
722
+ <p className="menu-label">Branches of it</p>
723
+ {copies.length === 0 ? <p className="mode ws-branch-none">None yet. New branch makes one, from now or as the World was.</p> : (
724
+ <ul className="ws-branch-list">{copies.filter((b) => hit(b.name)).map((b) => {
725
+ const row = <><span className="tick"><Icon name="git-branch" size={16} /></span><span className="name">{shortBranch(name, b.name)}</span><span className="mode">{b.at?.instant ? <>as of <At iso={b.at.instant} time /></> : 'live'}</span></>;
726
+ const goes = b.expiresAt ? `Goes away ${new Date(b.expiresAt).toLocaleString()}` : 'Kept';
727
+ return <li key={b.name}>{opens(b.name) ? <a href={`${BASE}/${b.name}`} title={goes} onClick={(e) => { e.preventDefault(); close(e); goBranch(b.name, b.name.split('/')); }}>{row}</a> : <span title={`${goes}. Its tokens went to whoever made it; it opens from their page.`}>{row}</span>}</li>;
728
+ })}</ul>
729
+ )}
730
+ <div className="ws-branch-foot"><a href={`${BASE}/${name}/~/branches`} onClick={(e) => { e.preventDefault(); close(e); go([...name.split('/'), '~', 'branches']); }}>Manage branches…</a></div>
731
+ </div>
732
+ <NewBranch name={name} token={token} open={making} onClose={() => setMaking(false)} onMade={onChanged} refused={refused} />
613
733
  </details>
614
734
  );
615
735
  }
616
736
 
617
- /** The branch picker, between the World's pages and the branch's (where Neon's sits): this World live, or one of the
618
- * host's branches of it; a snapshot names its parent as the way back. */
619
- function BranchPicker({ name, snapshot, branches, opens }: { name: string; snapshot: Snapshot | null; branches: Load<BranchesDoor> | null; opens: (name: string) => boolean }): React.JSX.Element {
620
- const short = (n: string): string => n.split('/').pop() ?? n;
621
- const live = snapshot?.parent ?? name;
622
- const others = branches?.state === 'ok' ? branches.data.branches.filter((b) => b.name !== name) : [];
737
+ /** Changes | History, as GitHub Desktop heads its left pane: the two tabs of one place, each with its count. */
738
+ function ChangesTabs({ name, view, changes, changesets }: { name: string; view: 'changes' | 'history'; changes: number; changesets: number }): React.JSX.Element {
739
+ const to = (parts: string[]) => (e: React.MouseEvent): void => { e.preventDefault(); go([...name.split('/'), '~', ...parts]); };
740
+ return (
741
+ <nav className="tabs ws-pane-tabs" aria-label="Changes and history">
742
+ <a href={`${BASE}/${name}/~/changes`} className={view === 'changes' ? 'on' : undefined} aria-current={view === 'changes' ? 'page' : undefined} onClick={to(['changes'])}>Changes{changes ? <span className="count">{changes}</span> : null}</a>
743
+ <a href={`${BASE}/${name}/~/changes/changesets`} className={view === 'history' ? 'on' : undefined} aria-current={view === 'history' ? 'page' : undefined} onClick={to(['changes', 'changesets'])}>History{changesets ? <span className="count">{changesets}</span> : null}</a>
744
+ </nav>
745
+ );
746
+ }
747
+
748
+ /** HISTORY, as GitHub Desktop's History tab is: the changesets down the left (what it says, when), newest first; the
749
+ * selected one beside them, headed by its message and what it changed (GitHub Desktop's "6 changed files +148 −107"),
750
+ * then its records beside the selected one's fields before and after. Review (where it went, the checks, the approvals,
751
+ * and Verify, Approve and Deploy) is one button in that head, a window over it: GitHub Desktop has no review, so this
752
+ * one is ours. */
753
+ function HistoryView({ name, token, sets, sub, signsAs, canWrite, rooted, refused, onChanged, changes, mirrored, required }: { name: string; token: string; sets: Load<{ changesets: ChangesetView[] }>; sub?: string; /** records not in a changeset, the Changes tab's count */ changes: number; /** the vendors with screens */ mirrored: string[]; /** approvals this World requires before a changeset deploys */ required: number; signsAs: string | null; canWrite: boolean; rooted: string[]; refused: (r: Response) => void; onChanged: () => void }): React.JSX.Element {
754
+ const [picked, setPicked] = useState<string | null>(null); const [reviewing, setReviewing] = useState(false);
755
+ const list = sets.state === 'ok' ? [...sets.data.changesets].sort((a, b) => b.createdAt.localeCompare(a.createdAt)) : [];
756
+ const c = list.find((x) => x.name === sub) ?? list[0];
757
+ const records = useMemo((): ChangedRecord[] => {
758
+ const by = new Map<string, ChangedRecord>();
759
+ for (const x of c ? changesetChanges(c) : []) { const key = `${x.twin}:${x.entry.subject.type}:${x.entry.subject.id}`; const r = by.get(key) ?? { key, twin: x.twin, writes: [] }; r.writes.push(x.entry); by.set(key, r); }
760
+ return [...by.values()];
761
+ }, [c]);
762
+ const record = records.find((r) => r.key === picked) ?? records[0];
763
+ const log = useDoor<{ entries: Entry[] }>(token, record ? `/-/${name}/log/${record.twin}` : null);
764
+ const tally = { added: 0, removed: 0, changed: 0 } as Record<string, number>; for (const r of records) tally[changeKind(r.writes).tone] = (tally[changeKind(r.writes).tone] ?? 0) + 1;
765
+ const sent = c ? sentOf(c) : null; const review = c ? reviewOf(c, required) : null;
623
766
  return (
624
- <details className="menu branch-picker">
625
- <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) : (branches?.state === 'ok' && branches.data.checkedOut) || short(live)}</span><span className="badge">{snapshot ? 'snapshot' : 'live'}</span></summary>
626
- <div className="popover">
627
- <p className="mode">Branch</p>
628
- <ul className="branch-list">
629
- <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>
630
- {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}
631
- {others.map((b) => (
632
- <li key={b.name}>{opens(b.name)
633
- ? <a href={`${BASE}/${b.name}`} onClick={(e) => { e.preventDefault(); goBranch(b.name, b.name.split('/')); }}>{short(b.name)} <span className="mode">{b.at?.instant ? <>as of <At iso={b.at.instant} /></> : 'copy'}</span></a>
634
- : <span className="mode" title="Its token was handed to whoever made it">{short(b.name)}</span>}</li>
635
- ))}
636
- </ul>
637
- {branches?.state === 'ok' && (branches.data.named?.length ?? 0) > 1 ? (
767
+ <div className="ws-two">
768
+ <div className="ws-files">
769
+ <ChangesTabs name={name} view="history" changes={changes} changesets={list.length} />
770
+ <Pending load={sets} what="the changesets" lines={4} />
771
+ <ul className="ws-list ws-commits" aria-label="Changesets">{list.map((x) => {
772
+ const on = c?.name === x.name; const notSent = sentOf(x).label === 'Not sent';
773
+ return (
774
+ <li key={x.name}><a href={`${BASE}/${name}/~/changes/changesets/${x.name}`} className={on ? 'on' : undefined} aria-current={on ? 'true' : undefined} onClick={(e) => { e.preventDefault(); setPicked(null); go([...name.split('/'), '~', 'changes', 'changesets', x.name]); }}>
775
+ <span className="ws-row-text"><b>{titleOf(x)}</b><small>{x.owner ?? (x.cutBy ? callerName(x.cutBy) : 'unrecorded')} • <At iso={x.createdAt} /></small></span>
776
+ {notSent ? <span className="ws-unpushed" title="Not pushed to the remote yet"><Icon name="arrow-up-from-line" size={16} /></span> : null}
777
+ </a></li>
778
+ );
779
+ })}</ul>
780
+ </div>
781
+ <div className="ws-pane-right">
782
+ {sets.state === 'ok' && !c ? <Empty title="No changesets yet"><p>Cut one from Changes: the cut box is at its foot.</p></Empty> : null}
783
+ {c && sent && review ? (
638
784
  <>
639
- <p className="mode">This World's branches</p>
640
- <ul className="branch-list">
641
- {branches.data.named!.map((b) => (
642
- <li key={b.name}><span aria-current={b.name === branches.data.checkedOut ? 'true' : undefined}>{b.name} <span className="mode">{b.name === branches.data.checkedOut ? 'served here' : <code>volter world checkout {b.name}</code>}</span></span></li>
643
- ))}
644
- </ul>
785
+ <div className="ws-commit-head">
786
+ <div className="ws-commit-title">
787
+ <h3>{titleOf(c)}</h3>
788
+ <p className="ws-commit-stat"><Icon name="file-diff" size={16} /> {counted(records.length, 'changed record')}{tally.added ? <span className="plus"> +{tally.added}</span> : null}{tally.removed ? <span className="minus"> −{tally.removed}</span> : null}{tally.changed ? <span className="dot"> •{tally.changed}</span> : null}</p>
789
+ <p className="mode ws-commit-meta"><code>{c.name}</code> · <At iso={c.createdAt} /></p>
790
+ <p className="ws-commit-who">{[['Owner', c.owner ?? (c.cutBy && /^(token|token:read)$|^key:(?!.* for:)/.test(c.cutBy) ? 'no person: a service’s' : 'unrecorded')], ['Cut by', c.cutBy ? (c.pushedBy && c.applied === null && c.cutBy === 'machine' ? 'the machine it was cut on' : callerName(c.cutBy)) : 'unrecorded'], ...(c.pushedBy ? [['Pushed by', callerName(c.pushedBy)]] : [])].map(([k, v]) => <span key={k}><span className="mode">{k}</span> {v}</span>)}</p>
791
+ {/* a copy that arrived by a push carries no push outcome of its own; the pushing World's copy does */}
792
+ {c.pushedBy ? <p className="mode ws-commit-meta">{c.applied === null ? "Owner and cut by are as the World it was cut in recorded them; pushed by is this World's own record." : 'Owner and cut by are this World’s record; pushed by is as the remote recorded it.'}</p> : null}
793
+ </div>
794
+ <button type="button" onClick={() => setReviewing(true)} title="Where it went, the checks, the approvals; Verify, Approve and Deploy"><span className={`badge cs-${sent.tone}`}>{sent.label}</span><span className={`badge cs-${review.tone}`}>{review.label}</span>Review…</button>
795
+ </div>
796
+ <div className="ws-two inner">
797
+ <div className="ws-files"><RecordList records={records} {...(record ? { selected: record.key } : {})} onPick={setPicked} label="Records it changed" /></div>
798
+ <div className="ws-detail">{record ? <RecordDiff record={record} history={log.state === 'ok' ? log.data.entries : log.state === 'error' ? [] : null} {...(mirrored.includes(record.twin) ? { screens: name } : {})} /> : <p className="mode pad">It changes nothing.</p>}</div>
799
+ </div>
800
+ <Dialog open={reviewing} onClose={() => setReviewing(false)} title={`Review: ${titleOf(c)}`} wide>
801
+ <ChangesetPage key={`h:${c.name}`} name={name} token={token} signsAs={signsAs} c={c} canWrite={canWrite} rooted={rooted} refused={refused} onChanged={onChanged} part="head" required={required} />
802
+ <ChangesetPage key={`r:${c.name}`} name={name} token={token} signsAs={signsAs} c={c} canWrite={canWrite} rooted={rooted} refused={refused} onChanged={onChanged} part="review" required={required} />
803
+ </Dialog>
645
804
  </>
646
805
  ) : null}
647
- {branches?.state === 'ok' && others.length === 0 && !snapshot && (branches.data.named?.length ?? 0) <= 1 ? <p className="mode">No branches yet. “View an earlier moment…” opens one.</p> : null}
648
806
  </div>
649
- </details>
807
+ </div>
650
808
  );
651
809
  }
652
810
 
653
- /** THE BRANCH'S OVERVIEW, as Neon's branch Overview is: its services in a table (each vendor with its state and what it
654
- * holds), then its monitoring: here the writes the World recorded, hour by hour over the last day, by vendor. */
655
- function BranchOverview({ name, token, map, vendors, tick }: { name: string; token: string; map: Load<WorldMap>; vendors: string[]; tick: number }): React.JSX.Element {
656
- const recent = useDoor<Timeline>(token, `/-/${name}/timeline?limit=500`, Math.floor(tick / 3));
657
- const rows = map.state === 'ok' ? map.data.twins : [];
658
- const open = (parts: string[]) => (e: React.MouseEvent): void => { e.preventDefault(); go([...name.split('/'), ...parts]); };
659
- // 24 hourly buckets ending now, each counting the entries that happened in it, per vendor
660
- const now = Date.now(); const hours = 24; const bucket = 3_600_000;
661
- const entries = recent.state === 'ok' ? recent.data.entries : [];
662
- const series = vendors.map((v) => {
663
- const counts = new Array<number>(hours).fill(0);
664
- 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; }
665
- return { vendor: v, counts };
666
- });
667
- const totals = new Array<number>(hours).fill(0).map((_, i) => series.reduce((n, s) => n + s.counts[i]!, 0));
668
- const peak = Math.max(1, ...totals);
669
- const W = 720; const H = 160; const bw = W / hours;
670
- const hues = ['var(--volter-data-base)', '#7c5cff', '#e3572b', '#1f9d6b', '#d9a400', '#2b7de3', '#c2417e', '#6b7280', '#0ea5a4'];
811
+ /** EVENTS, as Proxyman lists traffic: the vendors as tabs over a dense table of every write, newest first (when, vendor,
812
+ * what, the record, who, its receipt); the selected one under it, the request it came from beside what it wrote. */
813
+ function EventsView({ name, token, vendors, vendor, tick }: { name: string; token: string; vendors: string[]; vendor?: string; tick: number }): React.JSX.Element {
814
+ const [limit, setLimit] = useState(PAGE * 4); const [op, setOp] = useState(''); const [picked, setPicked] = useState<string | null>(null);
815
+ const load = useDoor<Timeline>(token, `/-/${name}/timeline?limit=${limit}${vendor ? `&twin=${encodeURIComponent(vendor)}` : ''}`, Math.floor(tick / 2));
816
+ const all = load.state === 'ok' ? foldLanded(load.data.entries) : [];
817
+ const ops = [...new Set(all.map((p) => said(p.entry).what))].sort();
818
+ const rows = all.filter((p) => !op || said(p.entry).what === op);
819
+ const sel = rows.find((p) => keyOf(p) === picked) ?? rows[0];
820
+ const to = (v?: string) => (e: React.MouseEvent): void => { e.preventDefault(); setPicked(null); go([...name.split('/'), '~', 'events', ...(v ? [v] : [])]); };
821
+ const body = sel?.entry.input && typeof sel.entry.input === 'object' ? (sel.entry.input as { body?: unknown }).body : undefined;
822
+ const t = sel ? traceOf(sel.entry.traceparent) : null; const settled = sel ? settledOf(sel) : null; const usage = sel ? usageOf(sel.entry) : [];
671
823
  return (
672
- <>
673
- <div className="block">
674
- <table className="dense services" aria-label="Vendors on this branch">
675
- <thead><tr><th>Vendor</th><th>Details</th><th /></tr></thead>
676
- <tbody>{vendors.map((v) => {
677
- const t = rows.find((x) => x.twin === v);
678
- const top = [...(t?.resources ?? [])].filter((r) => r.count > 0).sort((a, b) => b.count - a.count);
824
+ <div className="ws-events">
825
+ <div className="ws-filterbar">
826
+ <nav className="ws-seg" aria-label="Vendor">
827
+ <a href={`${BASE}/${name}/~/events`} className={!vendor ? 'on' : undefined} aria-current={!vendor ? 'page' : undefined} onClick={to()}>All</a>
828
+ {vendors.map((v) => <a key={v} href={`${BASE}/${name}/~/events/${v}`} className={vendor === v ? 'on' : undefined} aria-current={vendor === v ? 'page' : undefined} onClick={to(v)}><Mark vendor={v} />{vendorName(v)}</a>)}
829
+ </nav>
830
+ <select aria-label="Operation" value={op} onChange={(e) => { setOp(e.target.value); setPicked(null); }}><option value="">Every operation</option>{ops.map((o) => <option key={o} value={o}>{o}</option>)}</select>
831
+ </div>
832
+ <div className="ws-grid-wrap">
833
+ <Pending load={load} what="the events" lines={6} />
834
+ <table className="ws-grid" aria-label="Events">
835
+ <thead><tr><th>Time</th><th>Vendor</th><th>What</th><th>Record</th><th>By</th><th>Receipt</th></tr></thead>
836
+ <tbody>{rows.map((p) => {
837
+ const s = said(p.entry); const st = settledOf(p); const on = sel && keyOf(p) === keyOf(sel);
679
838
  return (
680
- <tr key={v}>
681
- <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>
682
- <td className="mode">{top.length ? top.slice(0, 4).map((r) => counted(r.count, r.type)).join(' · ') : 'nothing yet'}</td>
683
- <td className="num">{t?.mirror ? <a href={`${BASE}/${name}/~/vendors/${v}`} onClick={open(['~', 'vendors', v])}>Screens</a> : null}</td>
839
+ <tr key={keyOf(p)} className={on ? 'on' : undefined} aria-selected={on} onClick={() => setPicked(keyOf(p))}>
840
+ <td className="mono"><At iso={p.entry.occurredAt} time /></td>
841
+ <td><span className="vendor-cell"><Mark vendor={p.twin} />{vendorName(p.twin)}</span></td>
842
+ <td><b>{s.what}</b></td>
843
+ <td className="ws-cell-clip">{s.label ? `“${s.label}”` : <code>{shown(p.entry)}</code>}</td>
844
+ <td className="mode">{whoDid(p.entry, p.caller)}</td>
845
+ <td>{st ? <span className={st.bad ? 'badge cs-bad' : st.status === 'deployed' ? 'badge cs-done' : 'badge'}>{st.status}</span> : <span className="mode">{standing(p.entry)}</span>}</td>
684
846
  </tr>
685
847
  );
686
848
  })}</tbody>
687
849
  </table>
688
850
  </div>
689
- <div className="block chart-block">
690
- <div className="block-head"><h3>Activity, last 24 hours</h3><a href={`${BASE}/${name}/~/activity`} onClick={open(['~', 'activity'])}>All activity</a></div>
691
- <Pending load={recent} what="the activity" />
692
- {recent.state === 'ok' ? (
693
- <div className="pad">
694
- <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`}>
695
- {[0.5, 1].map((f) => <line key={f} x1={0} x2={W} y1={H - f * H} y2={H - f * H} className="grid" />)}
696
- <line x1={0} x2={W} y1={H} y2={H} className="axis" />
697
- {totals.map((_, i) => {
698
- let y = H;
699
- 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>;
700
- })}
701
- {[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>)}
702
- </svg>
703
- <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>
704
- {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}
705
- </div>
851
+ <div className="ws-event-detail">
852
+ {sel ? (
853
+ <>
854
+ <div className="ws-event-line"><span className="badge">{opOf(sel.entry)}</span>{settled ? <span className={settled.bad ? 'badge cs-bad' : 'badge cs-good'}>{settled.status}</span> : null}<code>{sel.entry.subject.type}:{shown(sel.entry)}</code><span className="mode"><At iso={sel.entry.occurredAt} /> · {whoDid(sel.entry, sel.caller)}</span>{t ? <a href={`${BASE}/${name}/~/trace/${t.trace}`} onClick={(e) => { e.preventDefault(); go([...name.split('/'), '~', 'trace', t.trace]); }}>Its trace →</a> : null}</div>
855
+ <div className="ws-event-panes">
856
+ <div role="region" aria-label="The request"><h4>Request</h4>{body !== undefined ? <RequestBody body={body} /> : <p className="mode">No request body: the World or the vendor wrote it.</p>}{usage.length ? <><h4>Usage</h4><dl className="fields">{usage.map(([k, v]) => <React.Fragment key={k}><dt>{k}</dt><dd>{v}</dd></React.Fragment>)}</dl></> : null}</div>
857
+ <div role="region" aria-label="What it wrote"><h4>Written</h4><Fields fields={sel.entry.fields} /><h4>{sel.copies.length ? 'Receipts' : 'Receipt'}</h4><p className="mode">{sel.copies.length ? sel.copies.map((c) => c.entry.receipt?.status ?? 'landed copy').join(' · ') : receiptOf(sel.entry)}</p></div>
858
+ </div>
859
+ </>
860
+ ) : <p className="mode pad">Nothing written here yet.</p>}
861
+ </div>
862
+ <div className="ws-foot mode"><span>{sel ? `${rows.indexOf(sel) + 1} of ${rows.length}` : rows.length} events{vendor ? ` at ${vendorName(vendor)}` : ''}</span>{load.state === 'ok' && load.data.next ? <button type="button" onClick={() => setLimit((l) => l + PAGE * 4)}>Load older</button> : null}</div>
863
+ </div>
864
+ );
865
+ }
866
+
867
+ /** A TRACE, as Jaeger opens one: what it is and how long it took over every write it made, each a row (the vendor and
868
+ * what it did) with its moment on the trace's own timeline; the selected one opens under its row. */
869
+ function TraceView({ name, token, id }: { name: string; token: string; id: string }): React.JSX.Element {
870
+ const valid = /^[0-9a-f]{32}$/.test(id);
871
+ const load = useDoor<Timeline>(token, valid ? `/-/${name}/timeline?trace=${id}&limit=500` : null);
872
+ const steps = load.state === 'ok' ? [...load.data.entries].sort((a, b) => -newestFirst(a, b)) : [];
873
+ const [open, setOpen] = useState<string | null>(null);
874
+ const t0 = steps[0] ? Date.parse(steps[0].entry.occurredAt) : 0;
875
+ const span = steps.length ? Math.max(1, Date.parse(steps[steps.length - 1]!.entry.occurredAt) - t0) : 1;
876
+ const lanes = [...new Set(steps.map((s) => s.twin))];
877
+ return (
878
+ <div className="ws-sheet ws-trace">
879
+ <p className="crumbs-line"><a href={`${BASE}/${name}/~/events`} onClick={(e) => { e.preventDefault(); go([...name.split('/'), '~', 'events']); }}><Icon name="arrow-left" size={16} /> Events</a></p>
880
+ <h2>{steps[0] ? `${said(steps[0].entry).what} …` : 'Trace'} <code className="mode">{id.slice(0, 12)}</code></h2>
881
+ {!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" />}
882
+ {steps.length ? <p className="ws-trace-meta">Started <b><At iso={steps[0]!.entry.occurredAt} /></b> · Took <b>{span} ms</b> · Vendors <b>{lanes.map(vendorName).join(', ')}</b> · Writes <b>{steps.length}</b></p> : null}
883
+ {load.state === 'ok' && steps.length === 0 ? <Empty title="Nothing in this World carries this trace"><p>An app instrumented with OpenTelemetry sends a <code>traceparent</code> with each call, and every write that call makes keeps it, so one click reads across every vendor.</p></Empty> : null}
884
+ {steps.length ? (
885
+ <div className="ws-spans" role="table" aria-label="The trace's writes">
886
+ <div className="ws-span head" role="row"><span role="columnheader">Vendor and what</span><span role="columnheader" className="ws-ticks"><span>0 ms</span><span>{Math.round(span / 2)} ms</span><span>{span} ms</span></span></div>
887
+ {steps.map((x) => {
888
+ const ms = Date.parse(x.entry.occurredAt) - t0; const k = keyOf(x); const s = said(x.entry);
889
+ return (
890
+ <React.Fragment key={k}>
891
+ <div className={open === k ? 'ws-span on' : 'ws-span'} role="row" onClick={() => setOpen(open === k ? null : k)}>
892
+ <span role="cell" className="ws-span-name"><Mark vendor={x.twin} /><b>{vendorName(x.twin)}</b> <span className="mode">{s.what}{s.label ? ` “${s.label}”` : ''}</span></span>
893
+ <span role="cell" className="ws-span-track"><span className="ws-span-dot" style={{ left: `${(ms / span) * 100}%` }} /><span className="ws-span-at mode" style={{ left: `${(ms / span) * 100}%` }}>+{ms} ms</span></span>
894
+ </div>
895
+ {open === k ? <div className="ws-span-detail"><p className="mode"><code>{opOf(x.entry)}</code> on <code>{x.entry.subject.type}:{shown(x.entry)}</code> · {whoDid(x.entry, x.caller)} · {standing(x.entry)}</p><Fields fields={x.entry.fields} /></div> : null}
896
+ </React.Fragment>
897
+ );
898
+ })}
899
+ </div>
900
+ ) : null}
901
+ </div>
902
+ );
903
+ }
904
+
905
+ /** A VENDOR'S PLACE, as TablePlus opens a database: what it stores by type down the left, the chosen type's records as
906
+ * rows, the selected record's fields at the right; its own screens and its events a click away. */
907
+ function VendorView({ name, token, vendor, mirrored, tick }: { name: string; token: string; vendor: string; mirrored: boolean; tick: number }): React.JSX.Element {
908
+ const tree = useDoor<{ tree: Array<Record<string, unknown> & { type: string; id: string }> }>(token, `/-/${name}/checkpoint/${vendor}`, Math.floor(tick / 6));
909
+ const rows = tree.state === 'ok' ? tree.data.tree.filter((r) => r.deleted !== true) : [];
910
+ const types = [...new Set(rows.map((r) => r.type))].map((t) => ({ type: t, count: rows.filter((r) => r.type === t).length })).sort((a, b) => Number(a.type.startsWith('_')) - Number(b.type.startsWith('_')) || kindOf(a.type).localeCompare(kindOf(b.type)));
911
+ const [type, setType] = useState<string | null>(null); const [picked, setPicked] = useState<string | null>(null);
912
+ const shownType = types.some((t) => t.type === type) ? type! : types[0]?.type;
913
+ const mine = rows.filter((r) => r.type === shownType);
914
+ const sel = mine.find((r) => r.id === picked) ?? mine[0];
915
+ // the columns a person reads a row by: its id, the first of its naming fields, a few short ones, and when it changed
916
+ const NAMING = ['name', 'email', 'title', 'text', 'description', 'subject', 'amount', 'percent_off', 'unit_amount', 'currency', 'status', 'channel', 'customer', 'product'];
917
+ const SKIP = new Set(['id', 'type', 'object', 'livemode', 'created', 'updated', 'updatedAt', 'deleted']);
918
+ const scalar = (k: string): boolean => !SKIP.has(k) && mine.some((r) => r[k] !== undefined && r[k] !== null && typeof r[k] !== 'object');
919
+ const keys = [...new Set(mine.flatMap((r) => Object.keys(r)))];
920
+ const cols = [...NAMING.filter(scalar), ...keys.filter((k) => !NAMING.includes(k) && scalar(k))].slice(0, 4);
921
+ return (
922
+ <div className="ws-table-app">
923
+ <nav className="ws-tables" aria-label={`${vendorName(vendor)}'s records`}>
924
+ <div className="ws-list-head"><span className="vendor-cell"><Mark vendor={vendor} />{vendorName(vendor)}</span></div>
925
+ <Pending load={tree} what="what it stores" lines={4} />
926
+ <p className="menu-label">Stores</p>
927
+ {types.map((t) => <a key={t.type} href={`#${t.type}`} className={shownType === t.type ? 'on' : undefined} aria-current={shownType === t.type ? 'true' : undefined} onClick={(e) => { e.preventDefault(); setType(t.type); setPicked(null); }}><Icon name="table-2" size={16} /><span>{kindOf(t.type)}</span><span className="count">{t.count}</span></a>)}
928
+ <p className="menu-label">Also</p>
929
+ <a href={`${BASE}/${name}/~/events/${vendor}`} onClick={(e) => { e.preventDefault(); go([...name.split('/'), '~', 'events', vendor]); }}><Icon name="activity" size={16} /><span>Its events</span></a>
930
+ {mirrored ? <a href={`${BASE}/${name}/~/screens/${vendor}`} onClick={(e) => { e.preventDefault(); go([...name.split('/'), '~', 'screens', vendor]); }}><Icon name="monitor" size={16} /><span>Its screens</span></a> : null}
931
+ </nav>
932
+ <div className="ws-grid-wrap">
933
+ {tree.state === 'ok' && types.length === 0 ? <Empty title={`${vendorName(vendor)} stores nothing yet`}><p>What your app makes there shows here, by type.</p></Empty> : null}
934
+ {mine.length ? (
935
+ <table className="ws-grid" aria-label={`${kindOf(shownType ?? '')} records`}>
936
+ <thead><tr><th>id</th>{cols.map((c) => <th key={c}>{c}</th>)}<th>updated</th></tr></thead>
937
+ <tbody>{mine.map((r) => (
938
+ <tr key={r.id} className={sel?.id === r.id ? 'on' : undefined} aria-selected={sel?.id === r.id} onClick={() => setPicked(r.id)}>
939
+ <td><code>{r.id}</code></td>
940
+ {cols.map((c) => <td key={c} className="ws-cell-clip">{r[c] === undefined || r[c] === null ? <span className="mode">—</span> : String(r[c])}</td>)}
941
+ <td className="mode">{typeof r.updatedAt === 'string' ? <At iso={r.updatedAt} /> : '—'}</td>
942
+ </tr>
943
+ ))}</tbody>
944
+ </table>
706
945
  ) : null}
707
946
  </div>
947
+ <aside className="ws-record" aria-label="The selected record">
948
+ {sel ? (
949
+ <>
950
+ <div className="ws-list-head"><span>{kindOf(sel.type)}</span><code>{sel.id}</code></div>
951
+ <dl className="ws-record-fields">{Object.entries(sel).filter(([k]) => k !== 'type' && k !== 'id').map(([k, v]) => <React.Fragment key={k}><dt>{k}</dt><dd><Value v={v} /></dd></React.Fragment>)}</dl>
952
+ </>
953
+ ) : null}
954
+ </aside>
955
+ <div className="ws-foot mode">{sel ? `${mine.indexOf(sel) + 1} of ${counted(mine.length, kindOf(shownType ?? 'record'))}` : ''}</div>
956
+ </div>
957
+ );
958
+ }
959
+
960
+ /** A VENDOR'S SCREENS: its own site over this World's state (the twin's mirror), the whole pane; what it holds and the
961
+ * way to its records or a tab of its own above it. */
962
+ function ScreenView({ name, vendor, holding, session, retry, show }: { name: string; vendor: string; holding: string; session: MirrorSession; retry: () => void; /** a record to open the screens on (`<type>:<id>`): the vendor's screens find where they show it */ show?: string }): React.JSX.Element {
963
+ return (
964
+ <div className="ws-screen">
965
+ <div className="ws-screen-bar">
966
+ <span className="vendor-cell"><Mark vendor={vendor} /><b>{vendorName(vendor)}</b></span>
967
+ <span className="mode">{holding}</span>
968
+ <a href={`${BASE}/${name}/~/vendors/${vendor}`} onClick={(e) => { e.preventDefault(); go([...name.split('/'), '~', 'vendors', vendor]); }}><Icon name="table-2" size={16} />Its records</a>
969
+ <a href={mirrorUrl(name, vendor)} target="_blank" rel="noopener noreferrer"><Icon name="external-link" size={16} />New tab</a>
970
+ </div>
971
+ {session.state === 'ok' && session.scope === 'read' ? <p className="mode ws-screens-note">You are looking read only: the screens browse, and refuse changes.</p> : null}
972
+ {session.state === 'pending' ? <p className="mode ws-screens-note" role="status">Opening…</p> : null}
973
+ {session.state === 'error' ? <p role="alert" className="ws-screens-note">The World did not open its screens: {session.error} <button type="button" onClick={retry}>Try again</button></p> : null}
974
+ {session.state === 'ok' ? <iframe key={`${vendor}:${show ?? ''}`} title={`${vendorName(vendor)}, as this World shows it`} src={`${mirrorUrl(name, vendor)}${show ? `?show=${encodeURIComponent(show)}` : ''}`} /> : null}
975
+ </div>
976
+ );
977
+ }
978
+
979
+ /** CHANGESETS, as GitHub Desktop's Repository Settings › Git Config says how a repository commits: whom a changeset cut
980
+ * here is recorded as, and what makes one ready to deploy. Each rule is the World's as it stands; where one is set
981
+ * elsewhere, the pane says where. */
982
+ function ChangesetsPane({ name, token, who, scope }: { name: string; token: string; who: { local: boolean; person: string | null; admin: boolean }; scope: Scope }): React.JSX.Element {
983
+ const [version, setVersion] = useState(0);
984
+ const twins = useDoor<{ twins: Twin[] }>(token, `/-/${name}/twins`);
985
+ const rules = useDoor<{ changesets: ChangesetRules }>(token, `/-/${name}/rules`, version);
986
+ const held = rules.state === 'ok' ? rules.data.changesets : null;
987
+ const [approvals, setApprovals] = useState<number | null>(null); const [summary, setSummary] = useState<'required' | 'generated' | null>(null);
988
+ const [busy, setBusy] = useState(false); const [log, setLog] = useState<Said[]>([]);
989
+ const nowApprovals = approvals ?? held?.approvals ?? 1; const nowSummary = summary ?? held?.summary ?? 'required';
990
+ const changed = held !== null && (nowApprovals !== (held.approvals ?? 1) || nowSummary !== (held.summary ?? 'required'));
991
+ const say = (ok: boolean, text: string): void => { setLog((l) => [{ ok, text, at: new Date().toISOString(), n: ++saidCount }, ...l].slice(0, 3)); if (!ok) toast(text, 'error'); };
992
+ const save = async (e: React.FormEvent): Promise<void> => {
993
+ e.preventDefault(); setBusy(true);
994
+ try {
995
+ const r = await door(token, `/-/${name}/rules`, { method: 'PUT', body: JSON.stringify({ changesets: { approvals: nowApprovals, summary: nowSummary } }) });
996
+ if (!r.ok) { say(false, `Save: ${await failure(r)}`); return; }
997
+ say(true, `Saved: ${nowApprovals === 0 ? 'no approval' : counted(nowApprovals, 'approval')} before a deploy; a summary ${nowSummary === 'required' ? 'required' : 'generated when none is given'}.`);
998
+ setApprovals(null); setSummary(null); setVersion((v) => v + 1);
999
+ } finally { setBusy(false); }
1000
+ };
1001
+ const POLICY: Record<string, string> = { gated: 'a person deploys an approved changeset', auto: 'a push deploys at once', hold: 'nothing deploys' };
1002
+ const as = who.person ? <><b>{who.person}</b>{who.local ? ', signed in on this machine' : ''}</> : who.local ? <><b>this machine</b>: no one is signed in here, so no person owns them until they are pushed by one</> : who.admin ? <><b>the host's admin</b></> : <><b>this token</b>, a service</>;
1003
+ const rooted = twins.state === 'ok' ? twins.data.twins.filter((t) => t.root?.url) : [];
1004
+ const off = scope !== 'write' ? 'This session reads the World; it changes no rules' : null;
1005
+ return (
1006
+ <>
1007
+ <h3 className="settings-head">Changesets</h3>
1008
+ <p className="mode">This World's rules, kept with it and shared by everyone who works in it. A person or the World's token changes them; a key never does, so an agent cannot relax the review its own changes go through.</p>
1009
+ <Pending load={rules} what="the rules" lines={2} />
1010
+ <form className="ws-rules" onSubmit={(e) => void save(e)} aria-label="Changeset rules">
1011
+ <div className="ws-rule">
1012
+ <h4>Recorded as</h4>
1013
+ <p>What you cut here is recorded as {as}. An agent's or an app's key cuts as itself, for the person it was made for; a World's token cuts as a service.</p>
1014
+ </div>
1015
+ <div className="ws-rule">
1016
+ <h4>A cut's summary</h4>
1017
+ <fieldset className="choice" disabled={Boolean(off) || held === null}>
1018
+ <label className="check"><input type="radio" name="summary" checked={nowSummary === 'required'} onChange={() => setSummary('required')} /> <span>Required: whoever cuts says what it does and why</span></label>
1019
+ <label className="check"><input type="radio" name="summary" checked={nowSummary === 'generated'} onChange={() => setSummary('generated')} /> <span>Generated from its changes when none is given</span></label>
1020
+ </fieldset>
1021
+ <p className="mode">Either way a cut takes every change not yet in a changeset: there is no staging.</p>
1022
+ </div>
1023
+ <div className="ws-rule">
1024
+ <h4>Ready to deploy</h4>
1025
+ <ul className="ws-rule-list"><li><Icon name="check" size={16} />Verified: replayed in a throwaway World, the World's checks passing</li></ul>
1026
+ <label className="ws-rule-row"><span>Approved by</span>
1027
+ <select value={nowApprovals} onChange={(e) => setApprovals(Number(e.target.value))} disabled={Boolean(off) || held === null} aria-label="Approvals required">
1028
+ {[0, 1, 2, 3, 4, 5].map((n) => <option key={n} value={n}>{n === 0 ? 'no one: verified is enough' : n === 1 ? '1 person' : `${n} different people`}</option>)}
1029
+ </select>
1030
+ <span className="mode">for its contents as they stand</span>
1031
+ </label>
1032
+ </div>
1033
+ <div className="ws-rule">
1034
+ <h4>Deploys to</h4>
1035
+ {twins.state === 'ok' && rooted.length === 0 ? <p className="mode">No vendor here is connected to its real service, so nothing deploys from this World.</p> : null}
1036
+ {rooted.length ? <ul className="ws-rule-list">{rooted.map((t) => <li key={t.twin}><Mark vendor={t.twin} />{vendorName(t.twin)}: {POLICY[(t.root as { deploy?: string }).deploy ?? 'gated'] ?? 'gated'}</li>)}</ul> : null}
1037
+ <p className="mode"><a href={`${BASE}/${name}/~/settings/services`} onClick={(e) => { e.preventDefault(); go([...name.split('/'), '~', 'settings', 'services']); }}>Real services</a> sets each vendor's deploy policy.</p>
1038
+ </div>
1039
+ <Narration log={log} name={name} />
1040
+ <div className="row-actions">
1041
+ <button type="submit" className="primary" disabled={Boolean(off) || busy || !changed} title={off ?? undefined}>{busy ? 'Saving…' : 'Save'}</button>
1042
+ {changed ? <button type="button" onClick={() => { setApprovals(null); setSummary(null); }}>Cancel</button> : null}
1043
+ {off ? <span className="mode">{off}.</span> : null}
1044
+ </div>
1045
+ </form>
708
1046
  </>
709
1047
  );
710
1048
  }
711
1049
 
712
- /** THE DASHBOARD, as the references open a project: Vercel leads with the live deployment (its preview, its address),
713
- * Neon with how to connect and the branches. A World's live preview is its vendors' own screens; beside them, how an
714
- * app connects and the branches; under them, what just happened. Counts sit with what they count, not in a row. */
715
- const THUMB_SCALE = 0.2;
716
- 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<BranchesDoor> | null; canWrite: boolean; tick: number; /** the World this one is a snapshot of, when it is one */ snapshotOf?: string }): React.JSX.Element {
717
- const diff = useDoor<Diff>(token, `/-/${name}/diff`, Math.floor(tick / 2));
718
- const recent = useDoor<Timeline>(token, `/-/${name}/timeline?limit=8`, tick);
719
- const rows = map.state === 'ok' ? map.data.twins : [];
720
- const latest = recent.state === 'ok' ? recent.data.entries : [];
721
- const screens = twins.state === 'ok' ? twins.data.twins.filter((t) => t.mirror).map((t) => t.twin) : [];
722
- const open = (parts: string[]) => (e: React.MouseEvent): void => { e.preventDefault(); go([...name.split('/'), ...parts]); };
723
- 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'; };
724
- const changes = diff.state === 'ok' ? diff.data.changes.length : null;
725
- const list = branches?.state === 'ok' ? branches.data.branches : [];
1050
+ /** ACCOUNT, as GitHub Desktop's Settings › Accounts is: who this page is for (this machine, a person the platform signed in,
1051
+ * the host's admin, a link or key, or a snapshot's reader), what it may do here, and the way out where there is one. */
1052
+ function AccountPane({ who, scope, snapshot, portal: platform, onSignOut }: { who: { local: boolean; person: string | null; admin: boolean }; scope: Scope; snapshot: Snapshot | null; portal: string; onSignOut: () => void }): React.JSX.Element {
1053
+ const access = scope === 'write' ? 'Reads and changes this World' : scope === 'read' ? 'Reads this World; changes nothing' : '…';
1054
+ const [title, line] = snapshot ? ['A snapshot', `of ${snapshot.parent}, read only`]
1055
+ : who.local && who.person ? [who.person, 'Signed in on this machine: this World is served here, and acts as you']
1056
+ : who.local ? ['This machine', 'Served here, with no one signed in: what you do is recorded as this machine']
1057
+ : who.person ? [who.person, platform ? `Signed in through ${new URL(platform).host}` : 'Signed in']
1058
+ : who.admin ? ["The host's admin", 'Opened with the host admin token: every World it serves']
1059
+ : ['A link or key', 'Opened with a token for this World'];
1060
+ return (
1061
+ <>
1062
+ <h3 className="settings-head">Account</h3>
1063
+ <div className="ws-account">
1064
+ <span className="ws-account-face" aria-hidden="true"><Icon name={who.person ? 'user' : who.local ? 'monitor' : snapshot ? 'clock' : 'key-round'} size={20} /></span>
1065
+ <div className="cell-label"><b>{title}</b><small>{line}</small><small>{access}</small></div>
1066
+ </div>
1067
+ {who.local || snapshot ? null : (
1068
+ <p><button type="button" onClick={onSignOut}><Icon name="log-out" size={16} />{who.person ? 'Sign out' : who.admin ? 'Sign out of the host' : 'Use another token'}</button></p>
1069
+ )}
1070
+ {platform && who.person ? <p className="mode">Your profile, orgs and keys are on <a href={`${platform}/`}>{new URL(platform).host}</a>.</p> : null}
1071
+ </>
1072
+ );
1073
+ }
1074
+
1075
+ /** THE WORKSPACE: the World this machine serves, as a desktop git app opens a repository (docs/contributing/console.md,
1076
+ * On this machine). One short toolbar, as TablePlus's and Proxyman's are: the World and the branch served here at its
1077
+ * left, where this is in its middle (the World, the branch, the origin and what waits for it, the World's clock), Pull,
1078
+ * Push and Settings at its right. A sidebar of places (Changes, History, Events; the vendors; the remote) beside the place
1079
+ * chosen. Settings and Branches open as windows over it, as GitHub Desktop's do. */
1080
+ function Workspace({ name, token, view, sub, arg, win, who, snapshot, onReturn, onSignOut, worlds, session, retry, twins, map, branches, clock, canWrite, refused, signsAs, opens, tick, admin, onRemoved, onClock, scope }: { name: string; token: string; view: WorkspaceView; sub?: string; arg?: string; win: WorkspaceWindow; /** who is here: this machine, a person signed in, the host's admin, a link or key */ who: { local: boolean; person: string | null; admin: boolean }; /** a snapshot in view: read only, with the way back to live */ snapshot: Snapshot | null; onReturn: (s: Snapshot) => Promise<void>; onSignOut: () => void; /** the Worlds this page opens (the World menu's switcher) */ worlds: string[]; /** the World's session, which vendors' screens read with, and its retry */ session: MirrorSession; retry: () => void; twins: Load<{ twins: Twin[] }>; map: Load<WorldMap>; branches: Load<BranchesDoor> | null; clock: Load<Clock>; canWrite: boolean; refused: (r: Response) => void; signsAs: string | null; opens: (name: string) => boolean; tick: number; admin: string | null; onRemoved: () => void; onClock: () => void; scope: Scope }): React.JSX.Element {
1081
+ const remote = usePushPull(name, token, tick, refused);
1082
+ const [version, setVersion] = useState(0);
1083
+ const every = version * 100_000 + Math.floor(tick / 2);
1084
+ const diff = useDoor<Diff>(token, `/-/${name}/diff`, every);
1085
+ const sets = useDoor<{ changesets: ChangesetView[] }>(token, `/-/${name}/changesets`, every);
1086
+ const rules = useDoor<{ changesets: ChangesetRules }>(token, `/-/${name}/rules`, every);
1087
+ const [picked, setPicked] = useState<string | null>(null);
1088
+ const to = (parts: string[]) => (e?: React.MouseEvent): void => { e?.preventDefault(); go([...name.split('/'), '~', ...parts]); };
1089
+ const vendors = twins.state === 'ok' ? twins.data.twins.map((t) => t.twin) : [];
1090
+ const mirrored = map.state === 'ok' ? map.data.twins.filter((t) => t.mirror).map((t) => t.twin) : [];
1091
+ const rooted = map.state === 'ok' ? map.data.twins.filter((t) => t.root).map((t) => t.twin) : [];
1092
+ 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);
1093
+ const served = (branches?.state === 'ok' && branches.data.checkedOut) || name.split('/')[1]!;
1094
+
1095
+ const records = useMemo((): ChangedRecord[] => {
1096
+ if (diff.state !== 'ok') return [];
1097
+ const by = new Map<string, ChangedRecord>();
1098
+ for (const c of diff.data.changes) { const key = `${c.twin}:${c.entry.subject.type}:${c.entry.subject.id}`; const r = by.get(key) ?? { key, twin: c.twin, writes: [] }; r.writes.push(c.entry); by.set(key, r); }
1099
+ return [...by.values()];
1100
+ }, [diff]);
1101
+ const record = records.find((r) => r.key === picked) ?? records[0];
1102
+ const log = useDoor<{ entries: Entry[] }>(token, view === 'changes' && record ? `/-/${name}/log/${record.twin}` : null, every);
1103
+ const writes = diff.state === 'ok' ? diff.data.changes.length : 0;
1104
+ const waiting = remote.waiting.length;
1105
+ const there = remote.there;
1106
+ const origin = remote.origin.state === 'ok' ? remote.origin.data.origin : null;
1107
+ const off = !canWrite ? 'This session reads the World; it changes nothing' : !there ? 'No remote: name one in Settings › Remote' : null;
1108
+ const place = (on: boolean, icon: React.ReactNode, label: string, href: string, onClick: (e: React.MouseEvent) => void, count?: string): React.JSX.Element => (
1109
+ <a href={`${BASE}/${name}/~/${href}`} className={on ? 'on' : undefined} aria-current={on ? 'page' : undefined} onClick={onClick}>{icon}<span>{label}</span>{count ? <span className="count">{count}</span> : null}</a>
1110
+ );
1111
+ const closeMenu = (e: React.MouseEvent): void => { (e.currentTarget as HTMLElement).closest('details')?.removeAttribute('open'); };
1112
+
726
1113
  return (
727
- <div className="dash">
728
- <section className="block dash-screens" aria-labelledby="screens-head">
729
- <div className="block-head">
730
- <h3 id="screens-head">Screens <span className="badge live">live</span></h3>
731
- <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>
732
- <a className="button" href={`${BASE}/${name}/~/canvas`} onClick={open(['~', 'canvas'])}>Open all screens</a>
1114
+ <div className="ws">
1115
+ <div className="ws-bar" role="toolbar" aria-label={`${name} toolbar`}>
1116
+ <div className="ws-left">
1117
+ <Brand href={`${BASE}/${name}`} onClick={(e) => { e.preventDefault(); to(['changes'])(); }} />
1118
+ <details className="menu ws-pick">
1119
+ <summary title="This World"><Icon name="globe" size={16} /><span className="ws-pick-label"><small>World</small><b>{name}</b></span><Icon name="chevron-down" size={16} /></summary>
1120
+ <div className="popover menu-list ws-world-menu">
1121
+ {who.local ? null : (
1122
+ <>
1123
+ <p className="menu-label">Worlds</p>
1124
+ {worlds.map((w) => <a key={w} href={`${BASE}/${w}`} className={w === name ? 'on' : undefined} onClick={(e) => { e.preventDefault(); closeMenu(e); go(w.split('/')); }}>{w === name ? <Icon name="check" size={16} /> : <Icon name="globe" size={16} />}{w}</a>)}
1125
+ <a href={`${home() && home() !== ORIGIN ? home() : ''}${BASE}/`} onClick={(e) => { if (home() && home() !== ORIGIN) return; e.preventDefault(); closeMenu(e); go([]); }}><Icon name="layout-grid" size={16} />All Worlds{portal() ? ` on ${new URL(portal()).host}` : ''}</a>
1126
+ <hr />
1127
+ </>
1128
+ )}
1129
+ <p className="menu-label">Connect an app</p>
1130
+ {portal() ? (
1131
+ <>
1132
+ <p className="ws-menu-text">Point your app's SDKs here with a key made on the platform; its calls to these vendors land in this World:</p>
1133
+ <pre>{WORLDS}/{name}/&lt;vendor&gt;</pre>
1134
+ <CopyAgentPrompt text={agentPrompt({ platform: portal(), world: name })} />
1135
+ </>
1136
+ ) : !who.local ? (
1137
+ <>
1138
+ <p className="ws-menu-text">Point your app's SDKs here with a key from Settings › Access; its calls to these vendors land in this World:</p>
1139
+ <pre>{WORLDS}/{name}/&lt;vendor&gt;</pre>
1140
+ <CopyAgentPrompt text={agentPrompt()} />
1141
+ </>
1142
+ ) : (
1143
+ <>
1144
+ <p className="ws-menu-text">Run it in this World; its calls to these vendors land here:</p>
1145
+ <pre>volter world run -- npm start</pre>
1146
+ <p className="ws-menu-text mode">Or point an SDK at <code>{WORLDS}/{name}/&lt;vendor&gt;</code> with the token in <code>.volter/token</code>.</p>
1147
+ <CopyAgentPrompt text={agentPrompt()} />
1148
+ </>
1149
+ )}
1150
+ <hr />
1151
+ <a href={`${BASE}/${name}/~/settings`} onClick={(e) => { closeMenu(e); to(['settings'])(e); }}><Icon name="settings" size={16} />Settings…</a>
1152
+ </div>
1153
+ </details>
1154
+ <BranchMenu name={name} token={token} branches={branches} opens={opens} refused={refused} onChanged={onClock} />
1155
+ </div>
1156
+ <div className="ws-where" title="Where this is">
1157
+ <span>{name}</span><span className="sep">:</span><span>{served}</span><span className="sep">:</span>
1158
+ {origin ? <a href={`${BASE}/${name}/~/remote`} onClick={to(['remote'])}>origin {origin.namespace}{waiting ? <b> ↑{waiting}</b> : null}</a> : <a href={`${BASE}/${name}/~/remote`} onClick={to(['remote'])}>no remote</a>}
1159
+ <span className="sep">:</span>
1160
+ <a href={`${BASE}/${name}/~/settings/clock`} onClick={to(['settings', 'clock'])}>{clock.state === 'ok' ? <>{clock.data.frozen ? 'stopped at ' : ''}<At iso={clock.data.at} time /></> : '…'}</a>
1161
+ </div>
1162
+ <div className="ws-right">
1163
+ <button type="button" className="ws-btn" onClick={() => void remote.pull()} disabled={Boolean(off) || remote.busy !== null} title={off ?? `Fetch what ${there} has and move this branch onto it`}><Icon name="arrow-down-to-line" size={16} />{remote.busy === 'Pull' ? 'Pulling…' : 'Pull'}</button>
1164
+ <button type="button" className="ws-btn" onClick={() => void remote.push()} disabled={Boolean(off) || remote.busy !== null || waiting === 0} title={off ?? (waiting ? `Push ${counted(waiting, 'changeset')} to ${there}` : `Every changeset is at ${there}`)}><Icon name="arrow-up-from-line" size={16} />{remote.busy === 'Push' ? 'Pushing…' : 'Push'}{waiting ? <span className="ws-btn-count">{waiting}</span> : null}</button>
1165
+ <button type="button" className="ws-btn icon-only" aria-label="Settings" title="Settings" onClick={() => to(['settings'])()}><Icon name="settings" size={16} /></button>
733
1166
  </div>
734
- {session.state === 'ok' ? (
735
- <ul className="thumbs">
736
- {vendors.map((v) => (
737
- <li key={v}>
738
- <a href={`${BASE}/${name}/~/vendors/${v}`} onClick={open(['~', 'vendors', v])} aria-label={`${vendorName(v)}'s screens`}>
739
- <span className="thumb">{screens.includes(v)
740
- ? <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})` }} />
741
- : <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>
742
- <span className="thumb-foot"><Mark vendor={v} /><b>{vendorName(v)}</b><span className="mode">{heldOf(v)}</span></span>
743
- </a>
744
- </li>
745
- ))}
746
- </ul>
747
- ) : <p className="mode pad">{session.state === 'error' ? `The World did not open its screens: ${session.error}` : 'Opening…'}</p>}
748
- </section>
749
- <div className="dash-side">
750
- {branches ? <section className="block" aria-labelledby="branches-head">
751
- <div className="block-head"><h3 id="branches-head">Branches</h3><a href={`${BASE}/${name}/~/branches`} onClick={open(['~', 'branches'])}>All</a></div>
752
- <ul className="mini-feed">
753
- <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>
754
- {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>)}
755
- </ul>
756
- {canWrite ? <p className="mode pad">“View an earlier moment…” above opens a branch as the World was then.</p> : null}
757
- </section> : null}
758
1167
  </div>
759
- <section className="block dash-activity" aria-labelledby="activity-head">
760
- <div className="block-head"><h3 id="activity-head">Recent activity</h3><a href={`${BASE}/${name}/~/activity`} onClick={open(['~', 'activity'])}>View all</a></div>
761
- <Pending load={recent} what="the activity" />
762
- {recent.state === 'ok' && latest.length === 0 ? (
763
- <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>
764
- ) : (
765
- <ul className="mini-feed">{latest.map((p) => {
766
- const s = said(p.entry);
767
- 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>;
768
- })}</ul>
769
- )}
770
- </section>
1168
+ {snapshot ? (
1169
+ <div className="ws-banner" role="region" aria-label="Snapshot">
1170
+ <Icon name="clock" size={16} /><span>You are looking at <b>{snapshot.parent}</b> as it was at <At iso={snapshot.at} />: a read-only snapshot{snapshot.expiresAt ? <>, gone <At iso={snapshot.expiresAt} /></> : null}.</span>
1171
+ <button type="button" onClick={() => void onReturn(snapshot)}>Return to live</button>
1172
+ </div>
1173
+ ) : null}
1174
+ {remote.log.length ? <div className="ws-said"><Narration log={remote.log.slice(0, 1)} name={name} /></div> : null}
1175
+
1176
+ <div className="ws-body">
1177
+ <nav className="ws-side" aria-label={`${name} places`}>
1178
+ {/* the screens: one way in, saying how many there are and whose; the canvas lists them along its left */}
1179
+ {mirrored.length ? (
1180
+ <a href={`${BASE}/${name}/~/screens`} className={view === 'screen' ? 'ws-screens-entry on' : 'ws-screens-entry'} onClick={to(['screens'])} title={`${counted(mirrored.length, 'screen')}: ${listed(mirrored.map(vendorName))}`}>
1181
+ <Icon name="layout-grid" size={16} /><span>Screens</span>
1182
+ <span className="ws-screens-marks">{mirrored.slice(0, 3).map((v) => <Mark key={v} vendor={v} />)}{mirrored.length > 3 ? <span className="more">+{mirrored.length - 3}</span> : null}</span>
1183
+ <span className="count">{mirrored.length}</span>
1184
+ </a>
1185
+ ) : null}
1186
+ <p className="side-group">Workspace</p>
1187
+ {place(view === 'changes' || view === 'history', <Icon name="file-diff" size={16} />, 'Changes', 'changes', to(['changes']), records.length ? String(records.length) : undefined)}
1188
+ {place((view === 'events' && !arg) || view === 'trace', <Icon name="activity" size={16} />, 'Events', 'events', to(['events']))}
1189
+ <p className="side-group">Records</p>
1190
+ {vendors.map((v) => <React.Fragment key={v}>{place(view === 'vendor' && arg === v, <Mark vendor={v} />, vendorName(v), `vendors/${v}`, to(['vendors', v]), held(v) ? String(held(v)) : undefined)}</React.Fragment>)}
1191
+ <p className="side-group">Remote</p>
1192
+ {place(false, <Icon name="cloud" size={16} />, there ? 'origin' : 'Name a remote…', 'remote', to(['remote']), waiting ? `↑${waiting}` : undefined)}
1193
+ </nav>
1194
+
1195
+ <div className="ws-main" role="main">
1196
+ {view === 'changes' ? (
1197
+ <div className="ws-status">
1198
+ <div className="ws-files">
1199
+ <ChangesTabs name={name} view="changes" changes={records.length} changesets={sets.state === 'ok' ? sets.data.changesets.length : 0} />
1200
+ <div className="ws-list-head"><span>Not in a changeset</span><span className="mode">{diff.state === 'ok' ? `${counted(records.length, 'record')} · ${counted(writes, 'write')}` : ''}</span></div>
1201
+ <Pending load={diff} what="the changes" lines={4} />
1202
+ <RecordList records={records} {...(record ? { selected: record.key } : {})} onPick={setPicked} label="Changed records" />
1203
+ </div>
1204
+ <div className="ws-detail">
1205
+ {diff.state === 'ok' && !record ? (
1206
+ <Empty title="No changes on this branch" action={waiting > 0 && there ? <button type="button" className="primary" disabled={Boolean(off) || remote.busy !== null} onClick={() => void remote.push()}>Push {counted(waiting, 'changeset')} to origin</button> : undefined}>
1207
+ <p>What your app writes to this World lands here, record by record, to cut into a changeset. Everything since {diff.data.base.at ? <At iso={diff.data.base.at} /> : <code>{diff.data.base.id}</code>} is in one already.</p>
1208
+ </Empty>
1209
+ ) : null}
1210
+ {record ? <RecordDiff record={record} history={log.state === 'ok' ? log.data.entries : log.state === 'error' ? [] : null} {...(mirrored.includes(record.twin) ? { screens: name } : {})} /> : null}
1211
+ </div>
1212
+ <CutChangeset pinned generated={rules.state === 'ok' && rules.data.changesets.summary === 'generated'} author={{ face: <Icon name={who.person ? 'user' : who.local ? 'monitor' : 'key-round'} size={18} />, who: who.person ?? (who.local ? 'This machine' : who.admin ? "The host's admin" : 'A token'), account: `${BASE}/${name}/~/settings/account`, branch: served }} name={name} token={token} changes={writes} canWrite={canWrite} refused={refused} onCut={() => { setVersion((v) => v + 1); setPicked(null); }} />
1213
+ </div>
1214
+ ) : null}
1215
+ {view === 'history' ? <HistoryView name={name} token={token} sets={sets} changes={records.length} mirrored={mirrored} required={rules.state === 'ok' ? rules.data.changesets.approvals ?? 1 : 1} {...(sub ? { sub } : {})} signsAs={signsAs} canWrite={canWrite} rooted={rooted} refused={refused} onChanged={() => setVersion((v) => v + 1)} /> : null}
1216
+ {view === 'events' ? <EventsView key={arg ?? ''} name={name} token={token} vendors={vendors} {...(arg && vendors.includes(arg) ? { vendor: arg } : {})} tick={tick} /> : null}
1217
+ {view === 'trace' ? <TraceView name={name} token={token} id={arg ?? ''} /> : null}
1218
+ {view === 'vendor' && arg ? <VendorView key={arg} name={name} token={token} vendor={arg} mirrored={mirrored.includes(arg)} tick={tick} /> : null}
1219
+ {view === 'screen' && arg ? <ScreenView key={arg} name={name} vendor={arg} {...(sub ? { show: sub } : {})} holding={holds(map.state === 'ok' ? map.data.twins.find((x) => x.twin === arg)?.resources ?? [] : [])} session={session} retry={retry} /> : null}
1220
+ </div>
1221
+ </div>
1222
+
1223
+ <Dialog open={win?.kind === 'settings'} onClose={() => to(['changes'])()} title="Settings" wide="window">
1224
+ {win?.kind === 'settings' ? (
1225
+ <div className="ws-settings">
1226
+ <nav aria-label="Settings">{SETTINGS_TABS.map(([k, label, icon]) => <a key={k} href={`${BASE}/${name}/~/settings/${k}`} className={win.tab === k ? 'on' : undefined} aria-current={win.tab === k ? 'page' : undefined} onClick={to(['settings', k])}><Icon name={icon} size={16} />{label}</a>)}</nav>
1227
+ <div className="ws-settings-pane">
1228
+ {scope === 'read' ? <p className="mode"><span className="badge read-only">Read only</span> This session reads the World; it changes nothing.</p> : null}
1229
+ {win.tab === 'account' ? <AccountPane who={who} scope={scope} snapshot={snapshot} portal={portal()} onSignOut={onSignOut} /> : null}
1230
+ {win.tab === 'changesets' ? <ChangesetsPane name={name} token={token} who={who} scope={scope} /> : null}
1231
+ {win.tab === 'remote' ? <Remote name={name} token={token} canWrite={canWrite} tick={tick} refused={refused} /> : null}
1232
+ {win.tab === 'access' || win.tab === 'services' || win.tab === 'checks' || win.tab === 'clock' ? <Settings name={name} token={token} admin={admin} canWrite={canWrite} refused={refused} onRemoved={onRemoved} only={win.tab} /> : null}
1233
+ {win.tab === 'vault' ? <Vault name={name} token={token} canWrite={canWrite} refused={refused} /> : null}
1234
+ {win.tab === 'appearance' ? <><h3 className="settings-head">Appearance</h3><ThemeChoice /></> : null}
1235
+ </div>
1236
+ </div>
1237
+ ) : null}
1238
+ </Dialog>
1239
+ <Dialog open={win?.kind === 'branches'} onClose={() => to(['changes'])()} title="Branches" wide>
1240
+ {win?.kind === 'branches' ? <Branches name={name} token={token} canWrite={canWrite} branches={branches} opens={opens} {...(win.open ? { open: win.open } : {})} onChanged={onClock} refused={refused} /> : null}
1241
+ </Dialog>
771
1242
  </div>
772
1243
  );
773
1244
  }
774
1245
 
1246
+ /** Connect's last line: the setup as a prompt for the person's coding agent (packages/cli/src/agent-prompt.ts, the one
1247
+ * text), copied whole. */
1248
+ function CopyAgentPrompt({ text }: { text: string }): React.JSX.Element {
1249
+ 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')); };
1250
+ return <button type="button" className="agent-prompt-button" onClick={copy}>Copy a prompt for your coding agent</button>;
1251
+ }
1252
+
1253
+
1254
+
1255
+ /** The World's origin as its door answers it: the World this one pushes to and pulls from, or none. */
1256
+ type OriginDoor = { origin: { url: string; namespace: string; clonedAt?: string; fetchedAt?: string } | null };
1257
+ /** The origin's address, as `volter remote add` names it: `<host>/<org>/<world>`. */
1258
+ const originAddress = (o: NonNullable<OriginDoor['origin']>): string => `${o.url.replace(/\/+$/, '')}/${o.namespace}`;
1259
+ /** The changesets a push would send: never sent, or refused (the push door's own queue). */
1260
+ const unpushed = (cs: ChangesetView[]): ChangesetView[] => cs.filter((c) => c.applied === null || c.applied.outcome === 'refused');
1261
+ /** What a vendor holds, in words: its resource counts, the largest first. */
1262
+ const holds = (resources: Array<{ type: string; count: number }>, most = 4): string => {
1263
+ const top = [...resources].filter((r) => r.count > 0).sort((a, b) => b.count - a.count);
1264
+ return top.length ? top.slice(0, most).map((r) => counted(r.count, r.type.replace(/^_+/, ''))).join(' · ') + (top.length > most ? ` · +${top.length - most} more` : '') : 'nothing yet';
1265
+ };
1266
+ const BRANCH_GLYPH = <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>;
1267
+
1268
+
775
1269
 
776
1270
  /** The World's clock, like a test clock's simulation banner: its time, and for a write token the controls that move it. */
777
- 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 {
1271
+ function ClockBar({ name, token, clock, canWrite, onChanged, refused, open: inline }: { name: string; token: string; clock: Load<Clock>; canWrite: boolean; onChanged: () => void; refused: (r: Response) => void; /** Settings' Clock: the controls laid out, not behind a disclosure */ open?: boolean }): React.JSX.Element {
778
1272
  const [setting, setSetting] = useState(false); const [value, setValue] = useState(''); const [by, setBy] = useState('');
779
1273
  const [note, setNote] = useState<{ ok: boolean; text: string } | null>(null); const [busy, setBusy] = useState(false);
780
1274
  const now = clock.state === 'ok' ? clock.data : null;
@@ -804,8 +1298,8 @@ function ClockBar({ name, token, clock, canWrite, onChanged, refused }: { name:
804
1298
  : <>World time <strong><At iso={now!.at} /></strong> <span className="mode">· real time</span></>}
805
1299
  </p>
806
1300
  {canWrite && now ? (
807
- <details className="clock-more"><summary>Simulate time…</summary><div className="popover">
808
- <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>
1301
+ <details className={inline ? 'clock-more inline' : 'clock-more'} open={inline || undefined}><summary>Simulate time…</summary><div className={inline ? undefined : 'popover'}>
1302
+ <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, the branch picker’s Manage branches… views an earlier moment.</p>
809
1303
  <div className="clock-controls">
810
1304
  <button type="button" aria-expanded={setting} onClick={() => { setSetting(!setting); setValue(localInput(nextMinute(now.at))); setNote(null); }}>Set time…</button>
811
1305
  <div role="group" aria-label="Advance the clock" className="advance">
@@ -879,440 +1373,314 @@ function AsOf({ name, token, clock, refused }: { name: string; token: string; cl
879
1373
  );
880
1374
  }
881
1375
 
882
- /** Overview: what the World holds, one card per twin (a resource explorer), and the branches cut from it. */
883
- function Overview({ name, token, canWrite, branches, opens, onBranchesChanged, refused }: { name: string; token: string; canWrite: boolean; branches: Load<BranchesDoor> | null; opens: (name: string) => boolean; onBranchesChanged: () => void; refused: (r: Response) => void }): React.JSX.Element {
884
- const map = useDoor<WorldMap>(token, `/-/${name}/map`);
885
- const [note, setNote] = useState<string | null>(null);
886
- const remove = async (b: string): Promise<void> => {
887
- 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;
888
- const r = await door(token, `/-/${name}/branches/${b}`, { method: 'DELETE' }).catch((e: unknown) => e as Error);
889
- if (r instanceof Error) { setNote(`delete ${b}: the World did not answer (${r.message})`); return; }
890
- if (!r.ok) { refused(r); setNote(`delete ${b}: ${await failure(r)}`); return; }
891
- forgetSnapshot(b); setNote(`deleted ${b}`); onBranchesChanged();
892
- };
893
- const total = map.state === 'ok' ? map.data.twins.reduce((n, t) => n + t.resources.reduce((m, x) => m + x.count, 0), 0) : 0;
894
- // the Branches tab: this World's copies; what it holds per vendor is the Vendors tab's list (kept here for a host
895
- // without branches, where this tab is not shown)
896
- if (branches) return <BranchList name={name} token={token} canWrite={canWrite} branches={branches} opens={opens} note={note} remove={remove} onChanged={onBranchesChanged} refused={refused} />;
897
- return (
898
- <>
899
- <h3>Map</h3>
900
- <Pending load={map} what="the World's map" />
901
- {map.state === 'ok' ? (map.data.twins.length === 0 ? <p>This World holds no twins.</p> : (
902
- <>
903
- <p className="mode">{map.data.twins.length} twin{map.data.twins.length === 1 ? '' : 's'} · {total} resource{total === 1 ? '' : 's'}</p>
904
- <ul className="map" aria-label="Twins in this World">
905
- {map.data.twins.map((t) => {
906
- const top = [...t.resources].sort((a, b) => b.count - a.count);
907
- return (
908
- <li key={t.twin} className="card" aria-labelledby={`map-${t.twin}`}>
909
- <div className="card-head">
910
- <h4 id={`map-${t.twin}`} className="vendor">{t.twin}</h4>
911
- <span className={t.root ? 'badge rooted' : 'badge'}>{t.root ? 'rooted' : 'simulated'}</span>
912
- </div>
913
- <p className="mode">{t.root ? <>rooted at <code>{t.root.url}</code></> : 'simulated — no vendor behind it'} · position {t.position}</p>
914
- {top.length === 0 ? <p className="mode">Holds nothing yet.</p> : (
915
- <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>
916
- )}
917
- <p className="card-actions">
918
- {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>}
919
- <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>
920
- </p>
921
- </li>
922
- );
923
- })}
924
- </ul>
925
- </>
926
- )) : null}
927
- </>
928
- );
929
- }
930
-
931
- /** A World's branches, as a database's are (Neon's Branches page): each a copy of it as of a moment, made here (as of now
932
- * or a moment you pick, for as long as you say), opened read-only in this tab, compared with its parent (what it changed
933
- * since it branched), reset to its parent as it is now, or removed. Each is the World's own branches door. */
934
- function BranchList({ name, token, canWrite, branches, opens, note, remove, onChanged, refused }: { name: string; token: string; canWrite: boolean; branches: Load<BranchesDoor>; opens: (name: string) => boolean; note: string | null; remove: (b: string) => Promise<void>; onChanged: () => void; refused: (r: Response) => void }): React.JSX.Element {
1376
+ /** THE BRANCHES, as Neon's Branches page is: one table of every branch (the World's own, the one served here marked,
1377
+ * and the host's copies of it), each row opening its page: what it is, its own changes, and its acts. "New branch"
1378
+ * makes one as of now or of a moment before it. Each is the World's own branches door. */
1379
+ /** New branch: from now (live, or stopped now) or as the World was at an instant, for how long; made, it is kept with
1380
+ * its tokens so it opens in this tab (the Branches page's, and the workspace's branch picker's). */
1381
+ function NewBranch({ name, token, open, onClose, onMade, refused }: { name: string; token: string; open: boolean; onClose: () => void; onMade: () => void; refused: (r: Response) => void }): React.JSX.Element {
935
1382
  const short = (n: string): string => n.split('/').pop() ?? n;
936
- const list = branches.state === 'ok' ? branches.data.branches : [];
937
- const [making, setMaking] = useState(false); const [busy, setBusy] = useState(false); const [error, setError] = useState<string | null>(null);
938
- const [label, setLabel] = useState(''); const [when, setWhen] = useState<'now' | 'at'>('now'); const [at, setAt] = useState(''); const [hours, setHours] = useState('24');
939
- // SERVE ANOTHER of the World's own branches here: the World switches under this page, which then reads it afresh
940
- const [serving, setServing] = useState<string | null>(null); const [serveError, setServeError] = useState<string | null>(null);
941
- const serveHere = async (branch: string): Promise<void> => {
942
- setServing(branch); setServeError(null);
943
- const r = await door(token, `/-/${name}/branches/${encodeURIComponent(branch)}/serve`, { method: 'POST' }).catch(() => null);
944
- if (r?.ok) { location.reload(); return; }
945
- setServing(null);
946
- setServeError(r ? ((await r.json().catch(() => null)) as { error?: string } | null)?.error ?? `${r.status}` : `${ORIGIN} did not answer`);
947
- };
948
- // made here: kept with its tokens so it opens in this tab; not `made` (a snapshot's "Return to live" never removes it: it lives as long as it was given)
949
- const make = async (body: Record<string, unknown>, opened: string): Promise<{ name: string } | string> => {
950
- const r = await door(token, `/-/${name}/branches`, { method: 'POST', body: JSON.stringify(body) }).catch(() => null);
951
- if (!r) return `${ORIGIN} did not answer`;
952
- if (!r.ok) { refused(r); return await failure(r); }
953
- const b = (await r.json()) as { name: string; token?: string; readToken: string; expiresAt: string | null; origin?: string };
954
- keepSnapshot({ name: b.name, parent: name, token: b.token ?? '', readToken: b.readToken, at: opened, expiresAt: b.expiresAt ?? null, parentOrigin: ORIGIN, ...(b.origin ? { origin: b.origin } : {}) });
955
- return b;
956
- };
1383
+ const [busy, setBusy] = useState(false); const [error, setError] = useState<string | null>(null);
1384
+ const [label, setLabel] = useState(''); const [when, setWhen] = useState<'now' | 'at'>('now'); const [at, setAt] = useState(''); const [hours, setHours] = useState('24'); const [live, setLive] = useState(true);
1385
+ useEffect(() => { if (open) { setError(null); setAt(localInput(new Date().toISOString())); } }, [open]);
957
1386
  const create = async (e: React.FormEvent): Promise<void> => {
958
1387
  e.preventDefault(); setError(null);
959
1388
  const instant = when === 'at' ? new Date(at) : null;
960
1389
  if (instant && Number.isNaN(instant.getTime())) { setError('Pick a date and time, or make it as of now.'); return; }
961
1390
  if (instant && instant.getTime() > Date.now()) { setError('A branch is made as of now or a moment before it.'); return; }
962
- const ttl = Math.round(Number(hours) * 3600);
963
1391
  setBusy(true);
964
- const b = await make({ ttl, ...(instant ? { at: { instant: instant.toISOString() } } : { live: true }), ...(label.trim() ? { label: label.trim() } : {}) }, (instant ?? new Date()).toISOString());
1392
+ const r = await door(token, `/-/${name}/branches`, { method: 'POST', body: JSON.stringify({ ttl: Math.round(Number(hours) * 3600), ...(instant ? { at: { instant: instant.toISOString() } } : live ? { live: true } : {}), ...(label.trim() ? { label: label.trim() } : {}) }) }).catch(() => null);
965
1393
  setBusy(false);
966
- if (typeof b === 'string') { setError(b); return; }
967
- setMaking(false); setLabel(''); toast(`Made ${short(b.name)}`); onChanged();
1394
+ if (!r) { setError(`${ORIGIN} did not answer`); return; }
1395
+ if (!r.ok) { refused(r); setError(await failure(r)); return; }
1396
+ // made here: kept with its tokens so it opens in this tab, and its own changes read with them
1397
+ const b = (await r.json()) as { name: string; token?: string; readToken: string; expiresAt: string | null; origin?: string };
1398
+ keepSnapshot({ name: b.name, parent: name, token: b.token ?? '', readToken: b.readToken, at: (instant ?? new Date()).toISOString(), expiresAt: b.expiresAt ?? null, parentOrigin: ORIGIN, ...(b.origin ? { origin: b.origin } : {}) });
1399
+ onClose(); setLabel(''); toast(`Made ${short(b.name)}`); onMade();
968
1400
  };
969
- const reset = async (b: Branch): Promise<void> => {
970
- if (!(await confirmAct({ title: `Reset ${short(b.name)} to ${short(name)}?`, body: `A fresh copy of ${short(name)} as it is now replaces it, with the same label and the time it had left; what was written to it goes. The copy has a new address and new keys: an app, a CI job or a pull request's preview that holds this branch's must be given the new one.`, action: 'Reset branch', danger: true }))) return;
971
- const left = b.expiresAt ? Math.max(3600, Math.round((Date.parse(b.expiresAt) - Date.now()) / 1000)) : 86_400;
972
- // a branch is named `<parent>-<label>-<4 hex>`: its label is what lies between
973
- const own = short(b.name).startsWith(`${short(name)}-`) ? short(b.name).slice(short(name).length + 1) : '';
974
- const labelOf = /^([a-z0-9][a-z0-9-]*)-[0-9a-f]{4}$/.exec(own)?.[1];
975
- const fresh = await make({ ttl: left, live: true, ...(labelOf && !labelOf.startsWith('at-') ? { label: labelOf } : {}) }, new Date().toISOString());
976
- if (typeof fresh === 'string') { toast(`Reset ${short(b.name)}: ${fresh}`, 'error'); return; }
977
- const r = await door(token, `/-/${name}/branches/${b.name}`, { method: 'DELETE' }).catch(() => null);
978
- if (r?.ok) forgetSnapshot(b.name);
979
- toast(r?.ok ? `Reset: ${short(fresh.name)} is ${short(name)} as it is now` : `Made ${short(fresh.name)}; ${short(b.name)} was not removed`); onChanged();
1401
+ const dialog = (
1402
+ <Dialog open={open} onClose={() => { if (!busy) onClose(); }} title={`New branch of ${short(name)}`} footer={<><button type="button" onClick={() => onClose()} disabled={busy}>Cancel</button><button type="submit" form="new-branch" className="primary" disabled={busy}>{busy ? 'Making…' : 'Make branch'}</button></>}>
1403
+ <form id="new-branch" onSubmit={(e) => void create(e)} className="stack">
1404
+ <label><span>Label <span className="mode">(optional)</span></span><input name="branchLabel" value={label} onChange={(e) => setLabel(e.target.value.toLowerCase())} placeholder="checkout-flow" pattern="[a-z0-9][a-z0-9\-]{0,23}" maxLength={24} /></label>
1405
+ <fieldset className="choice"><legend>From</legend>
1406
+ <label className="check"><input type="radio" name="branchWhen" checked={when === 'now'} onChange={() => setWhen('now')} /> <span>Now: the World as it is</span></label>
1407
+ {when === 'now' ? <label className="check sub"><input type="checkbox" name="branchLive" checked={live} onChange={(e) => setLive(e.target.checked)} /> <span>Live: its clock keeps this World's time, as a pull request's preview does (else it stops now)</span></label> : null}
1408
+ <label className="check"><input type="radio" name="branchWhen" checked={when === 'at'} onChange={() => setWhen('at')} /> <span>An instant before now: the World as it was then, its clock stopped there</span></label>
1409
+ {when === 'at' ? <input type="datetime-local" aria-label="As of" value={at} max={localInput(new Date().toISOString())} onChange={(e) => setAt(e.target.value)} /> : null}
1410
+ </fieldset>
1411
+ <label><span>Goes away after</span><select name="branchLife" value={hours} onChange={(e) => setHours(e.target.value)}><option value="1">1 hour</option><option value="24">1 day</option><option value="72">3 days</option><option value="168">7 days</option></select></label>
1412
+ {error ? <p role="alert">{error}</p> : null}
1413
+ </form>
1414
+ </Dialog>
1415
+ );
1416
+ return dialog;
1417
+ }
1418
+
1419
+ function Branches({ name, token, canWrite, branches, opens, open: opened, onChanged, refused, snapshotOf }: { name: string; token: string; canWrite: boolean; branches: Load<BranchesDoor> | null; opens: (name: string) => boolean; open?: string; onChanged: () => void; refused: (r: Response) => void; /** the World this one is a snapshot of: a snapshot is not branched again */ snapshotOf?: string }): React.JSX.Element {
1420
+ const clock = useDoor<Clock>(token, `/-/${name}/clock`);
1421
+ const short = (n: string): string => n.split('/').pop() ?? n;
1422
+ const [making, setMaking] = useState(false);
1423
+ // a host's branch is routed by its full name (`<org>/<world>`), one of the World's own by its name: the two never collide
1424
+ const page = (b?: string) => (e: React.MouseEvent): void => { e.preventDefault(); go([...name.split('/'), '~', 'branches', ...(b ? b.split('/') : [])]); };
1425
+ if (!branches) return <p className="mode">This host makes no branches of a World.</p>;
1426
+ const list = branches.state === 'ok' ? branches.data.branches : [];
1427
+ const named = branches.state === 'ok' && (branches.data.named?.length ?? 0) > 0 ? branches.data.named! : [{ name: short(name), running: true }];
1428
+ const served = branches.state === 'ok' ? branches.data.checkedOut ?? short(name) : short(name);
1429
+ const remove = async (b: string): Promise<void> => {
1430
+ if (!(await confirmAct({ title: `Remove the branch ${short(b)}?`, body: 'Whoever holds its tokens loses it, and its history goes with it.', action: 'Remove branch', danger: true }))) return;
1431
+ const r = await door(token, `/-/${name}/branches/${b}`, { method: 'DELETE' }).catch((e: unknown) => e as Error);
1432
+ if (r instanceof Error) { toast(`Remove ${short(b)}: the World did not answer (${r.message})`, 'error'); return; }
1433
+ if (!r.ok) { refused(r); toast(`Remove ${short(b)}: ${await failure(r)}`, 'error'); return; }
1434
+ forgetSnapshot(b); toast(`Removed ${short(b)}`); onChanged(); go([...name.split('/'), '~', 'branches']);
980
1435
  };
981
- const menu = (b: Branch): Array<{ label: string; onSelect: () => void; danger?: boolean }> => [
982
- ...(opens(b.name) ? [{ label: 'Compare with parent', onSelect: () => goBranch(b.name, [...b.name.split('/'), '~', 'changes']) }] : []),
983
- ...(canWrite ? [{ label: 'Reset to parent…', onSelect: () => void reset(b) }, { label: 'Delete…', danger: true, onSelect: () => void remove(b.name) }] : []),
984
- ];
1436
+ if (opened) {
1437
+ const copy = opened.includes('/') ? list.find((b) => b.name === opened) : undefined;
1438
+ const own = opened.includes('/') ? undefined : named.find((b) => b.name === opened);
1439
+ return (
1440
+ <>
1441
+ <p className="crumbs-line"><a href={`${BASE}/${name}/~/branches`} onClick={page()}>← Branches</a></p>
1442
+ <Pending load={branches} what="the branches" />
1443
+ {branches.state === 'ok' && !copy && !own ? <p className="mode">This World has no branch named {opened}.</p> : null}
1444
+ {copy ? <BranchPage name={name} b={copy} canWrite={canWrite} opens={opens(copy.name)} remove={() => void remove(copy.name)} /> : null}
1445
+ {own && !copy ? <OwnBranchPage name={name} token={token} b={own} served={own.name === served} canWrite={canWrite} canServe={branches.state === 'ok' && Boolean(branches.data.canServe)} /> : null}
1446
+ </>
1447
+ );
1448
+ }
985
1449
  return (
986
1450
  <div aria-labelledby="branches">
987
1451
  <div className="branches-head">
988
- <p className="lede">Branch this World to look at it as it was, or to change it without touching <b>{short(name)}</b>. A branch made here opens read-only in this tab; its parent removes it.</p>
989
- {canWrite ? <button type="button" className="primary" onClick={() => { setError(null); setAt(localInput(new Date().toISOString())); setMaking(true); }}>New branch</button> : null}
1452
+ <p className="lede">Branch this World to look at it as it was, or to change it without touching <b>{served}</b>. Each branch's name opens its page.</p>
1453
+ {canWrite ? <div className="head-actions">{snapshotOf ? null : <AsOf name={name} token={token} clock={clock} refused={refused} />}<button type="button" className="primary" onClick={() => setMaking(true)}>New branch</button></div> : null}
990
1454
  </div>
991
- {note ? <p role="status">{note}</p> : null}
992
1455
  <Pending load={branches} what="the branches" />
993
1456
  {branches.state === 'ok' ? (
994
1457
  <div className="block">
995
1458
  <table className="dense" aria-label="Branches">
996
- <thead><tr><th id="branches">Branch</th><th>Parent</th><th>As of</th><th>Made</th><th>Goes away</th><th /></tr></thead>
1459
+ <thead><tr><th id="branches">Branch</th><th>Parent</th><th>As of</th><th>Made</th><th>Goes away</th></tr></thead>
997
1460
  <tbody>
998
- <tr><td><b>{branches.data.checkedOut ?? short(name)}</b> <span className="badge">{branches.data.checkedOut ? 'served here' : 'default'}</span></td><td className="mode">—</td><td className="mode">live</td><td className="mode">—</td><td className="mode">kept</td><td /></tr>
999
- {list.map((b) => {
1000
- const items = menu(b);
1001
- return (
1002
- <tr key={b.name}>
1003
- <td><b>{short(b.name)}</b></td>
1004
- <td>{short(name)}</td>
1005
- <td>{b.at?.instant ? <At iso={b.at.instant} /> : 'when it was made'}</td>
1006
- <td><At iso={b.createdAt} /></td>
1007
- <td>{b.expiresAt ? <At iso={b.expiresAt} /> : 'kept'}</td>
1008
- <td className="num row-actions">
1009
- {opens(b.name) ? <button type="button" aria-label={`Open ${b.name}`} onClick={() => goBranch(b.name, b.name.split('/'))}>Open</button> : <span className="mode" title="Its token was handed to whoever made it">opened by whoever made it</span>}
1010
- {items.length ? <Menu label={`More for ${short(b.name)}`} items={items} /> : null}
1011
- </td>
1012
- </tr>
1013
- );
1014
- })}
1015
- </tbody>
1016
- </table>
1017
- {list.length === 0 ? <p className="mode pad">No snapshots yet.</p> : null}
1018
- </div>
1019
- ) : null}
1020
- {branches.state === 'ok' && (branches.data.named?.length ?? 0) > 0 ? (
1021
- <>
1022
- <p className="lede">This World's own branches (<code>volter world branch</code>). One runs at a time; {branches.data.canServe ? <>“Serve here” stops this one's twins and serves that one in its place.</> : <>this host serves one of them; another is served by <code>volter world checkout &lt;branch&gt;</code> where the World lives, then serving again.</>}</p>
1023
- {serveError ? <p role="alert">{serveError}</p> : null}
1024
- <div className="block">
1025
- <table className="dense" aria-label="World branches">
1026
- <thead><tr><th>Branch</th><th>State</th><th /></tr></thead>
1027
- <tbody>
1028
- {branches.data.named!.map((b) => (
1461
+ {named.map((b) => (
1462
+ <tr key={`own:${b.name}`}>
1463
+ <td><a href={`${BASE}/${name}/~/branches/${b.name}`} onClick={page(b.name)}><b>{b.name}</b></a>{b.name === served ? <> <span className="badge live">served here</span></> : b.running ? null : <> <span className="badge">stopped</span></>}</td>
1464
+ <td className="mode">—</td><td className="mode">live</td><td className="mode">—</td><td className="mode">kept</td>
1465
+ </tr>
1466
+ ))}
1467
+ {list.map((b) => (
1029
1468
  <tr key={b.name}>
1030
- <td><b>{b.name}</b>{b.name === branches.data.checkedOut ? <> <span className="badge">served here</span></> : null}</td>
1031
- <td className="mode">{b.running ? 'running' : 'stopped'}</td>
1032
- <td className="num row-actions">{canWrite && branches.data.canServe && b.name !== branches.data.checkedOut ? <button type="button" disabled={serving !== null} onClick={() => void serveHere(b.name)}>{serving === b.name ? 'Switching…' : 'Serve here'}</button> : null}</td>
1469
+ <td><a href={`${BASE}/${name}/~/branches/${b.name}`} onClick={page(b.name)}><b>{short(b.name)}</b></a></td>
1470
+ <td>{short(b.from ?? name)}</td>
1471
+ <td>{b.at?.instant ? <At iso={b.at.instant} /> : <span className="mode">live</span>}</td>
1472
+ <td><At iso={b.createdAt} /></td>
1473
+ <td>{b.expiresAt ? <At iso={b.expiresAt} /> : 'kept'}</td>
1033
1474
  </tr>
1034
1475
  ))}
1035
1476
  </tbody>
1036
1477
  </table>
1037
1478
  </div>
1038
- </>
1039
1479
  ) : null}
1040
- <Dialog open={making} onClose={() => { if (!busy) setMaking(false); }} title={`New branch of ${short(name)}`} footer={<><button type="button" onClick={() => setMaking(false)} disabled={busy}>Cancel</button><button type="submit" form="new-branch" className="primary" disabled={busy}>{busy ? 'Making…' : 'Make branch'}</button></>}>
1041
- <form id="new-branch" onSubmit={(e) => void create(e)} className="stack">
1042
- <label><span>Name <span className="mode">(optional)</span></span><input name="branchLabel" value={label} onChange={(e) => setLabel(e.target.value.toLowerCase())} placeholder="checkout-flow" pattern="[a-z0-9][a-z0-9-]{0,23}" maxLength={24} /></label>
1043
- <fieldset className="choice"><legend>As of</legend>
1044
- <label className="check"><input type="radio" name="branchWhen" checked={when === 'now'} onChange={() => setWhen('now')} /> <span>Now: the World as it is, its clock running on</span></label>
1045
- <label className="check"><input type="radio" name="branchWhen" checked={when === 'at'} onChange={() => setWhen('at')} /> <span>A moment before now: the World as it was then, its clock stopped there</span></label>
1046
- {when === 'at' ? <input type="datetime-local" aria-label="As of" value={at} max={localInput(new Date().toISOString())} onChange={(e) => setAt(e.target.value)} /> : null}
1047
- </fieldset>
1048
- <label><span>Goes away after</span><select name="branchLife" value={hours} onChange={(e) => setHours(e.target.value)}><option value="1">1 hour</option><option value="24">1 day</option><option value="72">3 days</option><option value="168">7 days</option></select></label>
1049
- {error ? <p role="alert">{error}</p> : null}
1050
- </form>
1051
- </Dialog>
1480
+ <NewBranch name={name} token={token} open={making} onClose={() => setMaking(false)} onMade={onChanged} refused={refused} />
1052
1481
  </div>
1053
1482
  );
1054
1483
  }
1055
1484
 
1056
- /** THE VENDORS: each vendor's own screens (its mirror), the World's live preview. A list of the vendors with what each
1057
- * holds, and the chosen one's screens at full size, at their own address; "Side by side" shows them all at once. */
1058
- 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 {
1059
- const map = useDoor<WorldMap>(token, `/-/${name}/map`);
1060
- const list = twins.state === 'ok' ? twins.data.twins : [];
1061
- const held = (v: string): string => {
1062
- const t = map.state === 'ok' ? map.data.twins.find((x) => x.twin === v) : undefined;
1063
- if (!t) return '';
1064
- const top = [...t.resources].filter((r) => r.count > 0).sort((a, b) => b.count - a.count);
1065
- return top.length === 0 ? 'nothing yet' : top.slice(0, 3).map((r) => counted(r.count, r.type)).join(' · ') + (top.length > 3 ? ' · …' : '');
1066
- };
1067
- const rootOf = (v: string): string | null => (map.state === 'ok' ? map.data.twins.find((x) => x.twin === v)?.root?.url ?? null : null);
1068
- const chosen = list.find((t) => t.twin === selected);
1069
- 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" />;
1485
+ /** One of the host's branches of this World (Neon's branch page): what it is, its own changes since it branched (read
1486
+ * with its own token, which only the tab that made it holds), and its acts. */
1487
+ function BranchPage({ name, b, canWrite, opens, remove }: { name: string; b: Branch; canWrite: boolean; opens: boolean; remove: () => void }): React.JSX.Element {
1488
+ const short = (n: string): string => n.split('/').pop() ?? n;
1489
+ const own = snapshotToken(b.name);
1490
+ const diff = useDoor<Diff>(own, own ? `/-/${b.name}/diff` : null);
1070
1491
  return (
1071
- <>
1072
- <Pending load={twins} what="the vendors" />
1073
- {twins.state === 'ok' && list.length === 0 ? <p>No vendors in this World yet.</p> : null}
1074
- {chosen ? (
1075
- <div className="vendors">
1076
- <div className="stage">
1077
- <div className="stage-bar">
1078
- <Mark vendor={chosen.twin} />
1079
- <span className="mode">{held(chosen.twin) || 'empty'}{rootOf(chosen.twin) ? ` · connected to ${rootOf(chosen.twin)}` : ' · stand-in'}</span>
1080
- <span className="stage-actions">
1081
- {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}
1082
- {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}
1083
- </span>
1084
- </div>
1085
- {session.state === 'ok' && session.scope === 'read' ? <p className="mode stage-note">You are looking read only: the screens browse, and refuse changes.</p> : null}
1086
- {session.state === 'pending' ? <p className="mode" role="status">Opening…</p> : null}
1087
- {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}
1088
- {session.state === 'ok' ? (chosen.mirror
1089
- ? <iframe key={chosen.twin} title={`${chosen.twin} mirror`} src={mirrorUrl(name, chosen.twin)} />
1090
- : <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}
1091
- </div>
1492
+ <div className="branch-page">
1493
+ <div className="changeset-head">
1494
+ <h3>{BRANCH_GLYPH} {short(b.name)}</h3>
1495
+ <div className="changeset-actions">
1496
+ {opens ? <button type="button" className="primary" onClick={() => goBranch(b.name, b.name.split('/'))}>Open</button> : null}
1497
+ {canWrite ? <button type="button" className="danger-button" onClick={remove}>Remove…</button> : null}
1092
1498
  </div>
1499
+ </div>
1500
+ <div className="block">
1501
+ <dl className="fields pad">
1502
+ <dt>Name</dt><dd><code>{b.name}</code></dd>
1503
+ <dt>Parent</dt><dd>{short(b.from ?? name)}</dd>
1504
+ <dt>As of</dt><dd>{b.at?.instant ? <At iso={b.at.instant} /> : 'live: it keeps its parent\'s time'}</dd>
1505
+ <dt>Made</dt><dd><At iso={b.createdAt} /></dd>
1506
+ <dt>Goes away</dt><dd>{b.expiresAt ? <At iso={b.expiresAt} /> : 'kept'}</dd>
1507
+ </dl>
1508
+ </div>
1509
+ {!opens ? <p className="mode">Its tokens were handed to whoever made it: it opens, and shows its own changes, in their tab.</p> : null}
1510
+ {diff.state === 'ok' ? (
1511
+ <>
1512
+ <h4>Its changes</h4>
1513
+ {diff.data.changes.length === 0 ? <p className="mode">Nothing written on it since it branched.</p> : <RecordChanges changes={diff.data.changes} idPrefix={`branch-${short(b.name)}`} />}
1514
+ </>
1093
1515
  ) : null}
1094
- </>
1516
+ </div>
1095
1517
  );
1096
1518
  }
1097
1519
 
1098
- /** Consecutive changes that are one action on one kind of thing, as runs (a single change is a run of one). */
1099
- function runsOf(entries: Entry[]): Entry[][] {
1100
- const out: Entry[][] = [];
1101
- 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]); }
1102
- return out;
1520
+ /** One of the World's own branches (`volter world branch`): whether it runs, and serving it here. */
1521
+ function OwnBranchPage({ name, token, b, served, canWrite, canServe }: { name: string; token: string; b: { name: string; running: boolean }; served: boolean; canWrite: boolean; canServe: boolean }): React.JSX.Element {
1522
+ const [serving, setServing] = useState(false); const [error, setError] = useState<string | null>(null);
1523
+ const serveHere = async (): Promise<void> => {
1524
+ setServing(true); setError(null);
1525
+ const r = await door(token, `/-/${name}/branches/${encodeURIComponent(b.name)}/serve`, { method: 'POST' }).catch(() => null);
1526
+ if (r?.ok) { location.reload(); return; }
1527
+ setServing(false);
1528
+ setError(r ? await failure(r) : `${ORIGIN} did not answer`);
1529
+ };
1530
+ return (
1531
+ <div className="branch-page">
1532
+ <div className="changeset-head">
1533
+ <h3>{BRANCH_GLYPH} {b.name} {served ? <span className="badge live">served here</span> : null}</h3>
1534
+ <div className="changeset-actions">{canWrite && canServe && !served ? <button type="button" className="primary" disabled={serving} onClick={() => void serveHere()}>{serving ? 'Switching…' : 'Serve here'}</button> : null}</div>
1535
+ </div>
1536
+ <div className="block">
1537
+ <dl className="fields pad">
1538
+ <dt>Name</dt><dd><code>{b.name}</code></dd>
1539
+ <dt>Kind</dt><dd>one of this World's own branches</dd>
1540
+ <dt>State</dt><dd>{b.running ? 'running' : 'stopped'}</dd>
1541
+ </dl>
1542
+ </div>
1543
+ {served ? <p className="mode">This is the branch these pages show: its changes are on <a href={`${BASE}/${name}/~/changes`} onClick={(e) => { e.preventDefault(); go([...name.split('/'), '~', 'changes']); }}>Changes</a>.</p> : canServe ? <p className="mode">“Serve here” stops this one's twins and serves {b.name} in its place.</p> : <p className="mode">This host serves one of the World's own branches at a time, and does not switch them from this page.</p>}
1544
+ {error ? <p role="alert">{error}</p> : null}
1545
+ </div>
1546
+ );
1103
1547
  }
1104
1548
 
1105
- /** Rows that say the same thing (nine labels created at one instant) as one line that opens to them. */
1106
- function collapsed(items: Placed[]): Array<Placed | Placed[]> {
1107
- const out: Array<Placed | Placed[]> = [];
1108
- for (const p of items) {
1109
- const last = out[out.length - 1];
1110
- const head = Array.isArray(last) ? last[0]! : last;
1111
- if (head && head.twin === p.twin && (head.entry.operation ?? head.entry.op) === (p.entry.operation ?? p.entry.op)) {
1112
- if (Array.isArray(last)) last.push(p); else out[out.length - 1] = [head, p];
1113
- } else out.push(p);
1114
- }
1115
- // a run of two stays two rows; three or more is one line
1116
- return out.flatMap((x) => (Array.isArray(x) && x.length < 3 ? x : [x]));
1117
- }
1118
1549
 
1119
1550
  const PAGE = 50;
1120
1551
  const keyOf = (p: Placed): string => `${p.twin}:${p.position}`;
1121
1552
  /** Newest first — the door's order: the instant, then the twin, then the position. */
1122
1553
  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;
1123
- /** The feed in days, each day in instants: entries at one instant stay together, in no finer order than the World knows. */
1124
- function grouped(items: Placed[]): Array<{ day: string; label: string; instants: Array<{ at: string; items: Placed[] }> }> {
1125
- const days: Array<{ day: string; label: string; instants: Array<{ at: string; items: Placed[] }> }> = [];
1126
- for (const p of items) {
1127
- const d = new Date(p.entry.occurredAt); const key = d.toDateString();
1128
- let day = days[days.length - 1];
1129
- if (!day || day.day !== key) { day = { day: key, label: d.toLocaleDateString(undefined, { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' }), instants: [] }; days.push(day); }
1130
- const last = day.instants[day.instants.length - 1];
1131
- if (last && Date.parse(last.at) === d.getTime()) last.items.push(p); else day.instants.push({ at: p.entry.occurredAt, items: [p] });
1132
- }
1133
- return days;
1554
+ /** Who made it: the vendor itself for what it records on its own, the actor's id where it has one, else the app. */
1555
+ /** Who made it: the World's own word for the credential that called (`caller`: a key by its name, a person, the World's
1556
+ * token) when the entry carries it; else the vendor itself for what it records on its own, the actor's id where it has
1557
+ * one, else the app. */
1558
+ /** A caller as a person reads it: a key as itself, for its person (`key:claude for:ada` → "claude, for ada"), a person by
1559
+ * name, this machine's page and a World's token as what they are. */
1560
+ const callerName = (c: string | undefined): string => {
1561
+ if (!c) return 'someone';
1562
+ if (c.startsWith('key:')) { const [key, person] = c.slice(4).split(' for:'); return person ? `${key}, for ${person}` : key || 'a key'; }
1563
+ if (c.startsWith('person:')) return c.slice(7);
1564
+ return c === 'machine' ? 'this machine' : c === 'person' ? 'a session with no name' : c === 'token:read' ? 'the read-only token' : c === 'token' ? "the World's token" : c;
1565
+ };
1566
+ const whoDid = (e: Entry, caller?: string): string => {
1567
+ const c = typeof caller === 'string' ? caller : typeof e.caller === 'string' ? e.caller : '';
1568
+ if (c.startsWith('key:') && c.length > 4) return `by ${callerName(c)}`;
1569
+ if (c.startsWith('person:') && c.length > 7) return `by ${c.slice(7)}`;
1570
+ if (c === 'machine') return 'by this machine';
1571
+ if (c === 'person') return 'by a session with no name';
1572
+ if (c === 'key') return 'by a key';
1573
+ if (c === 'token:read') return 'by the read-only token';
1574
+ if (c === 'token') return "by the World's token";
1575
+ return e.actor?.kind === 'system' ? 'by the vendor' : e.actor?.id ? `by ${e.actor.id}` : 'by an app';
1576
+ };
1577
+ const opOf = (e: Entry): string => e.operation ?? e.op;
1578
+ /** What an entry's write cost, where it says: tokens in and out (a model's `usage`, or a usage record's own fields) and a cost. */
1579
+ function usageOf(e: Entry): Array<[string, string]> {
1580
+ const f = (e.fields ?? {}) as Record<string, unknown>;
1581
+ const u = f.usage !== null && typeof f.usage === 'object' ? (f.usage as Record<string, unknown>) : f;
1582
+ const n = (...keys: string[]): number | undefined => keys.map((k) => u[k] ?? f[k]).find((v): v is number => typeof v === 'number');
1583
+ const out: Array<[string, string]> = [];
1584
+ const input = n('prompt_tokens', 'input_tokens'); const output = n('completion_tokens', 'output_tokens'); const total = n('total_tokens');
1585
+ if (input !== undefined) out.push(['tokens in', input.toLocaleString()]);
1586
+ if (output !== undefined) out.push(['tokens out', output.toLocaleString()]);
1587
+ if (total !== undefined) out.push(['tokens in all', total.toLocaleString()]);
1588
+ const cost = n('cost_usd', 'cost');
1589
+ if (cost !== undefined) out.push(['cost', `$${cost < 0.01 ? cost.toPrecision(2) : cost.toFixed(2)}`]);
1590
+ return out;
1134
1591
  }
1135
1592
 
1136
- function EntryRow({ name, p }: { name: string; p: Placed }): React.JSX.Element {
1137
- const e = p.entry; const t = traceOf(e.traceparent); const s = said(e); const n = notable(e);
1138
- return (
1139
- <li className="entry" title={`${e.operation ?? e.op} ${e.subject.type}:${e.subject.id} · ${actorOf(e)} · ${standing(e)}`}>
1140
- <span className="twin-chip"><Mark vendor={p.twin} />{vendorName(p.twin)}</span>
1141
- <span className="op">{s.what}</span>
1142
- {s.label ? <span className="label">“{s.label}”</span> : <code className="subject">{shown(e)}</code>}
1143
- <span className="actor mode">{byWhom(e, p.twin)}</span>
1144
- {n ? <span className="standing">{n}</span> : null}
1145
- {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}
1146
- </li>
1147
- );
1593
+ /** Text as a person reads it: whole when short, its start with the rest a click away when long. */
1594
+ function Folded({ text, at = 280 }: { text: string; at?: number }): React.JSX.Element {
1595
+ if (text.length <= at) return <span className="text">{text}</span>;
1596
+ return <details className="folded"><summary><span className="text">{text.slice(0, at)}…</span> <span className="mode">show all {text.length.toLocaleString()} characters</span></summary><span className="text">{text}</span></details>;
1148
1597
  }
1149
-
1150
- /** The feed of everything that happened, across twins — an activity feed: newest first, by day, newer changes held behind a pill. */
1151
- function TimelineTab({ name, token, vendors }: { name: string; token: string; vendors: string[] }): React.JSX.Element {
1152
- const [only, setOnly] = useState<string | null>(null);
1153
- const [items, setItems] = useState<Placed[] | null>(null); const [next, setNext] = useState<string | null>(null);
1154
- const [error, setError] = useState<string | null>(null); const [older, setOlder] = useState(false); const [olderError, setOlderError] = useState<string | null>(null);
1155
- const [fresh, setFresh] = useState<{ items: Placed[]; gap: Timeline | null }>({ items: [], gap: null });
1156
- const path = useCallback((before?: string): string => `/-/${name}/timeline?limit=${PAGE}${only ? `&twin=${encodeURIComponent(only)}` : ''}${before ? `&before=${encodeURIComponent(before)}` : ''}`, [name, only]);
1157
- const itemsRef = useRef<Placed[] | null>(null); itemsRef.current = items;
1158
- useEffect(() => {
1159
- let live = true;
1160
- setItems(null); setError(null); setFresh({ items: [], gap: null });
1161
- void (async () => {
1162
- const r = await door(token, path());
1163
- if (!live) return;
1164
- if (!r.ok) { setError(await failure(r)); return; }
1165
- const b = (await r.json()) as Timeline; if (!live) return;
1166
- setItems(b.entries); setNext(b.next);
1167
- })().catch((e: unknown) => { if (live) setError(`the World did not answer: ${e instanceof Error ? e.message : String(e)}`); });
1168
- // live: newer entries are counted, not inserted — the list never moves under the reader
1169
- const id = setInterval(() => {
1170
- const cur = itemsRef.current; if (cur === null || document.hidden) return;
1171
- void door(token, path()).then(async (r) => {
1172
- if (!r.ok || !live) return;
1173
- const b = (await r.json()) as Timeline; if (!live) return;
1174
- if (cur.length === 0) { if (b.entries.length) { setItems(b.entries); setNext(b.next); } return; }
1175
- const known = new Set((itemsRef.current ?? cur).map(keyOf));
1176
- const unseen = b.entries.filter((p) => !known.has(keyOf(p)));
1177
- // a full page with nothing known in it: more arrived than one page holds, and showing them starts over
1178
- setFresh({ items: unseen, gap: unseen.length === b.entries.length && b.next ? b : null });
1179
- }).catch(() => null);
1180
- }, 3000);
1181
- return () => { live = false; clearInterval(id); };
1182
- }, [token, path]);
1183
- const show = (): void => {
1184
- if (fresh.gap) { setItems(fresh.gap.entries); setNext(fresh.gap.next); } else setItems((cur) => [...fresh.items, ...(cur ?? [])].sort(newestFirst));
1185
- setFresh({ items: [], gap: null });
1186
- };
1187
- const loadOlder = async (): Promise<void> => {
1188
- if (!next) return; setOlder(true); setOlderError(null);
1189
- try {
1190
- const r = await door(token, path(next));
1191
- // what is shown stays shown: only the older page failed
1192
- if (!r.ok) { setOlderError(await failure(r)); return; }
1193
- const b = (await r.json()) as Timeline;
1194
- setItems((cur) => { const known = new Set((cur ?? []).map(keyOf)); return [...(cur ?? []), ...b.entries.filter((p) => !known.has(keyOf(p)))]; }); setNext(b.next);
1195
- } catch (e) { setOlderError(`the World did not answer: ${e instanceof Error ? e.message : String(e)}`); } finally { setOlder(false); }
1196
- };
1197
- const days = useMemo(() => grouped(items ?? []), [items]);
1198
- const n = fresh.items.length;
1199
- return (
1200
- <>
1201
- <div role="group" aria-label="Filter by twin" className="chips">
1202
- <button type="button" aria-pressed={only === null} onClick={() => setOnly(null)}>All vendors</button>
1203
- {vendors.map((v) => <button key={v} type="button" aria-pressed={only === v} aria-label={v} onClick={() => setOnly(only === v ? null : v)}>{vendorName(v)}</button>)}
1204
- </div>
1205
- <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>
1206
- {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 ? (
1207
- only ? <p>Nothing has happened in {only} yet.</p> : (
1208
- <div className="empty">
1209
- <p><strong>Nothing has happened in this World yet.</strong></p>
1210
- <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>
1211
- </div>
1212
- )
1213
- ) : (
1214
- <div className="feed" aria-label="Timeline">
1215
- {days.map((d) => (
1216
- <div key={d.day} role="group" aria-label={d.label} className="day">
1217
- <h3 className="day-head">{d.label}</h3>
1218
- <ol className="instants">
1219
- {d.instants.map((g) => (
1220
- <li key={`${g.at}:${keyOf(g.items[0]!)}`} className="instant">
1221
- <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>
1222
- <ul className="entries">{collapsed(g.items).map((x) => (Array.isArray(x)
1223
- ? <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>
1224
- : <EntryRow key={keyOf(x)} name={name} p={x} />))}</ul>
1225
- </li>
1226
- ))}
1227
- </ol>
1228
- </div>
1229
- ))}
1230
- {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>}
1231
- {olderError ? <p role="alert">Could not load older changes: {olderError}</p> : null}
1232
- </div>
1233
- )}
1234
- </>
1235
- );
1598
+ /** A value read as itself: text as text, anything else as its JSON, folded when long. */
1599
+ function Value({ v }: { v: unknown }): React.JSX.Element {
1600
+ if (typeof v === 'string') return <Folded text={v} />;
1601
+ if (v === null || typeof v !== 'object') return <code>{String(v)}</code>;
1602
+ const json = JSON.stringify(v, null, 2);
1603
+ return json.length <= 160 ? <code>{JSON.stringify(v)}</code> : <details className="folded"><summary><code>{JSON.stringify(v).slice(0, 120)}…</code></summary><pre>{json}</pre></details>;
1236
1604
  }
1237
-
1238
- /** A cause followed across vendors — a waterfall with a lane per twin, each step openable to its fields. */
1239
- function Trace({ name, token, id }: { name: string; token: string; id: string }): React.JSX.Element {
1240
- const valid = /^[0-9a-f]{32}$/.test(id);
1241
- const load = useDoor<Timeline>(token, valid ? `/-/${name}/timeline?trace=${id}&limit=500` : null);
1242
- const steps = load.state === 'ok' ? [...load.data.entries].sort((a, b) => -newestFirst(a, b)) : [];
1243
- const lanes = [...new Set(steps.map((s) => s.twin))];
1244
- const t0 = steps[0] ? Date.parse(steps[0].entry.occurredAt) : 0;
1245
- const back = <a href={`${BASE}/${name}/~/timeline`} onClick={(e) => { e.preventDefault(); go([...name.split('/'), '~', 'timeline']); }}>← Timeline</a>;
1605
+ /** A message's words: its content when it is text, the text parts of it when it is parts. */
1606
+ const wordsOf = (content: unknown): string => (typeof content === 'string' ? content : Array.isArray(content) ? content.map((c) => (c && typeof c === 'object' && typeof (c as { text?: unknown }).text === 'string' ? (c as { text: string }).text : '')).filter(Boolean).join('\n') : JSON.stringify(content ?? ''));
1607
+ /** The request an entry carried (`input.body`): a chat's messages as a conversation, every other field by name. */
1608
+ function RequestBody({ body }: { body: unknown }): React.JSX.Element {
1609
+ if (body === null || typeof body !== 'object' || Array.isArray(body)) return <Value v={body} />;
1610
+ const b = body as Record<string, unknown>;
1611
+ const messages = Array.isArray(b.messages) ? (b.messages as Array<{ role?: unknown; content?: unknown }>) : null;
1612
+ const rest = Object.entries(b).filter(([k]) => !(messages && k === 'messages'));
1246
1613
  return (
1247
1614
  <>
1248
- <p>{back}</p>
1249
- <h3>Trace <code>{id}</code></h3>
1250
- {!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" />}
1251
- {load.state === 'ok' && steps.length === 0 ? (
1252
- <div className="empty">
1253
- <p><strong>Nothing in this World carries this trace.</strong></p>
1254
- <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>
1255
- </div>
1256
- ) : null}
1257
- {steps.length > 0 ? (
1258
- <>
1259
- <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>
1260
- <div role="table" aria-label="Trace waterfall" className="waterfall" style={{ gridTemplateColumns: `minmax(130px, auto) repeat(${lanes.length}, minmax(140px, 1fr))` }}>
1261
- <div role="row" className="wf-row wf-head">
1262
- <span role="columnheader">time</span>
1263
- {lanes.map((l) => <span role="columnheader" key={l}>{l}</span>)}
1264
- </div>
1265
- {steps.map((s) => {
1266
- const ms = Date.parse(s.entry.occurredAt) - t0; const span = traceOf(s.entry.traceparent)?.span;
1267
- return (
1268
- <div role="row" className="wf-row" key={keyOf(s)}>
1269
- <span role="cell" className="wf-time"><At iso={s.entry.occurredAt} time /> <span className="mode">+{ms} ms</span></span>
1270
- {lanes.map((l) => (
1271
- <span role="cell" key={l} className={l === s.twin ? 'wf-cell on' : 'wf-cell'}>
1272
- {l === s.twin ? (
1273
- <details>
1274
- <summary>{s.entry.operation ?? s.entry.op} <code>{s.entry.subject.type}:{s.entry.subject.id}</code></summary>
1275
- <p className="mode">{actorOf(s.entry)} · {standing(s.entry)} · position {s.position}{span ? <> · span <code>{span}</code></> : null}</p>
1276
- <Fields fields={s.entry.fields} />
1277
- </details>
1278
- ) : null}
1279
- </span>
1280
- ))}
1281
- </div>
1282
- );
1283
- })}
1284
- </div>
1285
- </>
1286
- ) : null}
1615
+ {rest.length ? <dl className="fields">{rest.map(([k, v]) => <React.Fragment key={k}><dt>{k}</dt><dd><Value v={v} /></dd></React.Fragment>)}</dl> : null}
1616
+ {messages ? <ol className="messages">{messages.map((m, i) => <li key={i}><span className="role">{typeof m.role === 'string' ? m.role : 'message'}</span><Folded text={wordsOf(m.content)} /></li>)}</ol> : null}
1287
1617
  </>
1288
1618
  );
1289
1619
  }
1290
-
1291
1620
  function Fields({ fields }: { fields?: Record<string, unknown> }): React.JSX.Element {
1292
1621
  const rows = Object.entries(fields ?? {});
1293
1622
  if (rows.length === 0) return <p className="mode">No fields.</p>;
1294
- 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>;
1623
+ return <dl className="fields">{rows.map(([k, v]) => <React.Fragment key={k}><dt>{k}</dt><dd><Value v={v} /></dd></React.Fragment>)}</dl>;
1624
+ }
1625
+ /** Where an entry stands: a receipt when a push or a deploy gave it one, else where it came from. */
1626
+ const receiptOf = (e: Entry): string => (e.receipt ? `${e.receipt.status ?? ''}${e.receipt.externalId ? ` · ${e.receipt.externalId}` : ''}${e.receipt.reason ? ` · ${e.receipt.reason}` : ''}` : e.operation === 'observed' ? 'observed from the vendor' : e.provenance === 'placeholder' ? 'default data' : 'none: it has not left this World');
1627
+
1628
+ /** One event: when, at which vendor, what in words, on which record, by whom; opened, what it carried and what it wrote. */
1629
+ /** An event and the landed copies that are its receipts: a copy (`landsId`, or `aliasOf` under the vendor's id) is not an
1630
+ * event of its own but what a push or a deploy recorded of the entry it names. */
1631
+ type Folded = Placed & { copies: Placed[] };
1632
+ /** Each write once, its landed copies folded into it. A copy whose entry is not among these stays its own row, so nothing
1633
+ * the World answered is hidden. */
1634
+ function foldLanded(items: Placed[]): Folded[] {
1635
+ const byId = new Map<string, Folded>(); const out: Folded[] = []; const copies: Placed[] = [];
1636
+ const bySubject = new Map<string, Folded>();
1637
+ for (const p of items) {
1638
+ if (p.entry.landsId || p.entry.aliasOf) { copies.push(p); continue; }
1639
+ const f = { ...p, copies: [] }; out.push(f);
1640
+ if (!byId.has(`${p.twin}:${p.entry.id}`)) byId.set(`${p.twin}:${p.entry.id}`, f);
1641
+ if (!bySubject.has(`${p.twin}:${p.entry.subject.type}:${p.entry.subject.id}`)) bySubject.set(`${p.twin}:${p.entry.subject.type}:${p.entry.subject.id}`, f);
1642
+ }
1643
+ for (const c of copies) {
1644
+ // a copy folds into the entry its landsId names, and only there; the subject fallback is for a copy with no landsId
1645
+ const into = c.entry.landsId ? byId.get(`${c.twin}:${c.entry.landsId}`) : c.entry.aliasOf ? bySubject.get(`${c.twin}:${c.entry.subject.type}:${c.entry.aliasOf}`) : undefined;
1646
+ if (into) into.copies.push(c); else out.push({ ...c, copies: [] });
1647
+ }
1648
+ return out.sort(newestFirst);
1649
+ }
1650
+ /** How settled a receipt is: performed at the vendor over landed at the remote, a refusal or failure said as one. */
1651
+ const SETTLED: Record<string, number> = { deployed: 5, confirmed: 4, landed: 3, replayed: 2, skipped: 1 };
1652
+ function settledOf(f: Folded): { status: string; bad: boolean } | null {
1653
+ const all = [f.entry.receipt, ...f.copies.map((c) => c.entry.receipt)].filter((r): r is NonNullable<Entry['receipt']> => Boolean(r?.status));
1654
+ if (all.length === 0) return null;
1655
+ const bad = all.find((r) => r.status === 'failed' || r.status === 'refused');
1656
+ const best = [...all].sort((a, b) => (SETTLED[b.status!] ?? 0) - (SETTLED[a.status!] ?? 0))[0]!;
1657
+ return bad && !(SETTLED[best.status!]! >= 5) ? { status: bad.status!, bad: true } : { status: best.status!, bad: false };
1295
1658
  }
1296
1659
 
1660
+ type RequestReport = { hours: number; requests: number; journalFailures?: number; twins: Array<{ twin: string; requests: number; byStatus: Record<string, number>; p50Ms: number | null; p95Ms: number | null; topRoutes: Array<{ route: string; requests: number }> }> };
1661
+
1662
+
1663
+
1297
1664
  type MirrorSession = { state: 'pending' } | { state: 'ok'; scope: string } | { state: 'error'; error: string };
1298
1665
  /** THE CANVAS: every vendor's own screens as frames on one surface, the way a canvas app holds its frames (React
1299
1666
  * Flow; Figma's and tldraw's feel). Pan by dragging the background, zoom with the wheel or the controls, move a frame
1300
1667
  * by its title bar (kept in this browser), click a frame to use that vendor's screens and the background or Esc to
1301
1668
  * leave it, so a frame never captures the pan. A frame is its vendor's site at a desktop's size. */
1302
1669
  const FRAME_W = 1280; const FRAME_H = 820; const FRAME_GAP = 160;
1303
- type Screen = { world: string; vendor: string; active: boolean; open: (vendor: string) => void };
1670
+ type Screen = { world: string; vendor: string; active: boolean; open: (vendor: string) => void; /** its records changed and not yet cut: the screen's mark, as a changed file's */ changed: number };
1304
1671
  function ScreenFrame({ data }: NodeProps<Node<Screen>>): React.JSX.Element {
1305
- const { world, vendor, active, open } = data;
1672
+ const { world, vendor, active, open, changed } = data;
1306
1673
  return (
1307
1674
  <>
1308
1675
  <NodeToolbar isVisible position={Position.Top} align="start" className="frame-label">
1309
1676
  <Mark vendor={vendor} /><span>{vendorName(vendor)}</span>
1677
+ {changed ? <a className="frame-changed nodrag" href={`${BASE}/${world}/~/changes`} onClick={(e) => { e.preventDefault(); go([...world.split('/'), '~', 'changes']); }} title={`${counted(changed, 'record')} changed here, not in a changeset: see them in Changes`}><Icon name="square-dot" size={14} />{changed}</a> : null}
1310
1678
  </NodeToolbar>
1311
1679
  <div className={active ? 'frame on' : 'frame'} style={{ width: FRAME_W, height: FRAME_H }}>
1312
1680
  <div className="frame-bar" title="Drag to move">
1313
1681
  <Mark vendor={vendor} /><strong>{vendorName(vendor)}</strong>
1314
1682
  <span className="frame-url">{mirrorUrl(world, vendor).replace(/^https?:\/\//, '')}</span>
1315
- <a className="nodrag" href={`${BASE}/${world}/~/vendors/${vendor}`} onClick={(e) => { e.preventDefault(); open(vendor); }}>Open</a>
1683
+ <a className="nodrag" href={`${BASE}/${world}/~/screens/${vendor}`} onClick={(e) => { e.preventDefault(); open(vendor); }}>Open</a>
1316
1684
  <a className="nodrag" href={mirrorUrl(world, vendor)} target="_blank" rel="noopener noreferrer" aria-label={`Open ${vendorName(vendor)} in a new tab`}>↗</a>
1317
1685
  </div>
1318
1686
  <div className={active ? 'frame-body nodrag nopan nowheel' : 'frame-body'}>
@@ -1323,25 +1691,33 @@ function ScreenFrame({ data }: NodeProps<Node<Screen>>): React.JSX.Element {
1323
1691
  );
1324
1692
  }
1325
1693
  const FRAME_TYPES = { screen: ScreenFrame };
1326
- function Canvas(props: { name: string; twins: Load<{ twins: Twin[] }>; session: MirrorSession; retry: () => void; snapshot: Snapshot | null; back: () => void }): React.JSX.Element {
1694
+ function Canvas(props: { name: string; token: string; twins: Load<{ twins: Twin[] }>; session: MirrorSession; retry: () => void; snapshot: Snapshot | null; back: () => void }): React.JSX.Element {
1327
1695
  return <ReactFlowProvider><CanvasSurface {...props} /></ReactFlowProvider>;
1328
1696
  }
1329
1697
  /** 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
1330
1698
  * left (a canvas app's layers), zoom at the bottom left, the minimap at the bottom right. */
1331
- 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 {
1699
+ function CanvasSurface({ name, token, twins, session, retry, snapshot, back }: { name: string; token: string; twins: Load<{ twins: Twin[] }>; session: MirrorSession; retry: () => void; snapshot: Snapshot | null; back: () => void }): React.JSX.Element {
1700
+ // each screen's changes not yet cut, by vendor: the records, as Changes counts them
1701
+ const diff = useDoor<Diff>(token, `/-/${name}/diff`);
1702
+ const changed = useMemo((): Record<string, number> => {
1703
+ const by: Record<string, Set<string>> = {};
1704
+ if (diff.state === 'ok') for (const c of diff.data.changes) (by[c.twin] ??= new Set()).add(`${c.entry.subject.type}:${c.entry.subject.id}`);
1705
+ return Object.fromEntries(Object.entries(by).map(([v, rs]) => [v, rs.size]));
1706
+ }, [diff]);
1707
+ const changedAt = (v: string): number => changed[v] ?? 0;
1332
1708
  const frames = twins.state === 'ok' ? twins.data.twins.filter((t) => t.mirror).map((t) => t.twin) : [];
1333
1709
  const saved = `volter.canvas.${name}`;
1334
1710
  const [active, setActive] = useState<string | null>(null);
1335
1711
  const [layers, setLayers] = useState(true);
1336
1712
  const flow = useReactFlow();
1337
- const open = useCallback((v: string): void => go([...name.split('/'), '~', 'vendors', v]), [name]);
1713
+ const open = useCallback((v: string): void => go([...name.split('/'), '~', 'screens', v]), [name]);
1338
1714
  const cols = Math.max(1, Math.ceil(Math.sqrt(frames.length)));
1339
1715
  const placed = useCallback((): Array<Node<Screen>> => {
1340
1716
  let kept: Record<string, { x: number; y: number }> = {};
1341
1717
  try { kept = JSON.parse(localStorage.getItem(saved) ?? '{}') as Record<string, { x: number; y: number }>; } catch { /* a convenience */ }
1342
- 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 } }));
1718
+ 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, changed: changed[v] ?? 0 } }));
1343
1719
  // biome-ignore lint/correctness/useExhaustiveDependencies: the frames' names are what change it
1344
- }, [frames.join(','), name, cols, open]);
1720
+ }, [frames.join(','), name, cols, open, changed]);
1345
1721
  const [nodes, setNodes, onNodesChange] = useNodesState<Node<Screen>>([]);
1346
1722
  useEffect(() => { setNodes(placed()); }, [placed, setNodes]);
1347
1723
  useEffect(() => { setNodes((all) => all.map((n) => (n.data.active === (n.id === active) ? n : { ...n, data: { ...n.data, active: n.id === active } }))); }, [active, setNodes]);
@@ -1372,8 +1748,7 @@ function CanvasSurface({ name, twins, session, retry, snapshot, back }: { name:
1372
1748
  </div>
1373
1749
  {layers ? (
1374
1750
  <ul className="layer-list" aria-label="Screens">
1375
- {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>)}
1376
- {twins.state === 'ok' && frames.length === 0 ? <li className="mode">No vendor in this World has screens.</li> : null}
1751
+ {frames.map((v) => <li key={v}><button type="button" className={active === v ? 'on' : undefined} onClick={() => focus(v)}><Mark vendor={v} /><span>{vendorName(v)}</span>{changedAt(v) ? <span className="layer-changed" title={`${counted(changedAt(v), 'record')} changed, not in a changeset`}><Icon name="square-dot" size={14} />{changedAt(v)}</span> : null}</button></li>)}
1377
1752
  </ul>
1378
1753
  ) : null}
1379
1754
  <Pending load={twins} what="the vendors" />
@@ -1388,117 +1763,109 @@ function CanvasSurface({ name, twins, session, retry, snapshot, back }: { name:
1388
1763
  );
1389
1764
  }
1390
1765
 
1391
- /** What this World changed since its base — what a changeset would cut — in a compare view, by twin. */
1392
1766
  /** A changeset as the World's changesets door answers it: what it does at each vendor, the checks it ran, who signed
1393
- * which bytes, and whether it went to the real vendors (with a receipt for each change). */
1767
+ * which bytes, and where it went (`applied`: a push's or a deploy's receipts, one per change). */
1394
1768
  type ChangesetView = {
1395
- name: string; message?: string; narration?: string; createdAt: string; contentHash: string; /** the body's hash as it stands (GET …/changesets) */ currentHash?: string;
1769
+ /** who cut it, owns it and pushed it (a caller, a person, a caller), when recorded */
1770
+ cutBy?: string; owner?: string; pushedBy?: string;
1771
+ name: string; message?: string; narration?: string; createdAt: string; contentHash: string; /** the kernel's readiness under this World's rules (GET …/changesets) */ readiness?: { approvers: number; required: number; ready: boolean }; /** the body's hash as it stands (GET …/changesets) */ currentHash?: string;
1396
1772
  actions: Array<{ service: string; action: Omit<Entry, 'service' | 'op'> & { op?: string; operation?: string } }>;
1397
1773
  verification: { at: string; contentHash: string; passed: boolean; results: Array<{ id: string; service: string; passed: boolean; actual?: unknown }>; refusals?: Array<{ service: string; check: string; reason: string }> } | null;
1398
1774
  approvals: Array<{ principal: string; at: string; contentHash: string; note?: string }>;
1399
1775
  applied: { at: string; outcome: 'applied' | 'partial' | 'refused'; refusal?: string[]; receipts: Array<{ actionId: string; service: string; status: string; externalId?: string; url?: string; error?: string }> } | null;
1776
+ /** what a deploy did of each action, from the landed copies on each twin's log: only `deployed` was performed against the vendor */
1777
+ deployed?: Array<{ service: string; actionId: string; status: 'landed' | 'deployed' | 'refused' | 'failed' | 'skipped'; at: string; externalId?: string; url?: string; reason?: string }>;
1400
1778
  };
1401
- /** Its actions as the Changes view shows entries: by vendor. */
1402
- const changesetGroups = (c: ChangesetView): Array<[string, Entry[]]> => {
1403
- const m = new Map<string, Entry[]>();
1404
- for (const a of c.actions) m.set(a.service, [...(m.get(a.service) ?? []), { ...a.action, service: a.service, op: a.action.op ?? a.action.operation ?? '' } as Entry]);
1405
- return [...m.entries()];
1406
- };
1407
- /** Where it stands, in a word, and the state behind the word: a verification or an approval of other bytes is stale. */
1408
- function changesetStatus(c: ChangesetView): { label: string; tone: 'done' | 'good' | 'warn' | 'bad' | 'plain' } {
1409
- if (c.applied) return c.applied.outcome === 'applied' ? { label: 'Deployed', tone: 'done' } : c.applied.outcome === 'partial' ? { label: 'Partly deployed', tone: 'warn' } : { label: 'Deploy refused', tone: 'bad' };
1779
+ /** Its actions as the Changes page shows a diff's: each with its vendor. */
1780
+ const changesetChanges = (c: ChangesetView): Array<{ twin: string; entry: Entry }> => c.actions.map((a) => ({ twin: a.service, entry: { ...a.action, service: a.service, op: a.action.op ?? a.action.operation ?? '' } as Entry }));
1781
+ /** Where it went, from its receipts alone. Performed at the vendor: an action with a `deployed` receipt (a deploy from
1782
+ * here), or a push receipt `confirmed` (the remote performed it). Every action performed is Deployed, some is Partly
1783
+ * deployed; a refused or failed one is said as a failure; otherwise the push decides: every change replayed at the remote
1784
+ * is Pushed (the remote World, never a vendor), else Not sent. */
1785
+ function sentOf(c: ChangesetView): { label: string; tone: 'done' | 'good' | 'warn' | 'bad' | 'plain'; performed: number; failed: number; total: number } {
1786
+ const total = c.actions.length;
1787
+ const pushed = c.applied?.receipts ?? []; const deployed = c.deployed ?? [];
1788
+ const performed = new Set([...deployed.filter((r) => r.status === 'deployed'), ...pushed.filter((r) => r.status === 'confirmed')].map((r) => r.actionId)).size;
1789
+ const failed = new Set([...deployed.filter((r) => r.status === 'refused' || r.status === 'failed'), ...pushed.filter((r) => r.status === 'failed')].map((r) => r.actionId)).size;
1790
+ const n = { performed, failed, total };
1791
+ if (total > 0 && performed >= total) return { label: 'Deployed', tone: 'done', ...n };
1792
+ if (performed > 0) return { label: `Partly deployed: ${performed} of ${total}`, tone: 'warn', ...n };
1793
+ if (failed > 0) return { label: `${failed} refused or failed`, tone: 'bad', ...n };
1794
+ if (pushed.some((r) => r.status === 'replayed') && pushed.every((r) => r.status === 'replayed' || r.status === 'skipped')) return { label: 'Pushed', tone: 'good', ...n };
1795
+ return { label: 'Not sent', tone: 'plain', ...n };
1796
+ }
1797
+ /** Its review, in a word: a verification or an approval of other bytes is stale and does not count. */
1798
+ /** How this World's changesets are made and readied (its `rules` door): approvals needed, and whether a cut's summary is required. */
1799
+ type ChangesetRules = { approvals?: number; summary?: 'required' | 'generated' };
1800
+ /** A changeset's title: its author's summary, else what the summary generated from its changes did first (without the
1801
+ * fields it set, which its page lists), else its name. */
1802
+ const titleOf = (c: ChangesetView): string => c.message ?? c.narration?.split('\n')[0]?.replace(/ setting .*$/, '') ?? c.name;
1803
+ function reviewOf(c: ChangesetView, required = 1): { label: string; tone: 'good' | 'bad' | 'plain' } {
1410
1804
  const now = c.currentHash ?? c.contentHash;
1411
- const signed = c.approvals.filter((a) => a.contentHash === now).length;
1805
+ // the different people who approved its current contents, as readiness counts them (a key counts for its person)
1806
+ const signed = c.readiness?.approvers ?? new Set(c.approvals.filter((a) => a.contentHash === now).map((a) => a.principal)).size;
1412
1807
  const checked = c.verification && c.verification.contentHash === now ? c.verification : null;
1413
1808
  if (checked && !checked.passed) return { label: 'Checks failed', tone: 'bad' };
1809
+ if (signed > 0 && signed < required) return { label: `Approved by ${signed} of ${required}`, tone: 'plain' };
1414
1810
  if (signed > 0) return { label: `Approved by ${signed}`, tone: 'good' };
1415
1811
  if (checked) return { label: 'Verified', tone: 'good' };
1416
1812
  return { label: 'Not verified', tone: 'plain' };
1417
1813
  }
1418
1814
 
1419
- /** THE CHANGESETS: what this World would do, cut into reviewable pieces, listed as a repository lists pull requests;
1420
- * each one's page reviews it (its changes by vendor, its checks, its approvals) and, where the World has a real root,
1421
- * deploys it. Every button is the World's own door; a refusal is the door's words. */
1422
- function Changesets({ name, token, signsAs, canWrite, rooted, open: opened, refused }: { name: string; token: string; signsAs: string | null; canWrite: boolean; rooted: string[]; open?: string; refused: (r: Response) => void }): React.JSX.Element {
1423
- const [version, setVersion] = useState(0);
1424
- const list = useDoor<{ changesets: ChangesetView[] }>(token, `/-/${name}/changesets`, version);
1425
- const at = (c: string) => (e: React.MouseEvent): void => { e.preventDefault(); go([...name.split('/'), '~', 'changesets', c]); };
1426
- if (opened) {
1427
- const c = list.state === 'ok' ? list.data.changesets.find((x) => x.name === opened) : undefined;
1428
- return (
1429
- <>
1430
- <p className="crumbs-line"><a href={`${BASE}/${name}/~/changesets`} onClick={(e) => { e.preventDefault(); go([...name.split('/'), '~', 'changesets']); }}>← Changesets</a></p>
1431
- <Pending load={list} what="the changesets" />
1432
- {list.state === 'ok' && !c ? <p className="mode">This World has no changeset named {opened}.</p> : null}
1433
- {c ? <ChangesetPage name={name} token={token} signsAs={signsAs} c={c} canWrite={canWrite} rooted={rooted} refused={refused} onChanged={() => setVersion((v) => v + 1)} /> : null}
1434
- </>
1435
- );
1436
- }
1437
- const rows = list.state === 'ok' ? [...list.data.changesets].sort((a, b) => b.createdAt.localeCompare(a.createdAt)) : [];
1438
- return (
1439
- <>
1440
- <p className="lede">A changeset is what this World changed, cut into a piece a person reviews: what it does at each vendor, the World's checks over it, and who approved it. Where the World has a real account behind a vendor, an approved changeset deploys there, with a receipt for each change.</p>
1441
- <Pending load={list} what="the changesets" />
1442
- {list.state === 'ok' && rows.length === 0 ? (
1443
- <Empty title="No changesets yet">
1444
- <p>Cut one from what your app did in this World: <code>volter world changeset -m "What it does and why"</code>. Pushed to a shared World, it shows up here for review.</p>
1445
- </Empty>
1446
- ) : null}
1447
- {rows.length > 0 ? (
1448
- <div className="block">
1449
- <table className="dense" aria-label="Changesets">
1450
- <thead><tr><th>Changeset</th><th>Changes</th><th>Made</th><th>Status</th></tr></thead>
1451
- <tbody>{rows.map((c) => {
1452
- const s = changesetStatus(c);
1453
- const counts = changesetGroups(c).map(([v, es]) => `${es.length} at ${vendorName(v)}`).join(' · ');
1454
- return (
1455
- <tr key={c.name}>
1456
- <td><a href={`${BASE}/${name}/~/changesets/${c.name}`} onClick={at(c.name)}><b>{c.message ?? c.name}</b></a>{c.message ? <span className="mode mono"> {c.name}</span> : null}</td>
1457
- <td className="mode">{counts || 'nothing'}</td>
1458
- <td className="mode"><At iso={c.createdAt} /></td>
1459
- <td><span className={`badge cs-${s.tone}`}>{s.label}</span></td>
1460
- </tr>
1461
- );
1462
- })}</tbody>
1463
- </table>
1464
- </div>
1465
- ) : null}
1466
- </>
1467
- );
1468
- }
1469
1815
 
1470
- /** One changeset, reviewed: its words, its changes by vendor, its checks, its approvals and its receipts; Verify,
1471
- * Approve (signed as the person this session is for) and Deploy where they apply, each saying why when it does not. */
1472
- function ChangesetPage({ name, token, signsAs, c, canWrite, rooted, refused, onChanged }: { name: string; token: string; signsAs: string | null; c: ChangesetView; canWrite: boolean; rooted: string[]; refused: (r: Response) => void; onChanged: () => void }): React.JSX.Element {
1816
+ /** One changeset, reviewed: its words, its changes, its checks, its approvals, and where it went; Verify, Approve
1817
+ * (signed as the person this session is for) and Deploy where they apply, each saying why when it does not. */
1818
+ function ChangesetPage({ name, token, signsAs, c, canWrite, rooted, refused, onChanged, part, required }: { /** approvals this World requires */ required: number; name: string; token: string; signsAs: string | null; c: ChangesetView; canWrite: boolean; rooted: string[]; refused: (r: Response) => void; onChanged: () => void; /** which part of it (the workspace's History): its head with the acts, or its review (where it went, checks, approvals) */ part: 'head' | 'review' }): React.JSX.Element {
1819
+ const showHead = part === 'head'; const showReview = part === 'review';
1473
1820
  const [busy, setBusy] = useState<string | null>(null);
1474
1821
  const [approving, setApproving] = useState(false); const [note, setNote] = useState(''); const [as, setAs] = useState('');
1475
- const s = changesetStatus(c);
1476
- const groups = changesetGroups(c);
1477
- const touches = groups.map(([v]) => v);
1822
+ // why the last act was refused, in this page's words, with the acts that answer it
1823
+ const [problem, setProblem] = useState<{ what: string; text: string; acts: Act[] } | null>(null);
1824
+ const origin = useDoor<OriginDoor>(token, `/-/${name}/origin`);
1825
+ const sent = sentOf(c); const review = reviewOf(c, required);
1826
+ const changes = changesetChanges(c);
1827
+ const touches = [...new Set(changes.map((x) => x.twin))];
1478
1828
  const deployable = touches.some((v) => rooted.includes(v));
1479
1829
  const current = (h: string): boolean => h === (c.currentHash ?? c.contentHash);
1830
+ const receipts = c.applied?.receipts ?? [];
1831
+ const atVendor = (c.deployed ?? []).filter((r) => r.status !== 'landed');
1832
+ const byRemote = receipts.filter((r) => r.status === 'confirmed').length;
1833
+ const here = atVendor.filter((r) => r.status === 'deployed');
1480
1834
  const act = async (what: string, path: string, body?: unknown, done?: string): Promise<boolean> => {
1481
- setBusy(what);
1835
+ setBusy(what); setProblem(null);
1482
1836
  try {
1483
1837
  const r = await door(token, path, { method: 'POST', ...(body === undefined ? {} : { body: JSON.stringify(body) }) });
1484
- if (!r.ok) { refused(r); toast(`${what}: ${await failure(r)}`, 'error'); return false; }
1838
+ if (!r.ok) { refused(r); setProblem({ what, ...plainly(await doorWords(r)) }); return false; }
1485
1839
  if (done) toast(done);
1486
1840
  onChanged(); return true;
1487
- } catch (e) { toast(`${what}: the World did not answer (${e instanceof Error ? e.message : String(e)})`, 'error'); return false; } finally { setBusy(null); }
1841
+ } catch (e) { setProblem({ what, text: `the World did not answer (${e instanceof Error ? e.message : String(e)})`, acts: [] }); return false; } finally { setBusy(null); }
1488
1842
  };
1489
1843
  const verify = (): void => { void act('Verify', `/-/${name}/changesets/${c.name}/verify`, undefined, 'Verified: the checks ran over it'); };
1490
1844
  const approve = async (e: React.FormEvent): Promise<void> => { e.preventDefault(); if (await act('Approve', `/-/${name}/changesets/${c.name}/approve`, { ...(note.trim() ? { note: note.trim() } : {}), ...(signsAs === null && as.trim() ? { as: as.trim() } : {}) }, 'Approved')) { setApproving(false); setNote(''); } };
1491
1845
  const deploy = async (): Promise<void> => {
1492
- if (!(await confirmAct({ title: `Deploy ${c.name} to the real vendors?`, body: <>It performs its {c.actions.length} change{c.actions.length === 1 ? '' : 's'} against the real account{touches.filter((v) => rooted.includes(v)).length === 1 ? '' : 's'} behind {listed(touches.filter((v) => rooted.includes(v)).map(vendorName))}. This cannot be taken back here.</>, action: 'Deploy', danger: true, typed: c.name }))) return;
1493
- void act('Deploy', `/-/${name}/deploy/${c.name}`, { confirm: c.name }, 'Deployed: the receipts are below');
1846
+ const to = touches.filter((v) => rooted.includes(v));
1847
+ if (!(await confirmAct({ title: `Deploy ${c.name} to the real vendors?`, body: <>It performs its {c.actions.length} change{c.actions.length === 1 ? '' : 's'} against the real account{to.length === 1 ? '' : 's'} behind {listed(to.map(vendorName))}. This cannot be taken back here.</>, action: 'Deploy', danger: true, typed: c.name }))) return;
1848
+ setBusy('Deploy'); setProblem(null);
1849
+ try {
1850
+ const r = await door(token, `/-/${name}/deploy/${c.name}`, { method: 'POST', body: JSON.stringify({ confirm: c.name }) });
1851
+ if (!r.ok) { refused(r); setProblem({ what: 'Deploy', ...plainly(await doorWords(r)) }); return; }
1852
+ // what it did is the changeset's own receipts, read again: its actions' `deployed` receipts, never a count the door sums
1853
+ const again = await door(token, `/-/${name}/changesets/${c.name}`);
1854
+ const now = again.ok ? ((await again.json()) as ChangesetView) : null;
1855
+ if (!now) { toast('Deploy: sent; reading its receipts failed', 'info'); onChanged(); return; }
1856
+ const s = sentOf(now);
1857
+ toast(`Deploy: ${s.performed} of ${s.total} change${s.total === 1 ? '' : 's'} performed at the vendor${s.failed ? `; ${s.failed} refused or failed` : ''}`, s.failed ? 'error' : s.performed ? 'ok' : 'info');
1858
+ onChanged();
1859
+ } catch (e) { setProblem({ what: 'Deploy', text: `the World did not answer (${e instanceof Error ? e.message : String(e)})`, acts: [] }); } finally { setBusy(null); }
1494
1860
  };
1495
- // why a button is off, in words: the reader's scope, what already happened, or the World's own rule
1496
- const offWhy = !canWrite ? 'This session reads the World; it changes nothing' : c.applied ? 'Already deployed' : null;
1861
+ // why a button is off, in words: the reader's scope, or what already happened
1862
+ const offWhy = !canWrite ? 'This session reads the World; it changes nothing' : sent.label === 'Deployed' ? 'Already deployed: every change was performed at the vendor' : null;
1863
+ const there = origin.state === 'ok' && origin.data.origin ? originAddress(origin.data.origin) : null;
1497
1864
  return (
1498
1865
  <div className="changeset" aria-labelledby="changeset-title">
1499
- <div className="changeset-head">
1866
+ {showHead ? <><div className="changeset-head">
1500
1867
  <div>
1501
- <h3 id="changeset-title">{c.message ?? c.name} <span className={`badge cs-${s.tone}`}>{s.label}</span></h3>
1868
+ <h3 id="changeset-title">{c.message ?? c.name} <span className={`badge cs-${sent.tone}`}>{sent.label}</span> <span className={`badge cs-${review.tone}`}>{review.label}</span></h3>
1502
1869
  <p className="mode"><code>{c.name}</code> · made <At iso={c.createdAt} /> · {c.actions.length} change{c.actions.length === 1 ? '' : 's'} at {listed(touches.map(vendorName)) || 'no vendor'}</p>
1503
1870
  </div>
1504
1871
  <div className="changeset-actions">
@@ -1507,12 +1874,35 @@ function ChangesetPage({ name, token, signsAs, c, canWrite, rooted, refused, onC
1507
1874
  {deployable ? <button type="button" className="primary" onClick={() => void deploy()} disabled={Boolean(offWhy) || busy !== null} title={offWhy ?? 'Perform it against the real accounts behind this World'}>{busy === 'Deploy' ? 'Deploying…' : 'Deploy…'}</button> : null}
1508
1875
  </div>
1509
1876
  </div>
1510
- {offWhy && !c.applied ? <p className="mode">{offWhy}.</p> : null}
1511
- {!deployable && !c.applied ? <p className="mode">It stays in this World: no vendor it changes has a real account behind it here (a shared World with a root deploys; see Settings › Real services).</p> : null}
1877
+ {offWhy && sent.label !== 'Deployed' ? <p className="mode">{offWhy}.</p> : null}
1878
+ {problem ? (
1879
+ <div role="alert" className="refusal">
1880
+ <span><b>{problem.what}</b>: {problem.text}</span>
1881
+ {problem.acts.includes('verify') && canWrite ? <button type="button" onClick={verify} disabled={busy !== null}>Verify</button> : null}
1882
+ {problem.acts.includes('approve') && canWrite ? <button type="button" onClick={() => setApproving(true)} disabled={busy !== null}>Approve…</button> : null}
1883
+ <ActLinks name={name} acts={problem.acts} />
1884
+ </div>
1885
+ ) : null}
1886
+ </> : null}
1887
+ {showReview ? <>
1512
1888
  {c.narration ? <p className="narration">{c.narration}</p> : null}
1513
1889
 
1514
- <h4>Changes</h4>
1515
- {groups.length === 0 ? <p className="mode">It changes nothing.</p> : <ByVendor groups={groups} idPrefix={`cs-${c.name}`} />}
1890
+ <h4>Where it went</h4>
1891
+ <div className="block where">
1892
+ {sent.performed > 0 ? (
1893
+ <p className="pad">{sent.performed >= sent.total ? 'Deployed' : 'Partly deployed'}: {sent.performed} of {sent.total} change{sent.total === 1 ? '' : 's'} performed at {listed([...new Set([...here.map((r) => r.service), ...receipts.filter((r) => r.status === 'confirmed').map((r) => r.service)].map(vendorName))])}{here.length ? ` (${here.length} from here` : ' ('}{here.length && byRemote ? ', ' : ''}{byRemote ? `${byRemote} by the remote` : ''}).</p>
1894
+ ) : sent.label === 'Pushed' ? (
1895
+ <p className="pad">Pushed to the remote{there ? <> <code>{there}</code></> : null}{c.applied ? <> · <At iso={c.applied.at} /></> : null}: every change was replayed there. No vendor has performed any of it.</p>
1896
+ ) : sent.failed === 0 ? (
1897
+ <p className="pad">Not sent: it is only in this World.{c.applied?.outcome === 'refused' ? ' A push was refused (below); Push sends it again.' : <> Push sends it to the remote World{deployable ? '; Deploy, to the real vendors behind it' : ''}.</>}</p>
1898
+ ) : null}
1899
+ {sent.failed > 0 ? <p className="pad failed" role="alert">{sent.failed} change{sent.failed === 1 ? ' was' : 's were'} refused or failed at the vendor{[...atVendor.filter((r) => r.reason && (r.status === 'refused' || r.status === 'failed')).map((r) => r.reason!), ...receipts.filter((r) => r.status === 'failed' && r.error).map((r) => r.error!)].slice(0, 3).map((w) => `: ${plainly(w).text}`).join('')}.</p> : null}
1900
+ {c.applied?.refusal?.length ? <ul className="checks pad">{c.applied.refusal.map((r, i) => <li key={i} className="mode">{plainly(r).text}</li>)}</ul> : null}
1901
+ {atVendor.length ? <><p className="pad where-sub mode">At the vendor</p><ul className="receipts pad">{atVendor.map((r) => <li key={`v:${r.actionId}`}><span className={r.status === 'refused' || r.status === 'failed' ? 'badge cs-bad' : r.status === 'deployed' ? 'badge cs-done' : 'badge'}>{r.status}</span> {vendorName(r.service)} {r.status === 'deployed' ? (r.url ? <a href={r.url} target="_blank" rel="noopener noreferrer">{r.externalId ?? r.url}</a> : r.externalId ? <code>{r.externalId}</code> : null) : null}{r.reason ? <span className="mode"> · {plainly(r.reason).text}</span> : null} <span className="mode">· <At iso={r.at} /></span></li>)}</ul></> : null}
1902
+ {receipts.length ? <><p className="pad where-sub mode">At the remote</p><ul className="receipts pad">{receipts.map((r) => <li key={`r:${r.actionId}`}><span className={r.status === 'failed' ? 'badge cs-bad' : r.status === 'confirmed' ? 'badge cs-done' : 'badge'}>{r.status}</span> {vendorName(r.service)} <span className="mode">{r.status === 'replayed' ? 'replayed at the remote World' : r.status === 'confirmed' ? 'performed at the vendor by the remote' : ''}</span>{r.status === 'confirmed' && r.externalId ? <> <code>{r.externalId}</code></> : null}{r.error ? <span className="mode"> · {plainly(r.error).text}</span> : null}</li>)}</ul></> : null}
1903
+ </div>
1904
+ {!deployable && sent.label !== 'Deployed' ? <p className="mode">No vendor it changes is connected to its real service here, so it does not deploy from this World. <a href={`${BASE}/${name}/~/settings/services`} onClick={(e) => { e.preventDefault(); go([...name.split('/'), '~', 'settings', 'services']); }}>Connect a vendor in Settings › Real services</a> to deploy.</p> : null}
1905
+
1516
1906
 
1517
1907
  <h4>Checks</h4>
1518
1908
  {!c.verification ? <p className="mode">Not verified yet. Verify replays it in a throwaway World and runs this World's checks over the result.</p> : (
@@ -1527,15 +1917,7 @@ function ChangesetPage({ name, token, signsAs, c, canWrite, rooted, refused, onC
1527
1917
  {c.approvals.length === 0 ? <p className="mode">No one has approved it.</p> : (
1528
1918
  <ul className="approvals">{c.approvals.map((a, i) => <li key={i}><b>{a.principal}</b> <span className="mode"><At iso={a.at} />{current(a.contentHash) ? '' : ' · signed an earlier version'}</span>{a.note ? <div className="mode">“{a.note}”</div> : null}</li>)}</ul>
1529
1919
  )}
1530
-
1531
- {c.applied ? (
1532
- <>
1533
- <h4>Deployed</h4>
1534
- <p>{c.applied.outcome === 'applied' ? 'Every change reached the real vendors' : c.applied.outcome === 'partial' ? 'Some changes reached the real vendors' : 'The deploy was refused'} · <At iso={c.applied.at} /></p>
1535
- {c.applied.refusal?.length ? <ul className="checks">{c.applied.refusal.map((r, i) => <li key={i} className="mode">{r}</li>)}</ul> : null}
1536
- {c.applied.receipts.length ? <ul className="receipts">{c.applied.receipts.map((r) => <li key={r.actionId}><span className={r.status === 'failed' ? 'badge cs-bad' : 'badge cs-done'}>{r.status}</span> {vendorName(r.service)} {r.url ? <a href={r.url} target="_blank" rel="noopener noreferrer">{r.externalId ?? r.url}</a> : r.externalId ? <code>{r.externalId}</code> : null}{r.error ? <span className="mode"> · {r.error}</span> : null}</li>)}</ul> : null}
1537
- </>
1538
- ) : null}
1920
+ </> : null}
1539
1921
 
1540
1922
  <Dialog open={approving} onClose={() => { if (!busy) setApproving(false); }} title={`Approve ${c.name}`} footer={<><button type="button" onClick={() => setApproving(false)} disabled={busy !== null}>Cancel</button><button type="submit" form="approve-changeset" className="primary" disabled={busy !== null}>{busy === 'Approve' ? 'Approving…' : 'Approve'}</button></>}>
1541
1923
  <form id="approve-changeset" onSubmit={(e) => void approve(e)} className="stack">
@@ -1548,56 +1930,100 @@ function ChangesetPage({ name, token, signsAs, c, canWrite, rooted, refused, onC
1548
1930
  );
1549
1931
  }
1550
1932
 
1551
- /** What a set of entries does at each vendor, grouped by vendor, runs of three alike folded: the Changes view's and a
1552
- * changeset's page's one rendering. */
1553
- function ByVendor({ groups, idPrefix }: { groups: Array<[string, Entry[]]>; idPrefix: string }): React.JSX.Element {
1554
- return (<>{groups.map(([t, entries]) => (
1555
- <div key={t} className="compare" role="group" aria-labelledby={`${idPrefix}-${t}`}>
1556
- <h3 id={`${idPrefix}-${t}`} className="vendor-cell"><Mark vendor={t} />{vendorName(t)} <span className="mode">· {entries.length} change{entries.length === 1 ? '' : 's'}</span></h3>
1557
- <ul>{runsOf(entries).map((run) => run.length >= 3 ? (
1558
- <li key={run[0]!.id}>
1559
- <details>
1560
- <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>
1561
- <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>
1562
- </details>
1563
- </li>
1564
- ) : run.map((e) => (
1565
- <li key={e.id}>
1566
- <details>
1567
- <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>
1568
- <Fields fields={e.fields} />
1569
- </details>
1570
- </li>
1571
- )))}</ul>
1572
- </div>
1573
- ))}</>);
1933
+ /** Two field values the same, whatever their shape. */
1934
+ const same = (a: unknown, b: unknown): boolean => JSON.stringify(a) === JSON.stringify(b);
1935
+ /** What a set of changes does, as a working-tree diff reads (GitHub's): by vendor, then record by record, each write
1936
+ * with the fields it set, field by field: the value it had before (an earlier write here) and the value now. A field a
1937
+ * later write sets to what it already was is not a change and is not shown. The Changes page's, a changeset's and a
1938
+ * branch's one rendering. */
1939
+ function RecordChanges({ changes, idPrefix }: { changes: Array<{ twin: string; entry: Entry }>; idPrefix: string }): React.JSX.Element {
1940
+ const vendors = [...new Set(changes.map((c) => c.twin))];
1941
+ return (
1942
+ <>{vendors.map((t) => {
1943
+ const mine = changes.filter((c) => c.twin === t).map((c) => c.entry);
1944
+ const records = [...new Set(mine.map((e) => `${e.subject.type}:${e.subject.id}`))];
1945
+ return (
1946
+ <div key={t} className="compare" role="group" aria-labelledby={`${idPrefix}-${t}`}>
1947
+ <h3 id={`${idPrefix}-${t}`} className="vendor-cell"><Mark vendor={t} />{vendorName(t)} <span className="mode">· {counted(records.length, 'record')}, {counted(mine.length, 'write')}</span></h3>
1948
+ <ul>{records.map((rid) => {
1949
+ const writes = mine.filter((e) => `${e.subject.type}:${e.subject.id}` === rid);
1950
+ const head = writes[writes.length - 1]!; const s = said(head);
1951
+ let before: Record<string, unknown> = {};
1952
+ return (
1953
+ <li key={rid} className="record">
1954
+ <p className="record-head"><span className="kind">{head.subject.type.replace(/^_+/, '').replace(/[_-]+/g, ' ')}</span> {s.label ? <span className="label">“{s.label}”</span> : null} <code>{shown(head)}</code></p>
1955
+ {writes.map((e) => {
1956
+ const f = e.fields ?? {};
1957
+ const rows = Object.entries(f).filter(([k, v]) => !(k in before) || !same(before[k], v));
1958
+ const was = before; before = { ...before, ...f };
1959
+ return (
1960
+ <div key={e.id} className="write">
1961
+ <p className="mode"><span className="op">{said(e).what}</span> · <At iso={e.occurredAt} /> · {whoDid(e)}</p>
1962
+ {rows.length === 0 ? <p className="mode">It set no field to a new value.</p> : (
1963
+ <dl className="fields field-diff" aria-label={`Fields ${said(e).what} set`}>{rows.map(([k, v]) => (
1964
+ <React.Fragment key={k}>
1965
+ <dt>{k}</dt>
1966
+ <dd>{k in was ? <span className="changed"><del><Value v={was[k]} /></del><span className="arrow" aria-label="becomes">→</span><ins><Value v={v} /></ins></span> : <Value v={v} />}</dd>
1967
+ </React.Fragment>
1968
+ ))}</dl>
1969
+ )}
1970
+ </div>
1971
+ );
1972
+ })}
1973
+ </li>
1974
+ );
1975
+ })}</ul>
1976
+ </div>
1977
+ );
1978
+ })}</>
1979
+ );
1574
1980
  }
1575
1981
 
1576
- function Changes({ name, token }: { name: string; token: string }): React.JSX.Element {
1577
- const diff = useDoor<Diff>(token, `/-/${name}/diff`);
1578
- const byTwin = useMemo(() => {
1579
- const m = new Map<string, Entry[]>();
1580
- if (diff.state === 'ok') for (const c of diff.data.changes) m.set(c.twin, [...(m.get(c.twin) ?? []), c.entry]);
1581
- return [...m.entries()];
1582
- }, [diff]);
1982
+ /** Cut a changeset (committing from the working changes): this branch's changes not cut, as one changeset to review
1983
+ * and push. Said on the page when it is done or refused. */
1984
+ function CutChangeset({ name, token, changes, canWrite, refused, onCut, pinned, generated, inputRef, author }: { name: string; token: string; changes: number; canWrite: boolean; refused: (r: Response) => void; onCut: () => void; /** the workspace's: pinned along the foot of Changes, there (and off) when there is nothing to cut, as a commit box is */ pinned?: boolean; /** this World's rules take the summary generated from the changes when none is given */ generated?: boolean; /** who cuts it and onto which branch, as GitHub Desktop's commit box shows its author and "Commit 1 file to main" */ author?: { face: React.ReactNode; who: string; account: string; branch: string }; /** the message box, for the toolbar's Cut to focus */ inputRef?: React.RefObject<HTMLInputElement | null> }): React.JSX.Element {
1985
+ const [message, setMessage] = useState(''); const [busy, setBusy] = useState(false); const [log, setLog] = useState<Said[]>([]);
1986
+ const say = (ok: boolean, text: string, acts?: Act[]): void => { setLog((l) => [{ ok, text, at: new Date().toISOString(), n: ++saidCount, ...(acts?.length ? { acts } : {}) }, ...l].slice(0, 4)); };
1987
+ const cut = async (e: React.FormEvent): Promise<void> => {
1988
+ e.preventDefault(); setBusy(true);
1989
+ try {
1990
+ const r = await door(token, `/-/${name}/changesets`, { method: 'POST', body: JSON.stringify(message.trim() ? { message: message.trim() } : {}) });
1991
+ if (!r.ok) { refused(r); const p = plainly(await doorWords(r)); say(false, `Cut a changeset: ${p.text}`, p.acts); return; }
1992
+ const b = (await r.json()) as { name?: string; actions?: unknown[] };
1993
+ say(true, `Cut ${b.name ?? 'a changeset'}: ${counted(b.actions?.length ?? 0, 'change')}, ready to review in History and push.`); setMessage(''); onCut();
1994
+ } catch (err) { say(false, `Cut a changeset: the World did not answer (${err instanceof Error ? err.message : String(err)})`); } finally { setBusy(false); }
1995
+ };
1996
+ if (pinned) {
1997
+ const off = !canWrite ? 'This session reads the World; it changes nothing' : changes === 0 ? 'Nothing to cut: no change is outside a changeset' : null;
1998
+ return (
1999
+ <div className="ws-cut">
2000
+ <Narration log={log} name={name} />
2001
+ <form onSubmit={(e) => void cut(e)} aria-label="Cut a changeset">
2002
+ {author ? <a className="ws-author" href={author.account} title={`${author.who}: your account`} onClick={(e) => { e.preventDefault(); go(author.account.slice(BASE.length + 1).split('/')); }}>{author.face}</a> : null}
2003
+ <input ref={inputRef} name="changesetMessage" aria-label="Summary" value={message} onChange={(e) => setMessage(e.target.value)} placeholder={changes === 0 ? 'Nothing to cut' : generated ? 'Summary (optional)' : 'Summary (required)'} required={!generated} maxLength={200} disabled={Boolean(off)} title={off ?? undefined} />
2004
+ <button type="submit" className="primary" disabled={Boolean(off) || busy} title={off ?? undefined}>{busy ? 'Cutting…' : changes === 0 ? 'Cut a changeset' : <>Cut {counted(changes, 'change')}{author ? <> on <b>{author.branch}</b></> : null}</>}</button>
2005
+ </form>
2006
+ </div>
2007
+ );
2008
+ }
2009
+ // nothing to cut (or no write): only what the last cut said stays
2010
+ if (!canWrite || changes === 0) return <Narration log={log} name={name} />;
1583
2011
  return (
1584
2012
  <>
1585
- <Pending load={diff} what="the changes" />
1586
- {diff.state === 'ok' ? (
1587
- <>
1588
- <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>
1589
- {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> : <ByVendor groups={byTwin} idPrefix="changes" />}
1590
- </>
1591
- ) : null}
2013
+ <form className="block cut pad" onSubmit={(e) => void cut(e)} aria-label="Cut a changeset">
2014
+ <label><span>Cut {changes === 1 ? 'this change' : `these ${changes} changes`} into a changeset: what it does and why</span><input name="changesetMessage" value={message} onChange={(e) => setMessage(e.target.value)} placeholder="Welcome email for new sign-ups" required maxLength={200} disabled={changes === 0} /></label>
2015
+ <button type="submit" className="primary" disabled={busy || changes === 0}>{busy ? 'Cutting…' : 'Cut a changeset'}</button>
2016
+ </form>
2017
+ <Narration log={log} name={name} />
1592
2018
  </>
1593
2019
  );
1594
2020
  }
1595
2021
 
1596
- /** The operator's tables: each twin's standing and root, the requests, the changesets — and the host admin's rotate and remove. */
2022
+
1597
2023
  /** 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
1598
2024
  * made, listed with when it was last used, and revoked alone; `volter remote add` makes one through the platform. */
1599
2025
  function Keys({ name, token }: { name: string; token: string }): React.JSX.Element {
1600
- type Key = { id: string; name: string; scope: string; createdAt: string; lastUsedAt: string | null };
2026
+ type Key = { id: string; name: string; scope: string; createdAt: string; lastUsedAt: string | null; /** the person it acts for, when it names one */ person?: string | null };
1601
2027
  const [version, setVersion] = useState(0);
1602
2028
  const keys = useDoor<{ keys: Key[] }>(token, `/-/${name}/keys`, version);
1603
2029
  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);
@@ -1609,7 +2035,7 @@ function Keys({ name, token }: { name: string; token: string }): React.JSX.Eleme
1609
2035
  const label = `Read-only link · ${new Date().toLocaleDateString(undefined, { month: 'short', day: 'numeric' })}`;
1610
2036
  const r = await door(token, `/-/${name}/keys`, { method: 'POST', body: JSON.stringify({ name: label, scope: 'read' }) }).catch(() => null);
1611
2037
  const b = r ? ((await r.json().catch(() => ({}))) as { key?: string; error?: string }) : { error: `${ORIGIN} did not answer` };
1612
- if (!r?.ok || !b.key) { toast(b.error ?? 'refused', 'error'); return; }
2038
+ if (!r?.ok || !b.key) { toast(b.error ? plainly(b.error).text : 'refused', 'error'); return; }
1613
2039
  setLink(`${window.location.origin}${BASE}/${name}#${new URLSearchParams({ token: b.key }).toString()}`); setVersion((v) => v + 1);
1614
2040
  };
1615
2041
  const make = async (e: React.FormEvent): Promise<void> => {
@@ -1617,7 +2043,7 @@ function Keys({ name, token }: { name: string; token: string }): React.JSX.Eleme
1617
2043
  const r = await door(token, `/-/${name}/keys`, { method: 'POST', body: JSON.stringify({ name: keyName.trim(), scope }) }).catch(() => null);
1618
2044
  setBusy(false);
1619
2045
  const b = r ? ((await r.json().catch(() => ({}))) as { key?: string; error?: string }) : { error: `${ORIGIN} did not answer` };
1620
- if (!r?.ok || !b.key) { setError(b.error ?? 'refused'); return; }
2046
+ if (!r?.ok || !b.key) { setError(b.error ? plainly(b.error).text : 'refused'); return; }
1621
2047
  setMaking(false); setMade({ key: b.key, name: keyName.trim() }); setKeyName(''); setVersion((v) => v + 1);
1622
2048
  };
1623
2049
  const revoke = async (k: Key): Promise<void> => {
@@ -1633,9 +2059,9 @@ function Keys({ name, token }: { name: string; token: string }): React.JSX.Eleme
1633
2059
  <Pending load={keys} what="the keys" retry={() => setVersion((v) => v + 1)} />
1634
2060
  {keys.state === 'ok' ? (keys.data.keys.length === 0 ? <p className="mode">No keys yet. The World's own token opens it too.</p> : (
1635
2061
  <table className="dense" aria-label="Keys">
1636
- <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>
2062
+ <thead><tr><th>Name</th><th>For</th><th>Access</th><th>Made</th><th>Last used</th><th className="num"><span className="sr">Actions</span></th></tr></thead>
1637
2063
  <tbody>{keys.data.keys.map((k) => (
1638
- <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>
2064
+ <tr key={k.id}><td>{k.name}</td><td>{k.person ?? <span className="mode" title="It acts as itself: a service">no one: a service</span>}</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>
1639
2065
  ))}</tbody>
1640
2066
  </table>
1641
2067
  )) : null}
@@ -1658,182 +2084,298 @@ function Keys({ name, token }: { name: string; token: string }): React.JSX.Eleme
1658
2084
  );
1659
2085
  }
1660
2086
 
1661
- 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 {
1662
- const [twins, setTwins] = useState<Twin[] | null>(null);
1663
- const [changesets, setChangesets] = useState<Changeset[] | null>(null);
1664
- const [note, setNote] = useState<string | null>(null);
2087
+ /** What an act did, said on the page where it was done: its words, and whether it went. */
2088
+ type Said = { ok: boolean; text: string; at: string; n: number; acts?: Act[] };
2089
+ /** Each narrated line's own key: two acts can finish within one millisecond. */
2090
+ let saidCount = 0;
2091
+ function Narration({ log, name }: { log: Said[]; name: string }): React.JSX.Element | null {
2092
+ if (log.length === 0) return null;
2093
+ return <ul className="narration-log" role="status" aria-live="polite">{log.map((l) => <li key={l.n} className={l.ok ? 'ok' : 'bad'}><span className="mode"><At iso={l.at} time /></span> {l.text}{l.acts ? <ActLinks name={name} acts={l.acts} /> : null}</li>)}</ul>;
2094
+ }
2095
+
2096
+ /** Push and pull through the origin's doors, and what they read (the origin, the changes not cut, the changesets not
2097
+ * pushed): Remote's acts, and the workspace toolbar's. Each act is said in `log`, a refusal as a toast too. */
2098
+ function usePushPull(name: string, token: string, tick: number, refused: (r: Response) => void) {
2099
+ const [version, setVersion] = useState(0);
2100
+ const every = version * 100_000 + Math.floor(tick / 2);
2101
+ const origin = useDoor<OriginDoor>(token, `/-/${name}/origin`, every);
2102
+ const diff = useDoor<Diff>(token, `/-/${name}/diff`, every);
2103
+ const sets = useDoor<{ changesets: ChangesetView[] }>(token, `/-/${name}/changesets`, every);
2104
+ const [log, setLog] = useState<Said[]>([]); const [busy, setBusy] = useState<string | null>(null);
2105
+ const say = (ok: boolean, text: string, acts?: Act[]): void => { setLog((l) => [{ ok, text, at: new Date().toISOString(), n: ++saidCount, ...(acts?.length ? { acts } : {}) }, ...l].slice(0, 8)); if (!ok) toast(text, 'error'); };
2106
+ /** One act through its door: the answer's body, or null when it was refused (and said so). */
2107
+ const send = async <T,>(what: string, path: string, init: RequestInit): Promise<T | null> => {
2108
+ setBusy(what);
2109
+ try {
2110
+ const r = await door(token, `/-/${name}/${path}`, init);
2111
+ if (!r.ok) { refused(r); const p = plainly(await doorWords(r)); say(false, `${what}: ${p.text}`, p.acts); return null; }
2112
+ return (await r.json()) as T;
2113
+ } catch (e) { say(false, `${what}: the World did not answer (${e instanceof Error ? e.message : String(e)})`); return null; } finally { setBusy(null); setVersion((v) => v + 1); }
2114
+ };
2115
+ const there = origin.state === 'ok' && origin.data.origin ? originAddress(origin.data.origin) : null;
2116
+ const changes = diff.state === 'ok' ? diff.data.changes.length : null;
2117
+ const waiting = sets.state === 'ok' ? unpushed(sets.data.changesets).sort((a, b) => a.createdAt.localeCompare(b.createdAt)) : [];
2118
+ const push = async (): Promise<void> => {
2119
+ const b = await send<{ pushed: Array<{ changeset: string; outcome: string; receipts?: Array<{ status: string }>; refusal?: string[] }> }>('Push', 'origin/push', { method: 'POST' });
2120
+ if (!b) return;
2121
+ if (b.pushed.length === 0) { say(true, 'Nothing to push: every changeset is already at the remote.'); return; }
2122
+ for (const p of b.pushed) {
2123
+ const rs = p.receipts ?? [];
2124
+ const replayed = rs.filter((r) => r.status === 'replayed').length; const confirmed = rs.filter((r) => r.status === 'confirmed').length; const failed = rs.filter((r) => r.status === 'failed').length;
2125
+ const ok = p.outcome === 'applied' && failed === 0;
2126
+ if (p.outcome === 'refused') { say(false, `Push of ${p.changeset} was refused${p.refusal?.length ? `: ${p.refusal.map((w) => plainly(w).text).join('; ')}` : ''}.`); continue; }
2127
+ const parts = [`${replayed} of ${counted(rs.length, 'change')} replayed there`, ...(confirmed ? [`${confirmed} performed at the vendor by the remote`] : []), ...(failed ? [`${failed} failed`] : [])];
2128
+ say(ok, `${p.outcome === 'partial' ? 'Partly pushed' : 'Pushed'} ${p.changeset} to ${there ?? 'the remote'}: ${parts.join('; ')}${p.refusal?.length ? ` (${p.refusal.map((w) => plainly(w).text).join('; ')})` : ''}.`);
2129
+ }
2130
+ };
2131
+ const pull = async (): Promise<void> => {
2132
+ const b = await send<{ appended: Record<string, number>; rebased: Array<{ twin: string; moved: boolean; conflicts: Array<{ subject: { type: string; id: string }; field: string }> }> }>('Pull', 'origin/pull', { method: 'POST' });
2133
+ if (!b) return;
2134
+ const came = Object.entries(b.appended).filter(([, n]) => n > 0);
2135
+ const conflicts = b.rebased.flatMap((r) => r.conflicts.map((c) => `${vendorName(r.twin)} ${c.subject.type}:${c.subject.id} ${c.field}`));
2136
+ say(conflicts.length === 0, `Pulled from ${there ?? 'the remote'}: ${came.length ? came.map(([t, n]) => `${counted(n, 'entry')} at ${vendorName(t)}`).join(', ') : 'nothing new'}${conflicts.length ? `; conflicts at ${conflicts.join(', ')}` : ''}.`);
2137
+ };
2138
+ return { origin, diff, sets, there, changes, waiting, log, busy, say, send, push, pull };
2139
+ }
2140
+
2141
+ /** THE REMOTE, as GitHub Desktop's push and pull are: the World this one pushes to and pulls from (its origin), how much
2142
+ * is not yet pushed there, and the acts: cut the changes into a changeset, push, pull, name the origin. Each act says
2143
+ * what it did on this page. */
2144
+ function Remote({ name, token, canWrite, tick, refused }: { name: string; token: string; canWrite: boolean; tick: number; refused: (r: Response) => void }): React.JSX.Element {
2145
+ const { origin, there, changes, log, busy, say, send, push, pull } = usePushPull(name, token, tick, refused);
2146
+ const [url, setUrl] = useState(''); const [secret, setSecret] = useState(''); const [naming, setNaming] = useState(false);
2147
+ const name_ = async (e: React.FormEvent): Promise<void> => {
2148
+ e.preventDefault();
2149
+ const b = await send<{ origin: { url: string; namespace: string }; appended: Record<string, number> }>('Name the remote', 'origin', { method: 'PUT', body: JSON.stringify({ url: url.trim(), token: secret.trim() }) });
2150
+ if (b) { const n = Object.values(b.appended ?? {}).reduce((a, x) => a + x, 0); say(true, `The remote is ${originAddress(b.origin)}: its history came in (${counted(n, 'entry')}).`); setSecret(''); setNaming(false); }
2151
+ };
2152
+ const off = !canWrite ? 'This session reads the World; it changes nothing' : null;
2153
+ return (
2154
+ <>
2155
+ <div className="block remote">
2156
+ <div className="block-head"><h3>Origin</h3>{canWrite ? <button type="button" onClick={() => { setUrl(there ?? ''); setNaming(!naming); }} aria-expanded={naming}>{there ? 'Change the remote…' : 'Name a remote…'}</button> : null}</div>
2157
+ <div className="pad">
2158
+ {origin.state === 'ok' ? (there
2159
+ ? <p><code className="big">{there}</code>{origin.data.origin?.fetchedAt ? <span className="mode"> · last fetched <At iso={origin.data.origin.fetchedAt} /></span> : null}</p>
2160
+ : <p>No remote — this World keeps its default data. Name one to push its changesets to a shared World and pull what others pushed there.</p>) : <Pending load={origin} what="the remote" lines={1} />}
2161
+ {naming ? (
2162
+ <form className="stack origin-form" onSubmit={(e) => void name_(e)} aria-label="Name the remote">
2163
+ <label><span>The World's address</span><input name="originUrl" value={url} onChange={(e) => setUrl(e.target.value)} placeholder="https://worlds.example.com/acme/team" required /></label>
2164
+ <label><span>A key or token that opens it</span><input name="originToken" type="password" value={secret} onChange={(e) => setSecret(e.target.value)} required autoComplete="off" /></label>
2165
+ <p className="mode">Naming it brings its history in. Your pushes are recorded there as whoever this key was made for, or as the key itself.</p>
2166
+ <div className="row-actions"><button type="submit" className="primary" disabled={busy !== null}>{busy === 'Name the remote' ? 'Bringing its history in…' : 'Save the remote'}</button><button type="button" onClick={() => setNaming(false)}>Cancel</button></div>
2167
+ </form>
2168
+ ) : null}
2169
+ </div>
2170
+ </div>
2171
+ <Narration log={log} name={name} />
2172
+ </>
2173
+ );
2174
+ }
2175
+
2176
+ /** THE VAULT, as Supabase's is: the credentials this World holds for real vendors (each sealed for the one place it may
2177
+ * go: a vendor's root, or a link), whether each is sealed, when and by what, and sealing one; then the links, each a
2178
+ * vendor this World forwards to with its sealed credential, with their requests. A credential is never shown back. */
2179
+ function Vault({ name, token, canWrite, refused }: { name: string; token: string; canWrite: boolean; refused: (r: Response) => void }): React.JSX.Element {
2180
+ const [version, setVersion] = useState(0);
2181
+ const twins = useDoor<{ twins: Twin[] }>(token, `/-/${name}/twins`, version);
2182
+ const links = useDoor<{ links: Record<string, { origin: string }> }>(token, `/-/${name}/links`, version);
2183
+ const report = useDoor<RequestReport>(token, `/-/${name}/requests?hours=24`, version);
2184
+ const [linkCreds, setLinkCreds] = useState<Record<string, NonNullable<Twin['credential']> | null>>({});
2185
+ const [sealing, setSealing] = useState<{ vendor: string; to: string } | null>(null); const [secret, setSecret] = useState(''); const [busy, setBusy] = useState(false);
2186
+ const [log, setLog] = useState<Said[]>([]);
2187
+ const say = (ok: boolean, text: string, acts?: Act[]): void => { setLog((l) => [{ ok, text, at: new Date().toISOString(), n: ++saidCount, ...(acts?.length ? { acts } : {}) }, ...l].slice(0, 6)); if (!ok) toast(text, 'error'); };
2188
+ const linkNames = links.state === 'ok' ? Object.keys(links.data.links).sort() : [];
2189
+ // a link's credential is its own door's answer (`…/twins/<link>/credential`): sealed or not, never the secret
2190
+ useEffect(() => {
2191
+ let live = true;
2192
+ void Promise.all(linkNames.map(async (l) => { const r = await door(token, `/-/${name}/twins/${l}/credential`).catch(() => null); const b = r?.ok ? ((await r.json()) as { credential?: Twin['credential'] }) : null; return [l, b?.credential ?? null] as const; })).then((all) => { if (live) setLinkCreds(Object.fromEntries(all)); });
2193
+ return () => { live = false; };
2194
+ // biome-ignore lint/correctness/useExhaustiveDependencies: the links' names are what change it
2195
+ }, [name, token, linkNames.join(','), version]);
2196
+ const seal = async (e: React.FormEvent): Promise<void> => {
2197
+ e.preventDefault(); if (!sealing || !secret) return; setBusy(true);
2198
+ const r = await door(token, `/-/${name}/twins/${sealing.vendor}/credential`, { method: 'PUT', body: secret, headers: { 'content-type': 'text/plain' } }).catch(() => null);
2199
+ setBusy(false);
2200
+ if (!r) { say(false, `Seal ${sealing.vendor}: ${ORIGIN} did not answer`); return; }
2201
+ if (!r.ok) { refused(r); const p = plainly(await doorWords(r)); say(false, `Seal ${vendorName(sealing.vendor)}: ${p.text}`, p.acts); return; }
2202
+ const b = (await r.json().catch(() => ({}))) as { fingerprint?: string; boundTo?: string };
2203
+ say(true, `Sealed a credential for ${vendorName(sealing.vendor)}${b.fingerprint ? ` (${b.fingerprint})` : ''}: it opens for ${b.boundTo ?? sealing.to} only, and is never shown again.`);
2204
+ setSecret(''); setSealing(null); setVersion((v) => v + 1);
2205
+ };
2206
+ const removeLink = async (l: string): Promise<void> => {
2207
+ if (!(await confirmAct({ title: `Remove the link ${l}?`, body: `Requests to ${WORLDS}/${name}/${l}/… stop being forwarded to ${links.state === 'ok' ? links.data.links[l]?.origin : 'its origin'}.`, action: 'Remove link', danger: true }))) return;
2208
+ const r = await door(token, `/-/${name}/links/${l}`, { method: 'DELETE' }).catch(() => null);
2209
+ if (!r?.ok) { if (r) refused(r); say(false, `Remove ${l}: ${r ? await failure(r) : 'the World did not answer'}`); return; }
2210
+ say(true, `Removed the link ${l}.`); setVersion((v) => v + 1);
2211
+ };
2212
+ const sealedText = (c: Twin['credential']): React.JSX.Element => (c ? <><span className="badge rooted">sealed</span> <span className="mode">{c.fingerprint ? <code>{c.fingerprint}</code> : null} · <At iso={c.placedAt} />{c.sealedBy ? ` · by ${c.sealedBy === 'vault' ? 'the vault' : 'the host\'s key'}` : ''}</span></> : <span className="mode">none</span>);
2213
+ const list = twins.state === 'ok' ? twins.data.twins : [];
2214
+ return (
2215
+ <>
2216
+ <p className="lede">A real vendor's credential lives in one place, sealed: beside this World, bound to where it goes. It is never readable back; this page says only that one is sealed, when, by what, and for where.</p>
2217
+ <Narration log={log} name={name} />
2218
+ <section className="block" aria-labelledby="creds-head">
2219
+ <div className="block-head"><h3 id="creds-head">Vendors</h3></div>
2220
+ <Pending load={twins} what="the vendors" />
2221
+ {twins.state === 'ok' ? (
2222
+ <table className="dense" aria-label="Credentials by vendor">
2223
+ <thead><tr><th>Vendor</th><th>Credential</th><th>Bound to</th><th>Real service</th>{canWrite ? <th className="num"><span className="sr">Actions</span></th> : null}</tr></thead>
2224
+ <tbody>{list.map((t) => (
2225
+ <tr key={t.twin}>
2226
+ <td><span className="vendor-cell"><Mark vendor={t.twin} />{vendorName(t.twin)}</span></td>
2227
+ <td>{sealedText(t.credential)}</td>
2228
+ <td>{t.credential?.boundTo ? <code>{t.credential.boundTo}</code> : <span className="mode">—</span>}</td>
2229
+ <td>{t.root?.url ? <code>{t.root.url}</code> : <span className="mode">none: a stand-in</span>}</td>
2230
+ {canWrite ? <td className="num"><button type="button" disabled={!t.root?.url} title={t.root?.url ? `Seal ${vendorName(t.twin)}'s real credential for ${t.root.url}` : 'Connect it to its real service first (Settings › Real services): a credential is sealed for where it goes'} onClick={() => { setSecret(''); setSealing({ vendor: t.twin, to: t.root!.url! }); }}>{t.credential ? 'Replace…' : 'Seal a credential…'}</button></td> : null}
2231
+ </tr>
2232
+ ))}</tbody>
2233
+ </table>
2234
+ ) : null}
2235
+ {twins.state === 'ok' && list.every((t) => !t.root?.url) ? <p className="mode pad">No vendor here has a real service behind it, so none holds a credential. Connect one in Settings › Real services; its credential is sealed here.</p> : null}
2236
+ </section>
2237
+ <section className="block" aria-labelledby="links-head">
2238
+ <div className="block-head"><h3 id="links-head">Links</h3><span className="mode">a vendor with no twin, forwarded with its sealed credential</span></div>
2239
+ <Pending load={links} what="the links" />
2240
+ {links.state === 'ok' && linkNames.length === 0 ? <p className="mode pad">No links. A link forwards requests for a vendor with no twin here, at <code>{WORLDS}/{name}/&lt;link&gt;/…</code>, to that vendor's real API with the credential sealed for it.</p> : null}
2241
+ {linkNames.length ? (
2242
+ <table className="dense" aria-label="Links">
2243
+ <thead><tr><th>Link</th><th>Origin</th><th className="num">Requests, 24h</th><th>Credential</th>{canWrite ? <th className="num"><span className="sr">Actions</span></th> : null}</tr></thead>
2244
+ <tbody>{linkNames.map((l) => {
2245
+ const at = links.state === 'ok' ? links.data.links[l]!.origin : '';
2246
+ const traffic = report.state === 'ok' ? report.data.twins.find((t) => t.twin === l) : undefined;
2247
+ return (
2248
+ <tr key={l}>
2249
+ <td><b>{l}</b></td>
2250
+ <td><code>{at}</code></td>
2251
+ <td className="num">{traffic ? <span title={Object.entries(traffic.byStatus).map(([k, v]) => `${k}: ${v}`).join(' · ')}>{traffic.requests}</span> : <span className="mode">—</span>}</td>
2252
+ <td>{sealedText(linkCreds[l] ?? null)}{linkCreds[l]?.boundTo ? <div className="mode">bound to <code>{linkCreds[l]!.boundTo}</code></div> : null}</td>
2253
+ {canWrite ? <td className="num row-actions"><button type="button" onClick={() => { setSecret(''); setSealing({ vendor: l, to: at }); }}>{linkCreds[l] ? 'Replace…' : 'Seal a credential…'}</button><button type="button" onClick={() => void removeLink(l)}>Remove…</button></td> : null}
2254
+ </tr>
2255
+ );
2256
+ })}</tbody>
2257
+ </table>
2258
+ ) : null}
2259
+ </section>
2260
+ <Dialog open={Boolean(sealing)} onClose={() => { if (!busy) setSealing(null); }} title={`Seal ${sealing ? vendorName(sealing.vendor) : ''}'s credential`} footer={<><button type="button" onClick={() => setSealing(null)} disabled={busy}>Cancel</button><button type="submit" form="seal-credential" className="primary" disabled={busy || !secret}>{busy ? 'Sealing…' : 'Seal it'}</button></>}>
2261
+ <form id="seal-credential" className="stack" onSubmit={(e) => void seal(e)}>
2262
+ <p className="mode">It is sealed for <code>{sealing?.to}</code> and opens there only. It is never shown again, here or anywhere.</p>
2263
+ <label><span>The real key</span><input name="credential" type="password" value={secret} onChange={(e) => setSecret(e.target.value)} autoComplete="off" required autoFocus /></label>
2264
+ </form>
2265
+ </Dialog>
2266
+ </>
2267
+ );
2268
+ }
2269
+
2270
+ /** THE SETTINGS, as Neon's and Supabase's project settings are: who may reach this World (its keys and the read-only
2271
+ * link), its connections to the real services (a vendor's root, reading from it, its deploy policy), its checks, and the
2272
+ * host admin's acts on the World itself. */
2273
+ function Settings({ name, token, admin, canWrite, refused, onRemoved, only }: { name: string; token: string; admin: string | null; canWrite: boolean; refused: (r: Response) => void; onRemoved: () => void; /** one section alone (a tab of the workspace's Settings window); every section when absent */ only?: 'access' | 'services' | 'clock' | 'checks' }): React.JSX.Element {
2274
+ const shows = (k: 'access' | 'services' | 'clock' | 'checks'): boolean => !only || only === k;
2275
+ const [version, setVersion] = useState(0);
2276
+ const twins = useDoor<{ twins: Twin[] }>(token, `/-/${name}/twins`, version);
2277
+ const checks = useDoor<{ checks: string[] }>(token, `/-/${name}/checks`, version);
2278
+ const [clockVersion, setClockVersion] = useState(0);
2279
+ const clock = useDoor<Clock>(token, `/-/${name}/clock`, clockVersion);
2280
+ const [log, setLog] = useState<Said[]>([]);
1665
2281
  const [tokens, setTokens] = useState<{ token: string; readToken: string } | null>(null);
1666
- type Report = { hours: number; requests: number; journalFailures?: number; twins: Array<{ twin: string; requests: number; byStatus: Record<string, number>; p50Ms: number | null; p95Ms: number | null; topRoutes: Array<{ route: string; requests: number }> }> };
1667
- const [report, setReport] = useState<Report | null>(null);
1668
- const refresh = useCallback(async () => {
1669
- const t = await door(token, `/-/${name}/twins`);
1670
- setTwins(t.ok ? ((await t.json()) as { twins: Twin[] }).twins : []);
1671
- const rq = await door(token, `/-/${name}/requests?hours=24`); setReport(rq.ok ? ((await rq.json()) as Report) : null);
1672
- const c = await door(token, `/-/${name}/changesets`);
1673
- setChangesets(c.ok ? ((await c.json()) as { changesets: Changeset[] }).changesets : []);
1674
- }, [name, token]);
1675
- useEffect(() => { void refresh(); }, [refresh]);
1676
- const act = async (label: string, path: string, init: RequestInit): Promise<void> => {
1677
- setNote(null);
1678
- const r = await door(token, path, init);
1679
- if (!r.ok) refused(r);
1680
- setNote(r.ok ? `${label}: done` : `${label}: ${await failure(r)}`);
1681
- await refresh();
2282
+ const [connecting, setConnecting] = useState<Twin | null>(null); const [url, setUrl] = useState(''); const [policy, setPolicy] = useState<'gated' | 'auto' | 'hold'>('gated'); const [busy, setBusy] = useState(false);
2283
+ const say = (ok: boolean, text: string, acts?: Act[]): void => { setLog((l) => [{ ok, text, at: new Date().toISOString(), n: ++saidCount, ...(acts?.length ? { acts } : {}) }, ...l].slice(0, 6)); if (!ok) toast(text, 'error'); };
2284
+ const act = async (label: string, path: string, init: RequestInit): Promise<boolean> => {
2285
+ const r = await door(token, path, init).catch(() => null);
2286
+ if (!r) { say(false, `${label}: the World did not answer`); return false; }
2287
+ if (!r.ok) { refused(r); const p = plainly(await doorWords(r)); say(false, `${label}: ${p.text}`, p.acts); return false; }
2288
+ say(true, `${label}: done.`); setVersion((v) => v + 1); return true;
2289
+ };
2290
+ const connect = async (e: React.FormEvent): Promise<void> => {
2291
+ e.preventDefault(); if (!connecting) return; setBusy(true);
2292
+ const prior = (connecting.root ?? {}) as Record<string, unknown>;
2293
+ const ok = await act(url.trim() ? `Connect ${vendorName(connecting.twin)} to ${url.trim()}` : `Disconnect ${vendorName(connecting.twin)}`, `/-/${name}/twins/${connecting.twin}/root`, { method: 'PUT', body: JSON.stringify(url.trim() ? { ...prior, url: url.trim(), deploy: policy } : null) });
2294
+ setBusy(false); if (ok) setConnecting(null);
1682
2295
  };
1683
2296
  const rotate = async (): Promise<void> => {
1684
2297
  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;
1685
2298
  const r = await door(admin!, `/-/worlds/${name}/rotate`, { method: 'POST' });
1686
- 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}`);
2299
+ if (r.ok) { setTokens((await r.json()) as { token: string; readToken: string }); say(true, 'Rotated: the new tokens are shown once, below.'); } else say(false, `Rotate: ${await failure(r)}`);
1687
2300
  };
1688
2301
  const remove = async (): Promise<void> => {
1689
2302
  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;
1690
2303
  const r = await door(admin!, `/-/worlds/${name}`, { method: 'DELETE' }).catch((e: unknown) => e as Error);
1691
- if (r instanceof Error) { setNote(`remove: the host did not answer (${r.message})`); return; }
1692
- if (r.ok) onRemoved(); else setNote(`remove: answered ${r.status}`);
2304
+ if (r instanceof Error) { say(false, `Remove: the host did not answer (${r.message})`); return; }
2305
+ if (r.ok) onRemoved(); else say(false, `Remove: ${await failure(r)}`);
1693
2306
  };
2307
+ const list = twins.state === 'ok' ? twins.data.twins : [];
2308
+ const POLICY: Record<string, string> = { gated: 'gated: a person deploys an approved changeset', auto: 'auto: a push deploys at once', hold: 'hold: nothing deploys' };
1694
2309
  return (
1695
2310
  <>
1696
- {note ? <p role="status">{note}</p> : null}
1697
- {tokens ? <p role="status">token <code>{tokens.token}</code> · read token <code>{tokens.readToken}</code></p> : null}
1698
- {canWrite ? <Keys name={name} token={token} /> : null}
1699
- <h3>Real services</h3>
1700
- <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>
1701
- {twins === null ? <p>Loading…</p> : (
1702
- <table aria-label="Real services">
1703
- <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>
1704
- <tbody>
1705
- {twins.map((t) => (
1706
- <tr key={t.twin}>
1707
- <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>
1708
- <td>{t.root?.url ? <code>{t.root.url}</code> : <span className="mode">not connected</span>}</td>
1709
- <td>{t.credential ? `sealed ${t.credential.fingerprint ?? ''} · ${when(t.credential.placedAt)}` : <span className="mode">none</span>}</td>
1710
- <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>
1711
- <td>{t.lastReceipt ? `${t.lastReceipt.status ?? ''} ${t.lastReceipt.externalId ?? ''} ${t.lastReceipt.reason ?? ''}`.trim() : <span className="mode">—</span>}</td>
1712
- {canWrite ? (
1713
- <td>
1714
- <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>{' '}
1715
- <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>{' '}
1716
- <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>
1717
- </td>
1718
- ) : null}
1719
- </tr>
1720
- ))}
1721
- </tbody>
1722
- </table>
1723
- )}
1724
- <h3>Traffic (last 24 hours)</h3>
1725
- {report && (report.journalFailures ?? 0) > 0 ? <p className="mode" role="alert">{report.journalFailures} request {report.journalFailures === 1 ? 'line was' : 'lines were'} not kept by this World's server since it started (its links, and the twins it runs in its own process): those requests are missing below.</p> : null}
1726
- {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 kept unless the World runs with <code>VOLTER_TWIN_REQUEST_JOURNAL=0</code>; what the requests changed is always in Activity.</p> : (
1727
- <table aria-label="Requests by twin">
1728
- <thead><tr><th>vendor</th><th>requests</th><th>by status</th><th>p50</th><th>p95</th><th>top routes</th></tr></thead>
1729
- <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>
1730
- </table>
1731
- )}
1732
- <h3>Sending changes to the real services</h3>
1733
- <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>
1734
- {changesets === null ? <p>Loading…</p> : changesets.length === 0 ? <p className="mode">No changesets yet.</p> : (
1735
- <table>
1736
- <thead><tr><th>name</th><th>changes</th><th>approvals</th>{canWrite ? <th>actions</th> : null}</tr></thead>
1737
- <tbody>
1738
- {changesets.map((c) => (
1739
- <tr key={c.name}>
1740
- <td>{c.name}</td>
1741
- <td>{Array.isArray(c.actions) ? c.actions.length : '—'}</td>
1742
- <td>{Array.isArray(c.approvals) ? c.approvals.length : 0}</td>
1743
- {canWrite ? (
1744
- <td>
1745
- <button onClick={() => void act(`verify ${c.name}`, `/-/${name}/changesets/${encodeURIComponent(c.name)}/verify`, { method: 'POST' })}>Verify</button>{' '}
1746
- <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>{' '}
1747
- <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>
1748
- </td>
1749
- ) : null}
1750
- </tr>
1751
- ))}
1752
- </tbody>
1753
- </table>
1754
- )}
1755
- {admin ? (
2311
+ <Narration log={log} name={name} />
2312
+ {shows('access') ? <>
2313
+ <h3 className="settings-head">Access</h3>
2314
+ {canWrite ? <Keys name={name} token={token} /> : <p className="mode">This session reads the World: its keys are managed by whoever holds its token.</p>}
2315
+ </> : null}
2316
+ {shows('services') ? <>
2317
+ <h3 className="settings-head">Real services</h3>
2318
+ <p className="mode">{list.some((t) => t.root?.url) ? null : 'Every vendor here is a stand-in. '}Connect a vendor to its real service to read the real state into this World; its real credential is sealed in the Vault, and an approved changeset deploys there by its policy.</p>
2319
+ <Pending load={twins} what="the vendors" />
2320
+ {twins.state === 'ok' ? (
2321
+ <div className="block">
2322
+ <table className="dense" aria-label="Real services">
2323
+ <thead><tr><th>Vendor</th><th>Real service</th><th>Deploy policy</th><th>Last read from it</th>{canWrite ? <th className="num"><span className="sr">Actions</span></th> : null}</tr></thead>
2324
+ <tbody>{list.map((t) => {
2325
+ const root = t.root as { url?: string; deploy?: string } | null | undefined;
2326
+ return (
2327
+ <tr key={t.twin}>
2328
+ <td><span className="vendor-cell"><Mark vendor={t.twin} /><a href={`${BASE}/${name}/${t.twin}`} title={`Its log and stored state · ${t.position ?? 0} entries`} onClick={(e) => { e.preventDefault(); go([...name.split('/'), t.twin]); }}>{vendorName(t.twin)}</a></span></td>
2329
+ <td>{root?.url ? <code>{root.url}</code> : <span className="mode">not connected</span>}</td>
2330
+ <td className="mode">{root?.url ? POLICY[root.deploy ?? ''] ?? root.deploy ?? '—' : '—'}</td>
2331
+ <td>{t.refresh?.failed ? <span role="alert">failed ×{t.refresh.failed.count ?? 1}: {t.refresh.failed.reason ?? ''}</span> : root?.url ? when(t.refresh?.observedAt ?? t.refresh?.at) : <span className="mode">—</span>}</td>
2332
+ {canWrite ? (
2333
+ <td className="num row-actions">
2334
+ <button type="button" onClick={() => { setUrl(root?.url ?? ''); setPolicy(root?.deploy === 'auto' || root?.deploy === 'hold' ? root.deploy : 'gated'); setConnecting(t); }}>{root?.url ? 'Change…' : 'Connect…'}</button>
2335
+ <button type="button" disabled={!root?.url} title={root?.url ? "Read the real service's current state into this World" : 'Connect it first'} 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(`Read from ${vendorName(t.twin)}`, `/-/${name}/twins/${t.twin}/refresh`, { method: 'POST' }); })}>Read from it</button>
2336
+ </td>
2337
+ ) : null}
2338
+ </tr>
2339
+ );
2340
+ })}</tbody>
2341
+ </table>
2342
+ </div>
2343
+ ) : null}
2344
+ </> : null}
2345
+ {shows('clock') ? <>
2346
+ <h3 className="settings-head">Clock</h3>
2347
+ <p className="mode">The instant every vendor here stamps from. Set it and move it forward to see what happens later: renewals, expiries, scheduled sends.</p>
2348
+ <ClockBar name={name} token={token} clock={clock} canWrite={canWrite} onChanged={() => setClockVersion((v) => v + 1)} refused={refused} open />
2349
+ </> : null}
2350
+ {shows('checks') ? <>
2351
+ <h3 className="settings-head">Checks</h3>
2352
+ <p className="mode">JavaScript the World runs over every change before it is performed (a changeset's Verify runs them too).</p>
2353
+ <Pending load={checks} what="the checks" />
2354
+ {checks.state === 'ok' ? (checks.data.checks.length === 0 ? <p className="mode">No checks: every change is performed as it comes.</p> : (
2355
+ <div className="block"><table className="dense" aria-label="Checks"><tbody>{checks.data.checks.map((c) => <tr key={c}><td><code>{c}</code></td></tr>)}</tbody></table></div>
2356
+ )) : null}
2357
+ </> : null}
2358
+ {admin && !only ? (
1756
2359
  <div className="danger" role="region" aria-label="Danger zone">
1757
2360
  <h3>Danger zone</h3>
1758
- <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>
1759
- <p><button onClick={() => void remove()}>Remove world</button> <span className="mode">Deletes this World and everything in it.</span></p>
2361
+ {tokens ? <p role="status">token <code>{tokens.token}</code> · read token <code>{tokens.readToken}</code></p> : null}
2362
+ <p><button type="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>
2363
+ <p><button type="button" onClick={() => void remove()}>Remove world</button> <span className="mode">Deletes this World and everything in it.</span></p>
1760
2364
  </div>
1761
2365
  ) : null}
2366
+ <Dialog open={Boolean(connecting)} onClose={() => { if (!busy) setConnecting(null); }} title={`Connect ${connecting ? vendorName(connecting.twin) : ''} to its real service`} footer={<><button type="button" onClick={() => setConnecting(null)} disabled={busy}>Cancel</button><button type="submit" form="connect-root" className="primary" disabled={busy}>{busy ? 'Saving…' : url.trim() ? 'Connect' : 'Disconnect'}</button></>}>
2367
+ <form id="connect-root" className="stack" onSubmit={(e) => void connect(e)}>
2368
+ <label><span>The real service's API address</span><input name="rootUrl" value={url} onChange={(e) => setUrl(e.target.value)} placeholder="https://api.openai.com" autoFocus /><span className="mode">Empty disconnects it.</span></label>
2369
+ <label><span>Deploy policy</span><select name="rootDeploy" value={policy} onChange={(e) => setPolicy(e.target.value === 'auto' ? 'auto' : e.target.value === 'hold' ? 'hold' : 'gated')}>{(['gated', 'auto', 'hold'] as const).map((p) => <option key={p} value={p}>{POLICY[p]}</option>)}</select></label>
2370
+ </form>
2371
+ </Dialog>
1762
2372
  </>
1763
2373
  );
1764
2374
  }
1765
2375
 
1766
- function TwinView({ name, token, twin }: { name: string; token: string; twin: string }): React.JSX.Element {
1767
- const [tab, setTab] = useState<'log' | 'tree'>('log');
1768
- const [log, setLog] = useState<{ position: number; entries: Entry[] } | null>(null);
1769
- const [tree, setTree] = useState<Array<Record<string, unknown>> | null>(null);
1770
- useEffect(() => {
1771
- void (async () => {
1772
- const head = await door(token, `/-/${name}/log/${twin}?limit=1`);
1773
- if (!head.ok) { setLog({ position: 0, entries: [] }); return; }
1774
- const { position } = (await head.json()) as { position: number };
1775
- const after = Math.max(0, position - 200);
1776
- const r = await door(token, `/-/${name}/log/${twin}?after=${after}&limit=200&receipts=1`);
1777
- const body = (await r.json()) as { position: number; entries: Entry[] };
1778
- setLog({ position: body.position, entries: [...body.entries].reverse() });
1779
- const c = await door(token, `/-/${name}/checkpoint/${twin}`);
1780
- setTree(c.ok ? ((await c.json()) as { tree: Array<Record<string, unknown>> }).tree : []);
1781
- })();
1782
- }, [name, token, twin]);
1783
- // the twin's mirror (its vendor's own UI over this World's state), when its pack has one. Opening it
1784
- // posts the token to the World's own session door on the Worlds' origin — a form, so the session is
1785
- // set there first-party and the browser goes on to the mirror; the token never enters a URL
1786
- const [mirror, setMirror] = useState(false);
1787
- useEffect(() => { void door(token, `/-/${name}/twins/${twin}/status`).then(async (r) => setMirror(r.ok && ((await r.json()) as { mirror?: boolean }).mirror === true), () => setMirror(false)); }, [name, token, twin]);
1788
- const mirrorAt = `${WORLDS}/${name}/${twin}/mirror/`;
1789
- const openMirror = (): void => {
1790
- const form = document.createElement('form');
1791
- form.method = 'POST'; form.action = `${WORLDS}/-/${name}/session`;
1792
- for (const [k, v] of [['token', token], ['next', `/${name}/${twin}/mirror/`]]) { const input = document.createElement('input'); input.type = 'hidden'; input.name = k!; input.value = v!; form.append(input); }
1793
- document.body.append(form); form.submit(); form.remove();
1794
- };
1795
- const receipt = (e: Entry): string => (e.operation === 'observed' ? `observed${e.provenance === 'placeholder' ? ' (default data)' : ' (vendor)'}` : e.receipt ? `${e.receipt.status ?? ''} ${e.receipt.externalId ?? ''} ${e.receipt.reason ?? ''}`.trim() : e.provenance === 'placeholder' ? 'default data' : 'unpushed');
1796
- return (
1797
- <section aria-labelledby="twin">
1798
- <h2 id="twin">{twin} in {name}</h2>
1799
- <p>position {log?.position ?? '…'} · <code>{WORLDS}/{name}/{twin}</code>{mirror ? <> · <a href={mirrorAt} onClick={(e) => { e.preventDefault(); openMirror(); }}>Open as {twin} shows it</a></> : null}</p>
1800
- <div role="tablist">
1801
- <button role="tab" aria-selected={tab === 'log'} onClick={() => setTab('log')}>Log</button>
1802
- <button role="tab" aria-selected={tab === 'tree'} onClick={() => setTab('tree')}>Tree</button>
1803
- </div>
1804
- {tab === 'log' ? (
1805
- log === null ? <p>Loading…</p> : log.entries.length === 0 ? <p>No changes yet.</p> : (
1806
- <table>
1807
- <thead><tr><th>when</th><th>operation</th><th>subject</th><th>actor</th><th>standing</th></tr></thead>
1808
- <tbody>
1809
- {log.entries.map((e) => (
1810
- <tr key={e.id}>
1811
- <td>{when(e.occurredAt)}</td>
1812
- <td>{e.operation ?? e.op}</td>
1813
- <td><code>{e.subject.type}:{e.subject.id}</code></td>
1814
- <td>{e.actor ? `${e.actor.kind}${e.actor.id ? ` ${e.actor.id}` : ''}` : '—'}</td>
1815
- <td>{standing(e)}</td>
1816
- </tr>
1817
- ))}
1818
- </tbody>
1819
- </table>
1820
- )
1821
- ) : (
1822
- tree === null ? <p>Loading…</p> : tree.length === 0 ? <p>The tree is empty.</p> : (
1823
- <table>
1824
- <thead><tr><th>type</th><th>id</th><th>fields</th></tr></thead>
1825
- <tbody>
1826
- {tree.filter((r) => r.deleted !== true).slice(0, 500).map((r) => {
1827
- const { type, id, ...rest } = r as { type: string; id: string } & Record<string, unknown>;
1828
- return <tr key={`${type}:${id}`}><td>{type}</td><td><code>{id}</code></td><td><pre>{JSON.stringify(rest, null, 1).slice(0, 600)}</pre></td></tr>;
1829
- })}
1830
- </tbody>
1831
- </table>
1832
- )
1833
- )}
1834
- </section>
1835
- );
1836
- }
1837
2376
 
1838
- applyTheme(); // the person's theme before the first paint
1839
- createRoot(ROOT).render(<><App /><Toaster /><ConfirmHost /><MenuDismiss /></>);
2377
+ /** The console's whole page: what main.tsx mounts, and what a story renders over recorded doors. */
2378
+ export function ConsolePage(): React.JSX.Element {
2379
+ return <><App /><Toaster /><ConfirmHost /><MenuDismiss /></>;
2380
+ }
2381
+ export { ROOT };