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 <noreply@anthropic.com>
This commit is contained in:
rob 2026-01-26 00:38:35 -04:00
parent b60af09922
commit 20818956b3
2 changed files with 32 additions and 15 deletions

View File

@ -1,5 +1,6 @@
"""Data storage and file management for the dashboard.""" """Data storage and file management for the dashboard."""
import time
from datetime import date from datetime import date
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@ -78,7 +79,9 @@ class DashboardDataStore(QObject):
self._ideas_list: list[Goal] | None = None self._ideas_list: list[Goal] | None = None
# File watching # 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 = QFileSystemWatcher(self)
self._file_watcher.fileChanged.connect(self._on_file_changed) self._file_watcher.fileChanged.connect(self._on_file_changed)
self._file_watcher.directoryChanged.connect(self._on_directory_changed) self._file_watcher.directoryChanged.connect(self._on_directory_changed)
@ -262,14 +265,14 @@ class DashboardDataStore(QObject):
def save_todos(self) -> None: def save_todos(self) -> None:
"""Save todos with file watcher temporarily disabled.""" """Save todos with file watcher temporarily disabled."""
if self._todos_parser and self._todo_list: 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._todos_parser.save(self._todo_list)
self._ensure_file_watched() self._ensure_file_watched()
def save_goals(self) -> None: def save_goals(self) -> None:
"""Save goals with file watcher temporarily disabled.""" """Save goals with file watcher temporarily disabled."""
if self._goals_parser and self._goal_list: 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 = GoalsSaver(self._goals_path, self._goals_parser.frontmatter)
saver.save(self._goal_list) saver.save(self._goal_list)
self._ensure_file_watched() self._ensure_file_watched()
@ -277,7 +280,7 @@ class DashboardDataStore(QObject):
def save_milestones(self) -> None: def save_milestones(self) -> None:
"""Save milestones with file watcher temporarily disabled.""" """Save milestones with file watcher temporarily disabled."""
if self._milestones_parser and self._milestones_list: 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._milestones_parser.save(self._milestones_list)
self._ensure_file_watched() self._ensure_file_watched()
@ -286,7 +289,7 @@ class DashboardDataStore(QObject):
if self._is_global or not self._ideas_path: if self._is_global or not self._ideas_path:
return return
self._ignoring_file_change = True self._last_save_time = time.time()
lines = [] lines = []
lines.append("---") lines.append("---")
@ -864,10 +867,13 @@ class DashboardDataStore(QObject):
if file_path.parent.exists() and parent not in watched_dirs: if file_path.parent.exists() and parent not in watched_dirs:
self._file_watcher.addPath(parent) 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: def _on_file_changed(self, path: str) -> None:
"""Handle file modification events.""" """Handle file modification events."""
if self._ignoring_file_change: if self._is_within_save_window():
self._ignoring_file_change = False
self._ensure_file_watched() self._ensure_file_watched()
return return
@ -885,8 +891,7 @@ class DashboardDataStore(QObject):
def _on_directory_changed(self, path: str) -> None: def _on_directory_changed(self, path: str) -> None:
"""Handle directory changes.""" """Handle directory changes."""
if self._ignoring_file_change: if self._is_within_save_window():
self._ignoring_file_change = False
self._ensure_file_watched() self._ensure_file_watched()
return return

View File

@ -1,6 +1,8 @@
"""Project dashboard view.""" """Project dashboard view."""
import shutil import shutil
import subprocess
from datetime import datetime
from pathlib import Path from pathlib import Path
from PySide6.QtCore import Qt, Signal, QTimer, QThread 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.services.git_service import GitService
from development_hub.models.health import HealthStatus 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.parsers.todos_parser import TodosParser
from development_hub.models.todo import Todo, TodoList from development_hub.models.todo import Todo, TodoList
from development_hub.parsers.goals_parser import MilestonesParser, GoalsParser from development_hub.parsers.goals_parser import MilestonesParser, GoalsParser
@ -2079,6 +2082,7 @@ class ProjectDashboard(QWidget):
# Refresh UI # Refresh UI
self._load_todos() self._load_todos()
self._load_milestones()
# Show toast # Show toast
self.toast.show_message( self.toast.show_message(
@ -2105,6 +2109,7 @@ class ProjectDashboard(QWidget):
# Refresh UI # Refresh UI
self._load_todos() self._load_todos()
self._load_milestones()
# Show toast # Show toast
self.toast.show_message( self.toast.show_message(
@ -2672,13 +2677,15 @@ class ProjectDashboard(QWidget):
if confirm != QMessageBox.StandardButton.Ok: if confirm != QMessageBox.StandardButton.Ok:
return return
# Determine project key for the audit tool # Determine paths for the audit tool
if self.is_global: if self.is_global:
project_key = "global" project_name = "Global Goals"
project_root = None milestones_path = self._docs_root / "goals" / "milestones.md"
project_dir = None
else: else:
project_key = self.project.key project_name = self.project.key
project_root = Path(self.project.path) if self.project.path else None 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 # Create progress dialog
self._audit_dialog = QDialog(self) self._audit_dialog = QDialog(self)
@ -2720,7 +2727,12 @@ class ProjectDashboard(QWidget):
# Create worker and thread # Create worker and thread
self._audit_thread = QThread() 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) self._audit_worker.moveToThread(self._audit_thread)
# Connect signals # Connect signals