Initial log streamer app
This commit is contained in:
+186
@@ -0,0 +1,186 @@
|
||||
'use strict';
|
||||
|
||||
const MAX_VISIBLE_LINES = 5000;
|
||||
const RECONNECT_DELAY_MS = 1000;
|
||||
|
||||
const output = document.querySelector('#logOutput');
|
||||
const statusBadge = document.querySelector('#status');
|
||||
const filePath = document.querySelector('#filePath');
|
||||
const lineCount = document.querySelector('#lineCount');
|
||||
const pendingCount = document.querySelector('#pendingCount');
|
||||
const autoScroll = document.querySelector('#autoScroll');
|
||||
const pauseButton = document.querySelector('#pauseButton');
|
||||
const clearButton = document.querySelector('#clearButton');
|
||||
|
||||
let socket;
|
||||
let reconnectTimer;
|
||||
let paused = false;
|
||||
let visibleLines = [];
|
||||
let pendingChunks = [];
|
||||
|
||||
function setStatus(state, label) {
|
||||
statusBadge.dataset.state = state;
|
||||
statusBadge.textContent = label;
|
||||
}
|
||||
|
||||
function isNearBottom() {
|
||||
const distance = output.scrollHeight - output.scrollTop - output.clientHeight;
|
||||
return distance < 24;
|
||||
}
|
||||
|
||||
function scrollToBottom() {
|
||||
output.scrollTop = output.scrollHeight;
|
||||
}
|
||||
|
||||
function updateCounts() {
|
||||
lineCount.textContent = String(visibleLines.length);
|
||||
|
||||
if (pendingChunks.length === 0) {
|
||||
pendingCount.hidden = true;
|
||||
pendingCount.textContent = '0 buffered';
|
||||
return;
|
||||
}
|
||||
|
||||
pendingCount.hidden = false;
|
||||
pendingCount.textContent = `${pendingChunks.length} buffered`;
|
||||
}
|
||||
|
||||
function appendText(text) {
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
|
||||
const shouldScroll = autoScroll.checked && isNearBottom();
|
||||
const normalized = text.replace(/\r\n/g, '\n');
|
||||
const nextLines = normalized.split('\n');
|
||||
|
||||
if (visibleLines.length === 0) {
|
||||
visibleLines = nextLines;
|
||||
} else {
|
||||
visibleLines[visibleLines.length - 1] += nextLines[0];
|
||||
visibleLines.push(...nextLines.slice(1));
|
||||
}
|
||||
|
||||
if (visibleLines.length > MAX_VISIBLE_LINES) {
|
||||
visibleLines = visibleLines.slice(-MAX_VISIBLE_LINES);
|
||||
}
|
||||
|
||||
output.textContent = visibleLines.join('\n');
|
||||
updateCounts();
|
||||
|
||||
if (shouldScroll) {
|
||||
requestAnimationFrame(scrollToBottom);
|
||||
}
|
||||
}
|
||||
|
||||
function setSnapshot(lines) {
|
||||
visibleLines = Array.isArray(lines) ? lines.slice(-MAX_VISIBLE_LINES) : [];
|
||||
pendingChunks = [];
|
||||
output.textContent = visibleLines.join('\n');
|
||||
updateCounts();
|
||||
|
||||
if (autoScroll.checked) {
|
||||
requestAnimationFrame(scrollToBottom);
|
||||
}
|
||||
}
|
||||
|
||||
function handleStreamText(text) {
|
||||
if (paused) {
|
||||
pendingChunks.push(text);
|
||||
updateCounts();
|
||||
return;
|
||||
}
|
||||
|
||||
appendText(text);
|
||||
}
|
||||
|
||||
function flushPending() {
|
||||
if (pendingChunks.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const chunks = pendingChunks.join('');
|
||||
pendingChunks = [];
|
||||
appendText(chunks);
|
||||
}
|
||||
|
||||
function connect() {
|
||||
clearTimeout(reconnectTimer);
|
||||
setStatus(socket ? 'reconnecting' : 'connecting', socket ? 'Reconnecting' : 'Connecting');
|
||||
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
socket = new WebSocket(`${protocol}//${window.location.host}/stream`);
|
||||
|
||||
socket.addEventListener('open', () => {
|
||||
setStatus('connected', 'Connected');
|
||||
});
|
||||
|
||||
socket.addEventListener('message', (event) => {
|
||||
let message;
|
||||
|
||||
try {
|
||||
message = JSON.parse(event.data);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === 'hello') {
|
||||
filePath.textContent = message.file || 'Unknown file';
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === 'snapshot') {
|
||||
setSnapshot(message.lines);
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === 'append') {
|
||||
handleStreamText(message.text);
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === 'truncate') {
|
||||
setSnapshot([]);
|
||||
appendText(`[stream reset] ${message.message}\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === 'error') {
|
||||
setStatus('error', 'Error');
|
||||
appendText(`[stream error] ${message.message}\n`);
|
||||
}
|
||||
});
|
||||
|
||||
socket.addEventListener('close', () => {
|
||||
setStatus('disconnected', 'Disconnected');
|
||||
reconnectTimer = setTimeout(connect, RECONNECT_DELAY_MS);
|
||||
});
|
||||
|
||||
socket.addEventListener('error', () => {
|
||||
setStatus('error', 'Error');
|
||||
});
|
||||
}
|
||||
|
||||
pauseButton.addEventListener('click', () => {
|
||||
paused = !paused;
|
||||
pauseButton.textContent = paused ? 'Resume' : 'Pause';
|
||||
|
||||
if (!paused) {
|
||||
flushPending();
|
||||
}
|
||||
});
|
||||
|
||||
clearButton.addEventListener('click', () => {
|
||||
visibleLines = [];
|
||||
pendingChunks = [];
|
||||
output.textContent = '';
|
||||
updateCounts();
|
||||
});
|
||||
|
||||
autoScroll.addEventListener('change', () => {
|
||||
if (autoScroll.checked) {
|
||||
scrollToBottom();
|
||||
}
|
||||
});
|
||||
|
||||
connect();
|
||||
Reference in New Issue
Block a user