# Audit Fixes Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Fix all 29 findings from the full codebase correctness audit, organized by severity tier. **Architecture:** Fix by severity tier (Critical -> High -> Medium -> Low) with a `cargo build` gate after each tier. Each task is a single focused fix. Fixes #10 and #11 (launcher async + stderr) are combined because they touch the same function. **Tech Stack:** Rust, gtk4-rs, libadwaita-rs, rusqlite, serde_json, std::io --- ## Tier 1: Critical Fixes ### Task 1: Fix unsquashfs argument order in security.rs **Files:** - Modify: `src/core/security.rs:253-261` **Step 1: Fix the argument order** The `unsquashfs` command expects: `unsquashfs [options] [patterns]`. Currently the AppImage path comes after the pattern. Fix: ```rust let extract_output = Command::new("unsquashfs") .args(["-o", &offset, "-f", "-d"]) .arg(temp_dir.path()) .arg("-no-progress") .arg(appimage_path) .arg(lib_file_path.trim_start_matches("squashfs-root/")) .output() .ok()?; ``` Remove the `-e` flag (unsquashfs takes extract patterns as positional args after the archive path, not with `-e`). **Step 2: Build and verify** Run: `cargo build 2>&1 | grep "warning\|error"` Expected: Clean build --- ### Task 2: Quote Exec path in desktop entries **Files:** - Modify: `src/core/integrator.rs:97` **Step 1: Add quotes around the exec path** Change line 97 from: ``` Exec={exec} %U\n\ ``` to: ``` Exec=\"{exec}\" %U\n\ ``` This ensures paths with spaces (e.g., `/home/user/My Apps/Firefox.AppImage`) work correctly per the Desktop Entry Specification. **Step 2: Build and verify** Run: `cargo build 2>&1 | grep "warning\|error"` Expected: Clean build --- ### Task 3: Fix compare_versions total order violation **Files:** - Modify: `src/core/duplicates.rs:332-341` **Step 1: Use clean_version for equality check** Replace the function body: ```rust fn compare_versions(a: &str, b: &str) -> std::cmp::Ordering { use super::updater::version_is_newer; use super::updater::clean_version; let ca = clean_version(a); let cb = clean_version(b); if ca == cb { std::cmp::Ordering::Equal } else if version_is_newer(a, b) { std::cmp::Ordering::Greater } else { std::cmp::Ordering::Less } } ``` Note: `clean_version` is currently `fn` (private). It must be made `pub(crate)` in `src/core/updater.rs:704`. **Step 2: Make clean_version pub(crate)** In `src/core/updater.rs:704`, change `fn clean_version` to `pub(crate) fn clean_version`. **Step 3: Build and verify** Run: `cargo build 2>&1 | grep "warning\|error"` Expected: Clean build --- ### Task 4: Chunk-based signature detection in inspector.rs **Files:** - Modify: `src/core/inspector.rs:530-537` **Step 1: Replace fs::read with chunked BufReader** Replace the `detect_signature` function: ```rust fn detect_signature(path: &Path) -> bool { use std::io::{BufReader, Read}; let file = match fs::File::open(path) { Ok(f) => f, Err(_) => return false, }; let needle = b".sha256_sig"; let mut reader = BufReader::new(file); let mut buf = vec![0u8; 64 * 1024]; let mut carry = Vec::new(); loop { let n = match reader.read(&mut buf) { Ok(0) => break, Ok(n) => n, Err(_) => break, }; // Prepend carry bytes from previous chunk to handle needle spanning chunks let search_buf = if carry.is_empty() { &buf[..n] } else { carry.extend_from_slice(&buf[..n]); carry.as_slice() }; if search_buf.windows(needle.len()).any(|w| w == needle) { return true; } // Keep the last (needle.len - 1) bytes as carry for the next iteration let keep = needle.len() - 1; carry.clear(); if n >= keep { carry.extend_from_slice(&buf[n - keep..n]); } else { carry.extend_from_slice(&buf[..n]); } } false } ``` **Step 2: Build and verify** Run: `cargo build 2>&1 | grep "warning\|error"` Expected: Clean build --- ### Task 5: Read only first 12 bytes in verify_appimage **Files:** - Modify: `src/core/updater.rs:961-980` **Step 1: Replace fs::read with targeted read** ```rust fn verify_appimage(path: &Path) -> bool { use std::io::Read; let mut file = match fs::File::open(path) { Ok(f) => f, Err(_) => return false, }; let mut header = [0u8; 12]; if file.read_exact(&mut header).is_err() { return false; } // Check ELF magic if &header[0..4] != b"\x7FELF" { return false; } // Check AppImage Type 2 magic at offset 8: AI\x02 if header[8] == 0x41 && header[9] == 0x49 && header[10] == 0x02 { return true; } // Check AppImage Type 1 magic at offset 8: AI\x01 header[8] == 0x41 && header[9] == 0x49 && header[10] == 0x01 } ``` **Step 2: Build and verify** Run: `cargo build 2>&1 | grep "warning\|error"` Expected: Clean build --- ### Task 6: Tier 1 build gate Run: `cargo build 2>&1 | grep "warning\|error"` Expected: Zero output (clean build with no warnings or errors) --- ## Tier 2: High Fixes ### Task 7: Handle NULL severity in CVE summaries **Files:** - Modify: `src/core/database.rs:1348-1349` and `1369-1370` **Step 1: Change get_cve_summary to handle Option** At line 1348-1349, change: ```rust Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?)) ``` to: ```rust let severity: String = row.get::<_, Option>(0)? .unwrap_or_else(|| "MEDIUM".to_string()); Ok((severity, row.get::<_, i64>(1)?)) ``` **Step 2: Same fix for get_all_cve_summary** At line 1369-1370, apply the same change: ```rust let severity: String = row.get::<_, Option>(0)? .unwrap_or_else(|| "MEDIUM".to_string()); Ok((severity, row.get::<_, i64>(1)?)) ``` **Step 3: Build and verify** Run: `cargo build 2>&1 | grep "warning\|error"` Expected: Clean build --- ### Task 8: Fix potential deadlock in extract_metadata_files **Files:** - Modify: `src/core/inspector.rs:248` **Step 1: Change piped stderr to null** At line 248, change: ```rust .stderr(std::process::Stdio::piped()) ``` to: ```rust .stderr(std::process::Stdio::null()) ``` We don't use the stderr output (we only check the exit status), so piping it creates a deadlock risk for no benefit. **Step 2: Build and verify** Run: `cargo build 2>&1 | grep "warning\|error"` Expected: Clean build --- ### Task 9: Fix glob_match edge case **Files:** - Modify: `src/core/updater.rs:680-698` **Step 1: Reduce search text after matching the last part** Replace the function body from the `// Last part must match` section (line 680) through the return (line 700): ```rust // Last part must match at the end (unless pattern ends with *) let last = parts[parts.len() - 1]; let end_limit = if !last.is_empty() { if !text.ends_with(last) { return false; } text.len() - last.len() } else { text.len() }; // Middle parts must appear in order within the allowed range for part in &parts[1..parts.len() - 1] { if part.is_empty() { continue; } if pos >= end_limit { return false; } if let Some(found) = text[pos..end_limit].find(part) { pos += found + part.len(); } else { return false; } } true } ``` **Step 2: Build and verify** Run: `cargo build 2>&1 | grep "warning\|error"` Expected: Clean build --- ### Task 10: Fix backup archive filename collisions **Files:** - Modify: `src/core/backup.rs:122-132` and `195-198` **Step 1: Use home-relative paths in archive creation** Replace the tar archive entry loop (lines 122-132): ```rust let home_dir = dirs::home_dir().unwrap_or_else(|| PathBuf::from("/")); for entry in &entries { let source = Path::new(&entry.original_path); if source.exists() { if let Ok(rel) = source.strip_prefix(&home_dir) { tar_args.push("-C".to_string()); tar_args.push(home_dir.to_string_lossy().to_string()); tar_args.push(rel.to_string_lossy().to_string()); } else { // Path outside home dir - use parent/filename as fallback tar_args.push("-C".to_string()); tar_args.push( source.parent().unwrap_or(Path::new("/")).to_string_lossy().to_string(), ); tar_args.push( source.file_name().unwrap_or_default().to_string_lossy().to_string(), ); } } } ``` Note: Need `use std::path::PathBuf;` if not already imported. **Step 2: Fix restore to match the new archive layout** Replace the restore path lookup (lines 195-198): ```rust for entry in &manifest.paths { let source = Path::new(&entry.original_path); let extracted = if let Ok(rel) = source.strip_prefix(&home_dir) { temp_dir.path().join(rel) } else { let source_name = source.file_name().unwrap_or_default(); temp_dir.path().join(source_name) }; let target = Path::new(&entry.original_path); ``` Add `let home_dir = dirs::home_dir().unwrap_or_else(|| PathBuf::from("/"));` before the restore loop. **Step 3: Build and verify** Run: `cargo build 2>&1 | grep "warning\|error"` Expected: Clean build --- ### Task 11: Async crash detection + drop stderr pipe **Files:** - Modify: `src/core/launcher.rs:147-224` **Step 1: Remove blocking sleep, return immediately, drop stderr on success** Replace the entire `execute_appimage` function (lines 147-224): ```rust /// Execute the AppImage process with the given method. fn execute_appimage( appimage_path: &Path, method: &LaunchMethod, args: &[String], extra_env: &[(&str, &str)], ) -> LaunchResult { let mut cmd = match method { LaunchMethod::Direct => { let mut c = Command::new(appimage_path); c.args(args); c } LaunchMethod::ExtractAndRun => { let mut c = Command::new(appimage_path); c.env("APPIMAGE_EXTRACT_AND_RUN", "1"); c.args(args); c } LaunchMethod::Sandboxed => { let mut c = Command::new("firejail"); c.arg("--appimage"); c.arg(appimage_path); c.args(args); c } }; // Apply extra environment variables for (key, value) in extra_env { cmd.env(key, value); } // Detach stdin, let stderr go to /dev/null to prevent pipe buffer deadlock cmd.stdin(Stdio::null()); cmd.stderr(Stdio::null()); match cmd.spawn() { Ok(child) => { LaunchResult::Started { child, method: method.clone(), } } Err(e) => LaunchResult::Failed(e.to_string()), } } ``` The caller side (detail_view.rs and window.rs) already has async crash detection via `glib::timeout_future` + `child.try_wait()`. The 1.5s sleep was redundant with that pattern. Since we now use `Stdio::null()` for stderr, we need to update the `Crashed` variant handling in callers to not expect stderr content from the launcher - callers can capture stderr separately if needed. Actually, let me check if callers rely on the `Crashed` variant. Looking at the current flow: `execute_appimage` returns `Started` or `Failed`. The `Crashed` variant was produced by the sleep+try_wait logic we're removing. The callers in detail_view.rs (line 131) and window.rs pattern-match on `Crashed`. We need to remove those match arms since `execute_appimage` will never produce `Crashed` anymore. **Step 2: Update detail_view.rs caller** At line 131 in detail_view.rs, the match arm `Ok(launcher::LaunchResult::Crashed { exit_code, stderr, method })` should be removed. The caller already detects crashes async via the wayland analysis timeout (lines 119-129). However, crash detection UX needs to be preserved. Let's keep the `Crashed` variant in the enum but not produce it from `execute_appimage`. Instead, add a comment that callers should poll `child.try_wait()` after a delay to detect crashes. This is already the pattern used in window.rs. Actually, looking more carefully at the callers: the detail_view.rs code at line 112-146 handles `Started`, `Crashed`, and `Failed`. If we remove `Crashed` production from `execute_appimage`, immediate crashes (process exits within milliseconds of spawning) would show as `Started` and the user would see "Launched" with no feedback that it crashed. Better approach: keep crash detection but make it non-blocking. Change `execute_appimage` to not sleep. Instead, do a single non-blocking `try_wait()` with no sleep. This catches processes that fail immediately (e.g., missing executable, permission denied) without blocking for 1.5 seconds: ```rust match cmd.spawn() { Ok(mut child) => { // Non-blocking check: catch immediate spawn failures // (e.g., missing libs, exec format error) match child.try_wait() { Ok(Some(status)) => { // Already exited - immediate crash LaunchResult::Crashed { exit_code: status.code(), stderr: String::new(), method: method.clone(), } } _ => { // Still running or can't check - assume success LaunchResult::Started { child, method: method.clone(), } } } } Err(e) => LaunchResult::Failed(e.to_string()), } ``` This gives us: no blocking sleep, no stderr pipe deadlock, still catches instant crashes. The stderr field is empty (since we use Stdio::null) but that's fine - the crash dialog still shows the exit code. **Step 3: Build and verify** Run: `cargo build 2>&1 | grep "warning\|error"` Expected: Clean build --- ### Task 12: Tier 2 build gate Run: `cargo build 2>&1 | grep "warning\|error"` Expected: Zero output --- ## Tier 3: Medium Fixes ### Task 13: Gate scan on auto-scan-on-startup setting **Files:** - Modify: `src/window.rs:834-835` **Step 1: Wrap trigger_scan in settings check** Replace lines 834-835: ```rust // Always scan on startup to discover new AppImages and complete pending analyses self.trigger_scan(); ``` with: ```rust // Scan on startup if enabled in preferences if self.settings().boolean("auto-scan-on-startup") { self.trigger_scan(); } ``` **Step 2: Build and verify** Run: `cargo build 2>&1 | grep "warning\|error"` Expected: Clean build --- ### Task 14: Fix window size persistence **Files:** - Modify: `src/window.rs:1252` **Step 1: Use actual dimensions instead of default_size** Change line 1252 from: ```rust let (width, height) = self.default_size(); ``` to: ```rust let (width, height) = (self.width(), self.height()); ``` `self.width()` and `self.height()` return the current allocated size, which is what we want to persist. `default_size()` returns the value set by `set_default_size()`, which doesn't change on manual resize. **Step 2: Build and verify** Run: `cargo build 2>&1 | grep "warning\|error"` Expected: Clean build --- ### Task 15: Fix announce() to work with any container **Files:** - Modify: `src/ui/widgets.rs:347-363` **Step 1: Replace Box-specific logic with generic parent approach** Replace the `announce` function: ```rust pub fn announce(container: &impl gtk::prelude::IsA, text: &str) { let label = gtk::Label::builder() .label(text) .visible(false) .accessible_role(gtk::AccessibleRole::Alert) .build(); label.update_property(&[gtk::accessible::Property::Label(text)]); // Try common container types if let Some(box_widget) = container.dynamic_cast_ref::() { box_widget.append(&label); label.set_visible(true); let label_clone = label.clone(); let box_clone = box_widget.clone(); glib::timeout_add_local_once(std::time::Duration::from_millis(500), move || { box_clone.remove(&label_clone); }); } else if let Some(stack) = container.dynamic_cast_ref::() { // For stacks, add to the visible child if it's a box, else use overlay if let Some(child) = stack.visible_child() { if let Some(child_box) = child.dynamic_cast_ref::() { child_box.append(&label); label.set_visible(true); let label_clone = label.clone(); let box_clone = child_box.clone(); glib::timeout_add_local_once(std::time::Duration::from_millis(500), move || { box_clone.remove(&label_clone); }); } } } else if let Some(overlay) = container.dynamic_cast_ref::() { if let Some(child) = overlay.child() { if let Some(child_box) = child.dynamic_cast_ref::() { child_box.append(&label); label.set_visible(true); let label_clone = label.clone(); let box_clone = child_box.clone(); glib::timeout_add_local_once(std::time::Duration::from_millis(500), move || { box_clone.remove(&label_clone); }); } } } } ``` Note: Needs `use adw::prelude::*;` if not already imported in widgets.rs. **Step 2: Build and verify** Run: `cargo build 2>&1 | grep "warning\|error"` Expected: Clean build --- ### Task 16: Claim gesture in lightbox **Files:** - Modify: `src/ui/detail_view.rs:1948` **Step 1: Claim the gesture sequence** Change line 1948 from: ```rust pic_gesture.connect_released(|_, _, _, _| {}); ``` to: ```rust pic_gesture.connect_released(|gesture, _, _, _| { gesture.set_state(gtk::EventSequenceState::Claimed); }); ``` **Step 2: Build and verify** Run: `cargo build 2>&1 | grep "warning\|error"` Expected: Clean build --- ### Task 17: Use serde_json for CLI JSON output **Files:** - Modify: `src/cli.rs:114-131` **Step 1: Replace hand-crafted JSON with serde_json** Replace lines 114-131: ```rust if format == "json" { let items: Vec = records .iter() .map(|r| { serde_json::json!({ "name": r.app_name.as_deref().unwrap_or(&r.filename), "version": r.app_version.as_deref().unwrap_or(""), "path": r.path, "size": r.size_bytes, "integrated": r.integrated, }) }) .collect(); println!("{}", serde_json::to_string_pretty(&items).unwrap_or_else(|_| "[]".into())); return ExitCode::SUCCESS; } ``` **Step 2: Build and verify** Run: `cargo build 2>&1 | grep "warning\|error"` Expected: Clean build --- ### Task 18: Remove dead @media blocks from CSS **Files:** - Modify: `data/resources/style.css:174-211` **Step 1: Delete both @media blocks** Remove lines 174-211 (the `@media (prefers-color-scheme: dark)` and `@media (prefers-contrast: more)` blocks). GTK4 CSS does not support these media queries - they are silently ignored. The libadwaita named colors (`@warning_bg_color`, etc.) already adapt to dark mode automatically. Keep the comment at line 213-216 about `prefers-reduced-motion` since it explains *why* GTK handles it differently. **Step 2: Build and verify** Run: `cargo build 2>&1 | grep "warning\|error"` Expected: Clean build (CSS is compiled at build time via gresource) --- ### Task 19: Wire detail-tab persistence **Files:** - Modify: `src/ui/detail_view.rs:30-32` (read setting) and add save on tab switch **Step 1: Restore saved tab on detail view open** After the view_stack creation (line 32), after all tabs are added (around line 55), add: ```rust // Restore last selected tab let settings = gio::Settings::new(crate::config::APP_ID); let saved_tab = settings.string("detail-tab"); if view_stack.child_by_name(&saved_tab).is_some() { view_stack.set_visible_child_name(&saved_tab); } ``` **Step 2: Save tab on switch** After the tab restore code, add: ```rust // Save tab selection let settings_tab = settings.clone(); view_stack.connect_visible_child_name_notify(move |stack| { if let Some(name) = stack.visible_child_name() { settings_tab.set_string("detail-tab", &name).ok(); } }); ``` Ensure `use gtk::gio;` and `use crate::config::APP_ID;` are imported at the top of detail_view.rs (check if they already are). **Step 3: Build and verify** Run: `cargo build 2>&1 | grep "warning\|error"` Expected: Clean build --- ### Task 20: Remove invalid categories from metainfo **Files:** - Modify: `data/app.driftwood.Driftwood.metainfo.xml:54-58` **Step 1: Delete the categories block** Remove lines 54-58: ```xml System PackageManager GTK ``` Per AppStream spec, categories belong in the `.desktop` file only (where they already are). **Step 2: Build and verify** Run: `cargo build 2>&1 | grep "warning\|error"` Expected: Clean build --- ### Task 21: Byte-level search in fuse.rs **Files:** - Modify: `src/core/fuse.rs:189-192` **Step 1: Replace lossy UTF-8 conversion with byte-level search** Replace lines 188-192: ```rust let data = &buf[..n]; let haystack = String::from_utf8_lossy(data).to_lowercase(); haystack.contains("type2-runtime") || haystack.contains("libfuse3") ``` with: ```rust let data = &buf[..n]; fn bytes_contains_ci(haystack: &[u8], needle: &[u8]) -> bool { haystack.windows(needle.len()).any(|w| { w.iter().zip(needle).all(|(a, b)| a.to_ascii_lowercase() == *b) }) } bytes_contains_ci(data, b"type2-runtime") || bytes_contains_ci(data, b"libfuse3") ``` **Step 2: Build and verify** Run: `cargo build 2>&1 | grep "warning\|error"` Expected: Clean build --- ### Task 22: Tier 3 build gate Run: `cargo build 2>&1 | grep "warning\|error"` Expected: Zero output --- ## Tier 4: Low Fixes ### Task 23: Tighten Wayland env var detection **Files:** - Modify: `src/core/wayland.rs:389-393` **Step 1: Remove WAYLAND_DISPLAY from fallback** Change lines 389-393 from: ```rust has_wayland_socket = env_vars.iter().any(|(k, v)| { (k == "GDK_BACKEND" && v.contains("wayland")) || (k == "QT_QPA_PLATFORM" && v.contains("wayland")) || (k == "WAYLAND_DISPLAY" && !v.is_empty()) }); ``` to: ```rust has_wayland_socket = env_vars.iter().any(|(k, v)| { (k == "GDK_BACKEND" && v.contains("wayland")) || (k == "QT_QPA_PLATFORM" && v.contains("wayland")) }); ``` `WAYLAND_DISPLAY` is typically inherited from the parent environment and doesn't indicate the app is actually using Wayland. **Step 2: Build and verify** Run: `cargo build 2>&1 | grep "warning\|error"` Expected: Clean build --- ### Task 24: Add ELF magic validation to detect_architecture **Files:** - Modify: `src/core/inspector.rs:427-440` **Step 1: Add magic check and endianness-aware parsing** Replace the function: ```rust fn detect_architecture(path: &Path) -> Option { let mut file = fs::File::open(path).ok()?; let mut header = [0u8; 20]; file.read_exact(&mut header).ok()?; // Validate ELF magic if &header[0..4] != b"\x7FELF" { return None; } // ELF e_machine at offset 18, endianness from byte 5 let machine = if header[5] == 2 { // Big-endian u16::from_be_bytes([header[18], header[19]]) } else { // Little-endian (default) u16::from_le_bytes([header[18], header[19]]) }; match machine { 0x03 => Some("i386".to_string()), 0x3E => Some("x86_64".to_string()), 0xB7 => Some("aarch64".to_string()), 0x28 => Some("armhf".to_string()), _ => Some(format!("unknown(0x{:02X})", machine)), } } ``` **Step 2: Build and verify** Run: `cargo build 2>&1 | grep "warning\|error"` Expected: Clean build --- ### Task 25: Add timeout to extract_update_info_runtime **Files:** - Modify: `src/core/updater.rs:372-388` **Step 1: Add 5-second timeout** Replace the function: ```rust fn extract_update_info_runtime(path: &Path) -> Option { let mut child = std::process::Command::new(path) .arg("--appimage-updateinformation") .env("APPIMAGE_EXTRACT_AND_RUN", "1") .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::null()) .spawn() .ok()?; let timeout = std::time::Duration::from_secs(5); let start = std::time::Instant::now(); loop { match child.try_wait() { Ok(Some(status)) => { if status.success() { let mut output = String::new(); if let Some(mut stdout) = child.stdout.take() { use std::io::Read; stdout.read_to_string(&mut output).ok()?; } let info = output.trim().to_string(); if !info.is_empty() && info.contains('|') { return Some(info); } } return None; } Ok(None) => { if start.elapsed() >= timeout { let _ = child.kill(); let _ = child.wait(); log::warn!("Timed out reading update info from {}", path.display()); return None; } std::thread::sleep(std::time::Duration::from_millis(50)); } Err(_) => return None, } } } ``` **Step 2: Build and verify** Run: `cargo build 2>&1 | grep "warning\|error"` Expected: Clean build --- ### Task 26: Handle quoted args in parse_launch_args **Files:** - Modify: `src/core/launcher.rs:227-232` **Step 1: Implement simple shell-like tokenizer** Replace the function: ```rust pub fn parse_launch_args(args: Option<&str>) -> Vec { let Some(s) = args else { return Vec::new(); }; let s = s.trim(); if s.is_empty() { return Vec::new(); } let mut result = Vec::new(); let mut current = String::new(); let mut in_quotes = false; let mut chars = s.chars().peekable(); while let Some(c) = chars.next() { match c { '"' => in_quotes = !in_quotes, ' ' | '\t' if !in_quotes => { if !current.is_empty() { result.push(std::mem::take(&mut current)); } } _ => current.push(c), } } if !current.is_empty() { result.push(current); } result } ``` **Step 2: Build and verify** Run: `cargo build 2>&1 | grep "warning\|error"` Expected: Clean build --- ### Task 27: Stop watcher timer on window destroy **Files:** - Modify: `src/window.rs:1114-1121` **Step 1: Return Break when window is gone** Replace lines 1114-1121: ```rust glib::timeout_add_local(std::time::Duration::from_secs(1), move || { if changed.swap(false, std::sync::atomic::Ordering::Relaxed) { if let Some(window) = window_weak.upgrade() { window.trigger_scan(); } } glib::ControlFlow::Continue }); ``` with: ```rust glib::timeout_add_local(std::time::Duration::from_secs(1), move || { let Some(window) = window_weak.upgrade() else { return glib::ControlFlow::Break; }; if changed.swap(false, std::sync::atomic::Ordering::Relaxed) { window.trigger_scan(); } glib::ControlFlow::Continue }); ``` **Step 2: Build and verify** Run: `cargo build 2>&1 | grep "warning\|error"` Expected: Clean build --- ### Task 28: Add GSettings choices/range constraints **Files:** - Modify: `data/app.driftwood.Driftwood.gschema.xml` **Step 1: Add choices to enumerated string keys** For `view-mode` key, add choices: ```xml 'grid' ``` For `color-scheme` key: ```xml ``` For `detail-tab` key: ```xml ``` For `update-cleanup` key: ```xml ``` For `security-notification-threshold` key: ```xml ``` For `backup-retention-days`, add range: ```xml 30 ``` **Step 2: Build and verify** Run: `cargo build 2>&1 | grep "warning\|error"` Expected: Clean build --- ### Task 29: Remove unused CSS classes **Files:** - Modify: `data/resources/style.css` **Step 1: Remove unused class definitions** Remove these blocks: - `.badge-row` (lines 120-123) - defined but never added to any widget - `.detail-view-switcher` (lines 154-158) - defined but never added - `.quick-action-pill` (lines 160-164) - defined but never added Keep `.letter-icon` (lines 125-130) since the per-letter variants in widgets.rs inherit these base properties through CSS specificity. **Step 2: Build and verify** Run: `cargo build 2>&1 | grep "warning\|error"` Expected: Clean build --- ### Task 30: Define CSS for status-ok/status-attention or remove from code **Files:** - Modify: `data/resources/style.css` (add CSS rules) **Step 1: Add CSS rules for the card status classes** Add after the `.card` styles: ```css /* App card status indicators */ .status-ok { border: 1px solid alpha(@success_bg_color, 0.4); } .status-attention { border: 1px solid alpha(@warning_bg_color, 0.4); } ``` **Step 2: Build and verify** Run: `cargo build 2>&1 | grep "warning\|error"` Expected: Clean build --- ### Task 31: Tier 4 build gate + final verification Run: `cargo build 2>&1 | grep "warning\|error"` Expected: Zero output Run: `cargo run` and verify app launches. --- ## Verification Checklist After all tasks: 1. `cargo build` - zero errors, zero warnings 2. `cargo run` - app launches and shows library view 3. Navigate to preferences, toggle settings, verify they save 4. Open a detail view, switch tabs, reopen - tab should be restored