Initial log streamer app

This commit is contained in:
2026-06-24 15:35:10 +08:00
commit a105228653
8 changed files with 786 additions and 0 deletions
+186
View File
@@ -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();