feat: replace Git-DAG timeline with flat snapshot browser

Replace HistoryTree (DAG with branches, Graphviz rendering) with a flat
chronological SnapshotTimeline. New UI features: split-view layout,
snapshot compare/diff, cherry-pick restore of individual sequences or
fields, auto-snapshots with debounce, and pin/filter support.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-21 18:14:22 +01:00
parent 686d4687c3
commit 1ec3abf17a
9 changed files with 966 additions and 660 deletions
+33 -21
View File
@@ -242,7 +242,6 @@ class ProjectDB:
)
self.conn.commit()
@staticmethod
@staticmethod
def _migrate_lora_keys(data: dict) -> dict:
"""Split combined lora 'name:strength' into separate name and strength keys."""
@@ -340,27 +339,34 @@ class ProjectDB:
# ------------------------------------------------------------------
def save_history_tree(self, data_file_id: int, tree_data: dict) -> None:
"""Save history tree, extracting node snapshots into separate table."""
"""Save history tree, extracting snapshot data into separate table.
Supports both new format (snapshots dict) and old format (nodes dict).
"""
now = time.time()
nodes = tree_data.get("nodes", {})
if "snapshots" in tree_data:
entries = tree_data.get("snapshots", {})
entry_key = "snapshots"
else:
entries = tree_data.get("nodes", {})
entry_key = "nodes"
slim_tree = dict(tree_data)
slim_nodes = {}
for nid, node in nodes.items():
slim_nodes[nid] = {k: v for k, v in node.items() if k != "data"}
slim_tree["nodes"] = slim_nodes
slim_entries = {}
for eid, entry in entries.items():
slim_entries[eid] = {k: v for k, v in entry.items() if k != "data"}
slim_tree[entry_key] = slim_entries
self.conn.execute("BEGIN IMMEDIATE")
try:
# Extract snapshot data from nodes into history_snapshots table
for nid, node in nodes.items():
snap = node.get("data")
for eid, entry in entries.items():
snap = entry.get("data")
if snap:
self.conn.execute(
"INSERT INTO history_snapshots (data_file_id, node_id, snapshot_data, updated_at) "
"VALUES (?, ?, ?, ?) "
"ON CONFLICT(data_file_id, node_id) DO UPDATE SET "
"snapshot_data=excluded.snapshot_data, updated_at=excluded.updated_at",
(data_file_id, nid, json.dumps(snap), now),
(data_file_id, eid, json.dumps(snap), now),
)
self.conn.execute(
"INSERT INTO history_trees (data_file_id, tree_data, updated_at) "
@@ -463,24 +469,30 @@ class ProjectDB:
)
# Import history tree (extract snapshots into separate table)
# Supports both new format (snapshots dict) and old format (nodes dict)
history_tree = data.get(KEY_HISTORY_TREE)
if history_tree and isinstance(history_tree, dict):
now = time.time()
nodes = history_tree.get("nodes", {})
if "snapshots" in history_tree:
entries = history_tree.get("snapshots", {})
entry_key = "snapshots"
else:
entries = history_tree.get("nodes", {})
entry_key = "nodes"
slim_tree = dict(history_tree)
slim_nodes = {}
for nid, node in nodes.items():
snap = node.get("data")
slim_entries = {}
for eid, entry in entries.items():
snap = entry.get("data")
if snap:
self.conn.execute(
"INSERT INTO history_snapshots (data_file_id, node_id, snapshot_data, updated_at) "
"VALUES (?, ?, ?, ?) "
"ON CONFLICT(data_file_id, node_id) DO UPDATE SET "
"snapshot_data=excluded.snapshot_data, updated_at=excluded.updated_at",
(df_id, nid, json.dumps(snap), now),
(df_id, eid, json.dumps(snap), now),
)
slim_nodes[nid] = {k: v for k, v in node.items() if k != "data"}
slim_tree["nodes"] = slim_nodes
slim_entries[eid] = {k: v for k, v in entry.items() if k != "data"}
slim_tree[entry_key] = slim_entries
self.conn.execute(
"INSERT INTO history_trees (data_file_id, tree_data, updated_at) "
"VALUES (?, ?, ?) "
@@ -540,9 +552,9 @@ class ProjectDB:
# Load history tree (metadata only, no snapshot data)
tree = self.get_history_tree(df["id"])
if tree:
# Strip any residual snapshot data from nodes
for node in tree.get("nodes", {}).values():
node.pop("data", None)
# Strip any residual snapshot data (supports both formats)
for entry in tree.get("snapshots", tree.get("nodes", {})).values():
entry.pop("data", None)
data["history_tree"] = tree
t3 = time.time()