fix: UI visibility issues, export errors, filtering, and audit log enhancement
Some checks failed
Auto Tag / auto-tag (push) Successful in 5s
Release / build-linux-arm64 (push) Failing after 2m19s
Test / rust-fmt-check (push) Failing after 2m18s
Release / build-macos-arm64 (push) Successful in 7m41s
Test / rust-clippy (push) Successful in 12m4s
Test / rust-tests (push) Successful in 12m40s
Test / frontend-typecheck (push) Successful in 1m43s
Test / frontend-tests (push) Successful in 1m21s
Release / build-linux-amd64 (push) Successful in 20m49s
Release / build-windows-amd64 (push) Successful in 13m59s

- 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 <noreply@anthropic.com>
This commit is contained in:
Shaun Arman 2026-04-03 08:37:47 -05:00
parent b356eef44f
commit 0235541c9b
12 changed files with 340 additions and 23 deletions

1
.gitignore vendored
View File

@ -8,3 +8,4 @@ secrets.yml
secrets.yaml
artifacts/
*.png
/screenshots/

155
TICKET_SUMMARY.md Normal file
View File

@ -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)

88
src-tauri/Cargo.lock generated
View File

@ -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"

View File

@ -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"

View File

@ -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) \

View File

@ -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!(

View File

@ -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<String, String> {
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}")),
};

View File

@ -58,11 +58,11 @@ export function DocEditor({ content, onChange, version, updatedAt, onExport }: D
MD
</Button>
<Button size="sm" variant="outline" onClick={() => onExport("pdf")}>
<Download className="w-3 h-3 mr-1" />
<Download className="w-3 h-3 mr-1 text-foreground" />
PDF
</Button>
<Button size="sm" variant="outline" onClick={() => onExport("docx")}>
<Download className="w-3 h-3 mr-1" />
<Download className="w-3 h-3 mr-1 text-foreground" />
DOCX
</Button>
</div>

View File

@ -121,7 +121,8 @@ export default function History() {
</SelectContent>
</Select>
</div>
<Button variant="outline" onClick={handleSearch}>
<Button onClick={handleSearch}>
<Search className="w-4 h-4 mr-2" />
Search
</Button>
</div>

View File

@ -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)}`);
}
};

View File

@ -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)}`);
}
};

View File

@ -191,11 +191,16 @@ export default function Security() {
{isExpanded && (
<tr className="border-b bg-accent/20">
<td colSpan={5} className="px-3 py-3">
<div className="text-xs">
<p className="font-medium text-foreground mb-1">Transmitted Data:</p>
<pre className="bg-background/50 p-2 rounded text-xs overflow-x-auto text-foreground/80">
{entry.details}
<div className="text-xs space-y-2">
<p className="font-medium text-foreground">Transmitted Data:</p>
<pre className="bg-background/50 p-3 rounded text-xs overflow-x-auto text-foreground/80 whitespace-pre-wrap">
{JSON.stringify(JSON.parse(entry.details), null, 2)}
</pre>
<div className="flex items-center gap-2 text-muted-foreground pt-1">
<span>Entry ID: {entry.id}</span>
<span></span>
<span>Type: {entry.entity_type}</span>
</div>
</div>
</td>
</tr>