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

156 lines
13 KiB
Markdown

---
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.