--- /home/jwoltje/darkwing-scratch/r165/public-cb79be2e.diff 2026-10-18 16:59:27.558469838 -0500 +++ /mnt/storage/src/mosaic-stack/agents/darkwing/work/queue-165/public.diff 2026-10-18 16:59:27.562346562 -0500 @@ -1,8 +1,8 @@ diff --git a/packages/webui/src/public/bus.js b/packages/webui/src/public/bus.js -index 1e0be849d..32294be69 100644 +index 1e0be849d..67abd41f6 100644 --- a/packages/webui/src/public/bus.js +++ b/packages/webui/src/public/bus.js -@@ -671,17 +671,68 @@ +@@ -671,17 +671,70 @@ await assistantPost('/api/name',{instance,name},instance,result=>{if(typeof result?.name!=='string')return unknownAssistant;delete nameDrafts[instance];const prior=last.get('names');if(prior)last.set('names',{...prior,rows:[...prior.rows.filter(r=>r.instance!==result.instance),{instance:result.instance,name:result.name,at:result.at}]});return `Seat name set to ${result.name}.`;});} else if(e.target.dataset.assistantStop!==undefined)await assistantPost('/api/assistant-stop',{instance},instance,result=>Array.isArray(result?.stopped)?`Stopped ${plural(result.stopped.length,'run')}.`:unknownAssistant); }); @@ -11,7 +11,7 @@ + // offered: binding credentials stays a hand edit. When a change needs a bus restart the receipt + // says so; the Console never restarts the bus. Drafts survive polling and refusals; nothing retries. + let instanceView=null, instanceDrafts={add:null,change:null}, instanceBusy=false, instanceReceipt=''; -+ const RESTART_TEXT='Restart needed: the running bus keeps the roles it loaded at start. A bus restart needs Mos\'s go or your hand; the Console does not restart it.'; ++ const RESTART_TEXT='Restart needed: the running bus keeps the roles it loaded at start. The Console does not restart it.'; + const NO_RESTART_TEXT='No restart: the launcher reads the change at its next launch check.'; + function instancesSection(view){ + const title='

Role instances

'; @@ -43,6 +43,8 @@ + return payload; + } + const unknownInstance='Outcome unknown. Read the role instances again before trying. Draft kept.'; ++ // The answer replaces the view the next render draws, so it must have the read's whole shape. ++ const instanceAnswer=r=>/^[a-f0-9]{64}$/.test(r?.revision??'')&&typeof r.restart==='boolean'&&[r.instances,r.definitions].every(o=>o&&typeof o==='object'&&!Array.isArray(o)); + for(const kind of ['input','change'])document.addEventListener(kind,e=>{const form=e.target?.closest?.('.instance-form');if(form)instanceDrafts[form.dataset.op]=instanceFields(form);}); + document.addEventListener('click',e=>{if(e.target.id!=='instances-reload')return;instanceDrafts={add:null,change:null};instanceView=null;instanceReceipt='';last.delete('instances');render(true);}); + document.addEventListener('submit',async e=>{ @@ -51,8 +53,8 @@ + try{payload=instancePayload(op,instanceDrafts[op]);}catch(err){instanceReceipt=err.message;render(false);return;} + instanceBusy=true;instanceReceipt='';render(false); + try{const response=await fetch('/api/instances',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(payload),cache:'no-store',redirect:'manual'});if(response.type==='opaqueredirect'){instanceReceipt='Sign in again. Draft kept.';signOut();}else{const result=await response.json(); -+ if(response.ok&&/^[a-f0-9]{64}$/.test(result?.revision??'')&&typeof result.restart==='boolean'){instanceDrafts[op]=null;instanceView=result;const prior=last.get('instances');if(prior)last.set('instances',{...prior,rows:[result]});instanceReceipt=`${op==='add'?'Added':'Changed'} ${payload.instance}. ${result.restart?RESTART_TEXT:NO_RESTART_TEXT}`;} -+ else instanceReceipt=result?.sent==='unknown'?unknownInstance:`${result?.message||'Refused.'} Draft kept.`;}} ++ if(response.ok&&instanceAnswer(result)){instanceDrafts[op]=null;instanceView=result;const prior=last.get('instances');if(prior)last.set('instances',{...prior,rows:[result]});instanceReceipt=`${op==='add'?'Added':'Changed'} ${payload.instance}. ${result.restart?RESTART_TEXT:NO_RESTART_TEXT}`;} ++ else instanceReceipt=response.ok||result?.sent==='unknown'?unknownInstance:`${result?.message||'Refused.'} Draft kept.`;}} + catch{instanceReceipt=unknownInstance;}finally{instanceBusy=false;if(!out())render(false);} + }); async function settingsView(get) {