71 lines
4.5 KiB
JavaScript
71 lines
4.5 KiB
JavaScript
/* Read-only presentation of the existing local RX/TX logs. */
|
|
((root)=>{
|
|
const quiet=/^(HEARTBEAT|TIMESYNC|LINK_PROBE|HEAVY_PROBE|GCS)/;
|
|
function parse(line){
|
|
const match=String(line).match(/^(\S+) \[([^\]]+)\] ([\s\S]*)$/);
|
|
if(!match)return {time:'',type:'Message',payload:String(line),raw:String(line),tone:'normal',quiet:false};
|
|
const [,timestamp,category,message]=match;
|
|
let payload=message,type=category.replace(/^(RX|TX)_?/,'')||category;
|
|
try{payload=JSON.parse(message);type=payload.mavpackettype||type}catch(_){}
|
|
const tone=/BAD_DATA|ERROR|TIMEOUT|FAIL/.test(category)||type==='STATUSTEXT'&&Number(payload.severity)<=4?'warning':/COMMAND|ACK|PARAM_SET|OPERATOR/.test(category+' '+type)?'command':'normal';
|
|
const time=timestamp.match(/T(\d{2}:\d{2}:\d{2}(?:\.\d+)?)/)?.[1]||timestamp;
|
|
const pairs=typeof payload==='string'?[...payload.matchAll(/(?:^|\s)([a-zA-Z_]\w*)=([^\s]+)/g)]:[];
|
|
const fields=pairs.length?Object.fromEntries(pairs.map(([,key,value])=>[key,value])):payload;
|
|
return {time,type,payload,fields,raw:String(line),tone,quiet:tone!=='warning'&&(quiet.test(type)||/LINK_PROBE|HEAVY_PROBE|HEARTBEAT/.test(category))};
|
|
}
|
|
function valueText(value){
|
|
if(typeof value==='number')return Number.isInteger(value)?String(value):Number(value.toFixed(3)).toString();
|
|
if(Array.isArray(value))return value.slice(0,6).map(valueText).join(', ')+(value.length>6?'…':'');
|
|
return typeof value==='object'?JSON.stringify(value):String(value);
|
|
}
|
|
function makeRow(row,previous){
|
|
if(previous&&previous.dataset.key===row.raw&&previous.dataset.tone===row.tone)return previous;
|
|
const details=previous||document.createElement('details');
|
|
details.className='console-row';details.dataset.key=row.raw;details.dataset.tone=row.tone;
|
|
let summary=details.querySelector('summary'),time,body,type,fields,raw;
|
|
if(!summary){
|
|
details.replaceChildren();summary=document.createElement('summary');time=document.createElement('time');
|
|
body=document.createElement('span');body.className='console-message';type=document.createElement('strong');
|
|
fields=document.createElement('span');fields.className='console-fields';body.append(type,fields);summary.append(time,body);
|
|
raw=document.createElement('pre');details.append(summary,raw);
|
|
}else{
|
|
time=summary.querySelector('time');body=summary.querySelector('.console-message');
|
|
type=body.querySelector('strong');fields=body.querySelector('.console-fields');raw=details.querySelector('pre');
|
|
}
|
|
time.textContent=row.time;type.textContent=row.type;fields.replaceChildren();
|
|
const values=row.fields??row.payload;
|
|
if(typeof values==='object'&&values!==null){
|
|
for(const [key,value] of Object.entries(values).filter(([key])=>key!=='mavpackettype').slice(0,6)){
|
|
const item=document.createElement('span'),label=document.createElement('i'),text=document.createElement('b');
|
|
label.textContent=key+' ';text.textContent=valueText(value);item.append(label,text);fields.append(item);
|
|
}
|
|
}else fields.textContent=row.payload;
|
|
raw.textContent=typeof row.payload==='object'?JSON.stringify(row.payload,null,2):row.raw;
|
|
return details;
|
|
}
|
|
function render(host,lines,filter){
|
|
const atBottom=host.scrollHeight-host.scrollTop-host.clientHeight<36,scroll=host.scrollTop;
|
|
const rows=lines.map(parse).filter(row=>filter==='all'||!row.quiet);
|
|
const existing=new Map();
|
|
for(const node of host.querySelectorAll('details.console-row')){
|
|
const key=node.dataset.key,list=existing.get(key)||[];list.push(node);existing.set(key,list);
|
|
}
|
|
if(!rows.length){
|
|
if(!host.querySelector('.console-empty')){
|
|
const empty=document.createElement('p');empty.className='console-empty';
|
|
empty.textContent='No messages';
|
|
empty.title=filter==='all'?'Waiting for session messages.':'Routine heartbeat and probe traffic is hidden. Choose All traffic to see it.';
|
|
host.replaceChildren(empty);
|
|
}
|
|
return;
|
|
}
|
|
const wanted=rows.map(row=>makeRow(row,existing.get(row.raw)?.shift()||null));
|
|
let cursor=host.firstElementChild;
|
|
for(const node of wanted){if(node!==cursor)host.insertBefore(node,cursor);cursor=node.nextElementSibling}
|
|
while(cursor){const next=cursor.nextElementSibling;cursor.remove();cursor=next}
|
|
if(atBottom&&!host.querySelector('details[open]'))host.scrollTop=host.scrollHeight;else host.scrollTop=scroll;
|
|
}
|
|
root.ConsoleView={parse,render};
|
|
if(typeof module!=='undefined')module.exports=root.ConsoleView;
|
|
})(typeof window!=='undefined'?window:globalThis);
|