From 1fb93bb16093112a082d9b20de8c41208e503bbf Mon Sep 17 00:00:00 2001 From: dgtlmoon Date: Wed, 28 Jan 2026 11:23:09 +0100 Subject: [PATCH] misc improvements --- changedetectionio/store/__init__.py | 11 ++++++-- .../store/file_saving_datastore.py | 7 +++-- changedetectionio/store/updates.py | 28 ++++++++++++++----- 3 files changed, 34 insertions(+), 12 deletions(-) diff --git a/changedetectionio/store/__init__.py b/changedetectionio/store/__init__.py index 5c6ae1b2a..70198a37f 100644 --- a/changedetectionio/store/__init__.py +++ b/changedetectionio/store/__init__.py @@ -226,7 +226,7 @@ class ChangeDetectionStore(DatastoreUpdatesMixin, FileSavingDataStore): # update_26 will load the legacy data again and migrate to new format # Only run updates AFTER the legacy schema version (e.g., if legacy is at 25, only run 26+) - self.migrate_legacy_db_format() + self.run_updates() else: @@ -302,7 +302,7 @@ class ChangeDetectionStore(DatastoreUpdatesMixin, FileSavingDataStore): else: watch_class = get_custom_watch_obj_for_processor(entity.get('processor')) - if entity.get('uuid') != 'text_json_diff': + if entity.get('processor') != 'text_json_diff': logger.trace(f"Loading Watch object '{watch_class.__module__}.{watch_class.__name__}' for UUID {uuid}") entity = watch_class(datastore_path=self.datastore_path, default=entity) @@ -368,6 +368,13 @@ class ChangeDetectionStore(DatastoreUpdatesMixin, FileSavingDataStore): self.__data['watching'] = watching self._watch_hashes = watch_hashes + # Verify all watches have hashes + missing_hashes = [uuid for uuid in watching.keys() if uuid not in watch_hashes] + if missing_hashes: + logger.error(f"WARNING: {len(missing_hashes)} watches missing hashes after load: {missing_hashes[:5]}") + else: + logger.debug(f"All {len(watching)} watches have valid hashes") + def _delete_watch(self, uuid): """ Delete a watch from storage. diff --git a/changedetectionio/store/file_saving_datastore.py b/changedetectionio/store/file_saving_datastore.py index d45c8c485..fa8988614 100644 --- a/changedetectionio/store/file_saving_datastore.py +++ b/changedetectionio/store/file_saving_datastore.py @@ -322,8 +322,9 @@ def load_all_watches(datastore_path, rehydrate_entity_func, compute_hash_func): watch, raw_data = load_watch_from_file(watch_json, uuid_dir, rehydrate_entity_func) if watch and raw_data: watching[uuid_dir] = watch - # Compute hash from raw data BEFORE rehydration to match saved hash - watch_hashes[uuid_dir] = compute_hash_func(raw_data) + # Compute hash from rehydrated Watch object (as dict) to match how we compute on save + # This ensures hash matches what audit will compute from dict(watch) + watch_hashes[uuid_dir] = compute_hash_func(dict(watch)) loaded += 1 if loaded % 100 == 0: @@ -743,7 +744,7 @@ class FileSavingDataStore(DataStore): self._dirty_watches.add(uuid) changes_found += 1 logger.warning( - f"Audit detected unmarked change in watch {uuid[:8]}... " + f"Audit detected unmarked change in watch {uuid[:8]}... current {current_hash:8} stored hash {stored_hash[:8]}" f"(hash changed but not marked dirty)" ) self.needs_write = True diff --git a/changedetectionio/store/updates.py b/changedetectionio/store/updates.py index 95a4a5f7e..371253255 100644 --- a/changedetectionio/store/updates.py +++ b/changedetectionio/store/updates.py @@ -590,12 +590,10 @@ class DatastoreUpdatesMixin: watch_dict = dict(watch) watch_dir = os.path.join(self.datastore_path, uuid) save_watch_atomic(watch_dir, uuid, watch_dict) - # Initialize hash - self._watch_hashes[uuid] = self._compute_hash(watch_dict) saved_count += 1 if saved_count % 100 == 0: - logger.info(f" Progress: {saved_count}/{watch_count} watches saved...") + logger.info(f" Progress: {saved_count}/{watch_count} watches migrated...") except Exception as e: logger.error(f"Failed to save watch {uuid}: {e}") @@ -648,10 +646,25 @@ class DatastoreUpdatesMixin: # Success! Now reload from new format logger.critical("Reloading datastore from new format...") - self._load_state() + self._load_state() # Includes load_watches logger.success("Datastore reloaded from new format successfully") - self._load_watches() - logger.success("Reloading watches") + + + # Verify all watches have hashes after migration + missing_hashes = [uuid for uuid in self.data['watching'].keys() if uuid not in self._watch_hashes] + if missing_hashes: + logger.error(f"WARNING: {len(missing_hashes)} watches missing hashes after migration: {missing_hashes[:5]}") + else: + logger.success(f"All {len(self.data['watching'])} watches have valid hashes after migration") + + # Set schema version to latest available update + # This prevents re-running updates and re-marking all watches as dirty + updates_available = self.get_updates_available() + latest_schema = updates_available[-1] if updates_available else 26 + self.data['settings']['application']['schema_version'] = latest_schema + self.mark_settings_dirty() + logger.info(f"Set schema_version to {latest_schema} (migration complete, all watches already saved)") + logger.critical("=" * 80) logger.critical("MIGRATION COMPLETED SUCCESSFULLY!") logger.critical("=" * 80) @@ -669,4 +682,5 @@ class DatastoreUpdatesMixin: logger.info(f" - rm {os.path.join(self.datastore_path, 'url-watches.json')}") logger.info("") - # Schema version will be updated by run_updates() + def update_26(self): + self.migrate_legacy_db_format() \ No newline at end of file