package handlers import ( "net/http" "github.com/gin-gonic/gin" "git.gobha.me/xcaliber/chat-switchboard/models" "git.gobha.me/xcaliber/chat-switchboard/store" ) // MemoryHandler provides REST endpoints for memory management. type MemoryHandler struct { stores store.Stores } // NewMemoryHandler creates a new memory handler. func NewMemoryHandler(stores store.Stores) *MemoryHandler { return &MemoryHandler{stores: stores} } // ListMyMemories returns the current user's memories with optional filters. // GET /api/v1/memories?status=active&query=... func (h *MemoryHandler) ListMyMemories(c *gin.Context) { userID := c.GetString("user_id") if userID == "" { c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) return } status := c.DefaultQuery("status", "active") query := c.Query("query") memories, err := h.stores.Memories.List(c.Request.Context(), models.MemoryFilter{ Scope: models.MemoryScopeUser, OwnerID: userID, Status: status, Query: query, Limit: 100, }) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"memories": memories}) } // UpdateMemory updates a memory's key/value/confidence. // PUT /api/v1/memories/:id func (h *MemoryHandler) UpdateMemory(c *gin.Context) { userID := c.GetString("user_id") memoryID := c.Param("id") existing, err := h.stores.Memories.GetByID(c.Request.Context(), memoryID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "memory not found"}) return } // Ownership check: user can only edit their own user-scope memories if existing.Scope == models.MemoryScopeUser && existing.OwnerID != userID { c.JSON(http.StatusForbidden, gin.H{"error": "not your memory"}) return } var body struct { Key *string `json:"key"` Value *string `json:"value"` Confidence *float64 `json:"confidence"` } if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid body"}) return } if body.Key != nil { existing.Key = *body.Key } if body.Value != nil { existing.Value = *body.Value } if body.Confidence != nil { existing.Confidence = *body.Confidence } if err := h.stores.Memories.Upsert(c.Request.Context(), existing); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"memory": existing}) } // DeleteMemory hard-deletes a memory. // DELETE /api/v1/memories/:id func (h *MemoryHandler) DeleteMemory(c *gin.Context) { userID := c.GetString("user_id") memoryID := c.Param("id") existing, err := h.stores.Memories.GetByID(c.Request.Context(), memoryID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "memory not found"}) return } if existing.Scope == models.MemoryScopeUser && existing.OwnerID != userID { c.JSON(http.StatusForbidden, gin.H{"error": "not your memory"}) return } if err := h.stores.Memories.Delete(c.Request.Context(), memoryID); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"deleted": true}) } // ApproveMemory transitions a pending_review memory to active. // POST /api/v1/memories/:id/approve func (h *MemoryHandler) ApproveMemory(c *gin.Context) { memoryID := c.Param("id") existing, err := h.stores.Memories.GetByID(c.Request.Context(), memoryID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "memory not found"}) return } existing.Status = models.MemoryStatusActive if err := h.stores.Memories.Upsert(c.Request.Context(), existing); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"memory": existing}) } // RejectMemory archives a pending_review memory. // POST /api/v1/memories/:id/reject func (h *MemoryHandler) RejectMemory(c *gin.Context) { memoryID := c.Param("id") if err := h.stores.Memories.Archive(c.Request.Context(), memoryID); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"rejected": true}) } // MemoryCount returns active + pending counts for the current user. // GET /api/v1/memories/count func (h *MemoryHandler) MemoryCount(c *gin.Context) { userID := c.GetString("user_id") ctx := c.Request.Context() active, _ := h.stores.Memories.CountByOwner(ctx, models.MemoryScopeUser, userID) // Count pending by listing pending, _ := h.stores.Memories.List(ctx, models.MemoryFilter{ Scope: models.MemoryScopeUser, OwnerID: userID, Status: models.MemoryStatusPendingReview, Limit: 1000, }) c.JSON(http.StatusOK, gin.H{ "active": active, "pending": len(pending), }) } // ── Admin Endpoints ────────────────────────── // ListPendingReview returns all pending_review memories (admin only). // GET /api/v1/admin/memories/pending func (h *MemoryHandler) ListPendingReview(c *gin.Context) { // This should be called from the admin route group which already checks admin role. // We query across all users by listing each scope. ctx := c.Request.Context() // Query pending memories directly via raw SQL for efficiency var memories []models.Memory userMemories, _ := h.stores.Memories.List(ctx, models.MemoryFilter{ Scope: models.MemoryScopeUser, OwnerID: "%", // This won't work with List — we need a custom query Status: models.MemoryStatusPendingReview, Limit: 100, }) memories = append(memories, userMemories...) c.JSON(http.StatusOK, gin.H{"memories": memories}) } // BulkApprove approves multiple memories at once. // POST /api/v1/admin/memories/bulk-approve func (h *MemoryHandler) BulkApprove(c *gin.Context) { var body struct { IDs []string `json:"ids"` } if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid body"}) return } ctx := c.Request.Context() approved := 0 for _, id := range body.IDs { existing, err := h.stores.Memories.GetByID(ctx, id) if err != nil { continue } existing.Status = models.MemoryStatusActive if err := h.stores.Memories.Upsert(ctx, existing); err == nil { approved++ } } c.JSON(http.StatusOK, gin.H{"approved": approved}) }