fix: db/redis bleed - #102
Conversation
Sorge AI Code ReviewModel: gemini-2.5-flash (gemini) SummaryThis PR significantly enhances the application's project lifecycle management and timeline persistence. It introduces server-side storage for tracks and clips, enabling robust saving and loading of project timelines. Key improvements include atomic clip replacement, comprehensive project deletion (cascading through associated sequences, tracks, clips, assets, and AI jobs, including file cleanup), and refined memory store fallback logic to prevent data resurrection. The client-side UI has been updated to interact with these new backend capabilities, ensuring a more stable and persistent user experience. The addition of a dedicated test suite for project lifecycle is commendable and greatly improves confidence in the new features. Issues Found
ℹ️ apps/desktop-tauri/ui/src/backendApi.ts:289
ℹ️ services/orchestrator/app/db_repos.py:349
Recommendations
Routing (scheduled, 1 chunk(s))
Review generated by deepiri-sorge |
IMPORTANT:
Description
Fix the DB/Redis bleed where opening or creating a project showed the previous project's timeline: generated videos and clips from the last-opened project stayed on screen because the studio shell never tore down per-project state, and clips were never round-tripped to the server per sequence. Also changed "delete project" functionality so it doesn't leave half-deleted projects behind (gone from local but persists in db).
Changes
Timeline Persistence and fix bleed between projects
studio.py— addedClipUpsert(ClipCreate), which adds a requiredid: UUID. The client mints the clip id, so a save is an upsert against existing rows instead of a delete-and-recreate.timeline.py— newPUT /v1/sequences/{sequence_id}/clips. The body is alist[ClipUpsert]that becomes the sequence's complete clip list. 404s if the sequence does not exist.memory_store.py— newclip_replace_for_sequence(), runs under one lock and one_save(). Clips absent from the payload are dropped, clips already present are updated in place (created_atpreserved), and rows for a track outside of the sequence are ignoreddb_repos.py— newreplace_clips_for_sequence(), used to update the timeline when changing between projectsstudio.py—replace_clips_for_sequence()wires the memory store and the PG mirror together, returning the memory store's canonical rowsDelete Project Functionality
db_repos.py— addeddelete_project(), which deletes projects according to the id, and the three FKs intoassetsthat carry no delete rule defaults toNO ACTION(clips.asset_id,ai_job_artifacts.asset_id,ai_jobs.result_asset_id), and addedupdate_project()that takes the rowmemory_store.project_updatealready returnsstudio.py—delete_project()calls delete in db first then memory, so db failure leaves the project state untouched instead of half-deletedai_jobs.py—accept_ai_job()now 409s whenstudio.get_project(rec.project_id)comes back emptyDesktop UI (per-project teardown and rebuild)
types.ts— split the overloadedUiClip.serverIdinto two fields with distinct meanings:clipId(stable server clip id) andassetId(linked media, optional). AddednewClipId()(crypto.randomUUID()) so a clip holds one identity from creation through every savebackendApi.ts— added theClipUpsertinterface andorchestratorReplaceClips(sequenceId, clips)state.ts— addedresetProjectState(state, history), which clears everything belonging to a single project (timeline tracks, UI state, asset bin,nextClipId,lastJobId, and both history stacks)studioApp.tspersistTimeline()— flattens the timeline intoClipUpsertrows and PUTs them. Runs on Save, on navigating home, and at the top ofopenProject()so the outgoing project is saved before its ids are cleared.resetProjectView()— callsresetProjectState(), then releases everything state cannot clear on its own: playback, proxy polling, job polling, review buttons, job status text, the preview monitor (<video>and frame<img>).openProject()— rebuilds the timeline from the server. Assets are loaded first so clips can take their labels from the bin.awaitinopenProject(), plusrefreshAssets()andfetchFrameForPlayhead(), now re-checksstate.activeProjectId !== pidand bails.addTrack()- add track and add audio uses the helper to make it persist in the timeline (similar to import media, files in these tracks will get saved and be independent to the project id)Tests
test_project_lifecycle.pyandtests/test_job_pipeline.py)Related
Testing
DELETE /v1/projects/... HTTP/1.1" 200 OKImportant Notes (Optional)
127.0.0.1:8080,ffmpegon PATH (asset import + proxy generation), and at least one importable media fileWorkflow Checklist (Required)
Review Requests
@Team-Deepiri/support-team