Files
Ryan 71ef622b4f
Test and deploy MQTT backend / test (push) Successful in 2m8s
Test and deploy MQTT backend / deploy (push) Successful in 18s
Deploy MQTT test backend with private viewer and Gitea CI/CD
2026-10-08 14:48:53 +03:30

15 lines
5.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(()=>{
const q=(selector)=>document.querySelector(selector),limit=1000;
const timeline=q('#timeline'),notice=q('#notice'),streamLabel=q('#stream'),connection=q('#connection'),filters=q('#filters');
const items=[];let paused=false,overflow=0,lastId=0,source=null;
function filtered(event){const values=new FormData(filters);return (!values.get('direction')||String(event.direction||'').toLowerCase().includes(String(values.get('direction')).trim().toLowerCase()))&&(!values.get('topic')||String(event.topic||'').toLowerCase().includes(String(values.get('topic')).trim().toLowerCase()))&&(!values.get('device')||String(event.device_id||'').toLowerCase().includes(String(values.get('device')).trim().toLowerCase()))&&(!values.get('request')||String(event.request_id||'').toLowerCase().includes(String(values.get('request')).trim().toLowerCase()))}
function node(tag,className,text){const el=document.createElement(tag);if(className)el.className=className;if(text!==undefined)el.textContent=text;return el}
function renderEvent(e){const card=node('article','event '+e.direction);const head=node('div','event-head');head.append(node('span','kind',e.kind));head.append(node('time','time',new Date(e.observed_at).toLocaleTimeString()));const details=[e.direction,e.topic,e.device_id&&('device '+e.device_id),e.request_id&&('request '+e.request_id),'QoS '+e.qos,e.retain?'retained':'non-retained',e.outcome].filter(Boolean);head.append(node('span','meta',details.join(' · ')));card.append(head);const p=e.payload||{};if(p.text||p.omitted){card.append(node('pre','',p.omitted?('Payload omitted · '+p.bytes+' bytes'):p.text+(p.truncated?' … [preview truncated]':'')))}const props=e.properties||{};const values=[];if(props.response_topic)values.push('Response Topic '+props.response_topic);if(props.correlation_data)values.push('Correlation Data '+props.correlation_data);if(props.message_expiry_seconds!==undefined)values.push('Message Expiry '+props.message_expiry_seconds+'s');if(values.length)card.append(node('div','meta',values.join(' · ')));if(e.latency_ms!==undefined)card.append(node('div','meta','This process measured '+e.latency_ms+' ms'));return card}
function redraw(){timeline.replaceChildren();let shown=0;for(const e of items){if(!filtered(e))continue;timeline.append(renderEvent(e));shown++}if(!shown)timeline.append(node('div','empty',items.length?'No events match these filters.':'Waiting for events. Start the device simulator or ESP32 to see input and output.'));notice.textContent=overflow?'Event gap: '+overflow+' earlier event(s) are outside this page’s bounded view.':''}
function add(e){lastId=Math.max(lastId,e.id||0);if(paused){overflow++;return}items.push(e);if(items.length>limit)items.splice(0,items.length-limit);redraw()}
function compatibility(rows){const tbody=q('#compatibility');tbody.replaceChildren();for(const row of rows||[]){const tr=document.createElement('tr');tr.append(node('td','',row.feature));const td=node('td');td.append(node('span','status '+row.status,row.status));tr.append(td,node('td','',row.evidence));tbody.append(tr)}}
function stats(data){connection.textContent=data.connection||'starting';connection.className='badge '+(data.connection||'starting');const box=q('#stats');box.replaceChildren();const all=[['Retained events',data.events?.length||0],['Last event ID',data.last_event_id||0],['Received',data.counts?.message_received||0],['Contract errors',data.counts?.validation_failed||0],['Published',data.counts?.publish_attempt||0],['Transport ACK',data.counts?.transport_ack||0],['Receipt ACKs',data.counts?.receipt_ack_sent||0],['ACK failures',data.counts?.receipt_ack_failed||0],['ACK queue drops',data.counts?.receipt_ack_dropped||0],['Accepted',data.counts?.command_accepted||0],['Completed',data.counts?.command_completed||0],['Timeouts',data.counts?.command_timeout||0],['Event gaps',data.counts?.event_gap||0]];for(const [label,value]of all){const card=node('div','stat');card.append(node('b','',String(value)),node('span','',label));box.append(card)}compatibility(data.compatibility)}
async function connect(){streamLabel.textContent='connecting';try{const response=await fetch('/api/snapshot',{cache:'no-store'});if(!response.ok)throw Error();const data=await response.json();stats(data);if(!source){if(data.gap){lastId=Number(data.gap.reset_id)||lastId;overflow+=Number(data.gap.gap)||1}for(const event of data.events||[])add(event);source=new EventSource('/api/events?after='+encodeURIComponent(lastId));source.addEventListener('mqtt-event',e=>{try{add(JSON.parse(e.data).event)}catch{notice.textContent='Received an unreadable event.'}});source.addEventListener('gap',e=>{let value={};try{value=JSON.parse(e.data)}catch{}lastId=Number(value.reset_id)||lastId;overflow+=Number(value.gap)||1;streamLabel.textContent='resynced after event gap';redraw()});source.onopen=()=>{streamLabel.textContent='live'};source.onerror=()=>{streamLabel.textContent='reconnecting'}}}catch{connection.textContent='viewer unavailable';connection.className='badge disconnected';streamLabel.textContent='disconnected';setTimeout(connect,2000)}}
q('#pause').addEventListener('click',()=>{paused=!paused;q('#pause').textContent=paused?'Resume view':'Pause view';if(!paused){overflow=0;redraw()}});q('#clear').addEventListener('click',()=>{items.length=0;redraw()});filters.addEventListener('input',redraw);filters.addEventListener('reset',()=>setTimeout(redraw,0));connect();setInterval(async()=>{try{const r=await fetch('/api/snapshot',{cache:'no-store'});if(r.ok)stats(await r.json())}catch{}},3000)
})();