2026-10-14 17:28:33 -07:00
// Orgchain: create and operate lineage holdings. Lineage gives the structure; the chain adds time.
// Every change goes through the lab's lock (orgchain/store.js) and the reducer (orgchain/lineage.js); this file draws and asks.
import './orgchain.css' ;
import { MetadataIndex } from './adapters/metadata-index.js' ;
import { census , lineageEvents , reduce , rows , slug , summary , trail } from './orgchain/lineage.js' ;
import { LAB_KEY , beat , decodeLab , emptyLab , encodeLab , mutate , readLab , setTempo , tick } from './orgchain/store.js' ;
2026-10-14 22:25:38 -07:00
import { SAMPLE , act , minerOf , send , seedSample } from './orgchain/actions.js' ;
2026-10-14 20:14:11 -07:00
import { addressesOf , holderOf , keyOf , leavesOf , secretsOf } from './orgchain/keys.js' ;
2026-10-14 22:25:38 -07:00
import { DUST , balances , brief , btc , coins , ledger , most , owners , payee , payerOf , satsOf } from './orgchain/money.js' ;
2026-10-14 18:22:22 -07:00
import { readEnvelope } from './adapters/inscription.js' ;
2026-10-04 23:24:48 +00:00
const $ = id => document . getElementById ( id );
2026-10-14 17:28:33 -07:00
function h ( tag , attrs = {}, ... kids ) {
const el = document . createElement ( tag );
for ( const [ k , v ] of Object . entries ( attrs )) {
if ( v === null || v === undefined || v === false ) continue ;
if ( k === 'class' ) el . className = v ;
else if ( k === 'text' ) el . textContent = v ;
else if ( k . startsWith ( 'on' )) el . addEventListener ( k . slice ( 2 ), v );
else el . setAttribute ( k , v === true ? '' : String ( v ));
}
for ( const kid of kids . flat ( Infinity )) if ( kid !== null && kid !== undefined && kid !== false && kid !== '' ) el . append ( kid instanceof Node ? kid : String ( kid ));
return el ;
}
const plural = ( n , word ) => ` ${ n } ${ word }${ n === 1 ? '' : 's' } ` ;
const at = when => ( when === 'cued' ? 'cued' : `beat ${ when } ` );
const clockTime = ms => new Date ( ms ). toLocaleTimeString ([], { hour : '2-digit' , minute : '2-digit' , second : '2-digit' });
const json = value => JSON . stringify ( value , ( k , v ) => ( typeof v === 'bigint' ? String ( v ) : v ), 2 );
2026-10-05 00:56:42 +00:00
2026-10-14 17:28:33 -07:00
// ui.beat: null means now (cued acts included); a number is that past beat, read-only.
2026-10-14 22:25:38 -07:00
const ui = { holding : null , node : null , event : null , tx : null , beat : null , verb : 'birth' , busy : false , replay : null , closing : null , reveal : null };
2026-10-14 17:28:33 -07:00
let lab = emptyLab (), index = new MetadataIndex ( lab . chain ), events = [], live = reduce ([]), view = live ;
2026-10-14 22:25:38 -07:00
// The money (orgchain/money.js): whose each SegWit script is, every move in chain order, and balances now and as viewed.
let money = { owner : new Map (), ledger : [], now : new Map (), view : new Map () };
2026-10-14 17:28:33 -07:00
const tip = () => lab . chain . blocks . length ;
2026-10-14 22:25:38 -07:00
const cuedActs = () => events . filter ( e => e . status === 'pending' ). length ;
const cuedSends = () => money . ledger . filter ( m => m . beat === null && m . kind === 'pay' ). length ;
const cued = () => cuedActs () + cuedSends ();
const purse = id => money . view . get ( id ) ?? 0 ; // a balance at the viewed beat
const held = id => money . now . get ( id ) ?? 0 ; // a balance now, cued sends included
2026-10-14 17:28:33 -07:00
const node = ( id , state = live ) => ( id ? state . nodes . get ( id ) ?? null : null );
2026-10-14 22:25:38 -07:00
const select = next => Object . assign ( ui , { event : null , tx : null , closing : null , reveal : null }, next );
2026-10-14 17:28:33 -07:00
const pick = next => { select ( next ); render (); };
2026-10-05 01:23:21 +00:00
2026-10-14 17:28:33 -07:00
function say ( message , bad = false ) { const el = $ ( 'say' ); el . textContent = message ; el . classList . toggle ( 'bad' , bad ); }
2026-10-05 00:56:42 +00:00
2026-10-04 23:24:48 +00:00
function load () {
2026-10-14 17:28:33 -07:00
try { lab = readLab (); }
catch ( error ) { lab = emptyLab (); say ( `This browser's saved lab could not be read ( ${ error . message } ) Load a backup, or reset to start over.` , true ); }
derive ();
}
function derive () {
index = new MetadataIndex ( lab . chain );
events = lineageEvents ( index );
live = reduce ( events , { pending : true });
if ( ui . beat !== null && ui . beat >= tip ()) ui . beat = null ;
if ( ! node ( ui . holding )) ui . holding = live . roots . at ( - 1 ) ? . id ?? null ;
if ( ! node ( ui . node ) || node ( ui . node ). root !== ui . holding ) ui . node = ui . holding ;
if ( ui . event && ! live . log . some ( e => e . id === ui . event )) ui . event = null ;
view = ui . beat === null ? live : reduce ( events , { upTo : ui . beat });
2026-10-14 22:25:38 -07:00
const owner = owners ( lab . keys , events , live );
money = { owner , ledger : ledger ( lab . chain , owner ), now : balances ( lab . chain , owner ) };
money . view = ui . beat === null ? money . now : balances ( lab . chain , owner , ui . beat );
if ( ui . tx && ! money . ledger . some ( m => m . txid === ui . tx )) ui . tx = null ;
2026-10-04 23:24:48 +00:00
render ();
}
2026-10-14 17:28:33 -07:00
function setBeat ( value ) {
ui . beat = value >= tip () ? null : Math . max ( 0 , value );
view = ui . beat === null ? live : reduce ( events , { upTo : ui . beat });
2026-10-14 22:25:38 -07:00
money . view = ui . beat === null ? money . now : balances ( lab . chain , money . owner , ui . beat );
2026-10-14 17:28:33 -07:00
render ();
}
// One path for every change: the lab's lock re-reads, acts and saves. A failure changes nothing and says why.
async function run ( action , options ) {
if ( ui . busy ) return false ;
ui . busy = true ;
document . body . classList . add ( 'busy' );
try { lab = ( await mutate ( action , options )). lab ; return true ; }
catch ( error ) { say ( error ? . message || String ( error ), true ); return false ; }
finally { ui . busy = false ; document . body . classList . remove ( 'busy' ); derive (); }
}
// Act through a node, then say where it landed: at a beat, or cued for the next one.
async function perform ( op , by , fields , done ) {
let id = null ;
if ( ! ( await run ( l => { id = act ( l , op , by , fields ); }))) return false ;
const e = index . events . find ( x => x . id === id );
say ( done ( id , e ? . status === 'confirmed' ? `at beat ${ e . height } ` : 'cued for the next beat' ));
render ();
return true ;
}
2026-10-14 22:25:38 -07:00
// Run something that may beat, then say what the beat landed and whom it paid.
2026-10-14 17:28:33 -07:00
async function landCue ( action ) {
2026-10-14 22:25:38 -07:00
const acts = cuedActs (), sends = cuedSends (), height = tip ();
if ( ! ( await run ( action )) || tip () === height ) return ;
const landed = [[ acts - cuedActs (), 'cued act' ], [ sends - cuedSends (), 'cued send' ]]. filter (([ n ]) => n > 0 ). map (([ n , w ]) => plural ( n , w ));
const m = money . ledger . find ( x => x . kind === 'mine' && x . beat === tip ()), earned = ` ${ m ? node ( m . to ) ? . name : 'the lab wallet' } earned ${ btc ( m ? . amount ?? 5e9 ) } ` ;
say ( `Beat ${ tip () } : ${ landed . length ? ` ${ landed . join ( ' and ' ) } landed, and ${ earned } ` : earned } .` );
2026-10-14 17:28:33 -07:00
}
function render () { renderClock (); renderHoldings (); renderTree (); renderAct (); renderInspect (); renderTimeline (); }
function renderClock () {
const n = cued (), { tempo } = lab . clock ;
for ( const b of document . querySelectorAll ( '[data-tempo]' )) b . setAttribute ( 'aria-pressed' , String ( String ( tempo ) === b . dataset . tempo ));
$ ( 'clock-beat' ). textContent = String ( tip ());
$ ( 'clock-cued' ). textContent = n ? ` ${ n } cued · ${ cueNote () } ` : 'nothing cued' ;
2026-10-14 22:25:38 -07:00
const to = payee ( lab . keys , live , ui . holding ) ? node ( ui . holding ). name : 'the lab wallet' ;
$ ( 'clock-pays' ). textContent = `beats pay ${ to } ` ;
$ ( 'beat-now' ). title = `Beat now: ${ n ? 'land what is cued, and ' : '' } pay ${ to } 50 test BTC` ;
2026-10-14 17:28:33 -07:00
}
function cueNote () {
const { tempo , nextAt } = lab . clock ;
if ( tempo === 'manual' ) return 'waiting for Beat now' ;
if ( typeof tempo === 'number' && nextAt ) return `next beat in ${ Math . max ( 0 , Math . ceil (( nextAt - Date . now ()) / 1000 )) } s` ;
return 'landing on the next beat' ;
}
function renderHoldings () {
$ ( 'holdings-list' ). replaceChildren (... live . roots . map ( r => {
const c = census ( live , r . id ), on = r . id === ui . holding ;
2026-10-14 22:25:38 -07:00
const worth = [... live . nodes . values ()]. reduce (( sum , x ) => sum + ( x . root === r . id ? held ( x . id ) : 0 ), 0 );
const facts = [ at ( r . born ), plural ( c . orgs , 'org' ), plural ( c . agents , 'agent' ), plural ( c . belts , 'belt' ), worth > 0 && btc ( worth ), r . status === 'closed' && 'closed' ]. filter ( Boolean );
2026-10-14 17:28:33 -07:00
return h ( 'li' , {}, h ( 'button' , { type : 'button' , class : `hold ${ on ? ' on' : '' }${ r . status === 'closed' ? ' gone' : '' } ` , 'aria-pressed' : String ( on ),
onclick : () => pick ({ holding : r . id , node : r . id }) }, h ( 'span' , { class : 'hn' , text : r . name }), ' ' , h ( 'span' , { class : 'hm' , text : facts . join ( ' · ' ) })));
}));
$ ( 'holdings-empty' ). hidden = live . roots . length > 0 ;
$ ( 'holdings-sub' ). textContent = live . roots . length ? String ( live . roots . length ) : '' ;
$ ( 'sample' ). disabled = live . roots . some ( r => r . name === SAMPLE && r . status === 'active' );
}
function renderTree () {
const box = $ ( 'tree-rows' ), root = node ( ui . holding );
$ ( 'tree-sub' ). textContent = root ? ` ${ root . name } · ${ ui . beat === null ? 'now' : `beat ${ ui . beat } of ${ tip () } ` } ` : '' ;
if ( ! root ) return box . replaceChildren ( h ( 'p' , { class : 'wait' , text : 'No holding yet. Found one, or load the yolopreneur sample.' }));
if ( ! view . nodes . has ( root . id )) return box . replaceChildren ( h ( 'p' , { class : 'wait' , text : ` ${ root . name } is founded at ${ at ( root . born ) } . Move the slider forward.` }));
const focused = box . contains ( document . activeElement ) ? document . activeElement . dataset . id : null ;
box . replaceChildren (... rows ( view , root . id ). map ( r => ( r . type === 'group'
? h ( 'div' , { class : 'row grp' }, h ( 'span' , { class : 'g' , 'aria-hidden' : 'true' , text : r . guide }), h ( 'span' , { class : 'gl' , text : r . label }))
: treeRow ( r ))));
if ( focused ) box . querySelector ( `[data-id=" ${ focused } "]` ) ? . focus ();
}
function treeRow ({ node : n , guide , path }) {
const on = n . id === ui . node ;
const cls = [ 'row' , on && 'on' , n . status === 'closed' && 'gone' , n . born === 'cued' && 'cue' ]. filter ( Boolean ). join ( ' ' );
return h ( 'div' , { class : cls }, h ( 'button' , { type : 'button' , class : 'sel' , 'data-id' : n . id , 'aria-pressed' : String ( on ), onclick : () => pick ({ node : n . id }) },
h ( 'span' , { class : 'g' , 'aria-hidden' : 'true' , text : guide }), h ( 'span' , { class : 'p' , text : ` ${ path } ` }), h ( 'span' , { class : 'l' , text : n . name }), ' ' ,
h ( 'span' , { class : 'v' }, marks ( n ))));
}
function marks ( n ) {
const list = [
n . kind === 'agent' && h ( 'span' , { class : 'mk' , text : 'agent' }),
n . edge === 'adopted' && h ( 'span' , { class : 'mk ok' , text : 'graduated' }),
n . status === 'closed' && h ( 'span' , { class : 'mk bad' , text : 'closed' }),
n . born === 'cued' && h ( 'span' , { class : 'mk' , text : 'cued' }),
... n . badges . map ( belt ),
2026-10-14 22:25:38 -07:00
purse ( n . id ) > 0 && h ( 'span' , { class : 'mk sats' , title : ` ${ btc ( purse ( n . id )) } at its SegWit address` , text : brief ( purse ( n . id )) }),
2026-10-14 17:28:33 -07:00
]. filter ( Boolean );
return list . flatMap (( mark , i ) => ( i ? [ ' ' , mark ] : [ mark ])); // spaces keep accessible names readable
}
const belt = b => h ( 'span' , { class : `belt ${ b . belt } ` , title : ` ${ b . skill } : ${ b . belt } belt` }, b . skill , h ( 'span' , { class : 'sr' , text : ` ( ${ b . belt } belt)` }));
const below = ( x , n ) => { for ( let p = node ( x . parent ); p ; p = node ( p . parent )) if ( p . id === n . id ) return true ; return false ; };
// Who n can award: anyone else active in the holding, its own seats first (a belt is one skill for one seat).
const recipients = n => [... live . nodes . values ()]. filter ( x => x . root === n . root && x . id !== n . id && x . status === 'active' )
. map ( x => [ x , ( below ( x , n ) ? 0 : 2 ) + ( x . kind === 'agent' ? 0 : 1 )]). sort (( a , b ) => a [ 1 ] - b [ 1 ]). map (([ x ]) => x );
const derivedBelow = n => n . children . reduce (( sum , id ) => {
const c = node ( id );
return c && c . edge === 'derived' && c . status === 'active' ? sum + 1 + derivedBelow ( c ) : sum ;
}, 0 );
2026-10-14 22:25:38 -07:00
// Why n cannot send now ('' when it can).
function sendBlock ( n ) {
const wallet = payerOf ( lab . keys , live , n ), root = node ( n . root );
if ( ! wallet ) return 'This holding predates lab keys, so it has no SegWit address to send from' ;
if ( most ( lab . chain . coins ( wallet . script )) < DUST ) {
return n . id === n . root ? ` ${ n . name } has no test BTC yet. Each beat pays it 50` : ` ${ n . name } has no test BTC to send yet. Each beat pays ${ root ? . name ?? 'its holding' } , which can send some down` ;
}
return recipients ( n ). length ? '' : 'There is nobody else in this holding to send to yet' ;
}
2026-10-14 17:28:33 -07:00
// Why a verb is unavailable for this node ('' when it is available). The reducer has the final word.
function blocked ( n ) {
const parent = node ( n . parent ), closed = n . status === 'closed' ? ` ${ n . name } is closed` : '' ;
return {
birth : closed || ( n . kind === 'agent' ? 'Agents do not give birth' : '' ),
award : closed || ( n . kind === 'agent' ? 'Agents do not award belts' : recipients ( n ). length ? '' : 'There is nobody else in this holding to award yet' ),
decide : closed ,
2026-10-14 22:25:38 -07:00
send : closed || sendBlock ( n ),
2026-10-14 17:28:33 -07:00
graduate : closed || ( ! parent ? 'A holding has no parent to graduate from' : n . kind !== 'org' ? 'Only orgs graduate'
: n . edge === 'adopted' ? ` ${ n . name } has already graduated` : parent . status !== 'active' ? ` ${ parent . name } is closed` : '' ),
close : closed ,
};
}
function renderAct () {
const n = node ( ui . node ), past = ui . beat !== null ;
$ ( 'act-none' ). hidden = !! n ;
$ ( 'act-past' ). hidden = ! n || ! past ;
$ ( 'act-on' ). hidden = ! n || past ;
$ ( 'act-sub' ). textContent = n ? . name ?? '' ;
$ ( 'past-beat' ). textContent = String ( ui . beat ?? '' );
if ( ! n || past ) return ;
const why = blocked ( n ), parent = node ( n . parent );
for ( const b of document . querySelectorAll ( '[data-verb]' )) {
b . setAttribute ( 'aria-pressed' , String ( b . dataset . verb === ui . verb ));
b . classList . toggle ( 'off' , !! why [ b . dataset . verb ]);
}
for ( const form of document . querySelectorAll ( 'form[data-form]' )) form . hidden = form . dataset . form !== ui . verb || !! why [ ui . verb ];
$ ( 'act-why' ). textContent = why [ ui . verb ] ? ` ${ why [ ui . verb ] } .` : '' ;
for ( const el of document . querySelectorAll ( '#act-on [data-name]' )) el . textContent = n . name ;
for ( const el of document . querySelectorAll ( '#act-on [data-parent]' )) el . textContent = parent ? . name ?? '' ;
const sig = list => list . map ( o => ` ${ o . value } = ${ o . textContent } ` ). join ( '|' );
2026-10-14 22:25:38 -07:00
for ( const to of [ $ ( 'award-to' ), $ ( 'send-to' )]) {
const keep = to . value , options = recipients ( n ). map ( x => h ( 'option' , { value : x . id , text : ` ${ trail ( live , x . id ) }${ x . kind === 'agent' ? ' (agent)' : '' } ` }));
if ( sig ( options ) !== sig ([... to . options ])) { to . replaceChildren (... options ); if ( options . some ( o => o . value === keep )) to . value = keep ; }
}
$ ( 'send-has' ). textContent = btc ( held ( n . id ));
2026-10-14 17:28:33 -07:00
$ ( 'graduate-go' ). textContent = `Graduate ${ n . name } ` ;
2026-10-14 18:22:22 -07:00
const signer = ui . verb === 'graduate' ? parent : n , k = signer && keyOf ( lab . keys , live , signer );
2026-10-14 22:25:38 -07:00
const path = k && ( ui . verb === 'send' ? ` ${ k . from } · ${ payerOf ( lab . keys , live , signer ). path } ` : k . path );
$ ( 'act-key' ). textContent = ! signer || why [ ui . verb ] ? '' : k ? `Signs as ${ signer . name } : ${ path } ` : 'Signs with the lab wallet: this holding predates lab keys.' ;
2026-10-14 17:28:33 -07:00
const below = derivedBelow ( n ), arming = ui . closing === n . id ;
$ ( 'close-note' ). textContent = below
? `Closing ${ n . name } spends it for the last time, and its ${ plural ( below , 'derived descendant' ) } close with it. Graduated children hold their own roots and stay open.`
: `Closing ${ n . name } spends its inscription for the last time. Nothing can act through it afterwards.` ;
$ ( 'close-go' ). textContent = arming ? `Yes, close ${ n . name } ` : `Close ${ n . name } ` ;
$ ( 'close-keep' ). hidden = ! arming ;
}
function renderInspect () {
const box = $ ( 'inspect-body' ), e = ui . event ? live . log . find ( x => x . id === ui . event ) : null ;
if ( e ) { $ ( 'inspect-sub' ). textContent = 'event' ; return box . replaceChildren (... eventView ( e )); }
2026-10-14 22:25:38 -07:00
const m = ui . tx ? money . ledger . find ( x => x . txid === ui . tx ) : null ;
if ( m ) { $ ( 'inspect-sub' ). textContent = 'money' ; return box . replaceChildren (... txView ( m )); }
2026-10-14 17:28:33 -07:00
const known = node ( ui . node );
$ ( 'inspect-sub' ). textContent = known ? . kind ?? '' ;
if ( ! known ) return box . replaceChildren ( h ( 'p' , { class : 'note pad' , text : 'Select a node in the tree, or an event in the timeline.' }));
const n = node ( ui . node , view );
if ( ! n ) return box . replaceChildren ( h ( 'p' , { class : 'note pad' , text : ` ${ known . name } is not born yet at beat ${ ui . beat } .` }));
box . replaceChildren (... nodeView ( n ));
}
2026-10-14 19:09:50 -07:00
const fact = ([ k , v , note ]) => h ( 'div' , { class : 'f' }, h ( 'dt' , { text : k }), h ( 'dd' , {}, v , note && h ( 'span' , { class : 'fn' , text : note })));
2026-10-14 17:28:33 -07:00
function nodeView ( n ) {
const parent = node ( n . parent , view );
const facts = [
[ 'Born' , n . born === 'cued' ? 'cued for the next beat' : `beat ${ n . born } ` ],
parent && [ 'Parent' , ` ${ parent . name } · ${ n . kind } ${ n . index } '` ],
parent && [ 'Edge' , n . edge === 'adopted' ? `adopted at ${ at ( n . graduated ) } : it holds its own root` : `derived: ${ parent . name } holds its seed` ],
n . closed !== null && [ 'Closed' , at ( n . closed )],
[ 'Children' , n . children . length ? n . children . map ( id => node ( id , view ) ? . name ). join ( ', ' ) : 'none yet' ],
2026-10-14 22:25:38 -07:00
payerOf ( lab . keys , view , n ) && [ 'Balance' , btc ( purse ( n . id )), `Test BTC at its SegWit address ${ ui . beat === null ? '' : `, as of beat ${ ui . beat } ` } .` ],
2026-10-14 17:28:33 -07:00
[ 'Inscription' , n . id ],
]. filter ( Boolean );
return [
h ( 'div' , { class : 'ih' }, h ( 'p' , { class : 'eyebrow' , text : trail ( view , n . id ) }), h ( 'h3' , { class : 'it' , text : n . name }),
h ( 'div' , { class : 'chips' }, h ( 'span' , { class : 'chip' , text : n . kind }), h ( 'span' , { class : `chip ${ n . status === 'active' ? 'ok' : 'bad' } ` , text : n . status }),
n . edge === 'adopted' && h ( 'span' , { class : 'chip ok' , text : 'graduated' }), n . born === 'cued' && h ( 'span' , { class : 'chip' , text : 'cued' }))),
h ( 'div' , { class : 'ib' }, h ( 'dl' , { class : 'fl' }, facts . map ( fact )),
n . badges . length > 0 && h ( 'div' , {}, h ( 'p' , { class : 'lab' , text : 'Belts' }), h ( 'ul' , { class : 'list' }, n . badges . map ( b =>
h ( 'li' , {}, belt ( b ), h ( 'span' , { class : 'when' , text : ` from ${ node ( b . by ) ? . name ?? '?' } , ${ at ( b . beat ) } ` }))))),
n . decisions . length > 0 && h ( 'div' , {}, h ( 'p' , { class : 'lab' , text : 'Decisions' }), h ( 'ul' , { class : 'list' }, n . decisions . map ( d =>
2026-10-14 19:09:50 -07:00
h ( 'li' , {}, h ( 'b' , { text : d . title }), d . why && ` · ${ d . why } ` , h ( 'span' , { class : 'when' , text : ` ( ${ at ( d . beat ) } )` }))))),
2026-10-14 22:25:38 -07:00
moneyView ( n ), keysView ( n )),
2026-10-14 17:28:33 -07:00
];
}
2026-10-14 22:25:38 -07:00
// A node's latest money moves, as of the viewed beat.
function moneyView ( n ) {
const list = money . ledger . filter ( m => ( m . to === n . id || m . from === n . id ) && ( ui . beat === null || ( m . beat !== null && m . beat <= ui . beat ))). slice ( - 6 ). reverse ();
return list . length > 0 && h ( 'div' , {}, h ( 'p' , { class : 'lab' , text : 'Money' }), h ( 'ul' , { class : 'list' }, list . map ( m =>
h ( 'li' , {}, h ( 'b' , { text : flow ( m , n . id ) }), h ( 'span' , { class : 'when' , text : ` ( ${ at ( m . beat ?? 'cued' ) } )` })))));
}
// One money move as the node with this id sees it.
function flow ( m , id ) {
const name = x => node ( x ) ? . name ?? '?' ;
if ( m . kind === 'mine' ) return `+ ${ btc ( m . amount ) } , a beat's reward` ;
if ( m . kind === 'move' ) return ` ${ btc ( m . amount ) } moved to its new key, after a ${ m . fee } -sat fee` ;
return m . to === id ? `+ ${ btc ( m . amount ) } from ${ name ( m . from ) } ` : `− ${ btc ( m . amount ) } to ${ name ( m . to ) } , and a ${ m . fee } -sat fee` ;
}
// "40 tBTC yolopreneur → time"
function moneyLine ( m ) {
const name = id => node ( id ) ? . name ?? '?' ;
if ( m . kind === 'mine' ) return ` ${ btc ( m . amount ) } → ${ name ( m . to ) } ` ;
if ( m . kind === 'move' ) return ` ${ btc ( m . amount ) } → ${ name ( m . to ) } 's new key` ;
return ` ${ btc ( m . amount ) } ${ name ( m . from ) } → ${ name ( m . to ) } ` ;
}
2026-10-14 19:49:05 -07:00
// Lineage's keys for a node as of the viewed beat: its Taproot, SegWit and nostr addresses, where they come from, who
// holds its inscription now, and its leaves with the public lines to paste (the ones the lineage key tree page derives).
2026-10-14 18:22:22 -07:00
const HELD = { own : 'its own key' , lab : 'the lab wallet' , parent : "its parent's key (its handoff is cued)" , other : 'a key outside this lab' };
2026-10-14 19:09:50 -07:00
const LEAF = { ssh : 'shell access, certified by the CA' , git : 'commit signing' , dkim : 'mail signing' , wg : 'WireGuard peer key' ,
'ssh-ca' : 'SSH certificate authority' , mtls : 'TLS identity between nodes' , noise : 'Headscale noise key' };
function keysView ( n ) {
2026-10-14 18:22:22 -07:00
const k = keyOf ( lab . keys , view , n ), holder = holderOf ( lab , live , n . id );
2026-10-14 19:09:50 -07:00
const held = [ 'Held now by' , holder ? HELD [ holder . who ] : 'nobody: its inscription has been spent' ];
if ( ! k ) return h ( 'div' , { class : 'keys' }, h ( 'p' , { class : 'lab' , text : 'Keys' }),
2026-10-14 20:14:11 -07:00
h ( 'dl' , { class : 'fl' }, [[ 'Signs with' , 'the lab wallet: this holding predates lab keys, so it has no addresses, leaves or secrets of its own' ], held ]. map ( fact )));
2026-10-14 19:49:05 -07:00
const agent = n . kind === 'agent' , a = addressesOf ( lab . keys , view , n ), leaves = leavesOf ( lab . keys , view , n );
const lines = leaves . map (([, , line ]) => line ). filter ( Boolean ), reset = 'Reset the lab for standard BIP-86 keys.' ;
const holds = agent ? "Holds this seat's inscription, and inscriptions sent to it land here." : "Holds this org's inscription and signs its acts. Inscriptions sent to it land here." ;
const taproot = { bip86 : `BIP-86 at ${ a . taproot . path } . ${ holds } ` , tapkey : `Its npub's key with the BIP-86 tweak, the way v1.2 keyed this lab. ${ holds } ${ reset } ` ,
wallet : `An untweaked key at ${ a . taproot . path } , the way v1.1 keyed this lab. ${ holds } ${ reset } ` }[ k . scheme ];
2026-10-14 22:25:38 -07:00
const facts = [[ 'Taproot' , a . taproot . address , taproot ], [ 'SegWit' , a . segwit . address , `BIP-84 at ${ a . segwit . path } . Its money: ${ n . kind === 'holding' ? 'the beats it earns land here, and ' : '' } sends come and go here.` ],
2026-10-14 20:14:11 -07:00
[ 'Tapkey' , a . tapkey . address , 'Its npub as a Taproot address (its nostr key with the BIP-86 tweak), so the npub alone finds it. Nostr secrets get pasted into clients, so treat it as an inbox.' ],
2026-10-14 19:49:05 -07:00
[ 'npub' , a . npub . address , `NIP-06 at ${ a . npub . path } . Its nostr name; the leaves below hang off it.` ], [ 'From' , ` ${ k . from } · account ${ k . account } ` ], held ];
2026-10-14 19:09:50 -07:00
return h ( 'div' , { class : 'keys' },
h ( 'p' , { class : 'lab' , text : 'Keys' }),
2026-10-14 19:49:05 -07:00
h ( 'dl' , { class : 'fl' }, facts . map ( fact )),
2026-10-14 19:09:50 -07:00
h ( 'p' , { class : 'lab' }, 'Leaves' , h ( 'span' , { text : ` · HKDF-SHA256 from its ${ agent ? 'nostr secret' : 'seed' } ` })),
h ( 'ul' , { class : 'list leaves' }, leaves . map (([ label , value ]) =>
h ( 'li' , {}, h ( 'b' , { text : label }), h ( 'span' , { class : 'when' , text : ` ${ LEAF [ label ] } ` }), h ( 'code' , { text : value })))),
2026-10-14 20:14:11 -07:00
copyBlock ( agent ? 'Public lines to paste' : 'CA key for TrustedUserCAKeys' , lines . join ( '\n\n' )),
secretsView ( n , k ));
}
// A code block with a Copy button; shown can lay the text out differently from what is copied.
function copyBlock ( label , text , shown = text ) {
const pre = h ( 'pre' , { text : shown });
const button = h ( 'button' , { type : 'button' , class : 'cp' , 'aria-label' : `Copy ${ label } ` , onclick : () => copy ( text , button , pre ) }, 'Copy' );
return h ( 'div' , { class : 'code' }, h ( 'div' , { class : 'code-h cph' }, h ( 'span' , { text : label }), button ), pre );
}
async function copy ( text , button , pre ) {
const done = label => { button . textContent = label ; setTimeout (() => { button . textContent = 'Copy' ; }, 1400 ); };
try { await navigator . clipboard . writeText ( text ); done ( 'Copied' ); } catch { getSelection (). selectAllChildren ( pre ); done ( 'Selected' ); }
}
// A node's secrets behind Reveal, as on the lineage key tree page: hidden again whenever the selection changes.
function secretsView ( n , k ) {
const open = ui . reveal === n . id , s = open && secretsOf ( lab . keys , view , n ), org = node ( n . parent , view );
const toggle = () => { ui . reveal = open ? null : n . id ; renderInspect (); $ ( 'reveal' ) ? . focus (); };
const words = s ? . seed ? . mnemonic . split ( ' ' );
return h ( 'div' , { class : 'secret' },
h ( 'div' , { class : 'hz' }, h ( 'span' , { class : 'hz-l' , text : 'Secret · stays in this tab' }),
h ( 'button' , { type : 'button' , class : 'btn sm' , id : 'reveal' , 'aria-expanded' : String ( open ), onclick : toggle }, open ? 'Hide' : 'Reveal' )),
s && h ( 'div' , { class : 'sb' },
s . seed ? copyBlock ( `Seed · ${ s . seed . from } ` , s . seed . mnemonic , words . map (( w , i ) => ` ${ String ( i + 1 ). padStart ( 2 , '0' ) } ${ w } ` ). reduce (( rows , w , i ) => ( i % 3 ? rows [ rows . length - 1 ]. push ( w ) : rows . push ([ w ]), rows ), []). map ( r => r . join ( ' ' )). join ( '\n' ))
: h ( 'p' , { class : 'note' , text : `A seat has no seed of its own: these are account ${ k . account } of ${ org ? . name ?? 'its org' } 's seed.` }),
s . account . map (([ label , kind , text ]) => copyBlock ( ` ${ label } · ${ kind } ` , text )),
s . leaves . map (([ label , title , text ]) => copyBlock ( ` ${ label } · ${ title } ` , text ))));
2026-10-14 18:22:22 -07:00
}
// The key that signed an event's reveal, named when it belongs to the node that authorized it.
function signerOf ( e ) {
const witness = lab . chain . transactions . get ( e . id . slice ( 0 , - 2 )) ? . tx . vin . find ( i => i . witness ? . length === 3 ) ? . witness ;
if ( ! witness ) return '?' ;
const pubkey = readEnvelope ( witness ). pubkey , by = node ( e . parent ), own = e . op === 'found' ? node ( e . node ) : null ;
const named = [[ own && keyOf ( lab . keys , live , own ), ` ${ own ? . name } 's root` ], [ by && keyOf ( lab . keys , live , by ), ` ${ by ? . name } 's key` ],
[ by ? . edge === 'adopted' && keyOf ( lab . keys , live , { ... by , edge : 'derived' }), ` ${ by ? . name } 's derived key, from before it graduated` ]];
for ( const [ k , label ] of named ) if ( k && k . wallet . pubkey === pubkey ) return label ;
return lab . wallet ? . pubkey === pubkey ? 'the lab wallet' : `a key outside this lab ( ${ pubkey . slice ( 0 , 12 ) } …)` ;
}
2026-10-14 17:28:33 -07:00
function eventView ( e ) {
const raw = index . events . find ( x => x . id === e . id ), body = lab . chain . transactions . get ( e . id . slice ( 0 , - 2 )) ? . inscription ? . text ;
const facts = [
[ 'When' , e . pending ? 'cued for the next beat' : `beat ${ e . beat } ` ],
[ 'Authorized by' , e . parent ? ` ${ node ( e . parent ) ? . name ?? 'an unknown inscription' } , whose inscription this event spent` : 'nothing: a founding has no parent' ],
2026-10-14 18:22:22 -07:00
[ 'Signed by' , signerOf ( e )], [ 'Inscription' , e . id ], body && [ 'Body' , body ], raw ? . block && [ 'Block' , raw . block ],
2026-10-14 17:28:33 -07:00
]. filter ( Boolean );
const entry = raw && { id : raw . id , status : raw . status , height : raw . height , block : raw . block , parent : raw . parent ?? null ,
metaprotocol : raw . metaprotocol ?? null , contentType : raw . contentType , sat : raw . sat , location : raw . location };
return [
h ( 'div' , { class : 'ih' }, h ( 'p' , { class : 'eyebrow' , text : ` ${ e . op } · ${ e . pending ? 'cued' : `beat ${ e . beat } ` } ` }), h ( 'h3' , { class : 'it' , text : summary ( e , live ) }),
h ( 'div' , { class : 'chips' }, h ( 'span' , { class : 'chip' , text : raw ? . protocol ?? '?' }), h ( 'span' , { class : `chip ${ e . ok ? 'ok' : 'bad' } ` , text : e . ok ? 'applied' : 'rejected' })),
! e . ok && h ( 'p' , { class : 'idesc' , text : 'The reducer rejected this event. It stays on the chain, but it does not change the tree.' })),
h ( 'div' , { class : 'ib' }, h ( 'dl' , { class : 'fl' }, facts . map ( fact )),
h ( 'div' , { class : 'code' }, h ( 'p' , { class : 'code-h' , text : 'Metadata · CBOR, tag 5' }), h ( 'pre' , { text : json ( e . meta ) })),
entry && h ( 'div' , { class : 'code' }, h ( 'p' , { class : 'code-h' , text : 'Raw index entry' }), h ( 'pre' , { text : json ( entry ) }))),
];
}
2026-10-14 22:25:38 -07:00
// A money move: a beat's reward, a send, or money following a node to its new keys.
function txView ( m ) {
const name = id => node ( id ) ? . name ?? 'a key outside this lab' , record = lab . chain . transactions . get ( m . txid );
const facts = [
[ 'When' , m . beat === null ? 'cued for the next beat' : `beat ${ m . beat } ` ],
[ 'From' , m . kind === 'mine' ? "the beat's block reward (its coinbase)" : ` ${ name ( m . from ) } 's SegWit address ${ m . kind === 'move' ? ', from before it graduated' : '' } ` ],
[ 'To' , ` ${ name ( m . to ) } 's ${ m . kind === 'move' ? 'new ' : '' } SegWit address` ],
[ 'Amount' , btc ( m . amount )],
m . kind !== 'mine' && [ 'Fee' , ` ${ m . fee } sats, retired: this toy chain pays no fees to miners` ],
m . change > 0 && [ 'Change' , ` ${ btc ( m . change ) } back to ${ name ( m . from ) } ` ],
[ 'Signed by' , m . kind === 'mine' ? 'nobody: a coinbase spends nothing' : ` ${ name ( m . from ) } 's SegWit key (BIP-143, SIGHASH_ALL)` ],
[ 'Transaction' , m . txid ],
]. filter ( Boolean );
return [
h ( 'div' , { class : 'ih' }, h ( 'p' , { class : 'eyebrow' , text : ` ${ m . kind } · ${ m . beat === null ? 'cued' : `beat ${ m . beat } ` } ` }), h ( 'h3' , { class : 'it' , text : moneyLine ( m ) }),
h ( 'div' , { class : 'chips' }, h ( 'span' , { class : 'chip' , text : 'test BTC' }), h ( 'span' , { class : `chip ${ m . beat === null ? '' : ' ok' } ` , text : m . beat === null ? 'cued' : 'confirmed' }))),
h ( 'div' , { class : 'ib' }, h ( 'dl' , { class : 'fl' }, facts . map ( fact )), record && copyBlock ( 'Raw transaction' , record . raw )),
];
}
2026-10-14 17:28:33 -07:00
function renderTimeline () {
const t = tip (), slider = $ ( 'beat' ), root = node ( ui . holding );
slider . max = String ( t );
slider . value = String ( ui . beat ?? t );
slider . disabled = t === 0 ;
$ ( 'beat-label' ). textContent = ui . beat === null ? `Now · beat ${ t } ` : `Beat ${ ui . beat } of ${ t } ` ;
$ ( 'replay' ). textContent = ui . replay ? 'Stop' : 'Replay' ;
$ ( 'replay' ). disabled = ! root || t === 0 ;
$ ( 'to-now' ). disabled = ui . beat === null ;
$ ( 'timeline-sub' ). textContent = root ? . name ?? '' ;
2026-10-14 22:25:38 -07:00
const groups = new Map (), order = new Map ([... lab . chain . transactions . keys ()]. map (( txid , i ) => [ txid , i ]));
const add = ( k , item ) => groups . set ( k , [...( groups . get ( k ) ?? []), item ]);
for ( const e of live . log ) if ( e . root && e . root === ui . holding ) add ( e . pending ? 'cued' : e . beat , { e , at : order . get ( e . id . slice ( 0 , - 2 )) ?? 0 });
for ( const m of money . ledger ) if ([ m . to , m . from ]. some ( id => id && node ( id ) ? . root === ui . holding )) add ( m . beat ?? 'cued' , { m , at : order . get ( m . txid ) ?? 0 });
2026-10-14 17:28:33 -07:00
const keys = [... groups . keys ()]. sort (( a , b ) => ( a === 'cued' ? - 1 : b === 'cued' ? 1 : b - a ));
if ( ! keys . length ) return $ ( 'timeline-list' ). replaceChildren ( h ( 'li' , { class : 'wait' , text : root ? 'Nothing yet.' : 'Found a holding to start its timeline.' }));
$ ( 'timeline-list' ). replaceChildren (... keys . map ( k => {
const block = k === 'cued' ? null : lab . chain . blocks [ k - 1 ];
const later = ui . beat !== null && ( k === 'cued' || k > ui . beat ), current = ui . beat !== null && k === ui . beat ;
return h ( 'li' , { class : `beat ${ later ? ' later' : '' }${ current ? ' now' : '' } ` },
h ( 'p' , { class : 'bh' }, h ( 'span' , { class : 'bn' , text : k === 'cued' ? 'Cued' : `Beat ${ k } ` }),
h ( 'span' , { class : 'bt' , text : block ? clockTime ( block . time ) : lab . clock . tempo === 'manual' ? 'waiting for Beat now' : 'landing on the next beat' })),
2026-10-14 22:25:38 -07:00
h ( 'ol' , { class : 'evs' }, groups . get ( k ). sort (( a , b ) => a . at - b . at ). map (({ e , m }) => ( m ? moneyItem ( m ) : eventItem ( e )))));
2026-10-14 17:28:33 -07:00
}));
}
2026-10-14 22:25:38 -07:00
function eventItem ( e ) {
const [ verb , ... rest ] = summary ( e , live ). split ( ' ' );
return h ( 'li' , {}, h ( 'button' , { type : 'button' , class : `ev ${ e . ok ? '' : ' bad' }${ e . id === ui . event ? ' on' : '' } ` , 'aria-pressed' : String ( e . id === ui . event ),
onclick : () => pick ({ event : e . id , node : e . node ?? ui . node }) },
h ( 'span' , { class : 'op' , text : e . ok ? verb : `✕ ${ verb . replace ( /:$/ , '' ) } ` }), ' ' , h ( 'span' , { class : 'es' , text : rest . join ( ' ' ) })));
}
const moneyItem = m => h ( 'li' , {}, h ( 'button' , { type : 'button' , class : `ev money ${ m . txid === ui . tx ? ' on' : '' } ` , 'aria-pressed' : String ( m . txid === ui . tx ),
onclick : () => pick ({ tx : m . txid , node : m . to ?? ui . node }) }, h ( 'span' , { class : 'op' , text : m . kind }), ' ' , h ( 'span' , { class : 'es' , text : moneyLine ( m ) })));
2026-10-14 17:28:33 -07:00
// Verbs
$ ( 'f-found' ). addEventListener ( 'submit' , async event => {
event . preventDefault ();
const form = event . currentTarget , name = slug ( form . elements . name . value );
if ( ! name ) return say ( 'Name the holding: lowercase letters, digits and hyphens.' , true );
if ( await perform ( 'found' , null , { name }, ( id , where ) => { select ({ holding : id , node : id }); return ` ${ name } is founded, ${ where } .` ; })) form . reset ();
});
$ ( 'sample' ). addEventListener ( 'click' , async () => {
let id = null ;
say ( 'Rehearsing the yolopreneur tree over six beats…' );
if ( ! ( await run ( l => { id = seedSample ( l ); }))) return ;
pick ({ holding : id , node : id });
say ( 'Loaded the yolopreneur sample: six beats of history. Press Replay to watch it grow.' );
});
$ ( 'f-birth' ). addEventListener ( 'submit' , async event => {
event . preventDefault ();
const form = event . currentTarget , n = node ( ui . node ), name = slug ( form . elements . name . value ), kind = form . elements . kind . value ;
if ( ! name ) return say ( 'Name the child: lowercase letters, digits and hyphens.' , true );
if ( await perform ( 'birth' , n . id , { name , kind }, ( id , where ) => ` ${ name } is born to ${ n . name } , ${ where } .` )) form . elements . name . value = '' ;
});
$ ( 'f-award' ). addEventListener ( 'submit' , async event => {
event . preventDefault ();
const form = event . currentTarget , n = node ( ui . node ), to = form . elements . to . value , skill = slug ( form . elements . skill . value ), level = form . elements . belt . value ;
if ( ! skill ) return say ( 'Name the skill, for example launch.' , true );
if ( await perform ( 'award' , n . id , { to , skill , belt : level }, ( id , where ) => ` ${ node ( to ) ? . name } holds a ${ level } belt in ${ skill } , ${ where } .` )) form . elements . skill . value = '' ;
});
$ ( 'f-decide' ). addEventListener ( 'submit' , async event => {
event . preventDefault ();
const form = event . currentTarget , n = node ( ui . node ), title = form . elements . title . value . trim (), why = form . elements . why . value . trim ();
if ( ! title ) return say ( 'A decision needs a title.' , true );
if ( await perform ( 'decide' , n . id , { title , why }, ( id , where ) => ` ${ n . name } decided “ ${ title } ”, ${ where } .` )) form . reset ();
});
2026-10-14 22:25:38 -07:00
$ ( 'f-send' ). addEventListener ( 'submit' , async event => {
event . preventDefault ();
const form = event . currentTarget , n = node ( ui . node ), to = form . elements . to . value , amount = satsOf ( form . elements . amount . value );
if ( amount === null ) return say ( 'Write an amount in test BTC, for example 1.5 (at most eight decimals).' , true );
let txid = null ;
if ( ! ( await run ( l => { txid = send ( l , n . id , to , amount ); }))) return ;
const m = money . ledger . find ( x => x . txid === txid );
say ( ` ${ n . name } sent ${ btc ( amount ) } to ${ node ( to ) ? . name } , ${ m ? . beat ? `at beat ${ m . beat } ` : 'cued for the next beat' } .` );
form . elements . amount . value = '' ;
});
$ ( 'send-max' ). addEventListener ( 'click' , () => {
const n = node ( ui . node ), wallet = n && payerOf ( lab . keys , live , n );
if ( wallet ) $ ( 'send-amount' ). value = coins ( most ( lab . chain . coins ( wallet . script )));
});
2026-10-14 17:28:33 -07:00
$ ( 'f-graduate' ). addEventListener ( 'submit' , async event => {
event . preventDefault ();
const n = node ( ui . node ), parent = node ( n . parent );
await perform ( 'graduate' , parent . id , { of : n . id }, ( id , where ) => ` ${ n . name } graduates from ${ parent . name } , ${ where } . Its edge is adopted now.` );
});
$ ( 'f-close' ). addEventListener ( 'submit' , async event => {
event . preventDefault ();
const n = node ( ui . node );
if ( ui . closing !== n . id ) { ui . closing = n . id ; renderAct (); return $ ( 'close-go' ). focus (); }
ui . closing = null ;
await perform ( 'close' , n . id , {}, ( id , where ) => ` ${ n . name } is closed, ${ where } .` );
});
$ ( 'close-keep' ). addEventListener ( 'click' , () => { ui . closing = null ; renderAct (); $ ( 'close-go' ). focus (); });
for ( const b of document . querySelectorAll ( '[data-verb]' )) b . addEventListener ( 'click' , () => { ui . verb = b . dataset . verb ; ui . closing = null ; renderAct (); });
// Time: the tempo, Beat now, and the clock that lands cued acts when their beat is due
for ( const b of document . querySelectorAll ( '[data-tempo]' )) b . addEventListener ( 'click' , async () => {
const value = b . dataset . tempo , tempo = /^\d+$/ . test ( value ) ? Number ( value ) : value ;
2026-10-14 22:25:38 -07:00
if ( ! ( await run ( l => setTempo ( l , tempo , Date . now (), minerOf ( l , ui . holding ))))) return ;
2026-10-14 17:28:33 -07:00
say ( value === 'each' ? 'Each act is its own beat.' : value === 'manual' ? 'Acts are cued until you press Beat now.'
: `Acts are cued and land together on the next beat, at most ${ value } seconds after the first.` );
});
2026-10-14 22:25:38 -07:00
$ ( 'beat-now' ). addEventListener ( 'click' , () => landCue ( l => { beat ( l , minerOf ( l , ui . holding )); }));
2026-10-14 17:28:33 -07:00
setInterval (() => {
renderClock ();
const { tempo , nextAt } = lab . clock ;
if ( ui . busy || document . hidden || typeof tempo !== 'number' || ! lab . chain . mempool . length || ( nextAt && Date . now () < nextAt )) return ;
2026-10-14 22:25:38 -07:00
landCue ( l => tick ( l , Date . now (), minerOf ( l , ui . holding )));
2026-10-14 17:28:33 -07:00
}, 1000 );
addEventListener ( 'storage' , event => { if ( event . key === LAB_KEY || event . key === null ) load (); });
// The timeline: any beat, a replay, and back to now
function stopReplay () { if ( ui . replay ) { clearInterval ( ui . replay ); ui . replay = null ; renderTimeline (); } }
function startReplay () {
const root = node ( ui . holding );
if ( ! root || ! tip ()) return ;
setBeat ( typeof root . born === 'number' ? root . born : tip ());
if ( ui . beat === null ) return say ( 'Nothing to replay yet: this holding has one beat of history.' );
ui . replay = setInterval (() => { if ( ui . beat === null || ui . beat + 1 >= tip ()) stopReplay (); setBeat ( ui . beat === null ? tip () : ui . beat + 1 ); }, 800 );
renderTimeline ();
}
$ ( 'beat' ). addEventListener ( 'input' , event => { stopReplay (); setBeat ( Number ( event . target . value )); });
$ ( 'replay' ). addEventListener ( 'click' , () => ( ui . replay ? stopReplay () : startReplay ()));
for ( const id of [ 'to-now' , 'past-now' ]) $ ( id ). addEventListener ( 'click' , () => { stopReplay (); setBeat ( tip ()); });
// Theme: system, light, dark (public/theme.js applies the saved choice before first paint)
const THEMES = [ 'system' , 'light' , 'dark' ];
const showTheme = () => { $ ( 'theme' ). textContent = `Theme: ${ document . documentElement . dataset . theme ?? 'system' } ` ; };
$ ( 'theme' ). addEventListener ( 'click' , () => {
const next = THEMES [( THEMES . indexOf ( document . documentElement . dataset . theme ?? 'system' ) + 1 ) % THEMES . length ];
if ( next === 'system' ) delete document . documentElement . dataset . theme ; else document . documentElement . dataset . theme = next ;
try { if ( next === 'system' ) localStorage . removeItem ( 'orgchain-theme' ); else localStorage . setItem ( 'orgchain-theme' , next ); } catch { /* not remembered */ }
showTheme ();
});
// The lab: save, load and reset
function confirmBox ( text , label , onYes ) {
const box = $ ( 'confirm' );
box . replaceChildren ( h ( 'span' , { text }), h ( 'button' , { type : 'button' , class : 'btn sm danger' , text : label , onclick : () => { box . hidden = true ; onYes (); } }),
h ( 'button' , { type : 'button' , class : 'btn sm' , text : 'Cancel' , onclick : () => { box . hidden = true ; } }));
box . hidden = false ;
box . querySelector ( 'button' ). focus ();
}
const forgiving = { read : () => { try { return readLab (); } catch { return emptyLab (); } } };
async function replaceLab ( next , message ) {
stopReplay ();
if ( ! ( await run ( l => { Object . assign ( l , next ()); }, forgiving ))) return ;
2026-10-14 22:25:38 -07:00
Object . assign ( ui , { holding : null , node : null , event : null , tx : null , beat : null , closing : null });
2026-10-14 17:28:33 -07:00
derive ();
say ( message );
}
$ ( 'save' ). addEventListener ( 'click' , () => {
const link = h ( 'a' , { href : URL . createObjectURL ( new Blob ([ encodeLab ( lab )], { type : 'application/json' })), download : `orgchain-lab- ${ new Date (). toISOString (). slice ( 0 , 10 ) } .json` });
document . body . append ( link );
link . click ();
link . remove ();
setTimeout (() => URL . revokeObjectURL ( link . href ), 5000 );
say ( 'Saved a backup of this lab, test wallet included. Load it in any browser to carry on.' );
});
$ ( 'load' ). addEventListener ( 'click' , () => $ ( 'load-file' ). click ());
$ ( 'load-file' ). addEventListener ( 'change' , async event => {
const file = event . target . files ? .[ 0 ];
event . target . value = '' ;
if ( ! file ) return ;
let next ;
try { next = decodeLab ( await file . text ()); } catch ( error ) { return say ( ` ${ file . name } is not an Orgchain lab: ${ error . message } ` , true ); }
confirmBox ( `Replace this browser's lab with ${ file . name } ?` , 'Replace' , () => replaceLab (() => next , `Loaded ${ file . name } .` ));
});
$ ( 'reset' ). addEventListener ( 'click' , () => confirmBox ( "Erase this browser's lab: every holding, the chain and the test wallet?" , 'Erase' ,
() => replaceLab ( emptyLab , 'Erased. This is a fresh lab.' )));
showTheme ();
load ();