Skip to content

feat: web serial log viewer#140

Merged
sjordan0228 merged 4 commits into
devfrom
feature/web-serial-log
Apr 10, 2026
Merged

feat: web serial log viewer#140
sjordan0228 merged 4 commits into
devfrom
feature/web-serial-log

Conversation

@sjordan0228
Copy link
Copy Markdown
Contributor

@sjordan0228 sjordan0228 commented Apr 10, 2026

Summary

  • New LogBuffer class — 4KB ring buffer singleton with logPrintf() that writes to both Serial and the buffer, thread-safe via FreeRTOS mutex
  • New /logs page — dark-themed auto-scrolling log viewer matching the SpoolSense UI, polls /api/logs every 2 seconds
  • GET /api/logs returns buffer contents as plain text, POST /api/logs/clear clears it
  • Landing page "Serial Log" card and troubleshooting page "View Serial Log" link

Instrumented events

Source Events
NFCManager Tag detected (UID), NTAG variant, tag payload summaries (all 4 formats + Bambu), tag removed, all write outcomes
SpoolmanManager Spool sync/create/archive, filament changes, HTTP errors
HomeAssistantManager MQTT connected/failed

Test plan

  • Build succeeds (esp32s3devkitc)
  • Log viewer page loads at /logs with correct theme
  • Scanning tags shows detection + payload + Spoolman events
  • All tag formats appear (OpenPrintTag, TigerTag, OpenTag3D, OpenSpool)
  • Clear button works
  • Auto-scroll works
  • Troubleshooting page has "View Serial Log" link
  • Landing page has Serial Log card

Closes #139

Summary by CodeRabbit

Release Notes

  • New Features
    • Added a live Serial Log Viewer accessible from the web interface, displaying real-time device logs without requiring a serial terminal connection.
    • Log viewer includes copy, clear, and auto-scroll controls for enhanced diagnostics.
    • Added navigation links to the log viewer on the home page and troubleshooting page.
    • Enhanced logging throughout device operations for improved visibility into scanner activity.

Add LogBuffer ring buffer (4KB) that captures key events via logPrintf
and serves them at /logs with a dark-themed auto-scrolling viewer.

Instrumented events: tag detection (all formats), tag removal, NTAG
variant, tag payload summaries, Spoolman sync/create/archive/errors,
MQTT connection status, and all write outcomes.

Accessible from landing page card and troubleshooting page link.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 10, 2026

Warning

Rate limit exceeded

@sjordan0228 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 19 minutes and 30 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 19 minutes and 30 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 32d251d2-7671-4fe3-84ae-ef809a803e9b

📥 Commits

Reviewing files that changed from the base of the PR and between 8c3ca62 and dd334a1.

📒 Files selected for processing (3)
  • src/LogBuffer.cpp
  • src/LogBuffer.h
  • src/LogViewerHTML.h
📝 Walkthrough

Walkthrough

The PR introduces a log buffering and web viewer feature that captures serial output in a ring buffer and exposes it via a web interface. A new LogBuffer singleton class provides thread-safe circular buffer storage and formatted logging. Multiple managers now integrate logging calls to capture NFC tag events, MQTT reconnection status, and Spoolman synchronization operations. New web endpoints (GET /logs, GET /api/logs, POST /api/logs/clear) are added alongside an HTML viewer page that auto-polls and displays live log content.

Changes

Cohort / File(s) Summary
Core Logging Infrastructure
src/LogBuffer.h, src/LogBuffer.cpp
New LogBuffer singleton class implementing a thread-safe 4096-byte circular buffer with logPrintf() for variadic formatted logging, getLog() for buffer retrieval, and clear() for reset. Uses FreeRTOS mutex for thread safety and writes to both Serial and internal buffer.
Web UI & API Endpoints
src/LogViewerHTML.h, src/WebServerManager.h, src/WebServerManager.cpp
New LOG_VIEWER_HTML page with auto-scrolling log display, copy/clear buttons, and client-side fetch polling. Three new WebServerManager handlers (handleLogViewer(), handleApiLogs(), handleApiLogsClear()) register routes GET /logs, GET /api/logs, and POST /api/logs/clear with CORS headers.
Navigation UI Updates
src/LandingHTML.h, src/TroubleshootingHTML.h
Added navigation links to new /logs page in landing page tool grid and troubleshooting section info text.
Logging Integration
src/HomeAssistantManager.cpp, src/NFCManager.cpp, src/SpoolmanManager.cpp
Integrated LogBuffer::getInstance().logPrintf() calls to capture MQTT reconnection status, NFC tag detection/removal/write outcomes, and Spoolman spool synchronization events alongside existing Serial output.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Suggested labels

