Dev #7
No files matched your search
@@ -5,7 +5,10 @@ DATABASE_URL=sqlite:///data/mindsift.db
|
|||||||
JWT_SECRET=change_this_to_a_secure_random_key_min_32_chars
|
JWT_SECRET=change_this_to_a_secure_random_key_min_32_chars
|
||||||
BASE_URL=http://localhost:8080
|
BASE_URL=http://localhost:8080
|
||||||
ALLOW_REGISTRATION=false
|
ALLOW_REGISTRATION=false
|
||||||
|
# Logging level: debug, info, warn, error (Default: info)
|
||||||
LOG_LEVEL=info
|
LOG_LEVEL=info
|
||||||
|
# LOG_LEVEL=debug
|
||||||
|
# DEBUG=true
|
||||||
|
|
||||||
# Optional SMTP Server Configuration for Email Alarms (can also be set in Admin Settings UI)
|
# Optional SMTP Server Configuration for Email Alarms (can also be set in Admin Settings UI)
|
||||||
SMTP_HOST=
|
SMTP_HOST=
|
||||||
|
|||||||
+17
-10
@@ -4,7 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"embed"
|
"embed"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"log"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
@@ -23,11 +23,16 @@ var embeddedDist embed.FS
|
|||||||
func main() {
|
func main() {
|
||||||
cfg := config.Load()
|
cfg := config.Load()
|
||||||
|
|
||||||
log.Printf("🧠 Starting MindSift Server on port %s (DB: %s)", cfg.Port, cfg.DatabasePath)
|
level := cfg.SlogLevel()
|
||||||
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: level}))
|
||||||
|
slog.SetDefault(logger)
|
||||||
|
|
||||||
|
slog.Info("MindSift Server starting", "port", cfg.Port, "log_level", level.String())
|
||||||
|
|
||||||
store, err := storage.NewStore(cfg.DatabasePath)
|
store, err := storage.NewStore(cfg.DatabasePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to initialize storage: %v", err)
|
slog.Error("Failed to initialize storage", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
defer store.Close()
|
defer store.Close()
|
||||||
|
|
||||||
@@ -35,11 +40,11 @@ func main() {
|
|||||||
store.SeedSMTPDefaultsFromEnv(cfg.SMTPHost, cfg.SMTPPort, cfg.SMTPUser, cfg.SMTPPass, cfg.SMTPFrom, cfg.SMTPTLS)
|
store.SeedSMTPDefaultsFromEnv(cfg.SMTPHost, cfg.SMTPPort, cfg.SMTPUser, cfg.SMTPPass, cfg.SMTPFrom, cfg.SMTPTLS)
|
||||||
|
|
||||||
if cfg.DemoMode {
|
if cfg.DemoMode {
|
||||||
log.Printf("🧪 Demo-Modus aktiv (MINDA_DEMO=true): Richte Demo-Benutzer admin/admin und user/user ein...")
|
slog.Info("🧪 Demo-Modus aktiv (MINDA_DEMO=true): Richte Demo-Benutzer admin/admin und user/user ein...")
|
||||||
if err := store.EnsureDemoAccounts(); err != nil {
|
if err := store.EnsureDemoAccounts(); err != nil {
|
||||||
log.Printf("⚠️ Fehler beim Anlegen der Demo-Accounts: %v", err)
|
slog.Warn("⚠️ Fehler beim Anlegen der Demo-Accounts", "error", err)
|
||||||
} else {
|
} else {
|
||||||
log.Printf("✅ Demo-Accounts bereit: admin / admin (Admin-Rolle) und user / user (Standard-Rolle)")
|
slog.Info("✅ Demo-Accounts bereit: admin / admin (Admin-Rolle) und user / user (Standard-Rolle)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,7 +52,8 @@ func main() {
|
|||||||
|
|
||||||
staticFS, err := fs.Sub(embeddedDist, "dist")
|
staticFS, err := fs.Sub(embeddedDist, "dist")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to mount embedded dist filesystem: %v", err)
|
slog.Error("Failed to mount embedded dist filesystem", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
srv := api.NewServer(cfg, store, wsHub, staticFS)
|
srv := api.NewServer(cfg, store, wsHub, staticFS)
|
||||||
@@ -61,9 +67,10 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
log.Printf("✨ MindSift ready at http://0.0.0.0:%s", cfg.Port)
|
slog.Info("✨ MindSift ready", "url", "http://0.0.0.0:"+cfg.Port)
|
||||||
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
log.Fatalf("HTTP server error: %v", err)
|
slog.Error("HTTP server error", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -71,7 +78,7 @@ func main() {
|
|||||||
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
|
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
|
||||||
<-stop
|
<-stop
|
||||||
|
|
||||||
log.Println("Shutting down MindSift server gracefully...")
|
slog.Info("Shutting down MindSift server gracefully...")
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
_ = httpServer.Shutdown(ctx)
|
_ = httpServer.Shutdown(ctx)
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ services:
|
|||||||
- BASE_URL=${BASE_URL:-http://localhost:8080}
|
- BASE_URL=${BASE_URL:-http://localhost:8080}
|
||||||
- ALLOW_REGISTRATION=${ALLOW_REGISTRATION:-false}
|
- ALLOW_REGISTRATION=${ALLOW_REGISTRATION:-false}
|
||||||
- LOG_LEVEL=${LOG_LEVEL:-info}
|
- LOG_LEVEL=${LOG_LEVEL:-info}
|
||||||
|
# Set LOG_LEVEL=debug or DEBUG=true for detailed request and sync logging
|
||||||
|
# - LOG_LEVEL=debug
|
||||||
|
# - DEBUG=true
|
||||||
# Demo mode: if true, automatically seeds demo accounts admin/admin and user/user
|
# Demo mode: if true, automatically seeds demo accounts admin/admin and user/user
|
||||||
- MINDA_DEMO=${MINDA_DEMO:-false}
|
- MINDA_DEMO=${MINDA_DEMO:-false}
|
||||||
# Optional SMTP / Mail Configuration (can also be configured in Admin UI)
|
# Optional SMTP / Mail Configuration (can also be configured in Admin UI)
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ services:
|
|||||||
- BASE_URL=${BASE_URL:-http://localhost:8080}
|
- BASE_URL=${BASE_URL:-http://localhost:8080}
|
||||||
- ALLOW_REGISTRATION=${ALLOW_REGISTRATION:-false}
|
- ALLOW_REGISTRATION=${ALLOW_REGISTRATION:-false}
|
||||||
- LOG_LEVEL=${LOG_LEVEL:-info}
|
- LOG_LEVEL=${LOG_LEVEL:-info}
|
||||||
|
# Set LOG_LEVEL=debug or DEBUG=true for detailed request and sync logging
|
||||||
|
# - LOG_LEVEL=debug
|
||||||
|
# - DEBUG=true
|
||||||
# Demo mode: if true, automatically seeds demo accounts admin/admin and user/user
|
# Demo mode: if true, automatically seeds demo accounts admin/admin and user/user
|
||||||
- MINDA_DEMO=${MINDA_DEMO:-false}
|
- MINDA_DEMO=${MINDA_DEMO:-false}
|
||||||
# Optional SMTP / Mail Configuration (can also be configured in Admin UI)
|
# Optional SMTP / Mail Configuration (can also be configured in Admin UI)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/smtp"
|
"net/smtp"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -48,6 +49,7 @@ func (s *Server) Router() http.Handler {
|
|||||||
|
|
||||||
r.Use(middleware.RealIP)
|
r.Use(middleware.RealIP)
|
||||||
r.Use(middleware.Recoverer)
|
r.Use(middleware.Recoverer)
|
||||||
|
r.Use(s.apiLoggingMiddleware)
|
||||||
r.Use(cors.Handler(cors.Options{
|
r.Use(cors.Handler(cors.Options{
|
||||||
AllowedOrigins: []string{"*"},
|
AllowedOrigins: []string{"*"},
|
||||||
AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
|
AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
|
||||||
@@ -239,6 +241,54 @@ func writeError(w http.ResponseWriter, status int, message string) {
|
|||||||
writeJSON(w, status, map[string]string{"error": message})
|
writeJSON(w, status, map[string]string{"error": message})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) apiLoggingMiddleware(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Only log API requests (/api/...)
|
||||||
|
if !strings.HasPrefix(r.URL.Path, "/api") {
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor)
|
||||||
|
|
||||||
|
next.ServeHTTP(ww, r)
|
||||||
|
|
||||||
|
duration := time.Since(start)
|
||||||
|
durationMs := float64(duration.Microseconds()) / 1000.0
|
||||||
|
status := ww.Status()
|
||||||
|
if status == 0 {
|
||||||
|
status = http.StatusOK
|
||||||
|
}
|
||||||
|
|
||||||
|
method := r.Method
|
||||||
|
path := r.URL.Path
|
||||||
|
|
||||||
|
if status >= 500 {
|
||||||
|
slog.Error("HTTP request failed",
|
||||||
|
"method", method,
|
||||||
|
"path", path,
|
||||||
|
"status", status,
|
||||||
|
"duration_ms", durationMs,
|
||||||
|
)
|
||||||
|
} else if status >= 400 {
|
||||||
|
slog.Warn("HTTP request warning",
|
||||||
|
"method", method,
|
||||||
|
"path", path,
|
||||||
|
"status", status,
|
||||||
|
"duration_ms", durationMs,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
slog.Debug("HTTP request completed",
|
||||||
|
"method", method,
|
||||||
|
"path", path,
|
||||||
|
"status", status,
|
||||||
|
"duration_ms", durationMs,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) authMiddleware(next http.Handler) http.Handler {
|
func (s *Server) authMiddleware(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
// 1. Check Authorization Bearer Token
|
// 1. Check Authorization Bearer Token
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -14,6 +15,7 @@ type Config struct {
|
|||||||
BaseURL string
|
BaseURL string
|
||||||
AllowRegistration bool
|
AllowRegistration bool
|
||||||
LogLevel string
|
LogLevel string
|
||||||
|
Debug bool
|
||||||
DemoMode bool
|
DemoMode bool
|
||||||
|
|
||||||
// Optional SMTP environment variables (can also be overridden/configured in DB system_settings)
|
// Optional SMTP environment variables (can also be overridden/configured in DB system_settings)
|
||||||
@@ -40,7 +42,14 @@ func Load() *Config {
|
|||||||
jwtSecret := getEnv("JWT_SECRET", "mindsift-default-selfhosted-jwt-secret-key-2026")
|
jwtSecret := getEnv("JWT_SECRET", "mindsift-default-selfhosted-jwt-secret-key-2026")
|
||||||
baseURL := getEnv("BASE_URL", "http://localhost:"+port)
|
baseURL := getEnv("BASE_URL", "http://localhost:"+port)
|
||||||
allowReg := strings.ToLower(getEnv("ALLOW_REGISTRATION", "false")) == "true"
|
allowReg := strings.ToLower(getEnv("ALLOW_REGISTRATION", "false")) == "true"
|
||||||
logLevel := getEnv("LOG_LEVEL", "info")
|
|
||||||
|
debugVal := strings.ToLower(getEnv("DEBUG", "false"))
|
||||||
|
debug := debugVal == "true" || debugVal == "1" || debugVal == "yes" || debugVal == "on"
|
||||||
|
|
||||||
|
logLevel := strings.ToLower(getEnv("LOG_LEVEL", "info"))
|
||||||
|
if debug {
|
||||||
|
logLevel = "debug"
|
||||||
|
}
|
||||||
|
|
||||||
demoVal := strings.ToLower(getEnv("MINDA_DEMO", getEnv("MINDSIFT_DEMO", getEnv("DEMO_MODE", "false"))))
|
demoVal := strings.ToLower(getEnv("MINDA_DEMO", getEnv("MINDSIFT_DEMO", getEnv("DEMO_MODE", "false"))))
|
||||||
demoMode := demoVal == "true" || demoVal == "1" || demoVal == "yes" || demoVal == "on"
|
demoMode := demoVal == "true" || demoVal == "1" || demoVal == "yes" || demoVal == "on"
|
||||||
@@ -60,6 +69,7 @@ func Load() *Config {
|
|||||||
BaseURL: baseURL,
|
BaseURL: baseURL,
|
||||||
AllowRegistration: allowReg,
|
AllowRegistration: allowReg,
|
||||||
LogLevel: logLevel,
|
LogLevel: logLevel,
|
||||||
|
Debug: debug,
|
||||||
DemoMode: demoMode,
|
DemoMode: demoMode,
|
||||||
SMTPHost: smtpHost,
|
SMTPHost: smtpHost,
|
||||||
SMTPPort: smtpPort,
|
SMTPPort: smtpPort,
|
||||||
@@ -70,6 +80,20 @@ func Load() *Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SlogLevel parses Config.LogLevel into a standard slog.Level.
|
||||||
|
func (c *Config) SlogLevel() slog.Level {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(c.LogLevel)) {
|
||||||
|
case "debug":
|
||||||
|
return slog.LevelDebug
|
||||||
|
case "warn", "warning":
|
||||||
|
return slog.LevelWarn
|
||||||
|
case "error":
|
||||||
|
return slog.LevelError
|
||||||
|
default:
|
||||||
|
return slog.LevelInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func getEnv(key, fallback string) string {
|
func getEnv(key, fallback string) string {
|
||||||
if val, ok := os.LookupEnv(key); ok && strings.TrimSpace(val) != "" {
|
if val, ok := os.LookupEnv(key); ok && strings.TrimSpace(val) != "" {
|
||||||
return strings.TrimSpace(val)
|
return strings.TrimSpace(val)
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConfigLogLevelAndDebug(t *testing.T) {
|
||||||
|
os.Setenv("PORT", "9090")
|
||||||
|
defer os.Unsetenv("PORT")
|
||||||
|
|
||||||
|
// 1. Default info
|
||||||
|
os.Unsetenv("LOG_LEVEL")
|
||||||
|
os.Unsetenv("DEBUG")
|
||||||
|
cfg := Load()
|
||||||
|
if cfg.SlogLevel() != slog.LevelInfo {
|
||||||
|
t.Fatalf("expected LevelInfo, got %v", cfg.SlogLevel())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. LOG_LEVEL=debug
|
||||||
|
os.Setenv("LOG_LEVEL", "debug")
|
||||||
|
cfg = Load()
|
||||||
|
if cfg.SlogLevel() != slog.LevelDebug {
|
||||||
|
t.Fatalf("expected LevelDebug, got %v", cfg.SlogLevel())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. LOG_LEVEL=warn
|
||||||
|
os.Setenv("LOG_LEVEL", "warn")
|
||||||
|
cfg = Load()
|
||||||
|
if cfg.SlogLevel() != slog.LevelWarn {
|
||||||
|
t.Fatalf("expected LevelWarn, got %v", cfg.SlogLevel())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. LOG_LEVEL=error
|
||||||
|
os.Setenv("LOG_LEVEL", "error")
|
||||||
|
cfg = Load()
|
||||||
|
if cfg.SlogLevel() != slog.LevelError {
|
||||||
|
t.Fatalf("expected LevelError, got %v", cfg.SlogLevel())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. DEBUG=true overrides LOG_LEVEL
|
||||||
|
os.Setenv("LOG_LEVEL", "error")
|
||||||
|
os.Setenv("DEBUG", "true")
|
||||||
|
cfg = Load()
|
||||||
|
if cfg.SlogLevel() != slog.LevelDebug {
|
||||||
|
t.Fatalf("expected LevelDebug when DEBUG=true, got %v", cfg.SlogLevel())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. DEBUG=1 overrides LOG_LEVEL
|
||||||
|
os.Setenv("DEBUG", "1")
|
||||||
|
cfg = Load()
|
||||||
|
if cfg.SlogLevel() != slog.LevelDebug {
|
||||||
|
t.Fatalf("expected LevelDebug when DEBUG=1, got %v", cfg.SlogLevel())
|
||||||
|
}
|
||||||
|
os.Unsetenv("DEBUG")
|
||||||
|
os.Unsetenv("LOG_LEVEL")
|
||||||
|
}
|
||||||
+47
-3
@@ -2,7 +2,7 @@ package ws
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"log"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -477,13 +477,30 @@ func (r *DocRoom) Flush() {
|
|||||||
r.dirty = false
|
r.dirty = false
|
||||||
r.mu.Unlock()
|
r.mu.Unlock()
|
||||||
|
|
||||||
|
tagsCount := 0
|
||||||
|
for _, b := range blocksCopy {
|
||||||
|
tagsCount += len(domain.ExtractTags(b.Content))
|
||||||
|
}
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
r.hub.mu.RLock()
|
r.hub.mu.RLock()
|
||||||
store := r.hub.docStore
|
store := r.hub.docStore
|
||||||
r.hub.mu.RUnlock()
|
r.hub.mu.RUnlock()
|
||||||
|
|
||||||
if store != nil {
|
if store != nil {
|
||||||
if err := store.FlushDocumentState(docID, title, blocksCopy); err != nil {
|
if err := store.FlushDocumentState(docID, title, blocksCopy); err != nil {
|
||||||
log.Printf("[doc-room] flush error for doc %s: %v", docID, err)
|
slog.Error("SQLite document flush failed",
|
||||||
|
"doc_id", docID,
|
||||||
|
"error", err,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
durationMs := float64(time.Since(start).Microseconds()) / 1000.0
|
||||||
|
slog.Debug("SQLite document flushed",
|
||||||
|
"doc_id", docID,
|
||||||
|
"blocks_count", len(blocksCopy),
|
||||||
|
"tags_count", tagsCount,
|
||||||
|
"duration_ms", durationMs,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -492,7 +509,7 @@ func (r *DocRoom) Flush() {
|
|||||||
func (r *DocRoom) ServeDocWS(w http.ResponseWriter, req *http.Request, userID string, clientID string, canWrite bool) {
|
func (r *DocRoom) ServeDocWS(w http.ResponseWriter, req *http.Request, userID string, clientID string, canWrite bool) {
|
||||||
conn, err := upgrader.Upgrade(w, req, nil)
|
conn, err := upgrader.Upgrade(w, req, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[doc-ws] upgrade error for doc %s: %v", r.docID, err)
|
slog.Error("WebSocket upgrade error", "doc_id", r.docID, "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -518,6 +535,7 @@ func (r *DocRoom) ServeDocWS(w http.ResponseWriter, req *http.Request, userID st
|
|||||||
|
|
||||||
r.mu.Lock()
|
r.mu.Lock()
|
||||||
r.wsClients[client] = true
|
r.wsClients[client] = true
|
||||||
|
activeClients := len(r.wsClients)
|
||||||
if r.activeCursors == nil {
|
if r.activeCursors == nil {
|
||||||
r.activeCursors = make(map[string]*CursorPresence)
|
r.activeCursors = make(map[string]*CursorPresence)
|
||||||
}
|
}
|
||||||
@@ -542,6 +560,13 @@ func (r *DocRoom) ServeDocWS(w http.ResponseWriter, req *http.Request, userID st
|
|||||||
}
|
}
|
||||||
r.mu.Unlock()
|
r.mu.Unlock()
|
||||||
|
|
||||||
|
slog.Debug("WS client joined doc room",
|
||||||
|
"doc_id", r.docID,
|
||||||
|
"client_id", clientID,
|
||||||
|
"user_id", userID,
|
||||||
|
"active_clients", activeClients,
|
||||||
|
)
|
||||||
|
|
||||||
// Immediately send live in-memory state and current cursors to the newly connected client
|
// Immediately send live in-memory state and current cursors to the newly connected client
|
||||||
initEvt := DocSyncEvent{
|
initEvt := DocSyncEvent{
|
||||||
Type: "doc_state",
|
Type: "doc_state",
|
||||||
@@ -609,6 +634,13 @@ func (c *DocWSClient) readPump() {
|
|||||||
}
|
}
|
||||||
c.room.mu.Unlock()
|
c.room.mu.Unlock()
|
||||||
|
|
||||||
|
slog.Debug("WS client left doc room",
|
||||||
|
"doc_id", c.room.docID,
|
||||||
|
"client_id", c.clientID,
|
||||||
|
"user_id", c.userID,
|
||||||
|
"active_clients", remaining,
|
||||||
|
)
|
||||||
|
|
||||||
_ = c.conn.Close()
|
_ = c.conn.Close()
|
||||||
|
|
||||||
// Flush immediately when the last client leaves the document room
|
// Flush immediately when the last client leaves the document room
|
||||||
@@ -632,6 +664,18 @@ func (c *DocWSClient) readPump() {
|
|||||||
|
|
||||||
var evt DocSyncEvent
|
var evt DocSyncEvent
|
||||||
if err := json.Unmarshal(message, &evt); err == nil && evt.Type != "" {
|
if err := json.Unmarshal(message, &evt); err == nil && evt.Type != "" {
|
||||||
|
targetBlockID := evt.BlockID
|
||||||
|
if targetBlockID == "" && evt.Block != nil {
|
||||||
|
targetBlockID = evt.Block.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Debug("WS doc event received",
|
||||||
|
"type", evt.Type,
|
||||||
|
"doc_id", c.room.docID,
|
||||||
|
"block_id", targetBlockID,
|
||||||
|
"sender_client_id", c.clientID,
|
||||||
|
)
|
||||||
|
|
||||||
// Read-only clients can still move their cursor and send presence updates
|
// Read-only clients can still move their cursor and send presence updates
|
||||||
if !c.canWrite && evt.Type != "cursor_moved" {
|
if !c.canWrite && evt.Type != "cursor_moved" {
|
||||||
continue
|
continue
|
||||||
|
|||||||
+2
-2
@@ -3,7 +3,7 @@ package ws
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -627,7 +627,7 @@ func (c *Client) SendEvent(event string, payload interface{}) {
|
|||||||
func (h *Hub) ServeWS(w http.ResponseWriter, r *http.Request, roomID, userID string) {
|
func (h *Hub) ServeWS(w http.ResponseWriter, r *http.Request, roomID, userID string) {
|
||||||
conn, err := upgrader.Upgrade(w, r, nil)
|
conn, err := upgrader.Upgrade(w, r, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[ws] upgrade error: %v", err)
|
slog.Error("WebSocket upgrade error", "room_id", roomID, "user_id", userID, "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user