Files
lanspread/THEPLAN.md
T
ddidderr 9d14e63613 fix: harden application log viewer
Add an Application Logs window backed by a bounded persistent main log file.
The viewer loads history from lanspread.log, subscribes to live INFO/WARN/ERROR
log events, supports filtering/copy/pause controls, and keeps the menu/window
routing separate from the unpack log viewer.

The backend sink now owns serialized access to the log file. History reads and
append-time trimming use the same sink lock, so opening the logs window cannot
race with a concurrent write and rewrite away a freshly appended line. The sink
also keeps a persistent file handle instead of reopening the file for each
captured event.

Live log events carry sink-local sequence ids. The frontend uses the history
watermark plus returned history line counts to suppress live events that were
already included in the history response, while preserving buffered rows that
were trimmed out of the history file. Auto-scroll now follows the last visible
row identity, so it continues following after the in-memory cap keeps the row
count stable.

No timestamp code change was needed. On the Linux dev host, a temporary probe
showed time::OffsetDateTime::now_local() returning +02:00 while UTC was +00:00,
matching the host CEST offset.

Test Plan:
- just fmt
- just frontend-test
- just test
- just clippy
- just build
- git diff --cached --check
- temporary Linux probe of OffsetDateTime::now_local() showed local +02:00

Refs: none
2026-06-07 18:59:05 +02:00

13 KiB

sessionId
sessionId
session-260605-123454-ub3w

Requirements

Overview & Goals

Add a "main log output" viewer so users can inspect the application's runtime logs. Logs must still be written to stdout, must also be captured to a persistent single file in the app data directory, must be bounded to the last ~2 MB, and must be accessible via a dedicated window opened from the main UI.

Scope

In Scope:

  • Capture log:: and tracing:: output to a persistent log file in the app data directory.
  • Enforce ~2 MB size limit on the stored log (keep the most recent data).
  • Expose a Tauri command to retrieve the current log content.
  • Add a new companion window (?view=main-logs) with a viewer UI modeled on the existing unpack logs feature.
  • Support live tailing of the logs, displaying new log entries as they are emitted from the backend in real time.
  • Support dynamic log level filtering in the UI to toggle/filter visible logs by log level (TRACE, DEBUG, INFO, WARN, ERROR).
  • Add "Application logs" entry to the main window kebab menu that opens/focuses the logs window.

Out of Scope:

  • Configurable log levels or per-module filters in the backend.
  • Multi-file rotation or archival of old logs.
  • Persisting logs across app reinstalls or explicit export.

User Stories

  • As a user troubleshooting an issue, I want to open the application logs from the menu so that I can see recent log output without needing external tools.
  • As a power user, I want the log file to stay bounded (~2 MB) so that it does not grow unbounded on disk.
  • As a developer or user experiencing an application crash, I want the logs to be persistently saved to a file on disk so that I can inspect them externally even if the application UI cannot be opened or crashes.

Functional Requirements

  • Logger writes to stdout, app-data file target (lanspread.log), and a Tauri event stream for real-time UI tailing.
  • Log file is kept at ~2 MB; older content is dropped to keep the tail.
  • get_main_logs command returns up to the last 2 MB of log text.
  • Main logs window renders logs in real-time, automatically appending new log lines.
  • Auto-scroll to the bottom of the log viewer container when new lines are appended (active by default, can be toggled).
  • Pause/Resume control to freeze the log stream so the user can easily scroll, select, and read without being interrupted by new lines.
  • Case-insensitive regex filtering of the displayed lines, using the exact same regex input validation, compilation, and error styling (red input border and error label) as the unpack logs window.
  • Support dynamic log level filtering in the UI via a SegmentedRadio filter with options: "All", "Debug+", "Info+", "Warn+", "Error only".
  • Support copying all/filtered logs to the clipboard and clearing the active log window's in-memory rows. Clearing the viewer does not delete the persisted log file.
  • Window title: "Application Logs"; label: "main-logs".
  • Menu item appears in the same kebab menu as "Unpack logs".

Technical Design

Current Implementation

  • Logging setup: crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs:2223 creates tauri_plugin_log::Builder with only TargetKind::Stdout + LevelFilter::Info (plus mdns off). Plugin registered at 2237.
  • State & persistence pattern: LanSpreadState holds unpack_logs: Arc<RwLock<Vec<UnpackLogEntry>>>, state_dir: OnceLock<PathBuf>; load_unpack_logs / persist_unpack_logs + trim_unpack_logs (MAX=20) in the same file; exposed by get_unpack_logs (153).
  • UI pattern: src/windows/MainWindow.tsx:23 defines openLogsWindow using WebviewWindow with label unpack-logs and ?view=unpack-logs; menu item at 110; src/App.tsx:9 dispatches via isUnpackLogsView; full viewer in src/UnpackLogsWindow.tsx (filtering, list+detail, regex).
  • The repo uses mostly log:: in the Tauri and peer crates, with some tracing:: in shared crates. The current Tauri app does not bridge or subscribe to tracing:: output.

