Files
lanspread/THEPLAN.md
T
2026-06-07 16:17:31 +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.