--- 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>>`, `state_dir: OnceLock`; `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` 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('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.