package httpserver import ( "encoding/json" "net/http" "github.com/go-chi/chi/v5" "github.com/tim/cairnquire/apps/server/internal/collaboration" ) // Comments func (s *Server) handleListComments(w http.ResponseWriter, r *http.Request) { documentID := r.URL.Query().Get("document_id") if documentID == "" { writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": "document_id is required"}) return } document, err := s.repository.GetDocumentByID(r.Context(), documentID) if err != nil { writeJSONWithStatus(w, http.StatusNotFound, map[string]string{"error": "document not found"}) return } canRead, err := s.canReadDocumentRecord(r, *document) if err != nil { writeJSONWithStatus(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } if !canRead { writeJSONWithStatus(w, http.StatusNotFound, map[string]string{"error": "document not found"}) return } anchorHash := r.URL.Query().Get("anchor_hash") var comments []collaboration.Comment if anchorHash != "" { comments, err = s.collaboration.ListCommentsByAnchor(r.Context(), documentID, anchorHash) } else { comments, err = s.collaboration.ListComments(r.Context(), documentID) } if err != nil { s.logger.Warn("list comments", "error", err) writeJSONWithStatus(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } writeJSON(w, http.StatusOK, map[string]any{"comments": comments}) } func (s *Server) handleCreateComment(w http.ResponseWriter, r *http.Request) { principal, ok := requirePrincipal(r) if !ok { writeJSONWithStatus(w, http.StatusUnauthorized, map[string]string{"error": "authentication required"}) return } var input collaboration.CreateCommentInput if err := json.NewDecoder(r.Body).Decode(&input); err != nil { writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"}) return } document, err := s.repository.GetDocumentByID(r.Context(), input.DocumentID) if err != nil { writeJSONWithStatus(w, http.StatusNotFound, map[string]string{"error": "document not found"}) return } canWrite, err := s.canWriteDocumentRecord(r, *document) if err != nil { writeJSONWithStatus(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } if !canWrite { writeJSONWithStatus(w, http.StatusNotFound, map[string]string{"error": "document not found"}) return } comment, err := s.collaboration.CreateComment(r.Context(), principal, input) if err != nil { s.logger.Warn("create comment", "error", err) writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } writeJSONWithStatus(w, http.StatusCreated, comment) } func (s *Server) handleResolveComment(w http.ResponseWriter, r *http.Request) { principal, ok := requirePrincipal(r) if !ok { writeJSONWithStatus(w, http.StatusUnauthorized, map[string]string{"error": "authentication required"}) return } commentID := chi.URLParam(r, "id") comment, err := s.collaboration.GetComment(r.Context(), commentID) if err != nil { writeJSONWithStatus(w, http.StatusNotFound, map[string]string{"error": "comment not found"}) return } document, err := s.repository.GetDocumentByID(r.Context(), comment.DocumentID) if err != nil { writeJSONWithStatus(w, http.StatusNotFound, map[string]string{"error": "document not found"}) return } canWrite, err := s.canWriteDocumentRecord(r, *document) if err != nil { writeJSONWithStatus(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } if !canWrite { writeJSONWithStatus(w, http.StatusNotFound, map[string]string{"error": "comment not found"}) return } if err := s.collaboration.ResolveComment(r.Context(), principal, commentID); err != nil { s.logger.Warn("resolve comment", "error", err) writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } writeJSON(w, http.StatusOK, map[string]string{"status": "resolved"}) } // Notifications func (s *Server) handleListNotifications(w http.ResponseWriter, r *http.Request) { principal, ok := requirePrincipal(r) if !ok { writeJSONWithStatus(w, http.StatusUnauthorized, map[string]string{"error": "authentication required"}) return } unreadOnly := r.URL.Query().Get("unread") == "true" limit := 50 if r.URL.Query().Get("limit") != "" { // Parse limit if needed, but for now just use default } notifications, err := s.collaboration.ListNotifications(r.Context(), principal, unreadOnly, limit) if err != nil { s.logger.Warn("list notifications", "error", err) writeJSONWithStatus(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } writeJSON(w, http.StatusOK, map[string]any{"notifications": notifications}) } func (s *Server) handleMarkNotificationRead(w http.ResponseWriter, r *http.Request) { principal, ok := requirePrincipal(r) if !ok { writeJSONWithStatus(w, http.StatusUnauthorized, map[string]string{"error": "authentication required"}) return } notificationID := chi.URLParam(r, "id") if err := s.collaboration.MarkNotificationRead(r.Context(), principal, notificationID); err != nil { s.logger.Warn("mark notification read", "error", err) writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) } func (s *Server) handleMarkAllNotificationsRead(w http.ResponseWriter, r *http.Request) { principal, ok := requirePrincipal(r) if !ok { writeJSONWithStatus(w, http.StatusUnauthorized, map[string]string{"error": "authentication required"}) return } if err := s.collaboration.MarkAllNotificationsRead(r.Context(), principal); err != nil { s.logger.Warn("mark all notifications read", "error", err) writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) } func (s *Server) handleNotificationBell(w http.ResponseWriter, r *http.Request) { principal, ok := requirePrincipal(r) if !ok { writeJSON(w, http.StatusOK, collaboration.NotificationBell{UnreadCount: 0, Items: []collaboration.Notification{}}) return } bell, err := s.collaboration.GetBellStatus(r.Context(), principal) if err != nil { s.logger.Warn("notification bell", "error", err) writeJSONWithStatus(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } writeJSON(w, http.StatusOK, bell) } // Watchers func (s *Server) handleWatchDocument(w http.ResponseWriter, r *http.Request) { principal, ok := requirePrincipal(r) if !ok { writeJSONWithStatus(w, http.StatusUnauthorized, map[string]string{"error": "authentication required"}) return } var req struct { DocumentID string `json:"documentId"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"}) return } if err := s.collaboration.WatchDocument(r.Context(), principal, req.DocumentID); err != nil { s.logger.Warn("watch document", "error", err) writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } writeJSON(w, http.StatusOK, map[string]string{"status": "watching"}) } func (s *Server) handleUnwatchDocument(w http.ResponseWriter, r *http.Request) { principal, ok := requirePrincipal(r) if !ok { writeJSONWithStatus(w, http.StatusUnauthorized, map[string]string{"error": "authentication required"}) return } var req struct { DocumentID string `json:"documentId"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"}) return } if err := s.collaboration.UnwatchDocument(r.Context(), principal, req.DocumentID); err != nil { s.logger.Warn("unwatch document", "error", err) writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } writeJSON(w, http.StatusOK, map[string]string{"status": "unwatched"}) } func (s *Server) handleWatchFolder(w http.ResponseWriter, r *http.Request) { principal, ok := requirePrincipal(r) if !ok { writeJSONWithStatus(w, http.StatusUnauthorized, map[string]string{"error": "authentication required"}) return } var req struct { FolderPath string `json:"folderPath"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"}) return } if err := s.collaboration.WatchFolder(r.Context(), principal, req.FolderPath); err != nil { s.logger.Warn("watch folder", "error", err) writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } writeJSON(w, http.StatusOK, map[string]string{"status": "watching"}) } func (s *Server) handleUnwatchFolder(w http.ResponseWriter, r *http.Request) { principal, ok := requirePrincipal(r) if !ok { writeJSONWithStatus(w, http.StatusUnauthorized, map[string]string{"error": "authentication required"}) return } var req struct { FolderPath string `json:"folderPath"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"}) return } if err := s.collaboration.UnwatchFolder(r.Context(), principal, req.FolderPath); err != nil { s.logger.Warn("unwatch folder", "error", err) writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } writeJSON(w, http.StatusOK, map[string]string{"status": "unwatched"}) } func (s *Server) handleIsWatching(w http.ResponseWriter, r *http.Request) { principal, ok := requirePrincipal(r) if !ok { writeJSON(w, http.StatusOK, map[string]bool{"watching": false}) return } documentID := r.URL.Query().Get("document_id") watching, err := s.collaboration.IsWatching(r.Context(), principal, documentID) if err != nil { s.logger.Warn("is watching", "error", err) writeJSONWithStatus(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } writeJSON(w, http.StatusOK, map[string]bool{"watching": watching}) } // Document page with comments func (s *Server) renderDocumentPageWithComments(w http.ResponseWriter, r *http.Request, pagePath string) { // This is a helper that could be used to inject comments into the template // For now, comments are fetched client-side via JS s.renderDocumentPage(w, r, pagePath) }