Draft
Conversation
Adds the backend foundation for collaborative script editing (Phase 1 Batch 1): - pycrdt dependency for CRDT/Yjs-compatible document sync - ScriptDraft model following CompiledScript file-pointer pattern - draft_script_path setting for draft file storage - ERROR_SCRIPT_DRAFT_ACTIVE constant for 409 conflict responses Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Chains from fbb1b6bd8707 (CrewAssignment). Creates script_drafts table with unique constraint on revision_id and CASCADE delete from script_revisions. Also fixes FK reference in ScriptDraft model (user table, not users). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase 1 Batch 3 of collaborative editing: - line_to_ydoc.py: Two-phase conversion (main thread DB query + background thread Y.Doc construction) with selectinload for N+1 avoidance - script_room_manager.py: ScriptRoom (Y.Doc + client tracking + save lock) and RoomManager (lazy room creation, periodic checkpointing with atomic writes, idle eviction, stale draft cleanup) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase 1 Batch 4 of collaborative editing: - WebSocket: JOIN_SCRIPT_ROOM, LEAVE_SCRIPT_ROOM, YJS_SYNC (2-step), YJS_UPDATE, YJS_AWARENESS handlers with base64 binary transport - on_close: Auto-remove client from collaborative editing rooms - App server: Initialize RoomManager on startup, create draft_script_path directory, clean up stale draft records and unreferenced files Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
30 tests covering: - build_ydoc: empty script, single line, linked list traversal, multi-page, unordered input, multiple line parts, null handling - Base64 round-trip: full state and incremental updates - CRDT convergence: concurrent field edits, concurrent text inserts, offline edit and reconnect - ScriptRoom: client add/remove, sync state, apply update, broadcast with sender exclusion, failed write resilience, dirty tracking Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase 2 Steps 2.1-2.4: - yjs and lib0 dependencies - ScriptDocProvider: custom Yjs provider using existing WebSocket connection with base64 binary transport and OP code messages - useYjsBinding: Vue 2.7 reactive bindings for Y.Map, Y.Text, Y.Array - scriptDraft Vuex module: room state, Y.Doc lifecycle, provider management, collaborator tracking - WebSocket message routing: Yjs OPs handled in SOCKET_ONMESSAGE and dispatched to HANDLE_DRAFT_MESSAGE Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…5-2.6) Add a dual-write bridge between Y.Doc and TMP_SCRIPT Vuex state so existing ScriptLineEditor/ScriptLinePart components continue to work unchanged while collaborative sync happens through the Y.Doc. - Create yjsBridge.js with Y.Doc↔TMP_SCRIPT conversion utilities - Join/leave draft room in ScriptEditor lifecycle hooks - Set up observeDeep on Y.Doc pages for remote change propagation - Use 'local-bridge' transaction origin to prevent observer loops - Wire add/delete/update line operations to Y.Doc - Sync from Y.Doc on page navigation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ScriptEditor.vue assumed CURRENT_REVISION was already populated in Vuex, but it's only loaded by the ScriptRevisions component on a different route. Adding GET_SCRIPT_REVISIONS to ScriptEditor's beforeMount ensures the revision ID is available for joining the collaborative editing room. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove updateYDocLine bridge function — components now write directly to Y.Map/Y.Text. ScriptLinePart gains yPartMap prop with: - @input handler for keystroke-level Y.Text sync - Y.Map writes for character/group dropdown changes - Y.Text and Y.Map observers for remote change handling - Lifecycle setup/teardown for observers Export nullToZero/zeroToNull from yjsBridge for component use. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ScriptLineEditor gains yLineMap prop with: - Y.Map writes for act_id, scene_id, stage_direction_style_id on change - yPartMap pass-through to ScriptLinePart children via getYPartMap() - addLinePart creates Y.Map structure in Y.Doc for new parts - Y.Map observer for remote changes to line-level fields - Lifecycle setup/teardown for observers Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- lineChange() is now a no-op in collab mode (components write directly to Y.Map/Y.Text; observer handles TMP_SCRIPT sync) - Remove syncingFromYDoc guard flag and local-bridge origin skip from the Y.Doc deep observer — all changes flow through to TMP_SCRIPT - Add getYLineMap() and pass y-line-map prop to ScriptLineEditor - Rework add/delete/insert line operations: build complete lineObj (with inherited act_id/scene_id) before writing to Y.Doc - Extract addLineOfType() helper to reduce duplication across addNewLine, addStageDirection, addCueLine, addSpacing - Remove addLineToYDoc/deleteLineFromYDoc wrappers (inlined) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Server broadcasts ROOM_MEMBERS on client join/leave/disconnect so all participants have an up-to-date collaborator list. Clients send YJS_AWARENESS messages with their current editing position (page and line index) which are relayed to other room members for line-level presence tracking. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
CollaboratorPanel shows room members with role badges and awareness tooltips in the sticky header. ScriptLineViewer gets a colored left border and username badge when another user is editing that line. Colors are deterministically assigned by user ID. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move Y.Doc and ScriptDocProvider out of Vuex reactive state into module-level variables. Vue 2 deeply observes all state objects, which caused it to track Y.Doc internal properties as reactive dependencies — triggering infinite re-renders after Yjs transactions. Also adds loop guards to nextActs/nextScenes linked-list traversals and updates _broadcastAwareness to use the new DRAFT_PROVIDER getter. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ecycle Remove single-editor lock so multiple users can co-edit via CRDTs. Add backend RBAC enforcement on REQUEST_SCRIPT_EDIT, REQUEST_SCRIPT_CUTS, and JOIN_SCRIPT_ROOM. Establish mutual exclusion between edit and cuts modes with is_cutting session flag. Close rooms when the last editor leaves (checkpoint + ROOM_CLOSED broadcast to viewers). Frontend: new getters for editor/cutter state, draft-aware cue editing, revision switching blocked during active editing, ROOM_CLOSED WS routing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Show tooltip explaining why the button is disabled (active editors, active cutter, or unsaved draft). Uses span wrappers with manual border-radius to preserve button-group joined styling. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implement SAVE_SCRIPT_DRAFT WebSocket handler and the full Y.Doc→DB save pipeline for collaborative script editing. Backend: - ydoc_to_lines.py: two-pass _save_script_page() — new lines (UUID _id), changed lines (new ScriptLine + migrate CueAssociation and ScriptCuts), unchanged (pointer-only updates), deleted lines (cleanup). Fix: populate new_line_id_map/new_part_id_map for changed lines so the Y.Doc is correctly patched with new DB ids after each save. - line_helpers.py: extracted validate_line() + create_new_line() shared helpers used by both REST API and WS save paths - script_room_manager.py: save_draft() with per-room asyncio.Lock; save_room() broadcasts SCRIPT_SAVED + triggers compilation; discard_room() + _delete_draft() for draft discard flow - ws_controller.py: SAVE_SCRIPT_DRAFT + DISCARD_SCRIPT_DRAFT handlers - web_decorators.py: no_active_script_draft decorator for REST endpoints - config.py: hasDraft uses room._dirty (unsaved changes only) - script.py: REST write endpoints protected by @no_active_script_draft; line creation/update delegates to line_helpers - yjs_debug.py: debug utility for Y.Doc state inspection Frontend: - yjsBridge.js: UUID _id for new lines/parts; deleteYDocLine() pushes real DB ids to deleted_line_ids before removing from Y.Array - ScriptLineEditor.vue: UUID for new parts in addPartToYDoc() - scriptDraft.js: SAVE_SCRIPT_DRAFT dispatch, SCRIPT_SAVED/SAVE_ERROR handling, saveError state - ScriptEditor.vue: resume/discard draft modal, save UX with error feedback, draft status indicator in toolbar Tests: 44 unit tests in test_ydoc_to_lines.py, save_draft/save_room integration tests, WS handler tests (all passing) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ws_controller: add missing `await` to both room.apply_update() call sites (YJS_SYNC step=2 and YJS_UPDATE). The method is async and acquires save_lock before mutating the Y.Doc; calling it without await meant the lock was never held and updates could interleave with an in-progress save. - web_decorators: no_active_script_draft now raises HTTP 409 (Conflict) instead of 400 to match the REST API Compatibility spec in the plan. - Save progress UI: SAVE_PROGRESS messages were never reaching the frontend — not in the HANDLE_DRAFT_MESSAGE whitelist in main.js. Added routing + savePage/saveTotalPages state in scriptDraft.js + DRAFT_SAVE_PROGRESS getter. ScriptEditor.vue now shows "Saving page X of Y (P%)" in the save toast, and the toast opens when isSaving turns true (not just for the user who clicked Save) so collaborating editors also see progress. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two cooperating failures caused a newly-inserted line to be silently deleted within the same save operation: 1. deleted_line_ids was never cleared from the Y.Doc after a successful commit. Stale integer IDs accumulated and survived into future saves. 2. SQLite reused the integer ID of a previously-deleted line for the newly-created one. Pass 2 of _save_script_page found that ID in the stale deleted_line_ids, located the just-flushed association, and deleted it — creating then destroying the line in one session. Fix 1 (ydoc_to_lines.py): before the Pass 2 deletion loop, build newly_created_ids from new_line_id_map.values(). Skip any deleted_id that appears in this set with a warning log. Fix 2 (script_room_manager.py): immediately after session.commit() in save_draft(), wipe the deleted_line_ids Y.Array. This happens before state_before is captured so the clear is included in the broadcast delta — all connected editors' Y.Docs are wiped automatically. Two regression tests added to TestScriptRoomSaveDraft. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Guard _checkpoint_room() with `if room._dirty:` in both the STOP_SCRIPT_EDIT handler and the on_close disconnect callback. Previously the checkpoint was unconditional — after a save reset _dirty to False, stopping edit mode would re-create the draft file and ScriptDraft record. hasDraft then returned true, blocking cuts mode and showing the Discard Draft button despite the DB being up to date. The idle-eviction path (_evict_room) already had this guard; this brings the explicit-close paths into line with it. Adds two regression tests: - test_stop_edit_clean_room_does_not_checkpoint - test_stop_edit_dirty_room_checkpoints_before_close Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Stale closures in JOIN_DRAFT_ROOM meant the 10-second sync watchdog
timer could fire after LEAVE_DRAFT_ROOM destroyed the provider,
incorrectly reporting a sync failure. Fix: promote _syncIntervalId /
_syncTimeoutId to module-level vars so LEAVE_DRAFT_ROOM can cancel
them before destroy(), and add a provider identity check in the
timeout callback to guard against any timer that slips through.
Also adds log.debug() throughout the collab path (scriptDraft,
ScriptDocProvider, ScriptEditor bridge) and exposes the loglevel
singleton as window.log so log.setLevel('debug') works in DevTools.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…date save_draft now always updates meta.last_saved_at in the Y.Doc, which means save_room always broadcasts YJS_UPDATE before SCRIPT_SAVED. Update the test to consume YJS_UPDATE first. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two-part fix for the bug where editing after navigating to page 80+ via incrPage/decrPage produced no YJS_UPDATE messages at the server. Round 1 — Frontend (client-side): - ScriptEditor.vue: incrPage() and decrPage() now call syncCurrentPageFromYDoc() after loading the new page, matching the existing goToPageInner() behaviour. Without this, TMP_SCRIPT held stale REST API data while Y.Doc had a different line count, causing getYLineMap() to return null and yPartMap to be null in ScriptLinePart. - Added diagnostic log.debug calls to getYLineMap(), setupYDocBridge() (ScriptEditor.vue), onTextInput() (ScriptLinePart.vue), and _onDocUpdate() (ScriptDocProvider.js) for future failure diagnosis. - ScriptDocProvider._onDocUpdate now explicitly guards and logs when suppressed due to !_connected or !_synced. - Added loglevel import to ScriptLinePart.vue. Round 2 — Backend data repair (root cause fix): - build_ydoc traverses the linked list from the head and halts at the first broken next_line_id. Cross-page stitching pointers were broken in production revisions, causing build_ydoc to omit all pages after the first break from the Y.Doc (while the REST API, which queries WHERE page=N directly, returned those pages correctly). - Migration c2f8d4a6e0b3: repairs all broken cross-page pointers using the page-walk algorithm; deletes true orphans. 24 pointer fixes and 4 orphan deletions across the affected revisions. - Migration d3e9f0c1a2b4: adds composite self-referential FK constraints on script_line_revision_association — (revision_id, next_line_id) and (revision_id, previous_line_id) both reference (revision_id, line_id), DEFERRABLE INITIALLY DEFERRED — to prevent future corruption. - ScriptLineRevisionAssociation.__table_args__ updated to declare the new constraints so tests and Alembic autogenerate stay in sync. - Diagnostic tool: server/utils/script/diagnose_linked_list.py. Tests added: - TestBuildYdocBrokenChain: regression tests for traversal-halts-at- broken-pointer behaviour in build_ydoc. - TestRevisionLinkedListFKConstraints: verifies composite FK constraints reject cross-revision pointers and accept valid chains. - test_branch_preserves_linked_list_integrity: walks the full linked list after branching to catch future regressions in pointer copying. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All 9 collaborative editing server messages now use {"OP": "NOOP", "ACTION": "<name>"}
so the existing framework ACTION dispatch path handles them automatically — no
special-case routing needed anywhere.
Backend:
- ws_controller.py: COLLAB_ERROR and YJS_SYNC (steps 0 & 2) use NOOP + ACTION
- script_room_manager.py: YJS_UPDATE, YJS_AWARENESS, ROOM_MEMBERS, SAVE_PROGRESS,
SAVE_ERROR, SCRIPT_SAVED, ROOM_CLOSED all use NOOP + ACTION
Frontend:
- main.js: remove 14-line collab OP whitelist (now redundant with NOOP + ACTION)
- websocket.js: remove 8 dead case stubs from SOCKET_ONMESSAGE switch
- ScriptDocProvider.js: remove handleMessage() router; replace private
_handleSync/Update/Awareness with public applySync/applyUpdate/applyAwareness
(each takes DATA payload directly with embedded connection + room_id guards)
- scriptDraft.js: replace HANDLE_DRAFT_MESSAGE with 9 individual Vuex actions —
one per message type; COLLAB_ERROR now properly logs server errors
Tests:
- Backend: all OP assertions updated to NOOP + ACTION (58 passing)
- New ScriptDocProvider.test.js: 13 tests for applySync/applyUpdate/applyAwareness
- New scriptDraft.test.js: 21 tests for all 9 individual Vuex actions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Prevents load/create/delete revision operations from running while a collaborative edit session is active or an unsaved draft exists. Backend: - Add _revision_is_locked() helper (checks DB draft + in-memory room) - GET /revisions annotates each revision with has_draft field - POST /revisions returns 409 if parent revision is locked - POST /revisions/current returns 409 if current revision has active room - DELETE /revisions returns 409 if target revision is locked - Broadcast GET_SCRIPT_REVISIONS after every code path that creates or destroys a ScriptDraft (STOP_SCRIPT_EDIT, on_close, SAVE/DISCARD draft) - Add TestRevisionLifecycleGuards (8 tests); fix test_ws_controller for new GET_SCRIPT_REVISIONS message in checkpoint sequence Frontend: - Surface backend 409 error messages in revision action toast errors - Replace canChangeRevisions with targeted canLoadRevision / canDeleteRevision computed props and revisionHasDraft() method - Add v-b-tooltip.hover on span wrappers for disabled buttons; use '' not null so tooltips clear correctly when condition lifts - Add .btn-group-item CSS using :not(:last-child)/:not(:first-child) so solo Load button retains all rounded corners Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove the IS_DRAFT_ACTIVE && DRAFT_YDOC scaffolding guards added in Phase 2 for incremental development. Y.Doc is now the only editing path with no non-collab fallback. Frontend (ScriptEditor.vue): - Rename data.loaded → data.dataLoaded; add computed loaded property gated on IS_DRAFT_SYNCED so the loading spinner shows until Y.Doc sync completes (loaded = dataLoaded && (!IS_DRAFT_ACTIVE || IS_DRAFT_SYNCED)) - Delete lineChange() method and its @input binding; Y.Doc deep observer is the only sync path - Remove non-collab else-branches from addLineOfType(), deleteLine(), and insertLineAt() — all three now always write to Y.Doc directly - Remove IS_DRAFT_SYNCED guard from goToPageInner(), incrPage(), and decrPage() — syncCurrentPageFromYDoc() is always called - Simplify getYLineMap() — remove IS_DRAFT_ACTIVE check; retain DRAFT_YDOC and pageArray boundary null guards (still needed for the sync-window between IS_CURRENT_EDITOR and Y.Doc page population) - Remove ADD_BLANK_LINE, DELETE_LINE, INSERT_BLANK_LINE, SET_LINE from mapMutations (call sites deleted; mutations kept for Phase 8 cleanup) Backend (web_decorators.py): - Convert no_active_script_draft wrapper to async def - Return JSON 409 response (set_status + finish) instead of raising HTTPError, consistent with other 409 endpoints - After the existing DB draft check, also check the in-memory room manager so the decorator blocks REST writes within the first 30 seconds of a collab session (before the first checkpoint) Tests (test/utils/web/test_web_decorators.py): - 4 new tests covering no-draft pass-through, DB draft 409, in-memory room 409, and empty room pass-through Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Block collaborative editing write operations when a live show session is active. The guard prevents script edits from reaching the Y.Doc during a running show, with defence-in-depth on both server and client. Backend: - Add ERROR_EDIT_BLOCKED_BY_LIVE_SESSION constant (constants.py) - Add _is_live_session_active() async helper to WsController that checks current_session_id in settings and verifies the ShowSession exists in DB - Guard REQUEST_SCRIPT_EDIT and REQUEST_SCRIPT_CUTS before RBAC check; return REQUEST_EDIT_FAILURE with the new error constant - Guard JOIN_SCRIPT_ROOM, YJS_SYNC step 2, YJS_UPDATE, and SAVE_SCRIPT_DRAFT; return COLLAB_ERROR if live session active - Not blocked: LEAVE_SCRIPT_ROOM, YJS_SYNC step 1, YJS_AWARENESS, DISCARD_SCRIPT_DRAFT (none of these write to the Y.Doc) - 4 new tests in TestLiveSessionGuards (703 total backend tests) Frontend: - CAN_REQUEST_EDIT getter: return false when CURRENT_SHOW_SESSION is set - CAN_REQUEST_CUTS getter: return false when CURRENT_SHOW_SESSION is set - ScriptEditor: CURRENT_SHOW_SESSION in mapGetters; editDisabledReason and cutsDisabledReason surface live session message before other reasons - ScriptRevisions: canLoadRevision and canDeleteRevision check !CURRENT_SHOW_SESSION; load tooltip updated - New scriptConfig.test.js: 9 Vitest getter tests covering CAN_REQUEST_EDIT and CAN_REQUEST_CUTS under all combinations of live session and editor/cutter/draft state (136 total frontend tests) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.




No description provided.