Complete implementation of 7 slices addressing E2E testing gaps: Slice 0+1: Wire Actions + ReasonPrompt - FocusView now uses useActions hook instead of direct act() calls - Added pendingAction state pattern for skip/defer/complete actions - ReasonPrompt integration with proper confirm/cancel flow - Tags support in DecisionEntry interface Slice 2: Drag Reorder UI - Installed @dnd-kit (core, sortable, utilities) - QueueView with DndContext, SortableContext, verticalListSortingStrategy - SortableQueueItem wrapper component using useSortable hook - pendingReorder state with ReasonPrompt for reorder reasons - Cmd+Up/Down keyboard shortcuts for accessibility - Fixed: Store item ID in PendingReorder to avoid stale queue reference Slice 3: System Tray Integration - tray.rs with TrayState, setup_tray, toggle_window_visibility - Menu with Show/Quit items - Left-click toggles window visibility - update_tray_badge command updates tooltip with item count - Frontend wiring in AppShell Slice 4: E2E Test Updates - Fixed test selectors for InboxView, Queue badge - Exposed inbox store for test seeding Slice 5: Staleness Visualization - Already implemented in computeStaleness() with tests Slice 6: Quick Wiring - onStartBatch callback wired to QueueView - SyncStatus rendered in nav area - SettingsView renders Settings component Slice 7: State Persistence - settings-store with hydrate/update methods - Tauri backend integration via read_settings/write_settings - AppShell hydrates settings on mount Bug fixes from code review: - close_bead now has error isolation (try/catch) so decision logging and queue advancement continue even if bead close fails - PendingReorder stores item ID to avoid stale queue reference E2E tests for all ACs (tests/e2e/followup-acs.spec.ts): - AC-F1: Drag reorder (4 tests) - AC-F2: ReasonPrompt integration (7 tests) - AC-F5: Staleness visualization (3 tests) - AC-F6: Batch mode (2 tests) - AC-F7: SyncStatus (2 tests) - ReasonPrompt behavior (3 tests) Tests: 388 frontend + 119 Rust + 32 E2E all passing Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
99 lines
3.1 KiB
Rust
99 lines
3.1 KiB
Rust
//! File watcher for lore.db changes
|
|
//!
|
|
//! Watches the lore database file for modifications and emits events
|
|
//! to trigger frontend refresh when lore syncs new data.
|
|
|
|
use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
|
|
use std::path::PathBuf;
|
|
use std::sync::mpsc;
|
|
use std::time::Duration;
|
|
use tauri::AppHandle;
|
|
use tauri_specta::Event as TauriEvent;
|
|
|
|
use crate::events::LoreDataChanged;
|
|
|
|
/// Get the path to lore's database file
|
|
fn lore_db_path() -> Option<PathBuf> {
|
|
dirs::data_local_dir().map(|d| d.join("lore").join("lore.db"))
|
|
}
|
|
|
|
/// Start watching lore.db for changes.
|
|
///
|
|
/// When the file is modified, emits a "lore-data-changed" event to the frontend.
|
|
/// Returns the watcher handle (must be kept alive for watching to continue).
|
|
pub fn start_lore_watcher(app: AppHandle) -> Option<RecommendedWatcher> {
|
|
let db_path = lore_db_path()?;
|
|
|
|
// Check if the file exists
|
|
if !db_path.exists() {
|
|
tracing::warn!("lore.db not found at {:?}, skipping file watcher", db_path);
|
|
return None;
|
|
}
|
|
|
|
let parent_dir = db_path.parent()?;
|
|
|
|
// Create a channel for receiving events
|
|
let (tx, rx) = mpsc::channel();
|
|
|
|
// Create the watcher with a debounce of 1 second
|
|
let mut watcher = RecommendedWatcher::new(
|
|
move |res: Result<Event, notify::Error>| {
|
|
match res {
|
|
Ok(event) => {
|
|
if tx.send(event).is_err() {
|
|
// Receiver dropped -- watcher thread has exited
|
|
tracing::debug!("Watcher event channel closed, receiver dropped");
|
|
}
|
|
}
|
|
Err(e) => {
|
|
tracing::warn!("File watcher error: {}", e);
|
|
}
|
|
}
|
|
},
|
|
Config::default().with_poll_interval(Duration::from_secs(2)),
|
|
)
|
|
.ok()?;
|
|
|
|
// Watch the lore directory (not just the file, for atomic write detection)
|
|
if let Err(e) = watcher.watch(parent_dir, RecursiveMode::NonRecursive) {
|
|
tracing::error!("Failed to start lore.db watcher: {}", e);
|
|
return None;
|
|
}
|
|
|
|
tracing::info!("Watching lore.db for changes at {:?}", db_path);
|
|
|
|
// Spawn a thread to handle events
|
|
std::thread::spawn(move || {
|
|
for event in rx {
|
|
// Only react to modify events on the actual db file
|
|
if matches!(
|
|
event.kind,
|
|
EventKind::Modify(_) | EventKind::Create(_) | EventKind::Remove(_)
|
|
) {
|
|
let affects_db = event.paths.iter().any(|p| p.ends_with("lore.db"));
|
|
if affects_db {
|
|
tracing::debug!("lore.db changed, emitting refresh event");
|
|
if let Err(e) = LoreDataChanged.emit(&app) {
|
|
tracing::warn!("Failed to emit lore-data-changed event: {}", e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
Some(watcher)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_lore_db_path_returns_expected_location() {
|
|
let path = lore_db_path();
|
|
assert!(path.is_some());
|
|
let path = path.unwrap();
|
|
assert!(path.ends_with("lore/lore.db"));
|
|
}
|
|
}
|