Key Decisions

  • Store the main log as a single plain-text file at app.path().app_data_dir()?.join("lanspread.log").
  • Do not use TargetKind::LogDir for this feature. It writes to the OS log directory and uses file replacement rotation, which does not match the requested app-data location or "keep the latest tail" behavior.
  • Replace the current tauri_plugin_log logger setup with an app-owned logging setup initialized during .setup after the app data directory has been resolved.
    • Install a tracing_subscriber subscriber as the single fan-out path for stdout, persistent file writes, and live UI events.
    • Install tracing_log::LogTracer so existing log:: calls are captured by the same subscriber as tracing:: events.
    • If tauri-plugin-log is kept registered for future frontend log commands, configure it with skip_logger() so it does not compete for the global logger.
  • Use one backend formatter for both file and live-event output. Format lines as:
    • [YYYY-MM-DD][HH:MM:SS][target][LEVEL] message
    • This makes historical and live rows comparable and makes client-side level parsing deterministic.
  • Implement a shared MainLogSink that:
    • Appends formatted lines to lanspread.log.
    • Emits the same formatted line to the frontend via a Tauri event, e.g. main-log-line.
    • Bounds the file to the latest ~2 MB by rewriting the tail when the file exceeds the configured limit.
    • Never logs from inside the logging path; write/emit failures are ignored or recorded without recursive logging.
  • Handle the transition from loading history to receiving real-time logs in MainLogsWindow.tsx by using a buffering and deduplication strategy:
    • Register the main-log-line listener before requesting history.
    • Buffer incoming live rows while the initial get_main_logs file history is being requested.
    • Once history is loaded, append buffered rows after removing any rows already present in the loaded history by exact line match.
    • Transition to direct real-time appending with auto-scrolling to the bottom (if auto-scroll is enabled).
  • Implement dynamic log level filtering client-side using the existing SegmentedRadio component.
    • For live-streamed logs, use the structured event payload's level field.
    • For historical log files loaded via get_main_logs, parse the level from the controlled [LEVEL] field, falling back to Info.
  • Provide UI controls for "Pause / Resume", "Auto-scroll", and "Log Level Filter" to ensure high-quality user experience during busy logging periods.
  • Model the new viewer on the unpack-logs style, retaining the identical case-insensitive regex filter ability and regex error reporting UI, but customize it for linear line streaming (sans split-pane list+detail structure, since main logs are a single continuous log flow).

Proposed Changes

  • Rust dependencies: add tracing-subscriber and tracing-log to the workspace/Tauri crate.
  • Rust (lib.rs): initialize the custom logging pipeline in .setup, add bounded MainLogSink, add get_main_logs, emit main-log-line, and register the command.
  • Tauri capabilities: add a main-logs capability file for the new window label with core:default.
  • Frontend (MainLogsWindow.tsx): implement the scrollable log viewport using listen('main-log-line') for live tailing, supporting buffering/deduplication, pause/resume, auto-scroll, regex filtering, copying, and clearing.
  • Frontend (App.tsx & MainWindow.tsx): dispatch the new companion view and add the kebab menu entry to open the window.
  • No changes to peer crate, db, or protocol.

File Structure

  • Modified: Cargo.toml, Cargo.lock, crates/lanspread-tauri-deno-ts/src-tauri/Cargo.toml, crates/lanspread-tauri-deno-ts/src-tauri/Cargo.lock, crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs, crates/lanspread-tauri-deno-ts/src/windows/MainWindow.tsx, crates/lanspread-tauri-deno-ts/src/App.tsx
  • Added: crates/lanspread-tauri-deno-ts/src/MainLogsWindow.tsx (and .css if custom styles needed beyond shared)
  • Added: crates/lanspread-tauri-deno-ts/src-tauri/capabilities/main-logs.json

Risks

  • Trimming on every write would be expensive; mitigate by trimming only when the file exceeds the 2 MB limit plus a small slack threshold, then rewriting the latest tail.
  • The logging path is synchronous and global; keep it small, lock-protected, and non-recursive.
  • Log lines may contain very long messages; UI should handle wrapping/horizontal scrolling with stable dimensions.
  • On Windows release builds the console is hidden, so file becomes the only log source — acceptable.
  • Installing LogTracer plus a tracing_subscriber subscriber must happen once. Tests should avoid double-initializing global logging.