size/XL

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.16% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: web serial log viewer' clearly and concisely summarizes the main feature added—a web-accessible log viewer. It is specific, directly related to the core change, and follows conventional commit formatting.
Description check ✅ Passed The description covers Summary, Changes (instrumentation table), and a comprehensive Test plan with all items checked. However, it lacks explicit sections for 'How to Test' steps and 'Checklist' items required by the template.
Linked Issues check ✅ Passed The PR fully implements all coding requirements from issue #139: LogBuffer ring buffer with logPrintf, /logs web UI with auto-scroll/clear, GET/POST API endpoints, UI entry points, and comprehensive instrumentation of NFCManager, SpoolmanManager, and HomeAssistantManager.
Out of Scope Changes check ✅ Passed All changes are directly scoped to issue #139: LogBuffer implementation, web UI for log viewing, API endpoints, landing page/troubleshooting page updates with log links, and instrumentation of target managers. No unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/web-serial-log

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added the size/L Large change (200-500 lines) label Apr 10, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/LogBuffer.cpp`:
- Around line 36-40: The current write path uses xSemaphoreTake(mutex_,
pdMS_TO_TICKS(50)) to protect append(temp, len) and silently drops the buffer
write on timeout; update this by increasing the timeout (e.g., change
pdMS_TO_TICKS(50) to pdMS_TO_TICKS(100)) and/or add a fallback warning when
xSemaphoreTake fails so dropped writes are visible (use your existing
logging/Serial facility to report that append(temp, len) could not acquire
mutex_ and the message was not written to the ring buffer). Ensure
xSemaphoreGive(mutex_) remains unchanged and reference the same symbols
(xSemaphoreTake, mutex_, append, temp, len) when applying the change.
- Line 2: Replace the C header with the C++ header in LogBuffer.cpp: change the
include of <stdarg.h> to <cstdarg> so the translation unit uses the C++-style
variadic argument header (ensure any uses like va_list in functions such as
LogBuffer::append or other variadic helpers continue to compile and refer to
std::va_list if necessary).
- Around line 5-8: The constructor LogBuffer::LogBuffer currently assigns mutex_
= xSemaphoreCreateMutex() without checking for NULL; add a null-check
immediately after calling xSemaphoreCreateMutex() and handle failure (e.g., log
an error via your logging facility, assert/abort, or set an internal error flag
so later calls that use mutex_ such as xSemaphoreTake() can skip/avoid using it)
to prevent undefined behavior when memory is exhausted; ensure any code paths
that use mutex_ (e.g., methods calling xSemaphoreTake/xSemaphoreGive) check the
flag or mutex_ != NULL before calling.
- Around line 62-68: Modify LogBuffer::clear to return a bool indicating
success: attempt to take the mutex_ via xSemaphoreTake (pdMS_TO_TICKS(50)) and
if successful set head_ = 0 and count_ = 0, xSemaphoreGive(mutex_) and return
true; if the take times out return false to signal failure. Update the
declaration in LogBuffer.h accordingly and ensure callers (e.g., the API handler
invoking LogBuffer::clear) check the bool and propagate/report failure.

In `@src/LogBuffer.h`:
- Around line 10-33: Add deleted copy/move constructors and assignment operators
to the LogBuffer class to fully enforce the singleton; inside the class
declaration (near the private section with LogBuffer() and append()), add
declarations: LogBuffer(const LogBuffer&) = delete; LogBuffer& operator=(const
LogBuffer&) = delete; LogBuffer(LogBuffer&&) = delete; LogBuffer&
operator=(LogBuffer&&) = delete; so copying or moving the singleton via
copy/move ctor or assignment is a compile-time error.

In `@src/LogViewerHTML.h`:
- Around line 127-158: The polling allows overlapping requests:
setInterval(fetchLogs, 2000) can start a new fetchLogs before the prior one
completes causing stale responses (e.g., after clearLogs()). Fix by serializing
requests in fetchLogs using an "inFlight" flag (or an AbortController) so
fetchLogs returns immediately if a request is already running; update the flag
when the fetch promise settles; ensure clearLogs calls fetchLogs() but relies on
the inFlight guard so it won't race with an ongoing request. Reference:
fetchLogs, setInterval(fetchLogs, 2000), and clearLogs.

In `@src/WebServerManager.cpp`:
- Around line 287-297: The current WebServerManager::handleApiLogs allocates a
4KB buffer and calls _server.send which causes an extra internal copy and spikes
memory; change it to stream the log content instead (like
handleApiSpoolmanSpools does) by using _server.setContentLength(...) and
_server.sendContent/sendChunked with a small fixed-size buffer passed to
LogBuffer::getInstance().getLog to produce chunks, or by supplying a content
provider lambda that reads successive chunks from
LogBuffer::getInstance().getLog until exhausted; remove the large persistent
malloc/free and ensure headers (Access-Control-Allow-Origin) remain set before
starting the streamed response.

In `@src/WebServerManager.h`:
- Around line 76-79: The log endpoints expose sensitive data and a destructive
clear action without auth; add authentication guards in handleApiLogs and
handleApiLogsClear (and apply same guard to handleLogViewer) by validating a
configured API token or Authorization header (reject with 401 on failure),
restrict or remove Access-Control-Allow-Origin: * for those handlers, and ensure
handleApiLogsClear requires elevated creds (separate admin token or stronger
check) and logs auth failures; alternatively, if this behavior is intentional,
add clear inline comments and README documentation near WebServerManager
(referencing handleLogViewer, handleApiLogs, handleApiLogsClear) explaining the
deliberate lack of auth and the network isolation requirements.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3eec7001-64ff-4193-9e76-7c5aa611009e

📥 Commits

Reviewing files that changed from the base of the PR and between 971fa75 and 8c3ca62.

📒 Files selected for processing (10)
  • src/HomeAssistantManager.cpp
  • src/LandingHTML.h
  • src/LogBuffer.cpp
  • src/LogBuffer.h
  • src/LogViewerHTML.h
  • src/NFCManager.cpp
  • src/SpoolmanManager.cpp
  • src/TroubleshootingHTML.h
  • src/WebServerManager.cpp
  • src/WebServerManager.h

Comment thread src/LogBuffer.cpp Outdated
Comment thread src/LogBuffer.cpp Outdated
Comment on lines +5 to +8
LogBuffer::LogBuffer() {
mutex_ = xSemaphoreCreateMutex();
memset(buffer_, 0, BUFFER_SIZE);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add null check for mutex creation.

xSemaphoreCreateMutex() can return NULL if the system is out of memory. Subsequent xSemaphoreTake() calls on a null mutex would cause undefined behavior.

🐛 Proposed fix
 LogBuffer::LogBuffer() {
     mutex_ = xSemaphoreCreateMutex();
+    configASSERT(mutex_ != nullptr);  // or handle gracefully
     memset(buffer_, 0, BUFFER_SIZE);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
LogBuffer::LogBuffer() {
mutex_ = xSemaphoreCreateMutex();
memset(buffer_, 0, BUFFER_SIZE);
}
LogBuffer::LogBuffer() {
mutex_ = xSemaphoreCreateMutex();
configASSERT(mutex_ != nullptr); // or handle gracefully
memset(buffer_, 0, BUFFER_SIZE);
}
🧰 Tools
🪛 Clang (14.0.6)

[warning] 5-5: constructor does not initialize these fields: buffer_, mutex_

(cppcoreguidelines-pro-type-member-init)


[warning] 5-5: use '= default' to define a trivial default constructor

(modernize-use-equals-default)

🪛 Cppcheck (2.20.0)

[information] 5-5: Include file

(missingIncludeSystem)


[information] 6-6: Include file

(missingIncludeSystem)


[performance] 6-6: Variable 'mutex_' is assigned in constructor body. Consider performing initialization in initialization list.

(useInitializationList)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/LogBuffer.cpp` around lines 5 - 8, The constructor LogBuffer::LogBuffer
currently assigns mutex_ = xSemaphoreCreateMutex() without checking for NULL;
add a null-check immediately after calling xSemaphoreCreateMutex() and handle
failure (e.g., log an error via your logging facility, assert/abort, or set an
internal error flag so later calls that use mutex_ such as xSemaphoreTake() can
skip/avoid using it) to prevent undefined behavior when memory is exhausted;
ensure any code paths that use mutex_ (e.g., methods calling
xSemaphoreTake/xSemaphoreGive) check the flag or mutex_ != NULL before calling.

