viewer: stream artifacts, skip unchanged live refresh, debounce search

This commit is contained in:
CitrixPot committed 2026-09-28 20:11:19 +02:00
1 parent ac2d32db59
commit d74bd7ce56
1 file changed
+32 -7
+32 -7
View File
@@ -7,6 +7,7 @@ import json
import os import os
import pathlib import pathlib
import re import re
import shutil
import threading import threading
import time import time
import urllib.parse import urllib.parse
@@ -102,13 +103,19 @@ function select(i){selected=i;
jsonBox.textContent=JSON.stringify(e,null,2); jsonBox.textContent=JSON.stringify(e,null,2);
if(e.evidence&&e.evidence.sha256)loadPayload(e.evidence.sha256); if(e.evidence&&e.evidence.sha256)loadPayload(e.evidence.sha256);
else{payloadBytes=null;art.innerHTML='<h2>Payload</h2><pre>no payload in this event</pre>';}} else{payloadBytes=null;art.innerHTML='<h2>Payload</h2><pre>no payload in this event</pre>';}}
let lastBytes=-1,lastKey='';
async function load(){ async function load(){
const p=new URLSearchParams({q:document.getElementById('q').value,event:document.getElementById('event').value, const p=new URLSearchParams({q:document.getElementById('q').value,event:document.getElementById('event').value,
peer:document.getElementById('peer').value,payload:document.getElementById('payload').checked?'1':'0', peer:document.getElementById('peer').value,payload:document.getElementById('payload').checked?'1':'0',
hide:document.getElementById('hide').checked?'1':'0', hide:document.getElementById('hide').checked?'1':'0',
size:document.getElementById('size').value,page:page}); size:document.getElementById('size').value,page:page});
const key=p.toString();
const r=await fetch('/api/events?'+p);const d=await r.json(); const r=await fetch('/api/events?'+p);const d=await r.json();
current=d.events;lastScanned=d.scanned;lastHidden=d.hidden;hasMore=d.has_more;render(current); lastScanned=d.scanned;lastHidden=d.hidden;hasMore=d.has_more;
// Skip re-render when nothing changed on the live page (cheap 3s refresh).
if(page===0 && d.journal_bytes===lastBytes && key===lastKey){return;}
lastBytes=d.journal_bytes;lastKey=key;
current=d.events;render(current);
document.getElementById('page').textContent='page '+(page+1); document.getElementById('page').textContent='page '+(page+1);
document.getElementById('prev').disabled=page<=0; document.getElementById('prev').disabled=page<=0;
document.getElementById('next').disabled=!hasMore;} document.getElementById('next').disabled=!hasMore;}
@@ -117,6 +124,7 @@ document.getElementById('reload').onclick=reset;
document.getElementById('prev').onclick=()=>{if(page>0){page--;load();}}; document.getElementById('prev').onclick=()=>{if(page>0){page--;load();}};
document.getElementById('next').onclick=()=>{if(hasMore){page++;load();}}; document.getElementById('next').onclick=()=>{if(hasMore){page++;load();}};
document.getElementById('q').addEventListener('keydown',e=>{if(e.key==='Enter')reset();}); document.getElementById('q').addEventListener('keydown',e=>{if(e.key==='Enter')reset();});
let qTimer;document.getElementById('q').addEventListener('input',()=>{clearTimeout(qTimer);qTimer=setTimeout(reset,300);});
document.getElementById('peer').addEventListener('keydown',e=>{if(e.key==='Enter')reset();}); document.getElementById('peer').addEventListener('keydown',e=>{if(e.key==='Enter')reset();});
document.getElementById('event').onchange=reset; document.getElementById('event').onchange=reset;
document.getElementById('payload').onchange=reset; document.getElementById('payload').onchange=reset;
@@ -236,20 +244,37 @@ class Handler(http.server.BaseHTTPRequestHandler):
if len(rows) >= need: if len(rows) >= need:
break break
page_rows = rows[page * size:(page + 1) * size] page_rows = rows[page * size:(page + 1) * size]
try:
stat = self.server.journal.stat()
journal_bytes, journal_mtime = stat.st_size, int(stat.st_mtime)
except OSError:
journal_bytes, journal_mtime = 0, 0
body = json.dumps({'events': page_rows, 'scanned': scanned, 'page': page, 'size': size, body = json.dumps({'events': page_rows, 'scanned': scanned, 'page': page, 'size': size,
'has_more': len(rows) >= need, 'hidden': len(hidden), 'has_more': len(rows) >= need, 'hidden': len(hidden),
'hidden_ips': sorted(hidden)}, ensure_ascii=True).encode() 'hidden_ips': sorted(hidden), 'journal_bytes': journal_bytes,
'journal_mtime': journal_mtime}, ensure_ascii=True).encode()
self.send(200, body, 'application/json', (('Content-Disposition', 'inline'),)) self.send(200, body, 'application/json', (('Content-Disposition', 'inline'),))
def send_stream(self, path, download_name=None):
# Stream large payloads without loading them into memory.
self.send_response(200)
self.send_header('Content-Type', 'application/octet-stream')
self.send_header('Content-Length', str(path.stat().st_size))
self.send_header('X-Content-Type-Options', 'nosniff')
self.send_header('Cache-Control', 'no-store')
if download_name:
self.send_header('Content-Disposition', f'attachment; filename="{download_name}"')
self.end_headers()
if self.command != 'HEAD':
with path.open('rb') as stream:
shutil.copyfileobj(stream, self.wfile, length=1 << 20)
def artifact(self, sha, query): def artifact(self, sha, query):
path = artifact_path(self.server.artifacts, sha) path = artifact_path(self.server.artifacts, sha)
if path is None: if path is None:
return self.send(404, b'not found', 'text/plain; charset=utf-8') return self.send(404, b'not found', 'text/plain; charset=utf-8')
data = path.read_bytes() download = None if self.one(query, 'inline', '0') == '1' else f'{sha}.bin'
if self.one(query, 'inline', '0') == '1': self.send_stream(path, download)
return self.send(200, data, 'application/octet-stream')
self.send(200, data, 'application/octet-stream',
(('Content-Disposition', f'attachment; filename="{sha}.bin"'),))
def main(): def main():