feat(workspace): tabula-rasa outliner architecture, SQLite blocks/block_tags schema & echo-protected live room sync
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m12s

This commit is contained in:
sandrodach committed 2026-10-26 21:14:06 +01:00
1 parent f3b3f07e04
commit 86867b0d6f
17 files changed
+3409 -7178

No files matched your search

+25 -225
View File
@@ -3,7 +3,6 @@ package api
import (
"bytes"
"crypto/tls"
"encoding/base64"
"encoding/json"
"fmt"
"io"
@@ -32,7 +31,7 @@ type Server struct {
func NewServer(cfg *config.Config, store *storage.Store, wsHub *ws.Hub, staticFS fs.FS) *Server {
if wsHub != nil && store != nil {
wsHub.SetCRDTStore(store)
wsHub.SetDocStore(store)
}
srv := &Server{
cfg: cfg,
@@ -70,8 +69,6 @@ func (s *Server) Router() http.Handler {
r.Get("/api/v1/ws/spaces/{spaceId}", s.handleWebSocket)
r.Get("/api/v1/ws/lists/{listId}", s.handleWebSocket)
r.Get("/api/v1/ws/docs/{id}", s.handleDocWebSocket)
r.Get("/api/v1/sync/docs/{id}/stream", s.handleDocSyncStream)
r.Post("/api/v1/sync/docs/{id}/update", s.handleDocSyncUpdate)
r.Get("/api/v1/events", s.handleSSE)
r.Post("/api/v1/events/rooms", s.handleSSERooms)
@@ -144,18 +141,6 @@ func (s *Server) Router() http.Handler {
r.Post("/api/v1/documents/{id}/shares", s.handleCreateDocShare)
r.Delete("/api/v1/documents/{id}/shares/{userId}", s.handleDeleteDocShare)
// Document Locking
r.Get("/api/v1/documents/{id}/lock", s.handleGetDocLock)
r.Post("/api/v1/documents/{id}/lock", s.handleLockDoc)
r.Post("/api/v1/documents/{id}/lock/heartbeat", s.handleHeartbeatDocLock)
r.Delete("/api/v1/documents/{id}/lock", s.handleUnlockDoc)
// Document CRDT Realtime Sync
r.Get("/api/v1/documents/{id}/crdt", s.handleGetDocCRDT)
r.Put("/api/v1/documents/{id}/crdt", s.handleSaveDocCRDT)
r.Post("/api/v1/documents/{id}/awareness", s.handlePostDocAwareness)
r.Delete("/api/v1/documents/{id}/awareness", s.handleDeleteDocAwareness)
// Shared Lists & Realtime Collaboration
r.Get("/api/v1/lists", s.handleGetAllLists)
r.Post("/api/v1/lists", s.handleCreateList)
@@ -864,6 +849,18 @@ func (s *Server) handleGetDocument(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusNotFound, "Dokument nicht gefunden")
return
}
if s.wsHub != nil {
if liveTitle, liveBlocks, ok := s.wsHub.GetActiveDocState(docID); ok {
if liveTitle != "" {
doc.Title = liveTitle
}
if len(liveBlocks) > 0 {
doc.Blocks = liveBlocks
doc.MarkdownCache = domain.CompileBlocksToMarkdown(liveBlocks)
doc.Content = doc.MarkdownCache
}
}
}
writeJSON(w, http.StatusOK, doc)
}
@@ -889,6 +886,9 @@ func (s *Server) handleUpdateDocument(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusBadRequest, err.Error())
return
}
if s.wsHub != nil && (req.Title != nil || req.Blocks != nil) {
s.wsHub.SyncActiveDocRoom(docID, doc.Title, doc.Blocks)
}
if req.Title != nil || req.Icon != nil || req.FolderPath != nil || req.Blocks != nil || req.Tags != nil {
s.wsHub.BroadcastDocEvent(docID, "DOC_UPDATED", doc)
s.broadcastDocToAuthorizedUsers(docID, "DOC_UPDATED")
@@ -911,6 +911,9 @@ func (s *Server) handlePatchDocumentBlocks(w http.ResponseWriter, r *http.Reques
writeError(w, http.StatusBadRequest, err.Error())
return
}
if s.wsHub != nil {
s.wsHub.SyncActiveDocRoom(docID, doc.Title, doc.Blocks)
}
patchEvent := domain.DocBlocksPatchEvent{
DocID: docID,
@@ -933,6 +936,9 @@ func (s *Server) handlePatchDocumentBlocks(w http.ResponseWriter, r *http.Reques
func (s *Server) handleDeleteDocument(w http.ResponseWriter, r *http.Request) {
userID := auth.GetUserID(r.Context())
docID := chi.URLParam(r, "id")
if s.wsHub != nil {
s.wsHub.CloseDocRoom(docID)
}
if err := s.store.DeleteDocument(userID, docID); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
@@ -1307,30 +1313,7 @@ func (s *Server) handleGetCollaborators(w http.ResponseWriter, r *http.Request)
writeJSON(w, http.StatusOK, users)
}
// --- DOCUMENT LOCKING (Neutralized - Yjs CRDT Pessimistic Locks Removed) ---
func (s *Server) handleGetDocLock(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]interface{}{"locked": false})
}
func (s *Server) handleLockDoc(w http.ResponseWriter, r *http.Request) {
docID := chi.URLParam(r, "id")
writeJSON(w, http.StatusOK, map[string]interface{}{
"locked": false,
"acquired": true,
"docId": docID,
})
}
func (s *Server) handleHeartbeatDocLock(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]bool{"extended": true})
}
func (s *Server) handleUnlockDoc(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]bool{"unlocked": true})
}
// --- DOCUMENT CRDT REALTIME SYNC & DUAL TRANSPORT ---
// --- DOCUMENT REALTIME WEBSOCKET ROOM ---
func (s *Server) authenticateUserFromReq(r *http.Request) string {
token := r.URL.Query().Get("token")
@@ -1379,192 +1362,9 @@ func (s *Server) handleDocWebSocket(w http.ResponseWriter, r *http.Request) {
}
}
clientID := r.URL.Query().Get("clientId")
room := s.wsHub.GetOrCreateDocRoom(docID)
room.ServeDocWS(w, r, userID)
}
// GET /api/v1/sync/docs/{id}/stream
func (s *Server) handleDocSyncStream(w http.ResponseWriter, r *http.Request) {
docID := chi.URLParam(r, "id")
if docID == "" {
writeError(w, http.StatusBadRequest, "docId erforderlich")
return
}
userID := s.authenticateUserFromReq(r)
if userID == "" {
userID = auth.GetUserID(r.Context())
}
if userID == "" {
userID = "anonymous"
}
if userID != "anonymous" {
if _, err := s.store.GetDocumentByID(userID, docID); err != nil {
writeError(w, http.StatusForbidden, "Kein Zugriff auf dieses Dokument")
return
}
}
room := s.wsHub.GetOrCreateDocRoom(docID)
room.ServeDocSSE(w, r, userID)
}
// POST /api/v1/sync/docs/{id}/update
func (s *Server) handleDocSyncUpdate(w http.ResponseWriter, r *http.Request) {
docID := chi.URLParam(r, "id")
if docID == "" {
writeError(w, http.StatusBadRequest, "docId erforderlich")
return
}
userID := s.authenticateUserFromReq(r)
if userID == "" {
userID = auth.GetUserID(r.Context())
}
if userID == "" {
writeError(w, http.StatusUnauthorized, "Nicht autorisiert")
return
}
doc, err := s.store.GetDocumentByID(userID, docID)
if err != nil {
writeError(w, http.StatusNotFound, "Dokument nicht gefunden")
return
}
if !doc.IsOwner && doc.Permission != "write" {
writeError(w, http.StatusForbidden, "Keine Schreibberechtigung")
return
}
contentType := r.Header.Get("Content-Type")
var updateBytes []byte
if strings.Contains(contentType, "application/octet-stream") {
body, err := io.ReadAll(io.LimitReader(r.Body, 8*1024*1024))
if err != nil {
writeError(w, http.StatusBadRequest, "Fehler beim Lesen des Payloads")
return
}
updateBytes = body
} else {
var req struct {
Update string `json:"update"`
State string `json:"state"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
return
}
if req.State != "" {
if raw, err := base64.StdEncoding.DecodeString(req.State); err == nil && len(raw) > 0 {
room := s.wsHub.GetOrCreateDocRoom(docID)
room.CompactState(raw, userID)
}
}
if req.Update != "" {
raw, err := base64.StdEncoding.DecodeString(req.Update)
if err != nil {
writeError(w, http.StatusBadRequest, "Ungültiges Base64 für Update")
return
}
updateBytes = raw
}
}
if len(updateBytes) > 0 {
room := s.wsHub.GetOrCreateDocRoom(docID)
room.ApplyUpdate(updateBytes, userID, nil, nil)
}
writeJSON(w, http.StatusOK, map[string]interface{}{"success": true, "docId": docID})
}
func (s *Server) handleGetDocCRDT(w http.ResponseWriter, r *http.Request) {
userID := auth.GetUserID(r.Context())
docID := chi.URLParam(r, "id")
if _, err := s.store.GetDocumentByID(userID, docID); err != nil {
writeError(w, http.StatusNotFound, "Dokument nicht gefunden")
return
}
room := s.wsHub.GetOrCreateDocRoom(docID)
b64State, b64Updates := room.GetInitialSnapshot()
writeJSON(w, http.StatusOK, map[string]interface{}{
"docId": docID,
"state": b64State,
"updates": b64Updates,
})
}
func (s *Server) handleSaveDocCRDT(w http.ResponseWriter, r *http.Request) {
userID := auth.GetUserID(r.Context())
docID := chi.URLParam(r, "id")
doc, err := s.store.GetDocumentByID(userID, docID)
if err != nil {
writeError(w, http.StatusNotFound, "Dokument nicht gefunden")
return
}
if !doc.IsOwner && doc.Permission != "write" {
writeError(w, http.StatusForbidden, "Keine Schreibberechtigung")
return
}
var req struct {
State string `json:"state"`
Update string `json:"update"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "Ungültiger Request-Body")
return
}
room := s.wsHub.GetOrCreateDocRoom(docID)
if req.Update != "" {
raw, err := base64.StdEncoding.DecodeString(req.Update)
if err != nil {
writeError(w, http.StatusBadRequest, "Ungültiges Base64 Format für Update")
return
}
room.ApplyUpdate(raw, userID, nil, nil)
}
if req.State != "" {
raw, err := base64.StdEncoding.DecodeString(req.State)
if err != nil {
writeError(w, http.StatusBadRequest, "Ungültiges Base64 Format")
return
}
room.CompactState(raw, userID)
}
writeJSON(w, http.StatusOK, map[string]bool{"success": true})
}
func (s *Server) handlePostDocAwareness(w http.ResponseWriter, r *http.Request) {
userID := auth.GetUserID(r.Context())
docID := chi.URLParam(r, "id")
doc, err := s.store.GetDocumentByID(userID, docID)
if err != nil {
writeError(w, http.StatusNotFound, "Dokument nicht gefunden")
return
}
_ = doc
var payload json.RawMessage
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
return
}
s.wsHub.BroadcastRoom("doc:"+docID, "CRDT_AWARENESS", payload, nil)
writeJSON(w, http.StatusOK, map[string]bool{"success": true})
}
func (s *Server) handleDeleteDocAwareness(w http.ResponseWriter, r *http.Request) {
userID := auth.GetUserID(r.Context())
docID := chi.URLParam(r, "id")
s.wsHub.BroadcastRoom("doc:"+docID, "CRDT_AWARENESS_LEAVE", map[string]interface{}{
"docId": docID,
"userId": userID,
}, nil)
writeJSON(w, http.StatusOK, map[string]bool{"success": true})
room.ServeDocWS(w, r, userID, clientID)
}
// --- REALTIME LISTS & SHARING ---