Created
July 4, 2025 23:35
-
-
Save 0x4D31/78481b5446882309a6c2ff253915ea15 to your computer and use it in GitHub Desktop.
example SSE client to view Finch events
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!doctype html> | |
<html lang="en"> | |
<head> | |
<meta charset="utf-8" /> | |
<title>Finch SSE Events</title> | |
<meta name="viewport" content="width=device-width, initial-scale=1" /> | |
<style> | |
:root { | |
--bg: #fff; | |
--card-bg: #f8f9fa; | |
--key-color: #0056b3; | |
--val-color: #495057; | |
--label-color: #6c757d; | |
--text-sm: 0.82rem; | |
--highlight-bg: #fff3cd; | |
} | |
html,body{margin:0;padding:0;background:var(--bg);font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;color:#212529} | |
header{padding:.75rem 1rem;background:#0d6efd;color:#fff} | |
h1{margin:0;font-size:1.25rem;font-weight:600} | |
.container{ | |
max-width: 1200px; | |
margin: 0 auto; | |
padding: 1rem; | |
display: flex; | |
flex-direction: column; | |
gap: 1rem; | |
box-sizing: border-box; | |
} | |
#search{ | |
width: 100%; | |
padding: .75rem; | |
font-size: 1rem; | |
border: 1px solid #ced4da; | |
border-radius: 6px; | |
box-sizing: border-box; | |
} | |
#log{flex:1;height:65vh;overflow:auto} | |
.event{ | |
background: var(--card-bg); | |
border: 1px solid #dee2e6; | |
border-radius: 6px; | |
padding: 1rem; | |
margin-bottom: .75rem; | |
box-shadow: 0 1px 2px rgba(0,0,0,.04); | |
width: 100%; | |
box-sizing: border-box; | |
} | |
.metadata{font-weight:600;margin-bottom:.35em;font-size:1rem} | |
.details{margin-left:1.2em;font-size:var(--text-sm);line-height:1.28;} | |
.details div{margin:1px 0} | |
.details .label{color:var(--label-color);display:inline-block;margin-right:0.18em;font-size:0.95em;} | |
.details .value{font-family:ui-monospace,Menlo,Consolas,"Liberation Mono",monospace;font-size:1em;} | |
.raw{background:#fff;border:1px solid #e2e3e5;border-radius:4px;padding:.5em .7em;font-size:var(--text-sm);white-space:normal;word-break:break-all;overflow:auto;box-sizing:border-box;} | |
.j-key{color:var(--key-color)} | |
.j-val{color:var(--val-color)} | |
mark{background:var(--highlight-bg);color:inherit;padding:0} | |
@media(max-width:600px){ | |
.container{padding:.3em} | |
.event{padding:.7em} | |
.details{margin-left:.5em} | |
.details .label{min-width:0;} | |
} | |
</style> | |
</head> | |
<body> | |
<header><h1>Finch SSE Events</h1></header> | |
<div class="container"> | |
<input id="search" placeholder="Filter events…" /> | |
<div id="log" aria-live="polite"></div> | |
</div> | |
<script> | |
(() => { | |
const log = document.getElementById('log'); | |
const input = document.getElementById('search'); | |
const events = []; | |
// Helpers | |
const esc = s => s.replace(/[&<>]/g, c => ({'&':'&','<':'<','>':'>'}[c])); | |
const highlight = (text, kw) => | |
kw | |
? text.replace(new RegExp(kw.replace(/[.*+?^${}()|[\]\\]/g,'\\$&'),'gi'), m=>`<mark>${m}</mark>`) | |
: text; | |
// Highlighting only keys and values (not the tags) | |
function jsonPrettyHighlight(j, kw) { | |
// color and highlight keys and values | |
return esc(j).replace( | |
/(\"([^\"\\]|\\.)*\")(?=:)|(:)\s*(\"([^\"\\]|\\.)*\"|\d+|true|false|null)/g, | |
(m, key, _, colon, val) => { | |
if (key) return `<span class="j-key">${highlight(key, kw)}</span>`; | |
if (val) return `${colon || ''} <span class="j-val">${highlight(val, kw)}</span>`; | |
return m; | |
} | |
); | |
} | |
function render() { | |
const kw = input.value.toLowerCase(); | |
log.innerHTML = ''; | |
events.forEach(({obj,raw}) => { | |
if (kw && !raw.toLowerCase().includes(kw)) return; | |
const m=obj; | |
const src = m.srcIP || '—'; | |
const port = m.dstPort || '—'; | |
let path = m.request || ''; | |
if (typeof path!=='string') path=''; | |
if (path && !path.startsWith('/')) path='/'+path; | |
// event card | |
const card = document.createElement('div'); | |
card.className='event'; | |
card.innerHTML = ` | |
<div class="metadata">${highlight(`${src} → :${port}${path}`,kw)}</div> | |
<div class="details"> | |
${detailHTML('JA3', m.ja3 || '—', kw)} | |
${detailHTML('JA4', m.ja4 || '—', kw)} | |
${detailHTML('JA4H', m.ja4h || '—', kw)} | |
${detailHTML('UA', m.userAgent, kw)} | |
</div> | |
<div class="raw">${jsonPrettyHighlight(JSON.stringify(m), kw)}</div>`; | |
log.append(card); | |
}); | |
log.scrollTop=log.scrollHeight; | |
} | |
// tiny templater for labelled rows | |
const detailHTML = (label,val,kw)=> | |
`<div><span class="label">${label}:</span><span class="value">${highlight(esc(val),kw)}</span></div>`; | |
// stream | |
const es=new EventSource(location.protocol.startsWith('http')?'/events':'http://localhost:9036/events'); | |
es.onmessage = e => {try{events.push({obj:JSON.parse(e.data),raw:e.data})}catch{events.push({obj:{raw:e.data},raw:e.data})};render()}; | |
es.onerror = () => {events.push({obj:{msg:'Connection lost. Retrying…'},raw:'Connection lost.'});render()}; | |
input.addEventListener('input',render); | |
})(); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment