@volter/world-console 2.0.6 → 2.0.8

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.
@@ -427,6 +427,9 @@ type Placed = { twin: string; position: number; entry: Entry };
427
427
  type Timeline = { entries: Placed[]; next: string | null };
428
428
  type Diff = { base: { id: string; at: string | null }; changes: Array<{ twin: string; entry: Entry }> };
429
429
  type Branch = { name: string; at?: { instant?: string }; createdAt: string; expiresAt: string | null };
430
+ /** The branches door: the host's snapshots of this World, and the World's own named branches (`volter world branch`),
431
+ * one of which, `checkedOut`, is the one this serve holds. */
432
+ type BranchesDoor = { branches: Branch[]; checkedOut?: string; named?: Array<{ name: string; running: boolean }> };
430
433
  /** What the World's session answered: its scope; `unknown` when the host has no session door (a write then says). */
431
434
  type Scope = 'pending' | 'read' | 'write' | 'unknown';
432
435
 
@@ -502,7 +505,7 @@ function WorldView({ name, token, twin, tab, arg, admin, snapshot, onReturn, ope
502
505
  const [clockVersion, setClockVersion] = useState(0);
503
506
  const clock = useDoor<Clock>(token, `/-/${name}/clock`, tick + clockVersion);
504
507
  const twins = useDoor<{ twins: Twin[] }>(token, `/-/${name}/twins`);
505
- const branches = useDoor<{ branches: Branch[] }>(token, `/-/${name}/branches`, clockVersion);
508
+ const branches = useDoor<BranchesDoor>(token, `/-/${name}/branches`, clockVersion);
506
509
  const map = useDoor<WorldMap>(token, `/-/${name}/map`, Math.floor(tick / 2));
507
510
  // branches are the host's: a host that does not answer the door has no "as of", and the console shows none
508
511
  const hasBranches = !(branches.state === 'error' && (branches.status === 404 || branches.status === 405));
@@ -613,13 +616,13 @@ function Connect({ name }: { name: string }): React.JSX.Element {
613
616
 
614
617
  /** The branch picker, between the World's pages and the branch's (where Neon's sits): this World live, or one of the
615
618
  * host's branches of it; a snapshot names its parent as the way back. */
616
- function BranchPicker({ name, snapshot, branches, opens }: { name: string; snapshot: Snapshot | null; branches: Load<{ branches: Branch[] }> | null; opens: (name: string) => boolean }): React.JSX.Element {
619
+ function BranchPicker({ name, snapshot, branches, opens }: { name: string; snapshot: Snapshot | null; branches: Load<BranchesDoor> | null; opens: (name: string) => boolean }): React.JSX.Element {
617
620
  const short = (n: string): string => n.split('/').pop() ?? n;
618
621
  const live = snapshot?.parent ?? name;
619
622
  const others = branches?.state === 'ok' ? branches.data.branches.filter((b) => b.name !== name) : [];
620
623
  return (
621
624
  <details className="menu branch-picker">
622
- <summary><svg className="branch-glyph" viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M5 3.25a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Zm0 2.122a2.25 2.25 0 1 0-1.5 0v5.256a2.25 2.25 0 1 0 1.5 0V9.5h4.25A2.25 2.25 0 0 0 11.5 7.25v-.878a2.25 2.25 0 1 0-1.5 0v.878a.75.75 0 0 1-.75.75H5V5.372ZM4.25 12a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm6.5-9.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Z" /></svg><span className="branch-name">{snapshot ? short(name) : short(live)}</span><span className="badge">{snapshot ? 'snapshot' : 'live'}</span></summary>
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>
623
626
  <div className="popover">
624
627
  <p className="mode">Branch</p>
625
628
  <ul className="branch-list">
@@ -631,7 +634,17 @@ function BranchPicker({ name, snapshot, branches, opens }: { name: string; snaps
631
634
  : <span className="mode" title="Its token was handed to whoever made it">{short(b.name)}</span>}</li>
632
635
  ))}
633
636
  </ul>
634
- {branches?.state === 'ok' && others.length === 0 && !snapshot ? <p className="mode">No branches yet. “View an earlier moment…” opens one.</p> : null}
637
+ {branches?.state === 'ok' && (branches.data.named?.length ?? 0) > 1 ? (
638
+ <>
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>
645
+ </>
646
+ ) : 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}
635
648
  </div>
636
649
  </details>
637
650
  );
@@ -700,7 +713,7 @@ function BranchOverview({ name, token, map, vendors, tick }: { name: string; tok
700
713
  * Neon with how to connect and the branches. A World's live preview is its vendors' own screens; beside them, how an
701
714
  * app connects and the branches; under them, what just happened. Counts sit with what they count, not in a row. */
702
715
  const THUMB_SCALE = 0.2;
703
- function WorldOverview({ name, token, map, vendors, twins, session, branches, canWrite, tick, snapshotOf }: { name: string; token: string; map: Load<WorldMap>; vendors: string[]; twins: Load<{ twins: Twin[] }>; session: MirrorSession; branches: Load<{ branches: Branch[] }> | null; canWrite: boolean; tick: number; /** the World this one is a snapshot of, when it is one */ snapshotOf?: string }): React.JSX.Element {
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 {
704
717
  const diff = useDoor<Diff>(token, `/-/${name}/diff`, Math.floor(tick / 2));
705
718
  const recent = useDoor<Timeline>(token, `/-/${name}/timeline?limit=8`, tick);
706
719
  const rows = map.state === 'ok' ? map.data.twins : [];
@@ -867,7 +880,7 @@ function AsOf({ name, token, clock, refused }: { name: string; token: string; cl
867
880
  }
868
881
 
869
882
  /** Overview: what the World holds, one card per twin (a resource explorer), and the branches cut from it. */
870
- function Overview({ name, token, canWrite, branches, opens, onBranchesChanged, refused }: { name: string; token: string; canWrite: boolean; branches: Load<{ branches: Branch[] }> | null; opens: (name: string) => boolean; onBranchesChanged: () => void; refused: (r: Response) => void }): React.JSX.Element {
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 {
871
884
  const map = useDoor<WorldMap>(token, `/-/${name}/map`);
872
885
  const [note, setNote] = useState<string | null>(null);
873
886
  const remove = async (b: string): Promise<void> => {
@@ -918,11 +931,20 @@ function Overview({ name, token, canWrite, branches, opens, onBranchesChanged, r
918
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
919
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
920
933
  * since it branched), reset to its parent as it is now, or removed. Each is the World's own branches door. */
921
- function BranchList({ name, token, canWrite, branches, opens, note, remove, onChanged, refused }: { name: string; token: string; canWrite: boolean; branches: Load<{ branches: Branch[] }>; opens: (name: string) => boolean; note: string | null; remove: (b: string) => Promise<void>; onChanged: () => void; refused: (r: Response) => void }): React.JSX.Element {
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 {
922
935
  const short = (n: string): string => n.split('/').pop() ?? n;
923
936
  const list = branches.state === 'ok' ? branches.data.branches : [];
924
937
  const [making, setMaking] = useState(false); const [busy, setBusy] = useState(false); const [error, setError] = useState<string | null>(null);
925
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
+ };
926
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)
927
949
  const make = async (body: Record<string, unknown>, opened: string): Promise<{ name: string } | string> => {
928
950
  const r = await door(token, `/-/${name}/branches`, { method: 'POST', body: JSON.stringify(body) }).catch(() => null);
@@ -973,7 +995,7 @@ function BranchList({ name, token, canWrite, branches, opens, note, remove, onCh
973
995
  <table className="dense" aria-label="Branches">
974
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>
975
997
  <tbody>
976
- <tr><td><b>{short(name)}</b> <span className="badge">default</span></td><td className="mode">—</td><td className="mode">live</td><td className="mode">—</td><td className="mode">kept</td><td /></tr>
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>
977
999
  {list.map((b) => {
978
1000
  const items = menu(b);
979
1001
  return (
@@ -992,8 +1014,28 @@ function BranchList({ name, token, canWrite, branches, opens, note, remove, onCh
992
1014
  })}
993
1015
  </tbody>
994
1016
  </table>
995
- {list.length === 0 ? <p className="mode pad">No branches yet.</p> : null}
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; “Serve here” stops this one's twins and serves that one in its place.</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) => (
1029
+ <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 && 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>
1033
+ </tr>
1034
+ ))}
1035
+ </tbody>
1036
+ </table>
996
1037
  </div>
1038
+ </>
997
1039
  ) : null}
998
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></>}>
999
1041
  <form id="new-branch" onSubmit={(e) => void create(e)} className="stack">