From 20818956b37c4b5303b265fc3d34268c8227fe64 Mon Sep 17 00:00:00 2001 From: rob Date: Mon, 26 Jan 2026 00:38:35 -0400 Subject: [PATCH] Fix todo deletion not refreshing milestone widgets and file watcher race condition - Add _load_milestones() call after _load_todos() in _on_todo_deleted and _on_todo_edited to refresh milestone widgets showing linked todos - Replace boolean _ignoring_file_change flag with timestamp-based ignore window (0.5s) to handle multiple file system events from a single save - Add _is_within_save_window() helper method for cleaner event filtering Co-Authored-By: Claude Opus 4.5 --- .../views/dashboard/data_store.py | 23 +++++++++++------- .../views/dashboard/project_dashboard.py | 24 ++++++++++++++----- 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/src/development_hub/views/dashboard/data_store.py b/src/development_hub/views/dashboard/data_store.py index 756fc4b..ef8d493 100644 --- a/src/development_hub/views/dashboard/data_store.py +++ b/src/development_hub/views/dashboard/data_store.py @@ -1,5 +1,6 @@ """Data storage and file management for the dashboard.""" +import time from datetime import date from pathlib import Path from typing import Any @@ -78,7 +79,9 @@ class DashboardDataStore(QObject): self._ideas_list: list[Goal] | None = None # File watching - self._ignoring_file_change = False + # Use timestamp to ignore file events shortly after saving (handles multiple events) + self._last_save_time: float = 0 + self._save_ignore_window = 0.5 # seconds to ignore file events after save self._file_watcher = QFileSystemWatcher(self) self._file_watcher.fileChanged.connect(self._on_file_changed) self._file_watcher.directoryChanged.connect(self._on_directory_changed) @@ -262,14 +265,14 @@ class DashboardDataStore(QObject): def save_todos(self) -> None: """Save todos with file watcher temporarily disabled.""" if self._todos_parser and self._todo_list: - self._ignoring_file_change = True + self._last_save_time = time.time() self._todos_parser.save(self._todo_list) self._ensure_file_watched() def save_goals(self) -> None: """Save goals with file watcher temporarily disabled.""" if self._goals_parser and self._goal_list: - self._ignoring_file_change = True + self._last_save_time = time.time() saver = GoalsSaver(self._goals_path, self._goals_parser.frontmatter) saver.save(self._goal_list) self._ensure_file_watched() @@ -277,7 +280,7 @@ class DashboardDataStore(QObject): def save_milestones(self) -> None: """Save milestones with file watcher temporarily disabled.""" if self._milestones_parser and self._milestones_list: - self._ignoring_file_change = True + self._last_save_time = time.time() self._milestones_parser.save(self._milestones_list) self._ensure_file_watched() @@ -286,7 +289,7 @@ class DashboardDataStore(QObject): if self._is_global or not self._ideas_path: return - self._ignoring_file_change = True + self._last_save_time = time.time() lines = [] lines.append("---") @@ -864,10 +867,13 @@ class DashboardDataStore(QObject): if file_path.parent.exists() and parent not in watched_dirs: self._file_watcher.addPath(parent) + def _is_within_save_window(self) -> bool: + """Check if we're within the ignore window after a save.""" + return (time.time() - self._last_save_time) < self._save_ignore_window + def _on_file_changed(self, path: str) -> None: """Handle file modification events.""" - if self._ignoring_file_change: - self._ignoring_file_change = False + if self._is_within_save_window(): self._ensure_file_watched() return @@ -885,8 +891,7 @@ class DashboardDataStore(QObject): def _on_directory_changed(self, path: str) -> None: """Handle directory changes.""" - if self._ignoring_file_change: - self._ignoring_file_change = False + if self._is_within_save_window(): self._ensure_file_watched() return diff --git a/src/development_hub/views/dashboard/project_dashboard.py b/src/development_hub/views/dashboard/project_dashboard.py index 918b0b4..88da88e 100644 --- a/src/development_hub/views/dashboard/project_dashboard.py +++ b/src/development_hub/views/dashboard/project_dashboard.py @@ -1,6 +1,8 @@ """Project dashboard view.""" import shutil +import subprocess +from datetime import datetime from pathlib import Path from PySide6.QtCore import Qt, Signal, QTimer, QThread @@ -32,6 +34,7 @@ from development_hub.views.dashboard.undo_manager import UndoAction, UndoManager from development_hub.services.git_service import GitService from development_hub.models.health import HealthStatus +from development_hub.parsers.base import atomic_write from development_hub.parsers.todos_parser import TodosParser from development_hub.models.todo import Todo, TodoList from development_hub.parsers.goals_parser import MilestonesParser, GoalsParser @@ -2079,6 +2082,7 @@ class ProjectDashboard(QWidget): # Refresh UI self._load_todos() + self._load_milestones() # Show toast self.toast.show_message( @@ -2105,6 +2109,7 @@ class ProjectDashboard(QWidget): # Refresh UI self._load_todos() + self._load_milestones() # Show toast self.toast.show_message( @@ -2672,13 +2677,15 @@ class ProjectDashboard(QWidget): if confirm != QMessageBox.StandardButton.Ok: return - # Determine project key for the audit tool + # Determine paths for the audit tool if self.is_global: - project_key = "global" - project_root = None + project_name = "Global Goals" + milestones_path = self._docs_root / "goals" / "milestones.md" + project_dir = None else: - project_key = self.project.key - project_root = Path(self.project.path) if self.project.path else None + project_name = self.project.key + milestones_path = self._docs_root / "projects" / self.project.key / "milestones.md" + project_dir = Path(self.project.path) if self.project.path else None # Create progress dialog self._audit_dialog = QDialog(self) @@ -2720,7 +2727,12 @@ class ProjectDashboard(QWidget): # Create worker and thread self._audit_thread = QThread() - self._audit_worker = AuditWorker(project_key, project_root) + self._audit_worker = AuditWorker( + project_name=project_name, + goals_path=goals_path, + milestones_path=milestones_path if milestones_path.exists() else None, + project_dir=project_dir, + ) self._audit_worker.moveToThread(self._audit_thread) # Connect signals