@volter/world-console 2.0.18 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,15 +19,5 @@ volter-host serve --dir ./worlds # prints the console URL alongside the admin
19
19
 
20
20
  Built on `@volter/world-host` and `@volter/world-runtime`. Apache-2.0.
21
21
 
22
- ## Designing the pages
23
-
24
- `bun run storybook` (in `apps/console`) serves every page at http://127.0.0.1:6106 as the real console over a recorded
25
- World's doors (`stories/demo-world.json`) and a host's inventory as its admin reads it (`stories/demo-host.json`);
26
- `stories/doors.ts` answers them, and an act answers ok and changes nothing. A change to `client/` shows in the open story
27
- at once. A recording is a served World's doors read with its token (each door's answer, the World named `acme/web`), its
28
- tokens never kept: a World the stories show is made through the product (`volter world init`, the app's own writes
29
- through `volter world run`, cuts, pushes and keys through its doors), never written by hand. A vendor's screens are its own site, which no recording holds:
30
- `STORYBOOK_SCREENS=http://127.0.0.1:<port>/<org>/<world> STORYBOOK_SCREENS_TOKEN=<its token> bun run storybook` shows them
31
- from a World served on this machine. The pages' map is
32
- [docs/contributing/console.md](../../docs/contributing/console.md).
22
+ The pages' map is [docs/contributing/console.md](../../docs/contributing/console.md).
33
23
 
@@ -592,7 +592,6 @@ main.app.home { max-width: none; }
592
592
  .ws-screens-marks .mark { width: 16px; height: 16px; }
593
593
  .ws-screens-marks .more { font: 11px var(--mono); color: var(--faint); }
594
594
  .ws-screens-entry .count { margin-left: 6px !important; }
595
- .ws-on-screens { margin-left: auto; display: inline-flex; align-items: center; gap: 6px; font-size: 12.5px; white-space: nowrap; }
596
595
  .ws-account { display: flex; align-items: center; gap: 14px; margin: 8px 0 16px; padding: 14px; border: 1px solid var(--line); border-radius: 8px; background: var(--surface); }
597
596
  .ws-account-face { display: grid; place-items: center; width: 40px; height: 40px; border-radius: 50%; background: var(--surface-2); font-weight: 600; }
598
597
  .ws-account .cell-label small + small { margin-top: 2px; }
@@ -8,7 +8,7 @@ import { applyTheme, storedTheme, type Theme, At, Brand, ConfirmHost, MenuDismis
8
8
  import { agentPrompt } from '../../../packages/cli/src/agent-prompt.ts';
9
9
  import { ICONS } from './brand/icons.ts';
10
10
 
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 };
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; screens?: Workspace[] };
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
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 };
14
14
  type Mode = { kind: 'admin'; worlds: Row[] } | { kind: 'world'; worlds: Array<{ name: string; base: string; twins: Row['twins'] }> };
@@ -22,11 +22,11 @@ const ORIGIN = window.location.origin;
22
22
  const WORLDS = (ROOT.dataset.worlds ?? ORIGIN).replace(/\/+$/, '');
23
23
  /** The Worlds a World token may open, when the host does not list them (a hosted host keeps its tenants' names to itself). */
24
24
  const NAMES = (ROOT.dataset.names ?? '').split(',').filter((n) => n !== '');
25
- // Served on a World's own origin, beside the World's twins and mirrors (same-origin frames of this tab), the console
25
+ // Served on a World's own origin, beside the World's twins and their screens (same-origin frames of this tab), the console
26
26
  // keeps no token any script there could read: a token (typed, or carried in the fragment) is exchanged once for the
27
27
  // World's HttpOnly session, and the doors ride that cookie. SESSION stands in for the token from then on.
28
28
  const ON_WORLD = ROOT.dataset.worldOrigin === '1';
