Skip to content

Collaborative Script Editing#905

Draft
Tim020 wants to merge 25 commits intodevfrom
feature/collaborative-editing
Draft

Collaborative Script Editing#905
Tim020 wants to merge 25 commits intodevfrom
feature/collaborative-editing

Conversation

@Tim020
Copy link
Contributor

@Tim020 Tim020 commented Feb 11, 2026

No description provided.

Tim020 and others added 5 commits February 11, 2026 15:06
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
@Tim020 Tim020 added the claude Issues created by Claude label Feb 11, 2026
@Tim020 Tim020 linked an issue Feb 11, 2026 that may be closed by this pull request
@github-actions github-actions bot added server Pull requests changing back end code xlarge-diff labels Feb 11, 2026
@github-actions
Copy link

github-actions bot commented Feb 11, 2026

Client Test Results

99 tests  ±0   99 ✅ ±0   0s ⏱️ ±0s
 4 suites ±0    0 💤 ±0 
 1 files   ±0    0 ❌ ±0 

Results for commit ea167bd. ± Comparison against base commit 9750978.

♻️ This comment has been updated with latest results.

@github-actions
Copy link

github-actions bot commented Feb 11, 2026

Python Test Results

  1 files  ±  0    1 suites  ±0   1m 5s ⏱️ +7s
624 tests +117  623 ✅ +116  0 💤 ±0  1 ❌ +1 
629 runs  +117  628 ✅ +116  0 💤 ±0  1 ❌ +1 

For more details on these failures, see this check.

Results for commit ea167bd. ± Comparison against base commit 9750978.

♻️ This comment has been updated with latest results.

Tim020 and others added 6 commits February 11, 2026 15:26
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 <[email protected]>
…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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
- 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 <[email protected]>
@github-actions github-actions bot added the client Pull requests changing front end code label Feb 11, 2026
Tim020 and others added 4 commits February 11, 2026 19:27
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
Tim020 and others added 7 commits February 15, 2026 14:46
…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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
- 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 <[email protected]>
Tim020 and others added 3 commits February 19, 2026 13:42
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
@sonarqubecloud
Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
2 Security Hotspots
E Security Rating on New Code (required ≥ A)
C Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

claude Issues created by Claude client Pull requests changing front end code server Pull requests changing back end code xlarge-diff

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Collaborative script editing

1 participant

Comments