fix(ui): remove remaining JS template literals from dashboard; add link cards rendering

This commit is contained in:
Thomas Cravey 2025-08-16 19:21:23 -05:00
parent 3f94aa7068
commit a223663ce2

View file

@ -332,15 +332,7 @@ func (s *Server) handleUI(w http.ResponseWriter, r *http.Request) {
if(!res.ok){ throw new Error('HTTP '+res.status); } if(!res.ok){ throw new Error('HTTP '+res.status); }
return res.json(); return res.json();
} }
async function loadInfo(){ async function loadInfo(){ /* no-op for now; footer shows version */ }
try{ const data = await api('/api/info');
document.getElementById('version').textContent = data.version+ ' ('+data.commit+')';
document.getElementById('built').textContent = data.builtAt;
document.getElementById('uptime').textContent = data.uptime;
document.getElementById('connected').textContent = data.connected? 'yes':'no';
document.getElementById('counts').textContent = 'ingested ' + data.messagesIngested + ', notified ' + data.notificationsSent + ', pruned ' + data.messagesPruned;
}catch(e){ console.error(e); }
}
async function loadChannels(){ async function loadChannels(){
try{ const data = await api('/api/channels'); st.channels = data; renderChannels(); if(data.length>0){ selectChannel(data[0]); } } try{ const data = await api('/api/channels'); st.channels = data; renderChannels(); if(data.length>0){ selectChannel(data[0]); } }
catch(e){ console.error(e); } catch(e){ console.error(e); }
@ -353,11 +345,12 @@ func (s *Server) handleUI(w http.ResponseWriter, r *http.Request) {
catch(e){} catch(e){}
} } } } } }
function colorFor(nick){ let h=0; for(let i=0;i<nick.length;i++){ h=(h*31+nick.charCodeAt(i))>>>0 } return 'hsl('+(h%360)+',60%,'+(window.matchMedia('(prefers-color-scheme: dark)').matches? '70%':'35%')+')'; } function colorFor(nick){ let h=0; for(let i=0;i<nick.length;i++){ h=(h*31+nick.charCodeAt(i))>>>0 } return 'hsl('+(h%360)+',60%,'+(window.matchMedia('(prefers-color-scheme: dark)').matches? '70%':'35%')+')'; }
function lineHTML(m){ const ts = `<span class=ts>[${m.time}]</span>`; const nick = `<b style="color:${colorFor(m.author)}">${m.author}</b>`; const body = escapeHtml(m.body); return `${ts} ${nick}: ${linkify(body)}`; } function lineHTML(m){ const ts = '<span class=ts>[' + m.time + ']</span>'; const nick = '<b style="color:' + colorFor(m.author) + '\">' + m.author + '</b>'; const body = escapeHtml(m.body); return ts + ' ' + nick + ': ' + linkify(body); }
function escapeHtml(s){ return s.replace(/[&<>"]/g, c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c])); } function escapeHtml(s){ return s.replace(/[&<>"]/g, c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c])); }
function linkify(t){ return t.replace(/https?:\/\/\S+/g, u=> `<a href="${u}" target="_blank" rel="noopener">${u}</a>`); } function linkify(t){ return t.replace(/https?:\/\/\S+/g, function(u){ return '<a href="' + u + '" target="_blank" rel="noopener">' + u + '</a>'; }); }
function appendBatch(arr){ const el=document.getElementById('tail'); const frag=document.createDocumentFragment(); arr.forEach(m=>{ const div=document.createElement('div'); div.className='msg'; div.innerHTML=lineHTML(m); frag.appendChild(div); }); el.appendChild(frag); if(st.atBottom){ el.scrollTop = el.scrollHeight; } } function appendBatch(arr){ const el=document.getElementById('tail'); const frag=document.createDocumentFragment(); arr.forEach(m=>{ const div=document.createElement('div'); div.className='msg'; div.innerHTML=lineHTML(m); frag.appendChild(div); processLinks(div); }); el.appendChild(frag); if(st.atBottom){ el.scrollTop = el.scrollHeight; } }
function prependBatch(arr){ const el=document.getElementById('tail'); const oldTop=el.firstChild; const frag=document.createDocumentFragment(); arr.forEach(m=>{ const div=document.createElement('div'); div.className='msg'; div.innerHTML=lineHTML(m); frag.appendChild(div); }); el.insertBefore(frag, el.firstChild); if(oldTop){ oldTop.scrollIntoView(); } } function prependBatch(arr){ const el=document.getElementById('tail'); const oldTop=el.firstChild; const frag=document.createDocumentFragment(); arr.forEach(m=>{ const div=document.createElement('div'); div.className='msg'; div.innerHTML=lineHTML(m); frag.appendChild(div); processLinks(div); }); el.insertBefore(frag, el.firstChild); if(oldTop){ oldTop.scrollIntoView(); } }
function processLinks(scope){ const links = scope.querySelectorAll('a[href]:not([data-card])'); links.forEach(a=>{ a.setAttribute('data-card','1'); fetch('/api/linkcard?url='+encodeURIComponent(a.href)).then(r=>r.json()).then(card=>{ if(!card) return; if(card.title||card.description||card.image){ const c=document.createElement('div'); c.className='card'; var html=''; if(card.image){ html += '<div><img src="'+card.image+'" alt="" style="max-width:160px;max-height:120px;object-fit:cover;border-radius:.25rem"></div>'; } html += '<div style="flex:1;margin-left:.5rem">'; if(card.title){ html += '<div style="font-weight:600">'+escapeHtml(card.title)+'</div>'; } if(card.description){ html += '<div style="opacity:.8">'+escapeHtml(card.description)+'</div>'; } html += '</div>'; c.innerHTML = '<div style="display:flex;align-items:flex-start;gap:.5rem">'+html+'</div>'; a.parentNode.insertBefore(c, a.nextSibling); } }).catch(()=>{}); }); }
function startStream(){ const url=new URL('/api/stream', window.location.origin); url.searchParams.set('channel', st.current); const es=new EventSource(url); st.sse=es; es.onmessage=(ev)=>{ try{ const m=JSON.parse(ev.data); appendBatch([m]); }catch(e){} }; es.onerror=()=>{ es.close(); st.sse=null; setTimeout(startStream, 3000); } } function startStream(){ const url=new URL('/api/stream', window.location.origin); url.searchParams.set('channel', st.current); const es=new EventSource(url); st.sse=es; es.onmessage=(ev)=>{ try{ const m=JSON.parse(ev.data); appendBatch([m]); }catch(e){} }; es.onerror=()=>{ es.close(); st.sse=null; setTimeout(startStream, 3000); } }
async function doSumm(){ async function doSumm(){
const ch = document.getElementById('channel').value; const ch = document.getElementById('channel').value;
@ -373,7 +366,7 @@ func (s *Server) handleUI(w http.ResponseWriter, r *http.Request) {
}catch(e){ document.getElementById('summary').textContent = 'error: '+e; } }catch(e){ document.getElementById('summary').textContent = 'error: '+e; }
btn.disabled = false; prog.style.display = 'none'; btn.disabled = false; prog.style.display = 'none';
} }
window.addEventListener('DOMContentLoaded', ()=>{ loadInfo(); loadChannels(); }); window.addEventListener('DOMContentLoaded', ()=>{ loadChannels(); });
function onFollowToggle(cb){ function onFollowToggle(cb){
if(cb.checked){ if(cb.checked){
if(st.tailTimer) clearInterval(st.tailTimer); if(st.tailTimer) clearInterval(st.tailTimer);