From 0235541c9be3890bce7e3ccb3a8e099a7e63638b Mon Sep 17 00:00:00 2001 From: Shaun Arman Date: Fri, 3 Apr 2026 08:37:47 -0500 Subject: [PATCH] fix: UI visibility issues, export errors, filtering, and audit log enhancement - Fix download icons (PDF/DOCX) not visible in dark theme by adding text-foreground class - Fix "Read-only file system" error by using Downloads directory for exports with proper fallback - Fix Search button visibility in History page by changing variant and adding icon - Fix domain-only filtering in History page by adding missing filter.domain handling - Enhance audit log to capture full transmitted data (provider details, messages, content previews) - Add dirs crate dependency for cross-platform directory detection - Add success/error feedback for document exports with file path display - Update Security page to display pretty-printed JSON audit details Co-Authored-By: Claude Sonnet 4.5 --- .gitignore | 1 + TICKET_SUMMARY.md | 155 ++++++++++++++++++++++++++++++++ src-tauri/Cargo.lock | 88 ++++++++++++++++++ src-tauri/Cargo.toml | 1 + src-tauri/src/commands/ai.rs | 16 +++- src-tauri/src/commands/db.rs | 4 + src-tauri/src/commands/docs.rs | 66 +++++++++++--- src/components/DocEditor.tsx | 4 +- src/pages/History/index.tsx | 3 +- src/pages/Postmortem/index.tsx | 6 +- src/pages/RCA/index.tsx | 6 +- src/pages/Settings/Security.tsx | 13 ++- 12 files changed, 340 insertions(+), 23 deletions(-) create mode 100644 TICKET_SUMMARY.md diff --git a/.gitignore b/.gitignore index 8937c0a3..3aec03c3 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ secrets.yml secrets.yaml artifacts/ *.png +/screenshots/ diff --git a/TICKET_SUMMARY.md b/TICKET_SUMMARY.md new file mode 100644 index 00000000..78a7a424 --- /dev/null +++ b/TICKET_SUMMARY.md @@ -0,0 +1,155 @@ +# Ticket Summary - UI Fixes and Audit Log Enhancement + +## Description + +This ticket addresses multiple UI and functionality issues reported in the tftsr-devops_investigation application: + +1. **Download Icons Visibility**: Download icons (PDF, DOCX) in RCA and Post-Mortem pages were not visible in dark theme +2. **Export File System Error**: "Read-only file system (os error 30)" error when attempting to export documents +3. **History Search Button**: Search button not visible in the History page +4. **Domain Filtering**: Domain-only filtering not working in History page +5. **Audit Log Enhancement**: Audit log showed only internal IDs, lacking actual transmitted data for security auditing + +## Acceptance Criteria + +- [ ] Download icons are visible in both light and dark themes on RCA and Post-Mortem pages +- [ ] Documents can be exported successfully to Downloads directory without filesystem errors +- [ ] Search button is visible with proper styling in History page +- [ ] Domain filter works independently without requiring a search query +- [ ] Audit log displays full transmitted data including: + - AI chat messages with provider details, user message, and response preview + - Document generation with content preview and metadata + - All entries show properly formatted JSON with details + +## Work Implemented + +### 1. Download Icons Visibility Fix +**Files Modified:** +- `src/components/DocEditor.tsx:60-67` + +**Changes:** +- Added `text-foreground` class to Download icons for PDF and DOCX buttons +- Ensures icons inherit the current theme's foreground color for visibility + +### 2. Export File System Error Fix +**Files Modified:** +- `src-tauri/Cargo.toml:38` - Added `dirs = "5"` dependency +- `src-tauri/src/commands/docs.rs:127-170` - Rewrote `export_document` function +- `src/pages/RCA/index.tsx:53-60` - Updated error handling and user feedback +- `src/pages/Postmortem/index.tsx:52-59` - Updated error handling and user feedback + +**Changes:** +- Modified `export_document` to use Downloads directory by default instead of "." +- Falls back to `app_data_dir/exports` if Downloads directory unavailable +- Added proper directory creation with error handling +- Updated frontend to show success message with file path +- Empty `output_dir` parameter now triggers default behavior + +### 3. Search Button Visibility Fix +**Files Modified:** +- `src/pages/History/index.tsx:124-127` + +**Changes:** +- Changed button from `variant="outline"` to default variant +- Added Search icon to button for better visibility +- Button now has proper contrast in both themes + +### 4. Domain-Only Filtering Fix +**Files Modified:** +- `src-tauri/src/commands/db.rs:305-312` + +**Changes:** +- Added missing `filter.domain` handling in `list_issues` function +- Domain filter now properly filters by `i.category` field +- Filter works independently of search query + +### 5. Audit Log Enhancement +**Files Modified:** +- `src-tauri/src/commands/ai.rs:242-266` - Enhanced AI chat audit logging +- `src-tauri/src/commands/docs.rs:44-73` - Enhanced RCA generation audit logging +- `src-tauri/src/commands/docs.rs:90-119` - Enhanced postmortem generation audit logging +- `src/pages/Settings/Security.tsx:191-206` - Enhanced audit log display + +**Changes:** +- AI chat audit now captures: + - Provider name, model, and API URL + - Full user message + - Response preview (first 200 chars) + - Token count +- Document generation audit now captures: + - Issue ID and title + - Document type and title + - Content length and preview (first 300 chars) +- Security page now displays: + - Pretty-printed JSON with proper formatting + - Entry ID and entity type below the data + - Better layout with whitespace handling + +## Testing Needed + +### Manual Testing + +1. **Download Icons Visibility** + - [ ] Open RCA page in light theme + - [ ] Verify PDF and DOCX download icons are visible + - [ ] Switch to dark theme + - [ ] Verify PDF and DOCX download icons are still visible + +2. **Export Functionality** + - [ ] Generate an RCA document + - [ ] Click "PDF" export button + - [ ] Verify file is created in Downloads directory + - [ ] Verify success message displays with file path + - [ ] Check file opens correctly + - [ ] Repeat for "MD" and "DOCX" formats + - [ ] Test on Post-Mortem page as well + +3. **History Search Button** + - [ ] Navigate to History page + - [ ] Verify Search button is visible + - [ ] Verify button has search icon + - [ ] Test button in both light and dark themes + +4. **Domain Filtering** + - [ ] Navigate to History page + - [ ] Select a domain from dropdown (e.g., "Linux") + - [ ] Do NOT enter any search text + - [ ] Verify issues are filtered by selected domain + - [ ] Change domain selection + - [ ] Verify filtering updates correctly + +5. **Audit Log** + - [ ] Perform an AI chat interaction + - [ ] Navigate to Settings > Security > Audit Log + - [ ] Click "View" on a recent entry + - [ ] Verify transmitted data shows: + - Provider details + - User message + - Response preview + - [ ] Generate an RCA or Post-Mortem + - [ ] Check audit log for document generation entry + - [ ] Verify content preview and metadata are visible + +### Automated Testing + +```bash +# Type checking +npx tsc --noEmit + +# Rust compilation +cargo check --manifest-path src-tauri/Cargo.toml + +# Rust linting +cargo clippy --manifest-path src-tauri/Cargo.toml -- -D warnings + +# Frontend tests (if applicable) +npm run test:run +``` + +### Edge Cases to Test + +- Export when Downloads directory doesn't exist +- Export with very long document titles (special character handling) +- Domain filter with empty result set +- Audit log with very large payloads (>1000 chars) +- Audit log JSON parsing errors (malformed data) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 829a53f0..fd3f260c 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -918,6 +918,15 @@ dependencies = [ "dirs-sys 0.3.7", ] +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys 0.4.1", +] + [[package]] name = "dirs" version = "6.0.0" @@ -948,6 +957,18 @@ dependencies = [ "winapi", ] +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.4.6", + "windows-sys 0.48.0", +] + [[package]] name = "dirs-sys" version = "0.5.0" @@ -5293,6 +5314,7 @@ dependencies = [ "async-trait", "base64 0.22.1", "chrono", + "dirs 5.0.1", "futures", "hex", "printpdf", @@ -6492,6 +6514,15 @@ dependencies = [ "windows-targets 0.42.2", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -6543,6 +6574,21 @@ dependencies = [ "windows_x86_64_msvc 0.42.2", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -6600,6 +6646,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -6624,6 +6676,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -6648,6 +6706,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -6684,6 +6748,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -6708,6 +6778,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -6726,6 +6802,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -6750,6 +6832,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index ebf1a404..b7fccaaf 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -36,6 +36,7 @@ chrono = { version = "0.4", features = ["serde"] } futures = "0.3" async-trait = "0.1" base64 = "0.22" +dirs = "5" [dev-dependencies] tokio-test = "0.4" diff --git a/src-tauri/src/commands/ai.rs b/src-tauri/src/commands/ai.rs index 0cec7836..d9812462 100644 --- a/src-tauri/src/commands/ai.rs +++ b/src-tauri/src/commands/ai.rs @@ -239,12 +239,24 @@ pub async fn chat_message( ) .map_err(|e| e.to_string())?; - // Audit + // Audit - capture full transmission details + let audit_details = serde_json::json!({ + "provider": provider_config.name, + "model": provider_config.model, + "api_url": provider_config.api_url, + "user_message": user_msg.content, + "response_preview": if response.content.len() > 200 { + format!("{}...", &response.content[..200]) + } else { + response.content.clone() + }, + "token_count": user_msg.token_count, + }); let entry = AuditEntry::new( "ai_chat".to_string(), "issue".to_string(), issue_id, - serde_json::json!({ "provider": provider_config.name }).to_string(), + audit_details.to_string(), ); let _ = db.execute( "INSERT INTO audit_log (id, timestamp, action, entity_type, entity_id, user_id, details) \ diff --git a/src-tauri/src/commands/db.rs b/src-tauri/src/commands/db.rs index 2d3f1342..fbe5a77a 100644 --- a/src-tauri/src/commands/db.rs +++ b/src-tauri/src/commands/db.rs @@ -306,6 +306,10 @@ pub async fn list_issues( sql.push_str(&format!(" AND i.category = ?{}", params.len() + 1)); params.push(Box::new(category.clone())); } + if let Some(ref domain) = filter.domain { + sql.push_str(&format!(" AND i.category = ?{}", params.len() + 1)); + params.push(Box::new(domain.clone())); + } if let Some(ref search) = filter.search { let pattern = format!("%{search}%"); sql.push_str(&format!( diff --git a/src-tauri/src/commands/docs.rs b/src-tauri/src/commands/docs.rs index 80086779..df818d0b 100644 --- a/src-tauri/src/commands/docs.rs +++ b/src-tauri/src/commands/docs.rs @@ -40,13 +40,25 @@ pub async fn generate_rca( updated_at: now, }; - // Audit + // Audit - capture document metadata let db = state.db.lock().map_err(|e| e.to_string())?; + let audit_details = serde_json::json!({ + "issue_id": issue_id, + "issue_title": issue_detail.issue.title, + "doc_type": "rca", + "doc_title": document.title, + "content_length": content_md.len(), + "content_preview": if content_md.len() > 300 { + format!("{}...", &content_md[..300]) + } else { + content_md.clone() + }, + }); let entry = AuditEntry::new( "generate_rca".to_string(), "document".to_string(), doc_id, - serde_json::json!({ "issue_id": issue_id }).to_string(), + audit_details.to_string(), ); let _ = db.execute( "INSERT INTO audit_log (id, timestamp, action, entity_type, entity_id, user_id, details) \ @@ -87,13 +99,25 @@ pub async fn generate_postmortem( updated_at: now, }; - // Audit + // Audit - capture document metadata let db = state.db.lock().map_err(|e| e.to_string())?; + let audit_details = serde_json::json!({ + "issue_id": issue_id, + "issue_title": issue_detail.issue.title, + "doc_type": "postmortem", + "doc_title": document.title, + "content_length": content_md.len(), + "content_preview": if content_md.len() > 300 { + format!("{}...", &content_md[..300]) + } else { + content_md.clone() + }, + }); let entry = AuditEntry::new( "generate_postmortem".to_string(), "document".to_string(), doc_id, - serde_json::json!({ "issue_id": issue_id }).to_string(), + audit_details.to_string(), ); let _ = db.execute( "INSERT INTO audit_log (id, timestamp, action, entity_type, entity_id, user_id, details) \ @@ -129,7 +153,27 @@ pub async fn export_document( content_md: String, format: String, output_dir: String, + state: State<'_, AppState>, ) -> Result { + use std::path::PathBuf; + + // Determine the output directory + let base_dir = if output_dir.is_empty() || output_dir == "." { + // Try to use the Downloads directory, fall back to app data dir + dirs::download_dir() + .unwrap_or_else(|| { + let app_data = state.app_data_dir.clone(); + app_data.join("exports") + }) + } else { + PathBuf::from(&output_dir) + }; + + // Ensure the directory exists + std::fs::create_dir_all(&base_dir).map_err(|e| { + format!("Failed to create export directory {}: {}", base_dir.display(), e) + })?; + let safe_title: String = title .chars() .map(|c| { @@ -143,14 +187,16 @@ pub async fn export_document( let output_path = match format.as_str() { "markdown" | "md" => { - let path = format!("{output_dir}/{safe_title}.md"); - exporter::export_markdown(&content_md, &path).map_err(|e| e.to_string())?; - path + let path = base_dir.join(format!("{safe_title}.md")); + exporter::export_markdown(&content_md, path.to_str().unwrap()) + .map_err(|e| e.to_string())?; + path.to_string_lossy().to_string() } "pdf" => { - let path = format!("{output_dir}/{safe_title}.pdf"); - exporter::export_pdf(&content_md, &title, &path).map_err(|e| e.to_string())?; - path + let path = base_dir.join(format!("{safe_title}.pdf")); + exporter::export_pdf(&content_md, &title, path.to_str().unwrap()) + .map_err(|e| e.to_string())?; + path.to_string_lossy().to_string() } _ => return Err(format!("Unsupported export format: {format}")), }; diff --git a/src/components/DocEditor.tsx b/src/components/DocEditor.tsx index 48327ed5..b53840cb 100644 --- a/src/components/DocEditor.tsx +++ b/src/components/DocEditor.tsx @@ -58,11 +58,11 @@ export function DocEditor({ content, onChange, version, updatedAt, onExport }: D MD diff --git a/src/pages/History/index.tsx b/src/pages/History/index.tsx index ac0caaa2..9f1e8114 100644 --- a/src/pages/History/index.tsx +++ b/src/pages/History/index.tsx @@ -121,7 +121,8 @@ export default function History() { - diff --git a/src/pages/Postmortem/index.tsx b/src/pages/Postmortem/index.tsx index ebf83851..e21ae89d 100644 --- a/src/pages/Postmortem/index.tsx +++ b/src/pages/Postmortem/index.tsx @@ -52,9 +52,11 @@ export default function Postmortem() { const handleExport = async (format: "md" | "pdf" | "docx") => { if (!doc) return; try { - await exportDocumentCmd(doc.id, doc.title, content, format, "."); + const path = await exportDocumentCmd(doc.id, doc.title, content, format, ""); + setError(`Document exported to: ${path}`); + setTimeout(() => setError(null), 5000); } catch (err) { - setError(String(err)); + setError(`Export failed: ${String(err)}`); } }; diff --git a/src/pages/RCA/index.tsx b/src/pages/RCA/index.tsx index 8ac61b82..4cd48038 100644 --- a/src/pages/RCA/index.tsx +++ b/src/pages/RCA/index.tsx @@ -53,9 +53,11 @@ export default function RCA() { const handleExport = async (format: "md" | "pdf" | "docx") => { if (!doc) return; try { - await exportDocumentCmd(doc.id, doc.title, content, format, "."); + const path = await exportDocumentCmd(doc.id, doc.title, content, format, ""); + setError(`Document exported to: ${path}`); + setTimeout(() => setError(null), 5000); } catch (err) { - setError(String(err)); + setError(`Export failed: ${String(err)}`); } }; diff --git a/src/pages/Settings/Security.tsx b/src/pages/Settings/Security.tsx index 19499652..0c3da800 100644 --- a/src/pages/Settings/Security.tsx +++ b/src/pages/Settings/Security.tsx @@ -191,11 +191,16 @@ export default function Security() { {isExpanded && ( -
-

Transmitted Data:

-
-                                  {entry.details}
+                              
+

Transmitted Data:

+
+                                  {JSON.stringify(JSON.parse(entry.details), null, 2)}
                                 
+
+ Entry ID: {entry.id} + + Type: {entry.entity_type} +