Compare commits

..

3 Commits

Author SHA1 Message Date
ddidderr 8a8437036d fix: justfile default must be "run" not "setup" 2026-06-07 19:19:03 +02:00
ddidderr 639a06e224 deps: deno update --latest 2026-06-07 19:18:37 +02:00
ddidderr 27a054fcb7 cleanup: remove unnecessary THEPLAN.md 2026-06-07 19:18:16 +02:00
4 changed files with 9 additions and 162 deletions
-155
View File
@@ -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<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.
+6 -6
View File
@@ -6,8 +6,8 @@
"npm:@tauri-apps/plugin-dialog@^2.7.1": "2.7.1", "npm:@tauri-apps/plugin-dialog@^2.7.1": "2.7.1",
"npm:@tauri-apps/plugin-shell@^2.3.5": "2.3.5", "npm:@tauri-apps/plugin-shell@^2.3.5": "2.3.5",
"npm:@tauri-apps/plugin-store@^2.4.3": "2.4.3", "npm:@tauri-apps/plugin-store@^2.4.3": "2.4.3",
"npm:@types/react-dom@^19.2.3": "19.2.3_@types+react@19.2.16", "npm:@types/react-dom@^19.2.3": "19.2.3_@types+react@19.2.17",
"npm:@types/react@^19.2.16": "19.2.16", "npm:@types/react@^19.2.17": "19.2.17",
"npm:@vitejs/plugin-react@^6.0.2": "6.0.2_vite@8.0.16", "npm:@vitejs/plugin-react@^6.0.2": "6.0.2_vite@8.0.16",
"npm:react-dom@^19.2.7": "19.2.7_react@19.2.7", "npm:react-dom@^19.2.7": "19.2.7_react@19.2.7",
"npm:react@^19.2.7": "19.2.7", "npm:react@^19.2.7": "19.2.7",
@@ -226,14 +226,14 @@
"tslib" "tslib"
] ]
}, },
"@types/react-dom@19.2.3_@types+react@19.2.16": { "@types/react-dom@19.2.3_@types+react@19.2.17": {
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"dependencies": [ "dependencies": [
"@types/react" "@types/react"
] ]
}, },
"@types/react@19.2.16": { "@types/react@19.2.17": {
"integrity": "sha512-esJiCAnl0kfpNdE69f3So4WJUXy95dLZydX0KwK46riIHDzHM7O9Vtf9xCHW0PXIqvgqNrswl522kA/5yx+F4w==", "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
"dependencies": [ "dependencies": [
"csstype" "csstype"
] ]
@@ -436,7 +436,7 @@
"npm:@tauri-apps/plugin-shell@^2.3.5", "npm:@tauri-apps/plugin-shell@^2.3.5",
"npm:@tauri-apps/plugin-store@^2.4.3", "npm:@tauri-apps/plugin-store@^2.4.3",
"npm:@types/react-dom@^19.2.3", "npm:@types/react-dom@^19.2.3",
"npm:@types/react@^19.2.16", "npm:@types/react@^19.2.17",
"npm:@vitejs/plugin-react@^6.0.2", "npm:@vitejs/plugin-react@^6.0.2",
"npm:react-dom@^19.2.7", "npm:react-dom@^19.2.7",
"npm:react@^19.2.7", "npm:react@^19.2.7",
+1 -1
View File
@@ -18,7 +18,7 @@
"@tauri-apps/plugin-shell": "^2.3.5" "@tauri-apps/plugin-shell": "^2.3.5"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^19.2.16", "@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.2", "@vitejs/plugin-react": "^6.0.2",
"typescript": "^6.0.3", "typescript": "^6.0.3",
+2
View File
@@ -2,6 +2,8 @@ export RUSTFLAGS := "-C target-cpu=native"
export WEBKIT_DISABLE_COMPOSITING_MODE := "1" export WEBKIT_DISABLE_COMPOSITING_MODE := "1"
export DOCKER_CONFIG := env_var_or_default("DOCKER_CONFIG", ".lanspread-peer-cli/docker-config") export DOCKER_CONFIG := env_var_or_default("DOCKER_CONFIG", ".lanspread-peer-cli/docker-config")
default: run
setup: setup:
cargo install tauri-cli cargo install tauri-cli
cd crates/lanspread-tauri-deno-ts && deno install --frozen=true cd crates/lanspread-tauri-deno-ts && deno install --frozen=true