@cef-ai/cli 0.2.7 → 0.3.0

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.
Files changed (127) hide show
  1. package/README.md +75 -1247
  2. package/dist/build/duration.d.ts +22 -0
  3. package/dist/build/duration.d.ts.map +1 -0
  4. package/dist/build/duration.js +44 -0
  5. package/dist/build/duration.js.map +1 -0
  6. package/dist/build/esbuild-plugin.d.ts +32 -0
  7. package/dist/build/esbuild-plugin.d.ts.map +1 -0
  8. package/dist/build/esbuild-plugin.js +53 -0
  9. package/dist/build/esbuild-plugin.js.map +1 -0
  10. package/dist/build/lints.d.ts +55 -0
  11. package/dist/build/lints.d.ts.map +1 -0
  12. package/dist/build/lints.js +214 -0
  13. package/dist/build/lints.js.map +1 -0
  14. package/dist/build/load-config.d.ts +114 -0
  15. package/dist/build/load-config.d.ts.map +1 -0
  16. package/dist/build/load-config.js +150 -0
  17. package/dist/build/load-config.js.map +1 -0
  18. package/dist/build/manifest.d.ts +156 -0
  19. package/dist/build/manifest.d.ts.map +1 -0
  20. package/dist/build/manifest.js +111 -0
  21. package/dist/build/manifest.js.map +1 -0
  22. package/dist/build/targeting.d.ts +15 -0
  23. package/dist/build/targeting.d.ts.map +1 -0
  24. package/dist/build/targeting.js +69 -0
  25. package/dist/build/targeting.js.map +1 -0
  26. package/dist/cli.d.ts +2 -2
  27. package/dist/cli.d.ts.map +1 -0
  28. package/dist/cli.js +107 -62
  29. package/dist/cli.js.map +1 -0
  30. package/dist/commands/build.d.ts +41 -0
  31. package/dist/commands/build.d.ts.map +1 -0
  32. package/dist/commands/build.js +252 -0
  33. package/dist/commands/build.js.map +1 -0
  34. package/dist/commands/inspect.d.ts +49 -0
  35. package/dist/commands/inspect.d.ts.map +1 -0
  36. package/dist/commands/inspect.js +135 -0
  37. package/dist/commands/inspect.js.map +1 -0
  38. package/dist/commands/typegen.d.ts +36 -0
  39. package/dist/commands/typegen.d.ts.map +1 -0
  40. package/dist/commands/typegen.js +210 -0
  41. package/dist/commands/typegen.js.map +1 -0
  42. package/dist/index.d.ts +6 -0
  43. package/dist/index.d.ts.map +1 -0
  44. package/dist/index.js +6 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/lib.d.ts +12 -0
  47. package/dist/lib.d.ts.map +1 -0
  48. package/dist/lib.js +9 -0
  49. package/dist/lib.js.map +1 -0
  50. package/dist/typegen/augment.d.ts +57 -0
  51. package/dist/typegen/augment.d.ts.map +1 -0
  52. package/dist/typegen/augment.js +87 -0
  53. package/dist/typegen/augment.js.map +1 -0
  54. package/dist/typegen/fetch.d.ts +30 -0
  55. package/dist/typegen/fetch.d.ts.map +1 -0
  56. package/dist/typegen/fetch.js +38 -0
  57. package/dist/typegen/fetch.js.map +1 -0
  58. package/dist/typegen/lockfile.d.ts +42 -0
  59. package/dist/typegen/lockfile.d.ts.map +1 -0
  60. package/dist/typegen/lockfile.js +58 -0
  61. package/dist/typegen/lockfile.js.map +1 -0
  62. package/dist/typegen/schema-to-ts.d.ts +21 -0
  63. package/dist/typegen/schema-to-ts.d.ts.map +1 -0
  64. package/dist/typegen/schema-to-ts.js +29 -0
  65. package/dist/typegen/schema-to-ts.js.map +1 -0
  66. package/package.json +20 -58
  67. package/dist/commands/auth/index.d.ts +0 -2
  68. package/dist/commands/auth/index.js +0 -162
  69. package/dist/commands/deploy/clone.d.ts +0 -13
  70. package/dist/commands/deploy/clone.js +0 -143
  71. package/dist/commands/deploy/delete.d.ts +0 -14
  72. package/dist/commands/deploy/delete.js +0 -134
  73. package/dist/commands/deploy/deploy.d.ts +0 -24
  74. package/dist/commands/deploy/deploy.js +0 -654
  75. package/dist/commands/deploy/index.d.ts +0 -2
  76. package/dist/commands/deploy/index.js +0 -216
  77. package/dist/commands/deploy/status.d.ts +0 -14
  78. package/dist/commands/deploy/status.js +0 -61
  79. package/dist/commands/dev/index.d.ts +0 -2
  80. package/dist/commands/dev/index.js +0 -134
  81. package/dist/commands/dev/server.d.ts +0 -14
  82. package/dist/commands/dev/server.js +0 -195
  83. package/dist/commands/dev/static/app.js +0 -633
  84. package/dist/commands/dev/static/index.html +0 -205
  85. package/dist/commands/dev/static/static/app.js +0 -633
  86. package/dist/commands/dev/static/static/index.html +0 -205
  87. package/dist/commands/dev/static/static/style.css +0 -184
  88. package/dist/commands/dev/static/style.css +0 -184
  89. package/dist/commands/playground/index.d.ts +0 -2
  90. package/dist/commands/playground/index.js +0 -75
  91. package/dist/commands/playground/test.d.ts +0 -15
  92. package/dist/commands/playground/test.js +0 -166
  93. package/dist/commands/service/index.d.ts +0 -2
  94. package/dist/commands/service/index.js +0 -89
  95. package/dist/lib/api.d.ts +0 -146
  96. package/dist/lib/api.js +0 -330
  97. package/dist/lib/config.d.ts +0 -12
  98. package/dist/lib/config.js +0 -145
  99. package/dist/lib/credentials.d.ts +0 -19
  100. package/dist/lib/credentials.js +0 -68
  101. package/dist/lib/runtime/agent-router.d.ts +0 -12
  102. package/dist/lib/runtime/agent-router.js +0 -67
  103. package/dist/lib/runtime/context-builder.d.ts +0 -27
  104. package/dist/lib/runtime/context-builder.js +0 -166
  105. package/dist/lib/runtime/cubby-manager.d.ts +0 -20
  106. package/dist/lib/runtime/cubby-manager.js +0 -162
  107. package/dist/lib/runtime/index.d.ts +0 -51
  108. package/dist/lib/runtime/index.js +0 -181
  109. package/dist/lib/runtime/log-collector.d.ts +0 -17
  110. package/dist/lib/runtime/log-collector.js +0 -61
  111. package/dist/lib/runtime/stream-manager.d.ts +0 -10
  112. package/dist/lib/runtime/stream-manager.js +0 -77
  113. package/dist/lib/runtime/types.d.ts +0 -76
  114. package/dist/lib/runtime/types.js +0 -2
  115. package/dist/lib/transpile.d.ts +0 -1
  116. package/dist/lib/transpile.js +0 -9
  117. package/dist/lib/typecheck.d.ts +0 -31
  118. package/dist/lib/typecheck.js +0 -175
  119. package/dist/lib/types.d.ts +0 -246
  120. package/dist/lib/types.js +0 -2
  121. package/dist/lib/utils.d.ts +0 -4
  122. package/dist/lib/utils.js +0 -33
  123. package/example/.env.example +0 -6
  124. package/example/agents/object-detection/tasks/yolo.ts +0 -28
  125. package/example/agents/parking-violation-detector/tasks/detect.ts +0 -326
  126. package/example/cef.config.yaml +0 -91
  127. package/example/engagements/engagement.ts +0 -902
