From 5d45b8d8eb5dd133cd8a1cdd70e1bf4878c263f8 Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Sun, 19 Apr 2026 15:36:31 +0200 Subject: [PATCH] fix: timestamp collision, undo stack invalidation, label parsing, filter-aware clear - Use microsecond-precision timestamps to prevent version merging on sub-second scans - Clear undo stack when switching scan versions (stale row references) - Parse timestamp labels robustly instead of hard-coded string slicing - "Clear All" in hard negatives dialog respects active model filter - Remove time.sleep from tests (no longer needed with microsecond timestamps) Co-Authored-By: Claude Opus 4.6 --- core/db.py | 2 +- main.py | 27 +++++++++++++++++++++------ tests/test_db.py | 5 +---- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/core/db.py b/core/db.py index 3ba060b..1e2590e 100644 --- a/core/db.py +++ b/core/db.py @@ -504,7 +504,7 @@ class ProcessedDB: """ if not self._enabled: return - ts = datetime.now().strftime("%Y%m%d_%H%M%S") + ts = datetime.now().strftime("%Y%m%d_%H%M%S_%f") with self._lock: self._con.executemany( "INSERT INTO scan_results" diff --git a/main.py b/main.py index b81f1c3..8ae5274 100755 --- a/main.py +++ b/main.py @@ -8,6 +8,7 @@ import random import shutil import subprocess from concurrent.futures import ThreadPoolExecutor, as_completed +from datetime import datetime from pathlib import Path from PyQt6.QtWidgets import ( @@ -407,14 +408,22 @@ class HardNegativesDialog(QDialog): self._load() def _clear_all(self): + all_rows = self._db.get_hard_negatives(self._profile) + model_filter = self._cmb_filter.currentText() + if model_filter != "(all)": + target = [r for r in all_rows if r["source_model"] == model_filter] + msg = f"Delete {len(target)} hard negatives for model '{model_filter}'?" + else: + target = all_rows + msg = f"Delete all {len(target)} hard negatives for profile '{self._profile}'?" + if not target: + return reply = QMessageBox.question( - self, "Clear All", - f"Delete all hard negatives for profile '{self._profile}'?", + self, "Clear All", msg, QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, ) if reply == QMessageBox.StandardButton.Yes: - all_rows = self._db.get_hard_negatives(self._profile) - self._db.delete_hard_negatives_by_ids([r["id"] for r in all_rows]) + self._db.delete_hard_negatives_by_ids([r["id"] for r in target]) self._load() @@ -888,8 +897,13 @@ class ScanResultsPanel(QWidget): cmb.clear() for v in versions: ts = v["timestamp"] - # Format: "2026-04-19 14:30 (12 regions, best: 0.95)" - label = (f"{ts[:4]}-{ts[4:6]}-{ts[6:8]} {ts[9:11]}:{ts[11:13]}" + # Parse timestamp to readable date string + try: + dt = datetime.strptime(ts[:15], "%Y%m%d_%H%M%S") + date_str = dt.strftime("%Y-%m-%d %H:%M") + except (ValueError, IndexError): + date_str = ts + label = (f"{date_str}" f" ({v['count']} regions, best: {v['max_score']:.2f})") cmb.addItem(label, userData=ts) cmb.blockSignals(False) @@ -899,6 +913,7 @@ class ScanResultsPanel(QWidget): """Reload a tab's results when the user selects a different version.""" if idx < 0: return + self._undo_stack.clear() # version context changed, old undo entries invalid # Find the tab for this model for i in range(self._tabs.count()): if self._tabs.tabText(i).rsplit(" (", 1)[0] == model: diff --git a/tests/test_db.py b/tests/test_db.py index 51023d0..0ab5998 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -1,6 +1,5 @@ import os import tempfile -import time from core.db import ProcessedDB @@ -32,12 +31,10 @@ def test_scan_result_history(): path = f.name try: db = ProcessedDB(path) - # Save three versions with small delays so timestamps differ + # Save three versions (microsecond-precision timestamps avoid collisions) db.save_scan_results("v.mp4", "test", "MODEL_A", [(0, 8, 0.9)]) - time.sleep(1.1) db.save_scan_results("v.mp4", "test", "MODEL_A", [(0, 8, 0.8), (10, 18, 0.7)]) - time.sleep(1.1) db.save_scan_results("v.mp4", "test", "MODEL_A", [(5, 13, 0.95)]) versions = db.get_scan_versions("v.mp4", "test", "MODEL_A") assert len(versions) == 3