29
- // the browser's fetch as it was when the console loaded, before any mirror frame existed: a same-origin mirror that
29
+ // the browser's fetch as it was when the console loaded, before any screen frame existed: a same-origin screen that
30
30
  // later replaces `parent.fetch` does not see the console's requests through it. Hardening, not isolation (such a frame
31
31
  // reaches much else); a sign-out's fresh document is what ends its reach into this one
32
32
  const nativeFetch = window.fetch.bind(window);
@@ -206,7 +206,7 @@ function moveTo(origin: string, token: string, name: string, path?: string): voi
206
206
  }
207
207
 
208
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. */
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
209
+ // where the path is kept: the address bar, or (a page whose root says `data-route="memory"`, one embedded where it has no addresses of its own) this
210
210
  // page's memory, so a page that is not served at its own addresses still moves between its pages
211
211
  let memoryPath: string | null = ROOT.dataset.route === 'memory' ? '' : null;
212
212
  /** Move a memory-routed page to `path` (under the base), as following a link there would. */
@@ -395,7 +395,8 @@ function App(): React.JSX.Element {
395
395
  const tabSub = tabPart === 'changes' && argPart === 'changesets' && argMore && PART.test(argMore) ? argMore
396
396
  // a record on a vendor's screens: `~/screens/<vendor>/<type>/<id>`
397
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)
398
+ // Slack message's is its timestamp); it opens the vendor's screens, which open no record by name yet (architecture,
399
+ // "Workspace")
399
400
  : tabPart === 'screens' && argMore && argLast && /^[A-Za-z0-9_.-]+$/.test(argMore) && RECORD_ID.test(argLast) ? `${argMore}:${argLast}` : undefined;
400
401
  // only a World's name opens a World page (and so reaches its doors); any other path is the list of Worlds
401
402
  const name = worldOf(org, world);
@@ -516,7 +517,7 @@ function Worlds({ mode, admin, reload }: { mode: Mode; admin: string | null; rel
516
517
  }
517
518
 
518
519
  type Clock = { at: string; frozen: boolean };
519
- type WorldMap = { world: string; twins: Array<{ twin: string; position: number; mirror: boolean; root: { url: string } | null; resources: Array<{ type: string; count: number }> }> };
520
+ type WorldMap = { world: string; twins: Array<{ twin: string; position: number; screens?: Workspace[]; root: { url: string } | null; resources: Array<{ type: string; count: number }> }> };
520
521
  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 };
521
522
  type Timeline = { entries: Placed[]; next: string | null };
522
523
  type Diff = { base: { id: string; at: string | null }; changes: Array<{ twin: string; entry: Entry }> };
@@ -532,8 +533,14 @@ type Scope = 'pending' | 'read' | 'write' | 'unknown';
532
533
  * settings tabs addressed on their own, remote and vault). Anything else opens Changes. */
533
534
  const TABS = ['changes', 'events', 'trace', 'vendors', 'screens', 'canvas', 'branches', 'settings', 'remote', 'vault'] as const;