@@ -1,633 +0,0 @@
1
- (function(){
2
- const API='';
3
- let topology=null,activeEngagement=null,seqNum=0,ws=null,logFilter='',blastPayloads=null,blastAbort=false,batchRunning=false,batchAbort=false;
4
-
5
- async function api(method,path,body){
6
- const opts={method,headers:{'Content-Type':'application/json'}};
7
- if(body!==undefined)opts.body=JSON.stringify(body);
8
- const res=await fetch(API+path,opts);return res.json();
9
- }
10
-
11
- // WebSocket
12
- function connectWs(){
13
- const proto=location.protocol==='https:'?'wss:':'ws:';
14
- ws=new WebSocket(proto+'//'+location.host+'/ws');
15
- ws.onopen=()=>{$('status-dot').className='status-dot ok';$('status-pill').innerHTML='<span class="status-dot ok" id="status-dot"></span>'+location.host};
16
- ws.onclose=()=>{$('status-dot').className='status-dot err';$('status-pill').innerHTML='<span class="status-dot err" id="status-dot"></span>reconnecting';setTimeout(connectWs,2000)};
17
- ws.onmessage=(e)=>{const m=JSON.parse(e.data);if(m.type==='log')appendLog(m.data);if(m.type==='trace')appendTrace(m.data)};
18
- }
19
-
20
- // Logs
21
- function appendLog(entry){
22
- const c=$('log-container'),d=document.createElement('div');
23
- d.className='log-line'+(entry.level!=='log'?' '+entry.level:'');
24
- const ts=new Date(entry.timestamp).toISOString().slice(11,23);
25
- const ag=entry.agentAlias?(entry.agentAlias+(entry.taskAlias?'.'+entry.taskAlias:'')):'';
26
- const pf=ag?'<span class="agent">['+ag+']</span> ':'';
27
- const args=entry.args.map(a=>typeof a==='string'?a:JSON.stringify(a)).join(' ');
28
- d.innerHTML='<span class="ts">'+ts+'</span>'+pf+esc(args);
29
- if(logFilter&&!args.toLowerCase().includes(logFilter))d.style.display='none';
30
- c.appendChild(d);while(c.children.length>500)c.removeChild(c.firstChild);
31
- c.scrollTop=c.scrollHeight;
32
- }
33
-
34
- // Flow
35
- function appendTrace(ev){
36
- const c=$('flow-container'),d=document.createElement('div');
37
- d.className='flow-entry';
38
- const ts=new Date(ev.timestamp).toISOString().slice(11,23);
39
- let ind='',det='',cls='';
40
- switch(ev.type){
41
- case'engagement:start':cls='fe-eng';det=ev.callee||'';break;
42
- case'agent:call':ind=' ';cls='fe-agent';det=ev.callee+(ev.caller?' <span class="fe-dim">\u2190 '+esc(ev.caller)+'</span>':'');break;
43
- case'agent:return':ind=' ';cls=ev.success?'fe-ok':'fe-err';det=ev.callee+' <span class="fe-dim">'+(ev.durationMs||0).toFixed(1)+'ms</span>'+(ev.success?'':' <span class="fe-err">'+esc(ev.error||'FAILED')+'</span>');break;
44
- case'cubby:query':ind=' ';cls='fe-cubby';det=(ev.cubbyAlias||'')+' <span class="fe-dim">'+esc(ev.sql||'')+'</span>';break;
45
- case'cubby:exec':ind=' ';cls='fe-cubby';det=(ev.cubbyAlias||'')+' <span class="fe-dim">'+esc(ev.sql||'')+'</span>'+(ev.rowsAffected!=null?' <span class="fe-dim">('+ev.rowsAffected+')</span>':'');break;
46
- }
47
- d.innerHTML=ind+'<span class="fe-time">'+ts+'</span> <span class="fe-type '+cls+'">'+esc(ev.type)+'</span> '+det;
48
- c.appendChild(d);c.scrollTop=c.scrollHeight;
49
- }
50
-
51
- // Sidebar
52
- async function loadTopology(){topology=await api('GET','/api/topology');renderSidebar();updateEmptyStats()}
53
- function updateEmptyStats(){
54
- if(!topology)return;
55
- var el=$('empty-stats');if(!el)return;
56
- el.innerHTML=
57
- '<div class="stat-card"><div class="stat-num">'+topology.engagements.length+'</div><div class="stat-label">Engagements</div></div>'+
58
- '<div class="stat-card"><div class="stat-num">'+topology.agents.reduce(function(s,a){return s+a.tasks.length},0)+'</div><div class="stat-label">Agent Tasks</div></div>'+
59
- '<div class="stat-card"><div class="stat-num">'+topology.cubbies.length+'</div><div class="stat-label">Cubbies</div></div>';
60
- }
61
- function renderSidebar(){
62
- const el=$('sidebar');let h='';
63
- if(topology.engagements.length){
64
- h+='<div class="s-label">&#9654; Engagements</div>';
65
- for(const e of topology.engagements)h+='<div class="s-item" data-type="engagement" data-name="'+ea(e.name)+'"><span class="s-dot s-dot-eng"></span>'+esc(e.name)+'</div>';
66
- }
67
- if(topology.agents.length){
68
- h+='<div class="s-label">&#9881; Agents</div>';
69
- for(const a of topology.agents)for(const t of a.tasks)
70
- h+='<div class="s-item" data-type="agent" data-alias="'+ea(a.alias)+'" data-task="'+ea(t.alias)+'"><span class="s-dot s-dot-agent"></span>'+esc(a.alias)+'.<span class="s-task">'+esc(t.alias)+'</span></div>';
71
- }
72
- if(topology.cubbies.length){
73
- h+='<div class="s-label">&#9636; Cubbies</div>';
74
- for(const c of topology.cubbies)h+='<div class="s-item" data-type="cubby" data-alias="'+ea(c.alias)+'"><span class="s-dot s-dot-cubby"></span>'+esc(c.alias)+'</div>';
75
- const graphCubbies=topology.cubbies.filter(c=>c.alias);
76
- if(graphCubbies.length)h+='<div class="s-item" data-type="graph-viz" style="margin-top:4px"><span class="s-dot" style="background:#22c55e"></span> Graph Visualizer</div>';
77
- }
78
- el.innerHTML=h;
79
- el.querySelectorAll('.s-item').forEach(item=>{
80
- item.addEventListener('click',()=>{
81
- el.querySelectorAll('.s-item').forEach(i=>i.classList.remove('active'));
82
- item.classList.add('active');
83
- const t=item.dataset.type;
84
- if(t==='engagement')showEngagement(item.dataset.name);
85
- else if(t==='agent')showAgent(item.dataset.alias,item.dataset.task);
86
- else if(t==='cubby')showCubby(item.dataset.alias);
87
- else if(t==='graph-viz'){const gc=topology.cubbies[0];if(gc){showCubby(gc.alias);setTimeout(()=>{const tab=document.querySelector('#panel-cubby .tab[data-tab="graph"]');if(tab)tab.click();},50);}}
88
- });
89
- });
90
- }
91
-
92
- // Panels
93
- function hideAll(){document.querySelectorAll('.panel').forEach(p=>p.classList.remove('active'));$('empty-state').style.display='none'}
94
-
95
- function showEngagement(name){
96
- hideAll();$('panel-engagement').classList.add('active');
97
- $('eng-name').textContent=name;$('eng-result').innerHTML='';
98
- const ec=topology.engagements.find(e=>e.name===name),params=ec?.parameters;
99
- $('eng-desc').textContent=ec?.description||'';
100
- if(params&&params.properties){
101
- const s=schemaToSample(params);
102
- if('streamId' in s&&!s.streamId)s.streamId=name.toLowerCase().replace(/\s+/g,'-')+'-stream';
103
- $('eng-payload').value=JSON.stringify(s,null,2);
104
- }else{$('eng-payload').value=JSON.stringify({streamId:name.toLowerCase().replace(/\s+/g,'-')+'-stream'},null,2)}
105
- $('eng-schema-hints').innerHTML=renderSchemaHints(params);
106
- if(activeEngagement&&activeEngagement.name===name){$('eng-stream-area').style.display='';$('eng-stop').style.display='';$('eng-start').style.display='none'}
107
- else{$('eng-stream-area').style.display='none';$('eng-stop').style.display='none';$('eng-start').style.display=''}
108
- }
109
-
110
- function showAgent(alias,task){
111
- hideAll();$('panel-agent').classList.add('active');
112
- const lbl=$('agent-label');lbl.textContent=alias+'.'+task;lbl.dataset.alias=alias;lbl.dataset.task=task;
113
- $('agent-result').innerHTML='';
114
- const ac=topology.agents.find(a=>a.alias===alias),tc=ac?.tasks.find(t=>t.alias===task);
115
- const params=tc?.parameters,returns=tc?.returns;
116
- $('agent-desc').textContent=tc?.description||ac?.description||'';
117
- $('agent-payload').value=params&&params.properties?JSON.stringify(schemaToSample(params),null,2):'{}';
118
- $('agent-schema-hints').innerHTML=renderSchemaHints(params,returns);
119
- var bb=$('agent-batch');
120
- if(tc&&tc.batchSql){bb.style.display='';bb.dataset.batchSql=tc.batchSql;bb.dataset.batchCubby=tc.batchCubby||'';}
121
- else{bb.style.display='none';}
122
- }
123
-
124
- function showCubby(alias){
125
- hideAll();$('panel-cubby').classList.add('active');
126
- const lbl=$('cubby-label');lbl.textContent=alias;lbl.dataset.alias=alias;
127
- document.querySelectorAll('#panel-cubby .tab').forEach(t=>t.classList.remove('active'));
128
- document.querySelector('#panel-cubby .tab[data-tab="browse"]').classList.add('active');
129
- $('cubby-browse').style.display='';$('cubby-sql').style.display='none';$('cubby-graph').style.display='none';
130
- $('graph-tab').style.display='';
131
- $('panel-cubby').classList.add('panel-wide');
132
- qtRefreshSelect(alias);
133
- loadCubbyBrowse(alias);
134
- }
135
-
136
- function renderSchemaHints(params,returns){
137
- const hp=params&&params.properties&&Object.keys(params.properties).length>0;
138
- const hr=returns&&returns.properties&&Object.keys(returns.properties).length>0;
139
- if(!hp&&!hr)return'';
140
- let h='<div class="schema-pair">';
141
- if(hp){
142
- h+='<div class="schema-card"><div class="st-title">Input</div>';
143
- for(const[k,v]of Object.entries(params.properties)){
144
- const req=(params.required||[]).includes(k)?'<span class="sr">required</span>':'';
145
- h+='<div class="schema-row"><span class="sk">'+esc(k)+'</span><span class="st">'+esc(v.type||'any')+'</span>'+req;
146
- if(v.description)h+=' <span class="sd">'+esc(v.description)+'</span>';
147
- h+='</div>';
148
- }
149
- h+='</div>';
150
- }
151
- if(hr){
152
- h+='<div class="schema-card"><div class="st-title">Output</div>';
153
- for(const[k,v]of Object.entries(returns.properties))
154
- h+='<div class="schema-row"><span class="sk">'+esc(k)+'</span><span class="st">'+esc(v.type||'any')+'</span></div>';
155
- h+='</div>';
156
- }
157
- return h+'</div>';
158
- }
159
-
160
- function schemaToSample(s){
161
- if(!s||!s.properties)return{};const o={};
162
- for(const[k,v]of Object.entries(s.properties)){
163
- if(v.type==='string')o[k]='';else if(v.type==='number')o[k]=0;
164
- else if(v.type==='boolean')o[k]=false;else if(v.type==='array')o[k]=[];
165
- else if(v.type==='object')o[k]={};else o[k]=null;
166
- }return o;
167
- }
168
-
169
- // Engagement actions
170
- $('eng-file-blast').addEventListener('change',function(){
171
- if(!this.files||!this.files[0]){blastPayloads=null;return;}
172
- var reader=new FileReader();
173
- reader.onload=function(e){
174
- try{
175
- var parsed=JSON.parse(e.target.result);
176
- blastPayloads=Array.isArray(parsed)?parsed:[parsed];
177
- $('eng-result').innerHTML='<div class="success-banner">Loaded '+blastPayloads.length+' payloads from file</div>';
178
- $('eng-repeat').value=''+blastPayloads.length;
179
- }catch(err){blastPayloads=null;$('eng-result').innerHTML='<div class="error-banner">Invalid JSON: '+esc(err.message||String(err))+'</div>';}
180
- };
181
- reader.readAsText(this.files[0]);
182
- });
183
- $('eng-start').addEventListener('click',async function(){
184
- const name=$('eng-name').textContent;
185
- const delay=parseInt($('eng-delay').value)||0;
186
- let payloads;
187
- if(blastPayloads){
188
- payloads=blastPayloads;
189
- }else{
190
- let base;try{base=JSON.parse($('eng-payload').value)}catch{base={}}
191
- const n=Math.max(1,parseInt($('eng-repeat').value)||1);
192
- payloads=Array.from({length:n},function(){return Object.assign({},base);});
193
- }
194
- blastAbort=false;
195
- this.style.display='none';$('eng-stop').style.display='';
196
- let ok=0,errors=0;
197
- for(var i=0;i<payloads.length;i++){
198
- if(blastAbort)break;
199
- var payload=Object.assign({},payloads[i]);
200
- payload.streamId=name.toLowerCase().replace(/\s+/g,'-')+'-'+i+'-'+Date.now();
201
- $('eng-result').innerHTML='<div class="success-banner">Firing '+(i+1)+'/'+payloads.length+'...</div>';
202
- try{
203
- const r=await api('POST','/api/trigger',{engagement:name,payload});
204
- if(r.error){errors++;}else{ok++;if(payloads.length===1){activeEngagement={executionId:r.executionId,streamId:sid,name};seqNum=0;$('eng-stream-area').style.display='';}}
205
- }catch(e){errors++;}
206
- if(delay&&i<payloads.length-1)await new Promise(function(r){setTimeout(r,delay);});
207
- }
208
- var msg=blastAbort?'Aborted after '+(i)+'/'+payloads.length+' \u2014 '+ok+' ok':'Done \u2014 '+ok+'/'+payloads.length+' ok'+(errors?' ('+errors+' errors)':'');
209
- $('eng-result').innerHTML='<div class="'+(errors||blastAbort?'error-banner':'success-banner')+'">'+esc(msg)+'</div>';
210
- $('eng-stop').style.display='none';this.style.display='';
211
- });
212
- $('eng-stop').addEventListener('click',function(){
213
- blastAbort=true;activeEngagement=null;$('eng-stream-area').style.display='none';this.style.display='none';
214
- $('eng-start').style.display='';
215
- });
216
- // Stream push tab switching
217
- document.querySelectorAll('[data-stab]').forEach(tab=>{
218
- tab.addEventListener('click',()=>{
219
- document.querySelectorAll('[data-stab]').forEach(t=>t.classList.remove('active'));tab.classList.add('active');
220
- $('stream-single').style.display=tab.dataset.stab==='single'?'':'none';
221
- $('stream-bulk').style.display=tab.dataset.stab==='bulk'?'':'none';
222
- $('stream-file').style.display=tab.dataset.stab==='file'?'':'none';
223
- });
224
- });
225
-
226
- // Helper: push one packet to active stream
227
- async function pushOne(payloadStr){
228
- if(!activeEngagement)return;
229
- seqNum++;
230
- await api('POST','/api/streams/'+encodeURIComponent(activeEngagement.streamId)+'/push',{data:{sequenceNum:seqNum,payload:payloadStr}});
231
- }
232
-
233
- // Helper: parse lines from text (JSON Lines or JSON array)
234
- function parseLines(text){
235
- const trimmed=text.trim();
236
- if(trimmed.startsWith('[')){try{const arr=JSON.parse(trimmed);if(Array.isArray(arr))return arr.map(o=>JSON.stringify(o))}catch{}}
237
- return trimmed.split('\n').map(l=>l.trim()).filter(l=>l&&!l.startsWith('//'));
238
- }
239
-
240
- let bulkAbort=false;
241
-
242
- // Helper: send lines with delay and progress
243
- async function sendLines(lines,delayMs,progressEl,stopBtn,sendBtn){
244
- bulkAbort=false;stopBtn.style.display='';sendBtn.style.display='none';
245
- progressEl.innerHTML='<div class="progress-bar"><div class="fill" style="width:0%"></div></div><div class="progress-text">0 / '+lines.length+'</div>';
246
- const bar=progressEl.querySelector('.fill'),txt=progressEl.querySelector('.progress-text');
247
- let sent=0;
248
- for(const line of lines){
249
- if(bulkAbort)break;
250
- try{await pushOne(line);sent++}catch(e){progressEl.innerHTML+='<div class="error-banner">Failed at '+sent+': '+esc(e.message)+'</div>';break}
251
- const pct=Math.round(sent/lines.length*100);
252
- bar.style.width=pct+'%';txt.textContent=sent+' / '+lines.length;
253
- if(delayMs>0&&sent<lines.length)await new Promise(r=>setTimeout(r,delayMs));
254
- }
255
- stopBtn.style.display='none';sendBtn.style.display='';
256
- if(!bulkAbort)$('eng-push-result').innerHTML='<div class="success-banner">Sent '+sent+' packets</div>';
257
- else $('eng-push-result').innerHTML='<div class="success-banner">Stopped after '+sent+' / '+lines.length+'</div>';
258
- }
259
-
260
- // Single push
261
- $('eng-push').addEventListener('click',async function(){
262
- if(!activeEngagement)return;this.textContent='...';
263
- try{await pushOne($('eng-stream-payload').value);
264
- $('eng-push-result').innerHTML='<div class="success-banner">Pushed <span class="fe-dim">seq '+seqNum+'</span></div>';
265
- }catch(e){$('eng-push-result').innerHTML='<div class="error-banner">'+esc(e.message)+'</div>'}
266
- this.textContent='Push';
267
- });
268
-
269
- // Bulk send
270
- $('eng-bulk-send').addEventListener('click',async function(){
271
- if(!activeEngagement)return;
272
- const lines=parseLines($('eng-bulk-payload').value);
273
- if(!lines.length)return;
274
- const delay=parseInt($('eng-bulk-delay').value)||0;
275
- await sendLines(lines,delay,$('eng-bulk-progress'),$('eng-bulk-stop'),this);
276
- });
277
- $('eng-bulk-stop').addEventListener('click',()=>{bulkAbort=true});
278
-
279
- // File upload
280
- let fileLines=[];
281
- $('file-drop-zone').addEventListener('click',()=>$('eng-file-input').click());
282
- $('file-drop-zone').addEventListener('dragover',(e)=>{e.preventDefault();e.currentTarget.classList.add('drag-over')});
283
- $('file-drop-zone').addEventListener('dragleave',(e)=>{e.currentTarget.classList.remove('drag-over')});
284
- $('file-drop-zone').addEventListener('drop',(e)=>{e.preventDefault();e.currentTarget.classList.remove('drag-over');if(e.dataTransfer.files.length)handleFile(e.dataTransfer.files[0])});
285
- $('eng-file-input').addEventListener('change',(e)=>{if(e.target.files.length)handleFile(e.target.files[0])});
286
-
287
- function handleFile(file){
288
- const reader=new FileReader();
289
- reader.onload=(e)=>{
290
- fileLines=parseLines(e.target.result);
291
- $('file-info').textContent=file.name+' ('+fileLines.length+' events)';
292
- $('eng-file-send').disabled=false;
293
- };
294
- reader.readAsText(file);
295
- }
296
-
297
- $('eng-file-send').addEventListener('click',async function(){
298
- if(!activeEngagement||!fileLines.length)return;
299
- const delay=parseInt($('eng-file-delay').value)||0;
300
- await sendLines(fileLines,delay,$('eng-file-progress'),$('eng-file-stop'),this);
301
- });
302
- $('eng-file-stop').addEventListener('click',()=>{bulkAbort=true});
303
-
304
- // Agent actions
305
- $('agent-call').addEventListener('click',async function(){
306
- const lbl=$('agent-label'),alias=lbl.dataset.alias,task=lbl.dataset.task;
307
- let payload;try{payload=JSON.parse($('agent-payload').value)}catch{payload={}}
308
- this.textContent='...';
309
- try{
310
- const r=await api('POST','/api/agents/'+alias+'/'+task,{payload});
311
- if(r.error&&!r.success){$('agent-result').innerHTML='<div class="error-banner">'+esc(r.error)+'</div>'}
312
- else{$('agent-result').innerHTML='<div class="result"><pre>'+esc(JSON.stringify(r.result,null,2))+'</pre></div><div class="result-meta"><span>'+(r.durationMs||0).toFixed(1)+'ms</span><span class="badge '+(r.success?'badge-ok':'badge-err')+'">'+(r.success?'ok':'error')+'</span></div>'}
313
- }catch(e){$('agent-result').innerHTML='<div class="error-banner">'+esc(e.message)+'</div>'}
314
- this.textContent='Call';
315
- });
316
-
317
- $('agent-batch').addEventListener('click',async function(){
318
- if(batchRunning){batchAbort=true;return;}
319
- const lbl=$('agent-label'),alias=lbl.dataset.alias,task=lbl.dataset.task;
320
- const batchSql=this.dataset.batchSql,batchCubby=this.dataset.batchCubby;
321
- if(!batchSql||!batchCubby){$('agent-result').innerHTML='<div class="error-banner">No batch config on this task</div>';return;}
322
- $('agent-result').innerHTML='<div class="success-banner">Querying '+esc(batchCubby)+'...</div>';
323
- var r=await api('POST','/api/cubbies/'+batchCubby+'/query',{sql:batchSql,params:[]});
324
- if(r.error){$('agent-result').innerHTML='<div class="error-banner">Query failed: '+esc(r.error)+'</div>';return;}
325
- var rows=r.rows||[];
326
- if(!rows.length){$('agent-result').innerHTML='<div class="success-banner">No rows matched \u2014 nothing to run</div>';return;}
327
- const ac=topology.agents.find(function(a){return a.alias===alias;}),tc=ac&&ac.tasks.find(function(t){return t.alias===task;});
328
- const paramKeys=tc&&tc.parameters&&tc.parameters.properties?Object.keys(tc.parameters.properties):[];
329
- batchRunning=true;batchAbort=false;
330
- this.textContent='Stop';this.classList.replace('btn-secondary','btn-danger');
331
- var ok=0,errors=0,i=0;
332
- for(;i<rows.length;i++){
333
- if(batchAbort)break;
334
- $('agent-result').innerHTML='<div class="success-banner">Running '+(i+1)+'/'+rows.length+' \u2014 '+ok+' ok'+( errors?' '+errors+' err':'')+'</div>';
335
- var payload={};
336
- for(var j=0;j<paramKeys.length&&j<rows[i].length;j++)payload[paramKeys[j]]=rows[i][j];
337
- try{
338
- var res=await api('POST','/api/agents/'+alias+'/'+task,{payload});
339
- if(res.error&&!res.success)errors++;else ok++;
340
- }catch(e){errors++;}
341
- await new Promise(function(r){setTimeout(r,300);});
342
- }
343
- batchRunning=false;
344
- this.textContent='Run Batch';this.classList.replace('btn-danger','btn-secondary');
345
- var msg=batchAbort?'Stopped after '+i+'/'+rows.length+' \u2014 '+ok+' ok':'Done \u2014 '+ok+'/'+rows.length+' ok'+(errors?' ('+errors+' errors)':'');
346
- $('agent-result').innerHTML='<div class="'+(errors||batchAbort?'error-banner':'success-banner')+'">'+esc(msg)+'</div>';
347
- });
348
-
349
- // Cubby tabs
350
- document.querySelectorAll('#panel-cubby .tab').forEach(tab=>{
351
- tab.addEventListener('click',()=>{
352
- document.querySelectorAll('#panel-cubby .tab').forEach(t=>t.classList.remove('active'));tab.classList.add('active');
353
- $('cubby-browse').style.display=tab.dataset.tab==='browse'?'':'none';
354
- $('cubby-sql').style.display=tab.dataset.tab==='sql'?'':'none';
355
- $('cubby-graph').style.display=tab.dataset.tab==='graph'?'':'none';
356
- if(tab.dataset.tab==='graph'){$('panel-cubby').classList.add('panel-wide');loadCubbyGraph($('cubby-label').dataset.alias);}
357
- else if(tab.dataset.tab==='sql'){$('panel-cubby').classList.add('panel-wide');}
358
- else $('panel-cubby').classList.remove('panel-wide');
359
- });
360
- });
361
-
362
- // Graph tab
363
- const GRAPH_NODE_COLORS={
364
- artifact:{bg:'#6366f1',border:'#818cf8'},
365
- work:{bg:'#f59e0b',border:'#fbbf24'},
366
- operator:{bg:'#22c55e',border:'#4ade80'},
367
- eval:{bg:'#ec4899',border:'#f472b6'},
368
- artifact_map:{bg:'#52525b',border:'#71717a'},
369
- };
370
- let graphNet=null,graphVisNodes=null,hiddenTypes=new Set();
371
-
372
- async function loadCubbyGraph(alias){
373
- const canvas=$('graph-canvas');
374
- canvas.innerHTML='<div class="empty">Loading graph...</div>';
375
- $('graph-detail').style.display='none';
376
- $('graph-stats').textContent='';
377
- try{
378
- const [nr,er]=await Promise.all([
379
- api('POST','/api/cubbies/'+alias+'/query',{sql:"SELECT id,version,type,ref,created_at,attribute FROM nodes"}),
380
- api('POST','/api/cubbies/'+alias+'/query',{sql:"SELECT id,src,type,dst FROM edges"}),
381
- ]);
382
- if(nr.error){canvas.innerHTML='<div class="empty">This cubby has no nodes table \u2014 graph view requires nodes + edges tables</div>';return}
383
- const rows=nr.rows||[],erows=er.rows||[];
384
- if(!rows.length){canvas.innerHTML='<div class="empty">No nodes yet \u2014 run an ingest first</div>';return}
385
-
386
- // Build vis nodes
387
- const visNodes=[],versionedNode={},latestVer={},nodeVisId={};
388
- let idx=1;
389
- for(const r of rows){
390
- const[id,ver,type,ref,created_at,attr]=r;
391
- const col=GRAPH_NODE_COLORS[type]||GRAPH_NODE_COLORS.artifact_map;
392
- const a=safeJ(attr);
393
- const label=type==='artifact'?(a.title||ref||id).slice(0,22)+'\nv'+ver
394
- :type==='operator'?(a.name||ref||'operator').slice(0,18)
395
- :type==='work'?(a.type||'work')+'\nv'+ver
396
- :(type+' v'+ver);
397
- const shape=type==='operator'?'diamond':type==='work'?'box':'ellipse';
398
- visNodes.push({id:idx,label,
399
- color:{background:col.bg,border:col.border,highlight:{background:col.border,border:'#fff'}},
400
- font:{color:'#fff',size:11,face:"'SF Mono','Fira Code',monospace",multi:false},
401
- shape,borderWidth:2,
402
- _raw:{id,ver,type,ref,created_at,attr}});
403
- nodeVisId[id+'|'+ver]=idx;
404
- if(!versionedNode[id])versionedNode[id]={};
405
- versionedNode[id][ver]=idx;
406
- if(!latestVer[id]||parseInt(ver)>parseInt(latestVer[id].ver))latestVer[id]={ver,visId:idx};
407
- idx++;
408
- }
409
-
410
- // Build vis edges
411
- const nodeVer={};
412
- for(const r of rows)nodeVer[r[0]]=r[1];
413
-
414
- const visEdges=[];
415
- for(const r of erows){
416
- const[eid,src,type,dst]=r;
417
- const sVisId=latestVer[src]?.visId;
418
- const srcVersion=nodeVer[src];
419
- const dVisId=(versionedNode[dst]&&srcVersion&&versionedNode[dst][srcVersion])
420
- ?versionedNode[dst][srcVersion]
421
- :latestVer[dst]?.visId;
422
- if(!sVisId||!dVisId)continue;
423
- visEdges.push({id:eid,from:sVisId,to:dVisId,label:type,
424
- font:{size:9,color:'#52525b',align:'middle'},
425
- color:{color:'#3f3f46',highlight:'#818cf8'},
426
- arrows:{to:{enabled:true,scaleFactor:0.5}},
427
- smooth:{type:'curvedCW',roundness:0.2}});
428
- }
429
-
430
- // Synthetic PREV_VERSION dashed edges
431
- for(const[nodeId,verMap]of Object.entries(versionedNode)){
432
- const ntype=rows.find(function(r){return r[0]===nodeId;})?.[2];
433
- if(ntype!=='artifact')continue;
434
- const versions=Object.keys(verMap).map(Number).sort(function(a,b){return a-b;});
435
- for(var vi=1;vi<versions.length;vi++){
436
- const fromVisId=verMap[String(versions[vi-1])],toVisId=verMap[String(versions[vi])];
437
- if(!fromVisId||!toVisId)continue;
438
- visEdges.push({id:nodeId+'|PREV|v'+versions[vi],from:fromVisId,to:toVisId,
439
- label:'',dashes:true,
440
- font:{size:8,color:'#3f3f46',align:'middle'},
441
- color:{color:'#3f3f46',highlight:'#52525b'},
442
- arrows:{to:{enabled:true,scaleFactor:0.3}},
443
- smooth:{type:'continuous'}});
444
- }
445
- }
446
-
447
- // Count node types for legend
448
- const typeCounts={};
449
- for(const n of visNodes)typeCounts[n._raw.type]=(typeCounts[n._raw.type]||0)+1;
450
-
451
- $('graph-stats').innerHTML=visNodes.length+' nodes \u00b7 '+visEdges.length+' edges';
452
-
453
- // Legend toggles
454
- var legendHtml='<div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:8px">';
455
- for(const[type,col]of Object.entries(GRAPH_NODE_COLORS)){
456
- if(!typeCounts[type])continue;
457
- const isHidden=hiddenTypes.has(type);
458
- legendHtml+='<button onclick="window.graphToggleType(\''+type+'\')" style="display:flex;align-items:center;gap:5px;background:'+(isHidden?'var(--surface2)':col.bg+'22')+';border:1px solid '+(isHidden?'var(--border)':col.bg+'66')+';color:'+(isHidden?'var(--text3)':col.bg)+';border-radius:4px;padding:2px 8px;font-size:10px;cursor:pointer">'
459
- +'<span style="width:8px;height:8px;border-radius:50%;background:'+(isHidden?'var(--border)':col.bg)+';display:inline-block"></span>'+esc(type)+' <span style="opacity:.6">'+typeCounts[type]+'</span></button>';
460
- }
461
- legendHtml+='</div>';
462
- $('graph-stats').innerHTML+= legendHtml;
463
-
464
- canvas.innerHTML='';
465
- if(graphNet)graphNet.destroy();
466
- graphVisNodes=new window.vis.DataSet(visNodes.map(function(n){return Object.assign({},n,{hidden:hiddenTypes.has(n._raw.type)});}));
467
- graphNet=new window.vis.Network(canvas,
468
- {nodes:graphVisNodes,edges:new window.vis.DataSet(visEdges)},
469
- {physics:{barnesHut:{gravitationalConstant:-12000,springLength:200,springConstant:0.02,damping:0.15,centralGravity:0.05},stabilization:{iterations:200}},
470
- interaction:{hover:true},layout:{improvedLayout:rows.length<80}});
471
-
472
- graphNet.on('click',(p)=>{
473
- if(!p.nodes.length){$('graph-detail').style.display='none';return}
474
- const n=visNodes.find(v=>v.id===p.nodes[0]);if(!n)return;
475
- const{id,ver,type,ref,created_at,attr}=n._raw,a=safeJ(attr);
476
- const col=GRAPH_NODE_COLORS[type]||GRAPH_NODE_COLORS.artifact_map;
477
- $('gd-type-badge').textContent=type;
478
- $('gd-type-badge').style.cssText='background:'+col.bg+'22;color:'+col.bg+';border:1px solid '+col.bg+'44';
479
- const rows=[['id',id],['version',ver],['ref',ref],['created_at',created_at],
480
- ...Object.entries(a).filter(([k])=>k!=='segments'&&k!=='embedding').map(([k,v])=>[k,typeof v==='object'?JSON.stringify(v):String(v??'')])
481
- ].filter(([,v])=>v&&v!=='null');
482
- if(a.segments?.length)rows.push(['segments',a.segments.length+' blocks']);
483
- $('gd-attrs').innerHTML=rows.map(([k,v])=>'<div class="gd-row"><span class="gd-key">'+esc(k)+'</span><span class="gd-val">'+esc(String(v))+'</span></div>').join('');
484
- $('graph-detail').style.display='';
485
- });
486
- }catch(e){canvas.innerHTML='<div class="error-banner">'+esc(e.message||String(e))+'</div>'}
487
- }
488
-
489
- window.loadCubbyGraph=loadCubbyGraph;
490
- window.graphToggleType=function(type){
491
- if(hiddenTypes.has(type))hiddenTypes.delete(type);else hiddenTypes.add(type);
492
- if(!graphVisNodes)return;
493
- graphVisNodes.update(graphVisNodes.get().filter(function(n){return n._raw&&n._raw.type===type;}).map(function(n){return{id:n.id,hidden:hiddenTypes.has(type)};}));
494
- document.querySelectorAll('#graph-stats button').forEach(function(btn){
495
- var t=btn.textContent.trim().split(' ')[0];
496
- var col=GRAPH_NODE_COLORS[t];if(!col)return;
497
- var hidden=hiddenTypes.has(t);
498
- btn.style.background=hidden?'var(--surface2)':col.bg+'22';
499
- btn.style.borderColor=hidden?'var(--border)':col.bg+'66';
500
- btn.style.color=hidden?'var(--text3)':col.bg;
501
- var dot=btn.querySelector('span');if(dot)dot.style.background=hidden?'var(--border)':col.bg;
502
- });
503
- };
504
- function safeJ(s){try{return JSON.parse(s)}catch{return{}}}
505
- async function loadCubbyBrowse(alias){
506
- const el=$('cubby-tables');el.innerHTML='<div class="empty">Loading...</div>';
507
- try{
508
- const t=await api('POST','/api/cubbies/'+alias+'/query',{sql:"SELECT name FROM sqlite_master WHERE type='table' AND name != 'schema_migrations'"});
509
- if(!t.rows||!t.rows.length){el.innerHTML='<div class="empty">No tables</div>';return}
510
- let h='';
511
- for(const row of t.rows){
512
- const tn=row[0],cr=await api('POST','/api/cubbies/'+alias+'/query',{sql:'SELECT COUNT(*) FROM "'+tn+'"'});
513
- h+='<div class="cubby-table-item" onclick="browseCubbyTable(\''+ea(alias)+'\',\''+ea(tn)+'\')">';
514
- h+='<span class="cti-name">'+esc(tn)+'</span><span class="cti-count">'+(cr.rows?.[0]?.[0]??'?')+' rows</span></div>';
515
- }
516
- el.innerHTML=h;
517
- }catch(e){el.innerHTML='<div class="error-banner">'+esc(e.message)+'</div>'}
518
- }
519
- window.browseCubbyTable=async function(alias,table){
520
- const el=$('cubby-tables');
521
- try{const r=await api('POST','/api/cubbies/'+alias+'/query',{sql:'SELECT * FROM "'+table+'" LIMIT 100'});
522
- el.innerHTML='<div style="display:flex;gap:8px;justify-content:flex-end;margin-bottom:8px">'
523
- +'<button class="btn-ghost" onclick="loadCubbyBrowse(\''+ea(alias)+'\')">\u2190 Back</button>'
524
- +'<button class="btn-danger" style="font-size:11px;padding:5px 12px" onclick="clearCubbyTable(\''+ea(alias)+'\',\''+ea(table)+'\')">Clear table</button>'
525
- +'</div>'+renderTable(r);
526
- }catch(e){el.innerHTML='<div class="error-banner">'+esc(e.message)+'</div>'}
527
- };
528
- window.clearCubbyTable=async function(alias,table){
529
- if(!confirm('Delete all rows from "'+table+'"?'))return;
530
- try{
531
- await api('POST','/api/cubbies/'+alias+'/exec',{sql:'DELETE FROM "'+table+'"'});
532
- window.browseCubbyTable(alias,table);
533
- }catch(e){alert('Error: '+e.message);}
534
- };
535
- window.loadCubbyBrowse=loadCubbyBrowse;
536
-
537
- $('cubby-run').addEventListener('click',async function(){
538
- const alias=$('cubby-label').dataset.alias,sql=$('cubby-sql-input').value.trim();if(!sql)return;
539
- const isW=/^\s*(INSERT|UPDATE|DELETE|CREATE|DROP|ALTER)/i.test(sql),ep=isW?'exec':'query';
540
- this.textContent='...';
541
- try{const r=await api('POST','/api/cubbies/'+alias+'/'+ep,{sql});
542
- if(r.error){$('cubby-sql-result').innerHTML='<div class="error-banner">'+esc(r.error)+'</div>'}
543
- else if(r.columns){$('cubby-sql-result').innerHTML=renderTable(r)+'<div class="result-meta"><span>'+r.meta.rowsRead+' rows</span><span>'+(r.meta.duration*1000).toFixed(2)+'ms</span></div>'}
544
- else{$('cubby-sql-result').innerHTML='<div class="success-banner">'+(r.rowsAffected??0)+' rows affected</div>'}
545
- }catch(e){$('cubby-sql-result').innerHTML='<div class="error-banner">'+esc(e.message)+'</div>'}
546
- this.textContent='Run';
547
- });
548
-
549
- // Query template manager (stored in cubby saved_queries table)
550
- async function qtRefreshSelect(alias){
551
- var sel=$('query-select'),prev=sel.value;
552
- try{
553
- var r=await api('POST','/api/cubbies/'+alias+'/query',{sql:'SELECT name FROM saved_queries ORDER BY name',params:[]});
554
- sel.innerHTML='<option value="">\u2014 select a saved query \u2014</option>';
555
- (r.rows||[]).forEach(function(row){var o=document.createElement('option');o.value=row[0];o.textContent=row[0];sel.appendChild(o);});
556
- sel.value=prev||'';
557
- $('query-delete').style.display=sel.value?'':'none';
558
- }catch(e){}
559
- }
560
- $('query-select').addEventListener('change',async function(){
561
- var alias=$('cubby-label').dataset.alias,name=this.value;
562
- $('query-delete').style.display=name?'':'none';
563
- if(!name)return;
564
- try{
565
- var r=await api('POST','/api/cubbies/'+alias+'/query',{sql:'SELECT sql_text FROM saved_queries WHERE name=?',params:[name]});
566
- if(r.rows&&r.rows[0]){$('cubby-sql-input').value=r.rows[0][0];$('query-name').value=name;}
567
- }catch(e){}
568
- });
569
- $('query-save').addEventListener('click',async function(){
570
- var alias=$('cubby-label').dataset.alias,name=$('query-name').value.trim(),sql=$('cubby-sql-input').value.trim();
571
- if(!name||!sql)return;
572
- try{
573
- await api('POST','/api/cubbies/'+alias+'/exec',{sql:'INSERT OR REPLACE INTO saved_queries(name,sql_text) VALUES(?,?)',params:[name,sql]});
574
- await qtRefreshSelect(alias);$('query-select').value=name;$('query-delete').style.display='';
575
- }catch(e){}
576
- });
577
- $('query-delete').addEventListener('click',async function(){
578
- var alias=$('cubby-label').dataset.alias,name=$('query-select').value;if(!name)return;
579
- try{
580
- await api('POST','/api/cubbies/'+alias+'/exec',{sql:'DELETE FROM saved_queries WHERE name=?',params:[name]});
581
- await qtRefreshSelect(alias);$('query-name').value='';
582
- }catch(e){}
583
- });
584
-
585
- // Bottom panel: tabs, resize, collapse
586
- document.querySelectorAll('.btab').forEach(tab=>{
587
- tab.addEventListener('click',()=>{
588
- document.querySelectorAll('.btab').forEach(t=>t.classList.remove('active'));tab.classList.add('active');
589
- $('log-container').style.display=tab.dataset.btab==='logs'?'':'none';
590
- $('flow-container').style.display=tab.dataset.btab==='flow'?'':'none';
591
- });
592
- });
593
- $('log-filter').addEventListener('input',function(){
594
- logFilter=this.value.toLowerCase();
595
- $('log-container').querySelectorAll('.log-line').forEach(l=>{l.style.display=(!logFilter||l.textContent.toLowerCase().includes(logFilter))?'':'none'});
596
- });
597
- $('btn-clear').addEventListener('click',()=>{$('log-container').innerHTML='';$('flow-container').innerHTML=''});
598
- $('btn-collapse').addEventListener('click',function(){
599
- const b=$('bottom');
600
- if(b.classList.toggle('collapsed')){this.innerHTML='&#9650;';this.title='Expand'}
601
- else{this.innerHTML='&#9660;';this.title='Collapse'}
602
- });
603
-
604
- // Resize handle drag
605
- (function(){
606
- const handle=$('resize-handle'),bottom=$('bottom');
607
- let startY,startH;
608
- handle.addEventListener('mousedown',(e)=>{
609
- e.preventDefault();startY=e.clientY;startH=bottom.offsetHeight;
610
- const onMove=(e)=>{const h=Math.max(40,Math.min(window.innerHeight*0.7,startH-(e.clientY-startY)));bottom.style.height=h+'px';bottom.classList.remove('collapsed')};
611
- const onUp=()=>{document.removeEventListener('mousemove',onMove);document.removeEventListener('mouseup',onUp)};
612
- document.addEventListener('mousemove',onMove);document.addEventListener('mouseup',onUp);
613
- });
614
- })();
615
-
616
- // Tab in textareas
617
- document.querySelectorAll('textarea').forEach(ta=>{
618
- ta.addEventListener('keydown',function(e){if(e.key==='Tab'){e.preventDefault();const s=this.selectionStart;this.value=this.value.substring(0,s)+' '+this.value.substring(this.selectionEnd);this.selectionStart=this.selectionEnd=s+2}});
619
- });
620
-
621
- // Helpers
622
- function $(id){return document.getElementById(id)}
623
- function renderTable(r){
624
- if(!r.columns||!r.rows)return'';let h='<div class="table-wrap"><table><thead><tr>';
625
- for(const c of r.columns)h+='<th>'+esc(c)+'</th>';h+='</tr></thead><tbody>';
626
- for(const row of r.rows){h+='<tr>';for(const cell of row)h+='<td>'+esc(cell==null?'NULL':String(cell))+'</td>';h+='</tr>'}
627
- return h+'</tbody></table></div>';
628
- }
629
- function esc(s){if(s==null)return'';return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;')}
630
- function ea(s){return String(s).replace(/'/g,"\\'").replace(/"/g,'&quot;')}
631
-
632
- loadTopology();connectWs();
633
- })();