diff --git a/THEPLAN.md b/THEPLAN.md deleted file mode 100644 index da0fb41..0000000 --- a/THEPLAN.md +++ /dev/null @@ -1,155 +0,0 @@ ---- -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.