feat: collaboration temps reel - edition simultanee (#62)
CI / lint (push) Successful in 1m1s
CI / security (push) Successful in 41s
CI / test (push) Successful in 1m47s
CI / build (push) Successful in 1m20s
CI / e2e (push) Successful in 10m36s
Desktop Build / build-windows (push) Canceled after 0s
Desktop Build / build-linux (push) Canceled after 0s

WebSocket /ws/collab/{vault}/{path} (rooms par fichier), relais Yjs/CRDT, awareness (curseurs colores + presence), persistance serveur debounce 2s, auth WS + check_vault_access, reconnexion automatique. Frontend frontend/js/collab.js, backend backend/collab.py. Tests: 17 backend (5 clients simultanes) + 10 frontend. Docs: CHANGELOG, ROADMAP, fiche features/collaboration.md, README FR/EN.
This commit is contained in:
2026-09-11 23:27:22 -04:00
parent 3f7b7847a5
commit 063b02e996
19 changed files with 1741 additions and 33 deletions
+45 -1
View File
@@ -19,13 +19,14 @@ from typing import Any
import frontmatter
import mistune
from fastapi import Body, Depends, FastAPI, HTTPException, Query, Request
from fastapi import Body, Depends, FastAPI, HTTPException, Query, Request, WebSocket
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, Response, StreamingResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel, Field
from starlette.middleware.base import BaseHTTPMiddleware
from backend.attachment_indexer import get_attachment_stats, rescan_vault_attachments
from backend.collab import authenticate_websocket, collab_manager
from backend.history import (
get_bookmarks,
record_open,
@@ -739,6 +740,7 @@ async def lifespan(app: FastAPI):
yield
# Shutdown
await collab_manager.stop()
if _vault_watcher:
await _vault_watcher.stop()
_vault_watcher = None
@@ -4091,6 +4093,48 @@ async def api_conflict_resolve(body: dict = Body(...), current_user=Depends(requ
raise HTTPException(500, f"Error resolving conflict: {e!s}")
# ---------------------------------------------------------------------------
# Real-time collaboration — WebSocket endpoint (ROADMAP #62)
# ---------------------------------------------------------------------------
@app.websocket("/ws/collab/{vault_name}/{path:path}")
async def collab_websocket(websocket: WebSocket, vault_name: str, path: str):
"""Real-time collaborative editing over WebSocket (ROADMAP #62).
One *room* is created per ``vault::path``; all clients editing the same
file share Yjs/CRDT updates, awareness (cursors/selection) and a debounced
server-side persistence of the markdown content.
Authentication is performed manually (FastAPI ``Depends`` do not run for
WebSocket routes) and vault access is enforced per connection.
"""
from backend.services.errors import ServiceError
from backend.services.vaults import get_vault_root
user = authenticate_websocket(websocket)
if user is None:
await websocket.close(code=4401)
return
if not check_vault_access(vault_name, user):
await websocket.close(code=4403)
return
try:
vault_root = get_vault_root(vault_name)
file_path = _resolve_safe_path(vault_root, path)
except ServiceError:
await websocket.close(code=4404)
return
if not file_path.exists() or not file_path.is_file():
await websocket.close(code=4404)
return
await websocket.accept()
await collab_manager.connect(websocket, vault_name, path, file_path, user)
# ---------------------------------------------------------------------------
# Static files & SPA fallback
# ---------------------------------------------------------------------------