From 0c5f082d1600d93cc884dc2b26438381572efece Mon Sep 17 00:00:00 2001 From: Henry Park Date: Wed, 25 Feb 2026 16:00:09 -0800 Subject: [PATCH] feat(web): display logs newest-first in web gateway UI (#369) Reverse log display order so the most recent entries appear at the top, removing the need to scroll to see latest activity. Frontend: rename appendLogEntry to prependLogEntry, use prepend() for DOM insertion, cap oldest entries from the bottom, and auto-scroll to top. Backend: update recent_entries() doc comment to clarify the oldest-first return order works correctly with the frontend's prepend. Co-authored-by: Claude Opus 4.6 (1M context) --- Cargo.lock | 7 +++++++ src/channels/web/log_layer.rs | 3 +++ src/channels/web/static/app.js | 18 +++++++++--------- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3695f892..f44ea9c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5032,6 +5032,12 @@ dependencies = [ "digest", ] +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + [[package]] name = "sha2" version = "0.10.9" @@ -6207,6 +6213,7 @@ dependencies = [ "getrandom 0.4.1", "js-sys", "serde_core", + "sha1_smol", "wasm-bindgen", ] diff --git a/src/channels/web/log_layer.rs b/src/channels/web/log_layer.rs index d072d7f5..b599ab09 100644 --- a/src/channels/web/log_layer.rs +++ b/src/channels/web/log_layer.rs @@ -90,6 +90,9 @@ impl LogBroadcaster { } /// Snapshot of recent entries for replaying to a new subscriber. + /// + /// Returns entries oldest-first so that the frontend's `prepend()` + /// naturally places the newest entry at the top of the DOM. pub fn recent_entries(&self) -> Vec { self.recent .lock() diff --git a/src/channels/web/static/app.js b/src/channels/web/static/app.js index ce04e621..35766343 100644 --- a/src/channels/web/static/app.js +++ b/src/channels/web/static/app.js @@ -1105,7 +1105,7 @@ function connectLogSSE() { logBuffer.push(entry); return; } - appendLogEntry(entry); + prependLogEntry(entry); }); logEventSource.onerror = () => { @@ -1113,7 +1113,7 @@ function connectLogSSE() { }; } -function appendLogEntry(entry) { +function prependLogEntry(entry) { const output = document.getElementById('logs-output'); // Level filter @@ -1154,16 +1154,16 @@ function appendLogEntry(entry) { div.style.display = 'none'; } - output.appendChild(div); + output.prepend(div); - // Cap entries + // Cap entries (remove oldest at the bottom) while (output.children.length > LOG_MAX_ENTRIES) { - output.removeChild(output.firstChild); + output.removeChild(output.lastChild); } - // Auto-scroll + // Auto-scroll to top (newest entries are at the top) if (document.getElementById('logs-autoscroll').checked) { - output.scrollTop = output.scrollHeight; + output.scrollTop = 0; } } @@ -1173,9 +1173,9 @@ function toggleLogsPause() { btn.textContent = logsPaused ? 'Resume' : 'Pause'; if (!logsPaused) { - // Flush buffer + // Flush buffer: oldest-first + prepend naturally puts newest at top for (const entry of logBuffer) { - appendLogEntry(entry); + prependLogEntry(entry); } logBuffer = []; }