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

View File

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