From a1a85ecc4db13cb8cadf3f774957b177355772f6 Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Thu, 19 Mar 2026 09:52:46 +0100 Subject: [PATCH] Fix bugs in snapshot stripping: auto-note, mass update, DB-off, fallback - _auto_change_note now loads previous snapshot from DB instead of reading stripped in-memory node (was producing wrong commit messages) - _render_mass_update now strips snapshots after save (was leaking RAM) - Only strip snapshots when DB is enabled (preview/restore still works without DB via in-memory or disk fallback) - _render_data_preview adds disk fallback matching _restore_node Co-Authored-By: Claude Opus 4.6 --- tab_batch_ng.py | 27 ++++++++++++++++++--------- tab_timeline_ng.py | 8 ++++++++ 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/tab_batch_ng.py b/tab_batch_ng.py index 50f6c01..d5ba849 100644 --- a/tab_batch_ng.py +++ b/tab_batch_ng.py @@ -86,14 +86,19 @@ def find_insert_position(batch_list, parent_index, parent_seq_num): # --- Auto change note --- -def _auto_change_note(htree, batch_list): +def _auto_change_note(htree, batch_list, state=None, file_path=None): """Compare current batch_list against last snapshot and describe changes.""" # Get previous batch data from the current head if not htree.head_id or htree.head_id not in htree.nodes: return f'Initial save ({len(batch_list)} sequences)' - prev_data = htree.nodes[htree.head_id].get('data', {}) - prev_batch = prev_data.get(KEY_BATCH_DATA, []) + # Load previous snapshot from DB (nodes no longer hold data in memory) + prev_data = htree.nodes[htree.head_id].get('data') + if not prev_data and state and state.db_enabled and state.db and state.current_project and file_path: + df = state.db.get_data_file_by_names(state.current_project, file_path.stem) + if df: + prev_data = state.db.get_node_snapshot(df['id'], htree.head_id) + prev_batch = (prev_data or {}).get(KEY_BATCH_DATA, []) prev_by_seq = {int(s.get(KEY_SEQUENCE_NUMBER, 0)): s for s in prev_batch} curr_by_seq = {int(s.get(KEY_SEQUENCE_NUMBER, 0)): s for s in batch_list} @@ -359,7 +364,7 @@ def render_batch_processor(state: AppState): data[KEY_BATCH_DATA] = batch_list tree_data = data.get(KEY_HISTORY_TREE, {}) htree = HistoryTree(tree_data) - note = commit_input.value if commit_input.value else _auto_change_note(htree, batch_list) + note = commit_input.value if commit_input.value else _auto_change_note(htree, batch_list, state=state, file_path=file_path) # Single serialization: json roundtrip gives us an isolated snapshot # without the expensive deepcopy t1 = time.perf_counter() @@ -381,9 +386,9 @@ def render_batch_processor(state: AppState): t1 = time.perf_counter() await asyncio.to_thread(sync_to_db, state.db, state.current_project, file_path, save_snapshot) logger.info("save_and_snap sync_to_db %.3fs", time.perf_counter() - t1) - # Free snapshot data from memory — it's persisted in DB now - htree.strip_snapshots() - data[KEY_HISTORY_TREE] = htree.to_dict() + # Free snapshot data from memory — it's persisted in DB now + htree.strip_snapshots() + data[KEY_HISTORY_TREE] = htree.to_dict() state.restored_indicator = None commit_input.set_value('') logger.info("save_and_snap END (%.3fs)", time.perf_counter() - t_ss) @@ -843,8 +848,9 @@ def _render_mass_update(batch_list, data, file_path, state: AppState, refresh_li data[KEY_BATCH_DATA] = batch_list htree = HistoryTree(data.get(KEY_HISTORY_TREE, {})) - snapshot = {k: copy.deepcopy(v) for k, v in data.items() - if k != KEY_HISTORY_TREE} + snapshot_json = json.dumps({k: v for k, v in data.items() + if k != KEY_HISTORY_TREE}) + snapshot = json.loads(snapshot_json) try: htree.commit(snapshot, f"Mass update: {', '.join(selected_keys)}") except ValueError as e: @@ -855,6 +861,9 @@ def _render_mass_update(batch_list, data, file_path, state: AppState, refresh_li await asyncio.to_thread(save_json, file_path, save_snapshot) if state.db_enabled and state.current_project and state.db: await asyncio.to_thread(sync_to_db, state.db, state.current_project, file_path, save_snapshot) + # Free snapshot data from memory after persisting + htree.strip_snapshots() + data[KEY_HISTORY_TREE] = htree.to_dict() ui.notify(f'Updated {len(targets)} sequences', type='positive') if refresh_list: refresh_list.refresh() diff --git a/tab_timeline_ng.py b/tab_timeline_ng.py index d1c9882..efc4924 100644 --- a/tab_timeline_ng.py +++ b/tab_timeline_ng.py @@ -619,6 +619,14 @@ def _render_data_preview(nid, htree, state: AppState = None, file_path=None): df = state.db.get_data_file_by_names(state.current_project, file_path.stem) if df: node_data = state.db.get_node_snapshot(df['id'], nid) + if not node_data and file_path: + # Disk fallback: read snapshot from JSON file + try: + raw_data, _ = load_json(file_path) + tree_on_disk = raw_data.get(KEY_HISTORY_TREE, {}) + node_data = tree_on_disk.get('nodes', {}).get(nid, {}).get('data') + except Exception: + pass if not node_data: ui.label('Snapshot data not available.').classes('text-caption text-warning') return