Add snapshot branching and profile/session manager

Branching: snapshots now track parentId to form a tree structure.
Swapping to an old snapshot and editing forks into a new branch.
Sidebar and timeline show < 1/3 > navigators at fork points to
switch between branches. Pruning protects ancestors and fork points.
Deleting a fork point re-parents its children.

Profiles: save/load named sets of workflows as session profiles.
Backend stores profiles as JSON in data/profiles/. Sidebar has a
collapsible Profiles section with save, load, and delete.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-26 11:51:00 +01:00
parent 7518821447
commit bca7e7cf8f
3 changed files with 853 additions and 22 deletions

View File

@@ -122,9 +122,10 @@ async def prune_snapshots(request):
workflow_key = data.get("workflowKey")
max_snapshots = data.get("maxSnapshots")
source = data.get("source")
protected_ids = data.get("protectedIds")
if not workflow_key or max_snapshots is None:
return web.json_response({"error": "Missing workflowKey or maxSnapshots"}, status=400)
deleted = storage.prune(workflow_key, int(max_snapshots), source=source)
deleted = storage.prune(workflow_key, int(max_snapshots), source=source, protected_ids=protected_ids)
return web.json_response({"deleted": deleted})
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
@@ -147,3 +148,61 @@ async def migrate_snapshots(request):
return web.json_response({"error": str(e)}, status=400)
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
# ─── Profile Endpoints ───────────────────────────────────────────────
@routes.post("/snapshot-manager/profile/save")
async def save_profile(request):
try:
data = await request.json()
profile = data.get("profile")
if not profile or "id" not in profile:
return web.json_response({"error": "Missing profile with id"}, status=400)
storage.profile_put(profile)
return web.json_response({"ok": True})
except ValueError as e:
return web.json_response({"error": str(e)}, status=400)
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
@routes.get("/snapshot-manager/profile/list")
async def list_profiles(request):
try:
profiles = storage.profile_get_all()
return web.json_response(profiles)
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
@routes.post("/snapshot-manager/profile/get")
async def get_profile(request):
try:
data = await request.json()
profile_id = data.get("id")
if not profile_id:
return web.json_response({"error": "Missing id"}, status=400)
profile = storage.profile_get(profile_id)
if profile is None:
return web.json_response({"error": "Not found"}, status=404)
return web.json_response(profile)
except ValueError as e:
return web.json_response({"error": str(e)}, status=400)
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
@routes.post("/snapshot-manager/profile/delete")
async def delete_profile(request):
try:
data = await request.json()
profile_id = data.get("id")
if not profile_id:
return web.json_response({"error": "Missing id"}, status=400)
storage.profile_delete(profile_id)
return web.json_response({"ok": True})
except ValueError as e:
return web.json_response({"error": str(e)}, status=400)
except Exception as e:
return web.json_response({"error": str(e)}, status=500)