534
535
  type Tab = (typeof TABS)[number] | 'overview';
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). */
536
- const mirrorUrl = (name: string, vendor: string): string => `${WORLDS}/${name}/${vendor}/`;
536
+ /** A workspace screen of a twin's vendor, as its `GET /twin` lists it (Viewing a World). */
537
+ type Workspace = { id: string; path: string };
538
+ /** Where a twin's vendor screens are shown: its first workspace that names no parameter, at the twin's place in the
539
+ * World; null when it lists none. */
540
+ const screenUrl = (name: string, t: { twin: string; screens?: Workspace[] } | undefined): string | null => {
541
+ const shown = t?.screens?.find((w) => !/[{<]/.test(w.path));
542
+ return t && shown ? `${WORLDS}/${name}/${t.twin}${shown.path.startsWith('/') ? shown.path : `/${shown.path}`}` : null;
543
+ };
537
544
  /** What an entry did, in words: the thing's kind and the vendor's verb, the thing's own label where it has one. */
538
545
  const DONE: Record<string, string> = { verify: 'verified', use: 'used', issue: 'issued', revoke: 'revoked', rotate: 'rotated', seed: 'seeded', upsert: 'saved', set: 'set', start: 'started', finish: 'finished', fail: 'failed', schedule: 'scheduled', deliver: 'delivered', like: 'liked', follow: 'followed', reply: 'replied', create: 'created', update: 'updated', delete: 'deleted', record: 'recorded', remove: 'removed', add: 'added', send: 'sent', post: 'posted', postmessage: 'posted', cancel: 'canceled', close: 'closed', open: 'opened', publish: 'published', upload: 'uploaded', invite: 'invited', archive: 'archived', merge: 'merged', refund: 'refunded', capture: 'captured', confirm: 'confirmed', pay: 'paid' };
539
546
  function said(e: Entry): { what: string; label: string | null } {
@@ -556,7 +563,7 @@ function said(e: Entry): { what: string; label: string | null } {
556
563
  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);
557
564
 
558
565
  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 {
559
- // the session: its scope says which controls this token may use, and its cookie is what a mirror reads with
566
+ // the session: its scope says which controls this token may use, and its cookie is what a screen reads with
560
567
  const [scope, setScope] = useState<Scope>('pending');
561
568
  // why the session was not opened (the wall says it, with a way to try again), and that try
562
569
  const [sessionError, setSessionError] = useState<string | null>(null); const [attempt, setAttempt] = useState(0);
@@ -582,7 +589,7 @@ function WorldView({ name, token, twin, tab, arg, sub, admin, snapshot, parentTo
582
589
  return () => { live = false; };
583
590
  }, [name, token, attempt]);
584
591
  /** The session as the wall shows it: opening, opened (with its scope), or refused. */
585
- const session: MirrorSession = sessionError ? { state: 'error', error: sessionError } : scope === 'pending' ? { state: 'pending' } : { state: 'ok', scope };
592
+ const session: ViewSession = sessionError ? { state: 'error', error: sessionError } : scope === 'pending' ? { state: 'pending' } : { state: 'ok', scope };
586
593
  // fail closed: a token whose scope is not known (the session request failed) is shown no control it might not
587
594
  // hold; the server refuses such writes anyway, and a read-only viewer should not be offered them
588
595
  const canWrite = scope === 'write';
@@ -640,7 +647,7 @@ const kindOf = (t: string): string => t.replace(/^_+/, '').replace(/[_-]+/g, ' '
640
647
 
641
648
  /** One changed record as a diff reads: its fields as they stood before this branch's changes (folded from its history
642
649
  * 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 {
650
+ function RecordDiff({ record, history }: { record: ChangedRecord; history: Entry[] | null }): React.JSX.Element {
644
651
  const head = record.writes[record.writes.length - 1]!; const s = said(head); const k = changeKind(record.writes);
645
652
  const mine = new Set(record.writes.map((e) => e.id));
646
653
  const first = record.writes[0]!.occurredAt;
@@ -656,7 +663,6 @@ function RecordDiff({ record, history, screens }: { record: ChangedRecord; histo
656
663
  <span className={`ws-kind ${k.tone}`} title={k.word}><Icon name={k.icon} size={16} /></span>
657
664
  <Mark vendor={record.twin} />
658
665
  <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}
660
666
  </div>
661
667
  <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
668
  {history === null ? <p className="mode pad">Reading its history…</p> : rows.length === 0 ? <p className="mode pad">No field holds a new value.</p> : (
@@ -750,7 +756,7 @@ function ChangesTabs({ name, view, changes, changesets }: { name: string; view:
750
756
  * then its records beside the selected one's fields before and after. Review (where it went, the checks, the approvals,
751
757
  * and Verify, Approve and Deploy) is one button in that head, a window over it: GitHub Desktop has no review, so this
752
758
  * 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 {
759
+ function HistoryView({ name, token, sets, sub, signsAs, canWrite, rooted, refused, onChanged, changes, withScreens, 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 */ withScreens: 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
760
  const [picked, setPicked] = useState<string | null>(null); const [reviewing, setReviewing] = useState(false);
755
761
  const list = sets.state === 'ok' ? [...sets.data.changesets].sort((a, b) => b.createdAt.localeCompare(a.createdAt)) : [];
756
762
  const c = list.find((x) => x.name === sub) ?? list[0];
@@ -795,7 +801,7 @@ function HistoryView({ name, token, sets, sub, signsAs, canWrite, rooted, refuse
795
801
  </div>
796
802
  <div className="ws-two inner">
797
803
  <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>
804
+ <div className="ws-detail">{record ? <RecordDiff record={record} history={log.state === 'ok' ? log.data.entries : log.state === 'error' ? [] : null} /> : <p className="mode pad">It changes nothing.</p>}</div>
799
805
  </div>
800
806
  <Dialog open={reviewing} onClose={() => setReviewing(false)} title={`Review: ${titleOf(c)}`} wide>
801
807
  <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} />
@@ -904,7 +910,7 @@ function TraceView({ name, token, id }: { name: string; token: string; id: strin
904
910
 
905
911
  /** A VENDOR'S PLACE, as TablePlus opens a database: what it stores by type down the left, the chosen type's records as
906
912
  * 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 {
913
+ function VendorView({ name, token, vendor, withScreens, tick }: { name: string; token: string; vendor: string; withScreens: boolean; tick: number }): React.JSX.Element {
908
914
  const tree = useDoor<{ tree: Array<Record<string, unknown> & { type: string; id: string }> }>(token, `/-/${name}/checkpoint/${vendor}`, Math.floor(tick / 6));
909
915
  const rows = tree.state === 'ok' ? tree.data.tree.filter((r) => r.deleted !== true) : [];
910
916
  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)));
@@ -927,7 +933,7 @@ function VendorView({ name, token, vendor, mirrored, tick }: { name: string; tok
927
933
  {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
934
  <p className="menu-label">Also</p>
929
935
  <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}
936
+ {withScreens ? <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
937
  </nav>
932
938
  <div className="ws-grid-wrap">
933
939
  {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}
@@ -957,21 +963,22 @@ function VendorView({ name, token, vendor, mirrored, tick }: { name: string; tok
957
963
  );
958
964
  }
959
965
 
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
966
+ /** A VENDOR'S SCREENS: its own site over this World's state (its pack's workspaces), the whole pane; what it holds and the
961
967
  * 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 {
968
+ function ScreenView({ name, vendor, url, mapError, holding, session, retry }: { name: string; vendor: string; /** where its screens are: null when its pack lists none, undefined while the World's map is read */ url: string | null | undefined; /** the World's map did not answer */ mapError?: string; holding: string; session: ViewSession; retry: () => void }): React.JSX.Element {
963
969
  return (
964
970
  <div className="ws-screen">
965
971
  <div className="ws-screen-bar">
966
972
  <span className="vendor-cell"><Mark vendor={vendor} /><b>{vendorName(vendor)}</b></span>
967
973
  <span className="mode">{holding}</span>
968
974
  <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>
975
+ {url ? <a href={url} target="_blank" rel="noopener noreferrer"><Icon name="external-link" size={16} />New tab</a> : null}
970
976
  </div>
971
977
  {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}
978
+ {mapError ? <p role="alert" className="ws-screens-note">The World did not say where its screens are: {mapError}</p> : session.state === 'pending' || url === undefined ? <p className="mode ws-screens-note" role="status">Opening…</p> : null}
973
979
  {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}
980
+ {session.state === 'ok' && url === null ? <p className="mode ws-screens-note">{vendorName(vendor)}'s pack lists no screens.</p> : null}
981
+ {session.state === 'ok' && url ? <iframe key={vendor} title={`${vendorName(vendor)}, as this World shows it`} src={url} /> : null}
975
982
  </div>
976
983
  );
977
984
  }
@@ -1077,7 +1084,7 @@ function AccountPane({ who, scope, snapshot, portal: platform, onSignOut }: { wh
1077
1084
  * left, where this is in its middle (the World, the branch, the origin and what waits for it, the World's clock), Pull,
1078
1085
  * Push and Settings at its right. A sidebar of places (Changes, History, Events; the vendors; the remote) beside the place
1079
1086
  * 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 {
1087
+ 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: ViewSession; 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
1088
  const remote = usePushPull(name, token, tick, refused);
1082
1089
  const [version, setVersion] = useState(0);
1083
1090
  const every = version * 100_000 + Math.floor(tick / 2);
@@ -1087,7 +1094,8 @@ function Workspace({ name, token, view, sub, arg, win, who, snapshot, onReturn,
1087
1094
  const [picked, setPicked] = useState<string | null>(null);
1088
1095
  const to = (parts: string[]) => (e?: React.MouseEvent): void => { e?.preventDefault(); go([...name.split('/'), '~', ...parts]); };
1089
1096
  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) : [];
1097
+ const screens = new Map((map.state === 'ok' ? map.data.twins : []).flatMap((t) => { const at = screenUrl(name, t); return at ? [[t.twin, at] as const] : []; }));
1098
+ const withScreens = [...screens.keys()];
1091
1099
  const rooted = map.state === 'ok' ? map.data.twins.filter((t) => t.root).map((t) => t.twin) : [];
1092
1100
  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
1101
  const served = (branches?.state === 'ok' && branches.data.checkedOut) || name.split('/')[1]!;
@@ -1176,11 +1184,11 @@ function Workspace({ name, token, view, sub, arg, win, who, snapshot, onReturn,
1176
1184
  <div className="ws-body">
1177
1185
  <nav className="ws-side" aria-label={`${name} places`}>
1178
1186
  {/* 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))}`}>
1187
+ {withScreens.length ? (
1188
+ <a href={`${BASE}/${name}/~/screens`} className={view === 'screen' ? 'ws-screens-entry on' : 'ws-screens-entry'} onClick={to(['screens'])} title={`${counted(withScreens.length, 'screen')}: ${listed(withScreens.map(vendorName))}`}>
1181
1189
  <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>
1190
+ <span className="ws-screens-marks">{withScreens.slice(0, 3).map((v) => <Mark key={v} vendor={v} />)}{withScreens.length > 3 ? <span className="more">+{withScreens.length - 3}</span> : null}</span>
1191
+ <span className="count">{withScreens.length}</span>
1184
1192
  </a>
1185
1193
  ) : null}
1186
1194
  <p className="side-group">Workspace</p>
@@ -1207,16 +1215,16 @@ function Workspace({ name, token, view, sub, arg, win, who, snapshot, onReturn,
1207
1215
  <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
1216
  </Empty>
1209
1217
  ) : null}
1210
- {record ? <RecordDiff record={record} history={log.state === 'ok' ? log.data.entries : log.state === 'error' ? [] : null} {...(mirrored.includes(record.twin) ? { screens: name } : {})} /> : null}
1218
+ {record ? <RecordDiff record={record} history={log.state === 'ok' ? log.data.entries : log.state === 'error' ? [] : null} /> : null}
1211
1219
  </div>
1212
1220
  <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
1221
  </div>
1214
1222
  ) : 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}
1223
+ {view === 'history' ? <HistoryView name={name} token={token} sets={sets} changes={records.length} withScreens={withScreens} 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
1224
  {view === 'events' ? <EventsView key={arg ?? ''} name={name} token={token} vendors={vendors} {...(arg && vendors.includes(arg) ? { vendor: arg } : {})} tick={tick} /> : null}
1217
1225
  {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}
1226
+ {view === 'vendor' && arg ? <VendorView key={arg} name={name} token={token} vendor={arg} withScreens={withScreens.includes(arg)} tick={tick} /> : null}
1227
+ {view === 'screen' && arg ? <ScreenView key={arg} name={name} vendor={arg} url={map.state === 'ok' ? screens.get(arg) ?? null : undefined} {...(map.state === 'error' ? { mapError: map.error } : {})} holding={holds(map.state === 'ok' ? map.data.twins.find((x) => x.twin === arg)?.resources ?? [] : [])} session={session} retry={retry} /> : null}
1220
1228
  </div>
1221
1229
  </div>
1222
1230
 
@@ -1661,15 +1669,15 @@ type RequestReport = { hours: number; requests: number; journalFailures?: number
1661
1669
 
1662
1670
 
1663
1671
 
1664
- type MirrorSession = { state: 'pending' } | { state: 'ok'; scope: string } | { state: 'error'; error: string };
1672
+ type ViewSession = { state: 'pending' } | { state: 'ok'; scope: string } | { state: 'error'; error: string };
1665
1673
  /** THE CANVAS: every vendor's own screens as frames on one surface, the way a canvas app holds its frames (React
1666
1674
  * Flow; Figma's and tldraw's feel). Pan by dragging the background, zoom with the wheel or the controls, move a frame
1667
1675
  * by its title bar (kept in this browser), click a frame to use that vendor's screens and the background or Esc to
1668
1676
  * leave it, so a frame never captures the pan. A frame is its vendor's site at a desktop's size. */
1669
1677
  const FRAME_W = 1280; const FRAME_H = 820; const FRAME_GAP = 160;
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 };
1678
+ type Screen = { world: string; vendor: string; url: 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 };
1671
1679
  function ScreenFrame({ data }: NodeProps<Node<Screen>>): React.JSX.Element {
1672
- const { world, vendor, active, open, changed } = data;
1680
+ const { world, vendor, url, active, open, changed } = data;
1673
1681
  return (
1674
1682
  <>
1675
1683
  <NodeToolbar isVisible position={Position.Top} align="start" className="frame-label">
@@ -1679,24 +1687,24 @@ function ScreenFrame({ data }: NodeProps<Node<Screen>>): React.JSX.Element {
1679
1687
  <div className={active ? 'frame on' : 'frame'} style={{ width: FRAME_W, height: FRAME_H }}>
1680
1688
  <div className="frame-bar" title="Drag to move">
1681
1689
  <Mark vendor={vendor} /><strong>{vendorName(vendor)}</strong>
1682
- <span className="frame-url">{mirrorUrl(world, vendor).replace(/^https?:\/\//, '')}</span>
1690
+ <span className="frame-url">{url.replace(/^https?:\/\//, '')}</span>
1683
1691
  <a className="nodrag" href={`${BASE}/${world}/~/screens/${vendor}`} onClick={(e) => { e.preventDefault(); open(vendor); }}>Open</a>
1684
- <a className="nodrag" href={mirrorUrl(world, vendor)} target="_blank" rel="noopener noreferrer" aria-label={`Open ${vendorName(vendor)} in a new tab`}>↗</a>
1692
+ <a className="nodrag" href={url} target="_blank" rel="noopener noreferrer" aria-label={`Open ${vendorName(vendor)} in a new tab`}>↗</a>
1685
1693
  </div>
1686
1694
  <div className={active ? 'frame-body nodrag nopan nowheel' : 'frame-body'}>
1687
- <iframe title={`${vendor} mirror`} src={mirrorUrl(world, vendor)} loading="lazy" style={{ pointerEvents: active ? 'auto' : 'none' }} />
1695
+ <iframe title={`${vendor} screens`} src={url} loading="lazy" style={{ pointerEvents: active ? 'auto' : 'none' }} />
1688
1696
  </div>
1689
1697
  </div>
1690
1698
  </>
1691
1699
  );
1692
1700
  }
1693
1701
  const FRAME_TYPES = { screen: ScreenFrame };
1694
- function Canvas(props: { name: string; token: string; twins: Load<{ twins: Twin[] }>; session: MirrorSession; retry: () => void; snapshot: Snapshot | null; back: () => void }): React.JSX.Element {
1702
+ function Canvas(props: { name: string; token: string; twins: Load<{ twins: Twin[] }>; session: ViewSession; retry: () => void; snapshot: Snapshot | null; back: () => void }): React.JSX.Element {
1695
1703
  return <ReactFlowProvider><CanvasSurface {...props} /></ReactFlowProvider>;
1696
1704
  }
1697
1705
  /** 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
1698
1706
  * left (a canvas app's layers), zoom at the bottom left, the minimap at the bottom right. */
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 {
1707
+ function CanvasSurface({ name, token, twins, session, retry, snapshot, back }: { name: string; token: string; twins: Load<{ twins: Twin[] }>; session: ViewSession; retry: () => void; snapshot: Snapshot | null; back: () => void }): React.JSX.Element {
1700
1708
  // each screen's changes not yet cut, by vendor: the records, as Changes counts them
1701
1709
  const diff = useDoor<Diff>(token, `/-/${name}/diff`);
1702
1710
  const changed = useMemo((): Record<string, number> => {
@@ -1705,7 +1713,8 @@ function CanvasSurface({ name, token, twins, session, retry, snapshot, back }: {
1705
1713
  return Object.fromEntries(Object.entries(by).map(([v, rs]) => [v, rs.size]));
1706
1714
  }, [diff]);
1707
1715
  const changedAt = (v: string): number => changed[v] ?? 0;
1708
- const frames = twins.state === 'ok' ? twins.data.twins.filter((t) => t.mirror).map((t) => t.twin) : [];
1716
+ const urls = new Map((twins.state === 'ok' ? twins.data.twins : []).flatMap((t) => { const at = screenUrl(name, t); return at ? [[t.twin, at] as const] : []; }));
1717
+ const frames = [...urls.keys()];
1709
1718
  const saved = `volter.canvas.${name}`;
1710
1719
  const [active, setActive] = useState<string | null>(null);
1711
1720
  const [layers, setLayers] = useState(true);
@@ -1715,7 +1724,7 @@ function CanvasSurface({ name, token, twins, session, retry, snapshot, back }: {
1715
1724
  const placed = useCallback((): Array<Node<Screen>> => {
1716
1725
  let kept: Record<string, { x: number; y: number }> = {};
1717
1726
  try { kept = JSON.parse(localStorage.getItem(saved) ?? '{}') as Record<string, { x: number; y: number }>; } catch { /* a convenience */ }
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 } }));
1727
+ 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, url: urls.get(v)!, active: false, open, changed: changed[v] ?? 0 } }));
1719
1728
  // biome-ignore lint/correctness/useExhaustiveDependencies: the frames' names are what change it
1720
1729
  }, [frames.join(','), name, cols, open, changed]);
1721
1730
  const [nodes, setNodes, onNodesChange] = useNodesState<Node<Screen>>([]);
@@ -2237,6 +2246,7 @@ function Vault({ name, token, canWrite, refused }: { name: string; token: string
2237
2246
  <section className="block" aria-labelledby="links-head">
2238
2247
  <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
2248
  <Pending load={links} what="the links" />
2249
+ {report.state === 'ok' && (report.data.journalFailures ?? 0) > 0 ? <p className="mode pad" role="alert">{report.data.journalFailures} request {report.data.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 from what this page counts.</p> : null}
2240
2250
  {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
2251
  {linkNames.length ? (
2242
2252
  <table className="dense" aria-label="Links">
package/client/main.tsx CHANGED
@@ -1,4 +1,4 @@
1
- // The console's page, mounted: the host serves this entry (src/console.ts); a story renders ConsolePage itself.
1
+ // The console's page, mounted: the host serves this entry (src/console.ts).
2
2
  import React from 'react';
3
3
  import { createRoot } from 'react-dom/client';
4
4
  import { applyTheme } from './kit/kit.tsx';