Delivery Steps

Step 1: extend-logger-and-add-get-main-logs-command

Logger writes to stdout, persistent app-data lanspread.log, and a live UI event stream. A bounded sink enforces the 2 MB tail.

  • Add workspace/Tauri dependencies for tracing-subscriber and tracing-log.
  • Add constants MAIN_LOG_FILE_NAME: &str = "lanspread.log" and MAX_MAIN_LOG_BYTES: u64 = 2 * 1024 * 1024 near the unpack log consts.
  • Implement main_log_path(state_dir: &Path) -> PathBuf.
  • Implement trim_main_log_file(path: &Path) that checks file size and rewrites only the last ~2 MB if exceeded, preserving valid UTF-8 by trimming to a character boundary when needed.
  • Implement a MainLogSink or equivalent shared target that formats each log record once, appends it to lanspread.log, emits main-log-line, and trims when the file grows past the limit plus slack.
  • Initialize logging during .setup after resolving let state_dir = app.path().app_data_dir()? and creating the app data directory.
    • Install LogTracer for log:: macros.
    • Install a tracing_subscriber subscriber/layer that writes to stdout and the shared main-log sink.
    • Preserve the current global level behavior: Info by default and mdns_sd::service_daemon off.
  • Implement #[tauri::command] async fn get_main_logs(app_handle: tauri::AppHandle) -> tauri::Result<String> that resolves app_handle.path().app_data_dir(), trims lanspread.log if needed, reads the file (or returns empty string), and returns the content.
  • Add get_main_logs to the invoke_handler! macro list.
  • Add focused unit tests for tail trimming and level parsing/formatting helpers where practical.

Step 2: implement-main-logs-frontend-window-and-dispatch

Implement MainLogsWindow with loading + real-time streaming and handle routing.

  • Create crates/lanspread-tauri-deno-ts/src/MainLogsWindow.tsx:
    • Fetch existing logs via invoke('get_main_logs') on load and split into an array of lines.
    • Register listen<MainLogLinePayload>('main-log-line', ...) from @tauri-apps/api/event before fetching history.
    • Buffer incoming logs while loading history, and append/deduplicate them once loaded.
    • Store logs as objects containing the log line text and its associated LogLevel.
    • Extract LogLevel for live logs from the event payload. For historical logs, parse LogLevel from the controlled [LEVEL] field (default to Info).
    • Cap in-memory rows/bytes so a long-running logs window does not grow without bound.
    • Render the log lines in a scrollable view.
    • Implement the identical case-insensitive regex filter ability on lines (compiling regex with error catching, displaying error strings below header, and toggling an invalid input class name).
    • Implement dynamic log level filtering in the UI using the shared SegmentedRadio component with options: "All", "Debug+", "Info+", "Warn+", "Error only".
    • Implement UI controls: "Auto-scroll" checkbox (scrolls viewport to bottom when new logs arrive, active by default), "Pause / Resume" toggle (buffers/pauses incoming stream rendering), "Clear" button, "Copy to clipboard" button.
  • Implement isMainLogsView() helper that checks the view=main-logs query param (export it).
  • Update crates/lanspread-tauri-deno-ts/src/App.tsx to import MainLogsWindow, isMainLogsView and dispatch to it when the query matches (before falling back to MainWindow).
  • Create MainLogsWindow.css for styling the viewer, search bars, controls, and scrollable log pane.

Step 3: add-main-logs-capability

The new companion window can invoke commands and listen for log events.

  • Add crates/lanspread-tauri-deno-ts/src-tauri/capabilities/main-logs.json.
  • Set "windows": ["main-logs"].
  • Grant "permissions": ["core:default"].
  • No log:default permission is needed because the UI listens to the app-owned main-log-line event via Tauri core events.

Step 4: add-menu-item-and-window-opener-in-mainwindow

Users can open the main logs viewer from the existing kebab menu.

  • In crates/lanspread-tauri-deno-ts/src/windows/MainWindow.tsx, add an openMainLogsWindow async function (copy of openLogsWindow but with label 'main-logs', title 'Application Logs', url '/?view=main-logs').
  • Add a new KebabItem for 'Application logs' that calls openMainLogsWindow, placed near the existing 'Unpack logs' entry in the kebabItems array.
  • Verify that opening the window focuses an existing instance if already open (same pattern as unpack logs).

Step 5: verify

Use the repo's just commands.

  • just fmt
  • just clippy
  • just frontend-test
  • just test
  • Manual smoke test with just run: open "Application logs", verify history loads from app-data lanspread.log, new backend logs append live, filters work, pause/resume works, copy works, and the file remains bounded near 2 MB.