Comment thread src/LogBuffer.cpp
Comment on lines +36 to +40
// Write to ring buffer under mutex
if (xSemaphoreTake(mutex_, pdMS_TO_TICKS(50)) == pdTRUE) {
append(temp, len);
xSemaphoreGive(mutex_);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Log messages are silently dropped from the buffer if mutex acquisition times out.

When the 50ms mutex timeout expires, the message is written to Serial but not to the ring buffer. Under heavy concurrent logging from multiple managers (NFC, Spoolman, HomeAssistant), users viewing /logs may miss events.

Consider either:

  1. Increasing the timeout (e.g., 100ms)
  2. Logging a warning when drops occur
  3. Documenting this behavior for users
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/LogBuffer.cpp` around lines 36 - 40, The current write path uses
xSemaphoreTake(mutex_, pdMS_TO_TICKS(50)) to protect append(temp, len) and
silently drops the buffer write on timeout; update this by increasing the
timeout (e.g., change pdMS_TO_TICKS(50) to pdMS_TO_TICKS(100)) and/or add a
fallback warning when xSemaphoreTake fails so dropped writes are visible (use
your existing logging/Serial facility to report that append(temp, len) could not
acquire mutex_ and the message was not written to the ring buffer). Ensure
xSemaphoreGive(mutex_) remains unchanged and reference the same symbols
(xSemaphoreTake, mutex_, append, temp, len) when applying the change.

Comment thread src/LogBuffer.cpp
Comment on lines +62 to +68
void LogBuffer::clear() {
if (xSemaphoreTake(mutex_, pdMS_TO_TICKS(50)) == pdTRUE) {
head_ = 0;
count_ = 0;
xSemaphoreGive(mutex_);
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider returning success status from clear().

If the mutex acquisition times out, the clear operation silently fails. The web UI's "Clear" button will appear to succeed, but the next poll will show logs still present. Returning a bool would allow the API to report failure.

♻️ Proposed change

In src/LogBuffer.h:

-    void clear();
+    bool clear();

In src/LogBuffer.cpp:

-void LogBuffer::clear() {
+bool LogBuffer::clear() {
     if (xSemaphoreTake(mutex_, pdMS_TO_TICKS(50)) == pdTRUE) {
         head_ = 0;
         count_ = 0;
         xSemaphoreGive(mutex_);
+        return true;
     }
+    return false;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
void LogBuffer::clear() {
if (xSemaphoreTake(mutex_, pdMS_TO_TICKS(50)) == pdTRUE) {
head_ = 0;
count_ = 0;
xSemaphoreGive(mutex_);
}
}
bool LogBuffer::clear() {
if (xSemaphoreTake(mutex_, pdMS_TO_TICKS(50)) == pdTRUE) {
head_ = 0;
count_ = 0;
xSemaphoreGive(mutex_);
return true;
}
return false;
}
🧰 Tools
🪛 Clang (14.0.6)

[warning] 62-62: method 'clear' can be made static

(readability-convert-member-functions-to-static)

🪛 Cppcheck (2.20.0)

[style] 62-62: The function 'clear' is never used.

(unusedFunction)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/LogBuffer.cpp` around lines 62 - 68, Modify LogBuffer::clear to return a
bool indicating success: attempt to take the mutex_ via xSemaphoreTake
(pdMS_TO_TICKS(50)) and if successful set head_ = 0 and count_ = 0,
xSemaphoreGive(mutex_) and return true; if the take times out return false to
signal failure. Update the declaration in LogBuffer.h accordingly and ensure
callers (e.g., the API handler invoking LogBuffer::clear) check the bool and
propagate/report failure.

Comment thread src/LogBuffer.h
Comment on lines +10 to +33
class LogBuffer {
public:
static LogBuffer& getInstance();

// Write formatted output to both Serial and the ring buffer
void logPrintf(const char* format, ...) __attribute__((format(printf, 2, 3)));

// Copy current buffer contents into a pre-allocated char array.
// Returns number of bytes written (excluding null terminator).
size_t getLog(char* out, size_t outSize);

void clear();

private:
static constexpr size_t BUFFER_SIZE = 4096;

char buffer_[BUFFER_SIZE];
size_t head_ = 0;
size_t count_ = 0;
SemaphoreHandle_t mutex_;

LogBuffer();
void append(const char* data, size_t len);
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Delete copy/move constructors and assignment operators to fully enforce singleton.

The singleton pattern should prevent copying and moving. While the private constructor prevents external instantiation, explicitly deleting these operations provides clearer intent and compile-time enforcement.

♻️ Proposed fix
 class LogBuffer {
 public:
     static LogBuffer& getInstance();
+
+    // Singleton — no copy/move
+    LogBuffer(const LogBuffer&) = delete;
+    LogBuffer& operator=(const LogBuffer&) = delete;
+    LogBuffer(LogBuffer&&) = delete;
+    LogBuffer& operator=(LogBuffer&&) = delete;

     // Write formatted output to both Serial and the ring buffer
     void logPrintf(const char* format, ...) __attribute__((format(printf, 2, 3)));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
class LogBuffer {
public:
static LogBuffer& getInstance();
// Write formatted output to both Serial and the ring buffer
void logPrintf(const char* format, ...) __attribute__((format(printf, 2, 3)));
// Copy current buffer contents into a pre-allocated char array.
// Returns number of bytes written (excluding null terminator).
size_t getLog(char* out, size_t outSize);
void clear();
private:
static constexpr size_t BUFFER_SIZE = 4096;
char buffer_[BUFFER_SIZE];
size_t head_ = 0;
size_t count_ = 0;
SemaphoreHandle_t mutex_;
LogBuffer();
void append(const char* data, size_t len);
};
class LogBuffer {
public:
static LogBuffer& getInstance();
// Singleton — no copy/move
LogBuffer(const LogBuffer&) = delete;
LogBuffer& operator=(const LogBuffer&) = delete;
LogBuffer(LogBuffer&&) = delete;
LogBuffer& operator=(LogBuffer&&) = delete;
// Write formatted output to both Serial and the ring buffer
void logPrintf(const char* format, ...) __attribute__((format(printf, 2, 3)));
// Copy current buffer contents into a pre-allocated char array.
// Returns number of bytes written (excluding null terminator).
size_t getLog(char* out, size_t outSize);
void clear();
private:
static constexpr size_t BUFFER_SIZE = 4096;
char buffer_[BUFFER_SIZE];
size_t head_ = 0;
size_t count_ = 0;
SemaphoreHandle_t mutex_;
LogBuffer();
void append(const char* data, size_t len);
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/LogBuffer.h` around lines 10 - 33, Add deleted copy/move constructors and
assignment operators to the LogBuffer class to fully enforce the singleton;
inside the class declaration (near the private section with LogBuffer() and
append()), add declarations: LogBuffer(const LogBuffer&) = delete; LogBuffer&
operator=(const LogBuffer&) = delete; LogBuffer(LogBuffer&&) = delete;
LogBuffer& operator=(LogBuffer&&) = delete; so copying or moving the singleton
via copy/move ctor or assignment is a compile-time error.

Comment thread src/LogViewerHTML.h
Comment on lines +127 to +158
function fetchLogs() {
fetch('/api/logs')
.then(function(r) { return r.text(); })
.then(function(data) {
logEl.textContent = data || '(empty)';
if (autoEl.checked) logEl.scrollTop = logEl.scrollHeight;
})
.catch(function(e) { logEl.textContent = 'Error: ' + e.message; });
}
function copyLogs() {
var text = logEl.textContent;
if (navigator.clipboard) {
navigator.clipboard.writeText(text).then(function() {
var btn = document.querySelector('.btn');
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
} else {
var ta = document.createElement('textarea');
ta.value = text;
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
}
}
function clearLogs() {
fetch('/api/logs/clear', { method: 'POST' })
.then(function() { fetchLogs(); });
}
fetchLogs();
setInterval(fetchLogs, 2000);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid overlapping /api/logs polls.

setInterval(fetchLogs, 2000) can queue a new GET before the previous one finishes. On the ESP32 that can leave multiple open requests to the same endpoint, and an older response can repaint stale content immediately after clearLogs() succeeds.

Suggested fix
-        function fetchLogs() {
-            fetch('/api/logs')
-                .then(function(r) { return r.text(); })
-                .then(function(data) {
-                    logEl.textContent = data || '(empty)';
-                    if (autoEl.checked) logEl.scrollTop = logEl.scrollHeight;
-                })
-                .catch(function(e) { logEl.textContent = 'Error: ' + e.message; });
-        }
+        var logsRequestInFlight = false;
+        function fetchLogs() {
+            if (logsRequestInFlight) return Promise.resolve();
+            logsRequestInFlight = true;
+            return fetch('/api/logs')
+                .then(function(r) { return r.text(); })
+                .then(function(data) {
+                    logEl.textContent = data || '(empty)';
+                    if (autoEl.checked) logEl.scrollTop = logEl.scrollHeight;
+                })
+                .catch(function(e) {
+                    logEl.textContent = 'Error: ' + e.message;
+                })
+                .finally(function() {
+                    logsRequestInFlight = false;
+                });
+        }
         function clearLogs() {
             fetch('/api/logs/clear', { method: 'POST' })
                 .then(function() { fetchLogs(); });
         }
         fetchLogs();
-        setInterval(fetchLogs, 2000);
+        (function poll() {
+            fetchLogs().finally(function() {
+                setTimeout(poll, 2000);
+            });
+        })();
🧰 Tools
🪛 Clang (14.0.6)

[warning] 138-138: if with identical then and else branches

(bugprone-branch-clone)


[note] 144-144: else branch starts here

(clang)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/LogViewerHTML.h` around lines 127 - 158, The polling allows overlapping
requests: setInterval(fetchLogs, 2000) can start a new fetchLogs before the
prior one completes causing stale responses (e.g., after clearLogs()). Fix by
serializing requests in fetchLogs using an "inFlight" flag (or an
AbortController) so fetchLogs returns immediately if a request is already
running; update the flag when the fetch promise settles; ensure clearLogs calls
fetchLogs() but relies on the inFlight guard so it won't race with an ongoing
request. Reference: fetchLogs, setInterval(fetchLogs, 2000), and clearLogs.

Comment thread src/WebServerManager.cpp
Comment on lines +287 to +297
void WebServerManager::handleApiLogs() {
_server.sendHeader("Access-Control-Allow-Origin", "*");
char* buf = (char*)malloc(4097);
if (!buf) {
_server.send(500, "text/plain", "out of memory");
return;
}
LogBuffer::getInstance().getLog(buf, 4097);
_server.send(200, "text/plain", buf);
free(buf);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider using chunked/streaming response to reduce peak memory usage.

The handler allocates a 4KB buffer, then _server.send() internally copies it again, briefly doubling memory usage (~8KB). On a memory-constrained ESP32, this could cause allocation failures during high activity.

Consider using setContentLength() + sendContent() for streaming, similar to how handleApiSpoolmanSpools() (lines 501-522) streams large responses.

♻️ Optional streaming approach
 void WebServerManager::handleApiLogs() {
     _server.sendHeader("Access-Control-Allow-Origin", "*");
     char* buf = (char*)malloc(4097);
     if (!buf) {
         _server.send(500, "text/plain", "out of memory");
         return;
     }
-    LogBuffer::getInstance().getLog(buf, 4097);
-    _server.send(200, "text/plain", buf);
+    size_t len = LogBuffer::getInstance().getLog(buf, 4097);
+    _server.setContentLength(len);
+    _server.send(200, "text/plain", "");
+    _server.sendContent(buf, len);
     free(buf);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
void WebServerManager::handleApiLogs() {
_server.sendHeader("Access-Control-Allow-Origin", "*");
char* buf = (char*)malloc(4097);
if (!buf) {
_server.send(500, "text/plain", "out of memory");
return;
}
LogBuffer::getInstance().getLog(buf, 4097);
_server.send(200, "text/plain", buf);
free(buf);
}
void WebServerManager::handleApiLogs() {
_server.sendHeader("Access-Control-Allow-Origin", "*");
char* buf = (char*)malloc(4097);
if (!buf) {
_server.send(500, "text/plain", "out of memory");
return;
}
size_t len = LogBuffer::getInstance().getLog(buf, 4097);
_server.setContentLength(len);
_server.send(200, "text/plain", "");
_server.sendContent(buf, len);
free(buf);
}
🧰 Tools
🪛 Clang (14.0.6)

[warning] 287-287: method 'handleApiLogs' can be made static

(readability-convert-member-functions-to-static)


[warning] 290-290: implicit conversion 'char *' -> bool

(readability-implicit-bool-conversion)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/WebServerManager.cpp` around lines 287 - 297, The current
WebServerManager::handleApiLogs allocates a 4KB buffer and calls _server.send
which causes an extra internal copy and spikes memory; change it to stream the
log content instead (like handleApiSpoolmanSpools does) by using
_server.setContentLength(...) and _server.sendContent/sendChunked with a small
fixed-size buffer passed to LogBuffer::getInstance().getLog to produce chunks,
or by supplying a content provider lambda that reads successive chunks from
LogBuffer::getInstance().getLog until exhausted; remove the large persistent
malloc/free and ensure headers (Access-Control-Allow-Origin) remain set before
starting the streamed response.

Comment thread src/WebServerManager.h
Comment on lines +76 to +79
// Log viewer
void handleLogViewer();
void handleApiLogs();
void handleApiLogsClear();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE=$(fd -i '^WebServerManager\.cpp$' | head -n1)
echo "Inspecting: ${FILE}"

echo
echo "=== Log route registrations / handlers ==="
rg -n -C3 '"/logs"|"/api/logs"|handleLogViewer|handleApiLogs|handleApiLogsClear' "$FILE"

echo
echo "=== Existing auth / sensitive-route patterns for comparison ==="
rg -n -C3 'authenticate|authorized|auth|login|password|token|session|/config|/update' "$FILE"

Repository: SpoolSense/spoolsense_scanner

Length of output: 3808


Add authentication guards to /api/logs and /api/logs/clear handlers, or document the intentional absence of auth.

The log routes lack authentication checks and are accessible to any LAN client. All three handlers (handleLogViewer, handleApiLogs, handleApiLogsClear) set Access-Control-Allow-Origin: * but have no auth guards. While this is consistent with other routes in WebServerManager.cpp (/config, /update, /api/config are also unprotected), the destructive /api/logs/clear endpoint and the exposure of sensitive diagnostics (device IDs, tag UIDs, broker hosts) warrant explicit protection. Either implement auth guards (headers-based, network-level, or token validation) or document that the product intentionally has no auth model and relies on network isolation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/WebServerManager.h` around lines 76 - 79, The log endpoints expose
sensitive data and a destructive clear action without auth; add authentication
guards in handleApiLogs and handleApiLogsClear (and apply same guard to
handleLogViewer) by validating a configured API token or Authorization header
(reject with 401 on failure), restrict or remove Access-Control-Allow-Origin: *
for those handlers, and ensure handleApiLogsClear requires elevated creds
(separate admin token or stronger check) and logs auth failures; alternatively,
if this behavior is intentional, add clear inline comments and README
documentation near WebServerManager (referencing handleLogViewer, handleApiLogs,
handleApiLogsClear) explaining the deliberate lack of auth and the network
isolation requirements.

@sjordan0228 sjordan0228 merged commit cb71860 into dev Apr 10, 2026
2 checks passed
@sjordan0228 sjordan0228 deleted the feature/web-serial-log branch April 10, 2026 04:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/L Large change (200-500 lines)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant