From 4004817d696a6d9191f564dfc300f822c0594966 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 12 Jan 2026 22:06:29 +0000 Subject: [PATCH 01/10] Initial plan From dff559c12d1b535b619bc8ffb8ef934f18e01de1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 12 Jan 2026 22:10:50 +0000 Subject: [PATCH 02/10] Add standardized API response structure and update all route handlers Co-authored-by: mudler <2420543+mudler@users.noreply.github.com> --- routes.go | 165 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 128 insertions(+), 37 deletions(-) diff --git a/routes.go b/routes.go index f6f96dc..1d84641 100644 --- a/routes.go +++ b/routes.go @@ -20,6 +20,49 @@ type collectionList map[string]*rag.PersistentKB var collections = collectionList{} +// APIResponse represents a standardized API response +type APIResponse struct { + Success bool `json:"success"` + Message string `json:"message,omitempty"` + Data interface{} `json:"data,omitempty"` + Error *APIError `json:"error,omitempty"` +} + +// APIError represents a detailed error response +type APIError struct { + Code string `json:"code"` + Message string `json:"message"` + Details string `json:"details,omitempty"` +} + +// Error codes +const ( + ErrCodeNotFound = "NOT_FOUND" + ErrCodeInvalidRequest = "INVALID_REQUEST" + ErrCodeInternalError = "INTERNAL_ERROR" + ErrCodeUnauthorized = "UNAUTHORIZED" + ErrCodeConflict = "CONFLICT" +) + +func successResponse(message string, data interface{}) APIResponse { + return APIResponse{ + Success: true, + Message: message, + Data: data, + } +} + +func errorResponse(code string, message string, details string) APIResponse { + return APIResponse{ + Success: false, + Error: &APIError{ + Code: code, + Message: message, + Details: details, + }, + } +} + func newVectorEngine( vectorEngineType string, llmClient *openai.Client, @@ -73,7 +116,7 @@ func registerAPIRoutes(e *echo.Echo, openAIClient *openai.Client, maxChunkingSiz } } - return c.JSON(http.StatusUnauthorized, errorMessage("Unauthorized")) + return c.JSON(http.StatusUnauthorized, errorResponse(ErrCodeUnauthorized, "Unauthorized", "Invalid or missing API key")) } }) } @@ -99,7 +142,7 @@ func createCollection(collections collectionList, client *openai.Client, embeddi r := new(request) if err := c.Bind(r); err != nil { - return c.JSON(http.StatusBadRequest, errorMessage("Invalid request")) + return c.JSON(http.StatusBadRequest, errorResponse(ErrCodeInvalidRequest, "Invalid request", err.Error())) } collection := newVectorEngine(vectorEngine, client, openAIBaseURL, openAIKey, r.Name, collectionDBPath, embeddingModel, maxChunkingSize) @@ -108,7 +151,11 @@ func createCollection(collections collectionList, client *openai.Client, embeddi // Register the new collection with the source manager sourceManager.RegisterCollection(r.Name, collection) - return c.JSON(http.StatusCreated, collection) + response := successResponse("Collection created successfully", map[string]interface{}{ + "name": r.Name, + "created_at": time.Now().Format(time.RFC3339), + }) + return c.JSON(http.StatusCreated, response) } } @@ -117,7 +164,7 @@ func deleteEntryFromCollection(collections collectionList) func(c echo.Context) name := c.Param("name") collection, exists := collections[name] if !exists { - return c.JSON(http.StatusNotFound, errorMessage("Collection not found")) + return c.JSON(http.StatusNotFound, errorResponse(ErrCodeNotFound, "Collection not found", fmt.Sprintf("Collection '%s' does not exist", name))) } type request struct { @@ -126,14 +173,20 @@ func deleteEntryFromCollection(collections collectionList) func(c echo.Context) r := new(request) if err := c.Bind(r); err != nil { - return c.JSON(http.StatusBadRequest, errorMessage("Invalid request")) + return c.JSON(http.StatusBadRequest, errorResponse(ErrCodeInvalidRequest, "Invalid request", err.Error())) } if err := collection.RemoveEntry(r.Entry); err != nil { - return c.JSON(http.StatusInternalServerError, errorMessage("Failed to remove entry: "+err.Error())) + return c.JSON(http.StatusInternalServerError, errorResponse(ErrCodeInternalError, "Failed to remove entry", err.Error())) } - return c.JSON(http.StatusOK, collection.ListDocuments()) + remainingEntries := collection.ListDocuments() + response := successResponse("Entry deleted successfully", map[string]interface{}{ + "deleted_entry": r.Entry, + "remaining_entries": remainingEntries, + "entry_count": len(remainingEntries), + }) + return c.JSON(http.StatusOK, response) } } @@ -142,16 +195,20 @@ func reset(collections collectionList) func(c echo.Context) error { name := c.Param("name") collection, exists := collections[name] if !exists { - return c.JSON(http.StatusNotFound, errorMessage("Collection not found")) + return c.JSON(http.StatusNotFound, errorResponse(ErrCodeNotFound, "Collection not found", fmt.Sprintf("Collection '%s' does not exist", name))) } if err := collection.Reset(); err != nil { - return c.JSON(http.StatusInternalServerError, errorMessage("Failed to reset collection: "+err.Error())) + return c.JSON(http.StatusInternalServerError, errorResponse(ErrCodeInternalError, "Failed to reset collection", err.Error())) } delete(collections, name) - return nil + response := successResponse("Collection reset successfully", map[string]interface{}{ + "collection": name, + "reset_at": time.Now().Format(time.RFC3339), + }) + return c.JSON(http.StatusOK, response) } } @@ -160,7 +217,7 @@ func search(collections collectionList) func(c echo.Context) error { name := c.Param("name") collection, exists := collections[name] if !exists { - return c.JSON(http.StatusNotFound, errorMessage("Collection not found")) + return c.JSON(http.StatusNotFound, errorResponse(ErrCodeNotFound, "Collection not found", fmt.Sprintf("Collection '%s' does not exist", name))) } type request struct { @@ -170,11 +227,9 @@ func search(collections collectionList) func(c echo.Context) error { r := new(request) if err := c.Bind(r); err != nil { - return c.JSON(http.StatusBadRequest, errorMessage("Invalid request")) + return c.JSON(http.StatusBadRequest, errorResponse(ErrCodeInvalidRequest, "Invalid request", err.Error())) } - fmt.Println(r) - if r.MaxResults == 0 { if len(collection.ListDocuments()) >= 5 { r.MaxResults = 5 @@ -185,10 +240,16 @@ func search(collections collectionList) func(c echo.Context) error { results, err := collection.Search(r.Query, r.MaxResults) if err != nil { - return c.JSON(http.StatusInternalServerError, errorMessage("Failed to search collection: "+err.Error())) + return c.JSON(http.StatusInternalServerError, errorResponse(ErrCodeInternalError, "Failed to search collection", err.Error())) } - return c.JSON(http.StatusOK, results) + response := successResponse("Search completed successfully", map[string]interface{}{ + "query": r.Query, + "max_results": r.MaxResults, + "results": results, + "count": len(results), + }) + return c.JSON(http.StatusOK, response) } } @@ -201,10 +262,16 @@ func listFiles(collections collectionList) func(c echo.Context) error { name := c.Param("name") collection, exists := collections[name] if !exists { - return c.JSON(http.StatusNotFound, errorMessage("Collection not found")) + return c.JSON(http.StatusNotFound, errorResponse(ErrCodeNotFound, "Collection not found", fmt.Sprintf("Collection '%s' does not exist", name))) } - return c.JSON(http.StatusOK, collection.ListDocuments()) + entries := collection.ListDocuments() + response := successResponse("Entries retrieved successfully", map[string]interface{}{ + "collection": name, + "entries": entries, + "count": len(entries), + }) + return c.JSON(http.StatusOK, response) } } @@ -215,19 +282,19 @@ func uploadFile(collections collectionList, fileAssets string) func(c echo.Conte collection, exists := collections[name] if !exists { xlog.Error("Collection not found") - return c.JSON(http.StatusNotFound, errorMessage("Collection not found")) + return c.JSON(http.StatusNotFound, errorResponse(ErrCodeNotFound, "Collection not found", fmt.Sprintf("Collection '%s' does not exist", name))) } file, err := c.FormFile("file") if err != nil { xlog.Error("Failed to read file", err) - return c.JSON(http.StatusBadRequest, errorMessage("Failed to read file: "+err.Error())) + return c.JSON(http.StatusBadRequest, errorResponse(ErrCodeInvalidRequest, "Failed to read file", err.Error())) } f, err := file.Open() if err != nil { xlog.Error("Failed to open file", err) - return c.JSON(http.StatusBadRequest, errorMessage("Failed to open file: "+err.Error())) + return c.JSON(http.StatusBadRequest, errorResponse(ErrCodeInvalidRequest, "Failed to open file", err.Error())) } defer f.Close() @@ -235,35 +302,45 @@ func uploadFile(collections collectionList, fileAssets string) func(c echo.Conte out, err := os.Create(filePath) if err != nil { xlog.Error("Failed to create file", err) - return c.JSON(http.StatusInternalServerError, errorMessage("Failed to create file "+err.Error())) + return c.JSON(http.StatusInternalServerError, errorResponse(ErrCodeInternalError, "Failed to create file", err.Error())) } defer out.Close() _, err = io.Copy(out, f) if err != nil { xlog.Error("Failed to copy file", err) - return c.JSON(http.StatusInternalServerError, errorMessage("Failed to copy file: "+err.Error())) + return c.JSON(http.StatusInternalServerError, errorResponse(ErrCodeInternalError, "Failed to copy file", err.Error())) } if collection.EntryExists(file.Filename) { xlog.Info("Entry already exists") - return c.JSON(http.StatusBadRequest, errorMessage("Entry already exists")) + return c.JSON(http.StatusBadRequest, errorResponse(ErrCodeConflict, "Entry already exists", fmt.Sprintf("File '%s' has already been uploaded to collection '%s'", file.Filename, name))) } // Save the file to disk err = collection.Store(filePath, map[string]string{}) if err != nil { xlog.Error("Failed to store file", err) - return c.JSON(http.StatusInternalServerError, errorMessage("Failed to store file: "+err.Error())) + return c.JSON(http.StatusInternalServerError, errorResponse(ErrCodeInternalError, "Failed to store file", err.Error())) } - return c.JSON(http.StatusOK, collection) + response := successResponse("File uploaded successfully", map[string]interface{}{ + "filename": file.Filename, + "collection": name, + "uploaded_at": time.Now().Format(time.RFC3339), + }) + return c.JSON(http.StatusOK, response) } } // listCollections returns all collections func listCollections(c echo.Context) error { - return c.JSON(http.StatusOK, rag.ListAllCollections(collectionDBPath)) + collectionsList := rag.ListAllCollections(collectionDBPath) + response := successResponse("Collections retrieved successfully", map[string]interface{}{ + "collections": collectionsList, + "count": len(collectionsList), + }) + return c.JSON(http.StatusOK, response) } // registerExternalSource handles registering an external source for a collection @@ -272,7 +349,7 @@ func registerExternalSource(collections collectionList) func(c echo.Context) err name := c.Param("name") collection, exists := collections[name] if !exists { - return c.JSON(http.StatusNotFound, errorMessage("Collection not found")) + return c.JSON(http.StatusNotFound, errorResponse(ErrCodeNotFound, "Collection not found", fmt.Sprintf("Collection '%s' does not exist", name))) } type request struct { @@ -282,7 +359,7 @@ func registerExternalSource(collections collectionList) func(c echo.Context) err r := new(request) if err := c.Bind(r); err != nil { - return c.JSON(http.StatusBadRequest, errorMessage("Invalid request")) + return c.JSON(http.StatusBadRequest, errorResponse(ErrCodeInvalidRequest, "Invalid request", err.Error())) } if r.UpdateInterval < 1 { @@ -294,10 +371,15 @@ func registerExternalSource(collections collectionList) func(c echo.Context) err // Add the source to the manager if err := sourceManager.AddSource(name, r.URL, time.Duration(r.UpdateInterval)*time.Minute); err != nil { - return c.JSON(http.StatusInternalServerError, errorMessage("Failed to register source: "+err.Error())) + return c.JSON(http.StatusInternalServerError, errorResponse(ErrCodeInternalError, "Failed to register source", err.Error())) } - return c.JSON(http.StatusOK, map[string]string{"message": "External source registered successfully"}) + response := successResponse("External source registered successfully", map[string]interface{}{ + "collection": name, + "url": r.URL, + "update_interval": r.UpdateInterval, + }) + return c.JSON(http.StatusOK, response) } } @@ -312,14 +394,18 @@ func removeExternalSource(collections collectionList) func(c echo.Context) error r := new(request) if err := c.Bind(r); err != nil { - return c.JSON(http.StatusBadRequest, errorMessage("Invalid request")) + return c.JSON(http.StatusBadRequest, errorResponse(ErrCodeInvalidRequest, "Invalid request", err.Error())) } if err := sourceManager.RemoveSource(name, r.URL); err != nil { - return c.JSON(http.StatusInternalServerError, errorMessage("Failed to remove source: "+err.Error())) + return c.JSON(http.StatusInternalServerError, errorResponse(ErrCodeInternalError, "Failed to remove source", err.Error())) } - return c.JSON(http.StatusOK, map[string]string{"message": "External source removed successfully"}) + response := successResponse("External source removed successfully", map[string]interface{}{ + "collection": name, + "url": r.URL, + }) + return c.JSON(http.StatusOK, response) } } @@ -329,22 +415,27 @@ func listSources(collections collectionList) func(c echo.Context) error { name := c.Param("name") collection, exists := collections[name] if !exists { - return c.JSON(http.StatusNotFound, errorMessage("Collection not found")) + return c.JSON(http.StatusNotFound, errorResponse(ErrCodeNotFound, "Collection not found", fmt.Sprintf("Collection '%s' does not exist", name))) } // Get sources from the collection sources := collection.GetExternalSources() // Convert sources to a more frontend-friendly format - response := []map[string]interface{}{} + sourcesList := []map[string]interface{}{} for _, source := range sources { - response = append(response, map[string]interface{}{ + sourcesList = append(sourcesList, map[string]interface{}{ "url": source.URL, "update_interval": int(source.UpdateInterval.Minutes()), "last_update": source.LastUpdate.Format(time.RFC3339), }) } + response := successResponse("Sources retrieved successfully", map[string]interface{}{ + "collection": name, + "sources": sourcesList, + "count": len(sourcesList), + }) return c.JSON(http.StatusOK, response) } } From 376b4ecc1c9c68f2679de546d9c163999cc8ab90 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 12 Jan 2026 22:12:40 +0000 Subject: [PATCH 03/10] Update frontend to handle standardized API responses Co-authored-by: mudler <2420543+mudler@users.noreply.github.com> --- static/js/collectionManager.js | 141 ++++++++++++++++----------------- 1 file changed, 69 insertions(+), 72 deletions(-) diff --git a/static/js/collectionManager.js b/static/js/collectionManager.js index 466613c..e85afba 100644 --- a/static/js/collectionManager.js +++ b/static/js/collectionManager.js @@ -6,6 +6,20 @@ function appRouter() { mobileMenuOpen: false, collections: [], + // Utility function to handle API responses consistently + handleAPIResponse(response) { + return response.json().then(data => { + if (!response.ok || (data.success === false)) { + // Extract error details + const error = new Error(data.error?.message || 'Operation failed'); + error.code = data.error?.code; + error.details = data.error?.details; + throw error; + } + return data; + }); + }, + navigate(page) { this.currentPage = page; // Update URL hash for bookmarkable pages @@ -46,14 +60,13 @@ function appRouter() { fetchCollections() { return fetch('/api/collections') - .then(response => { - if (!response.ok) throw new Error('Failed to fetch collections'); - return response.json(); - }) + .then(response => this.handleAPIResponse(response)) .then(data => { - if (Array.isArray(data)) { - this.collections = data; - return data; + // Extract collections from the data field + const collectionsList = data.data?.collections || []; + if (Array.isArray(collectionsList)) { + this.collections = collectionsList; + return collectionsList; } else { this.collections = []; console.error('collections data:', data); @@ -62,7 +75,7 @@ function appRouter() { }) .catch(error => { console.error('Error fetching collections:', error); - this.showToast('error', 'Failed to fetch collections'); + this.showToast('error', error.message || 'Failed to fetch collections'); return []; }); }, @@ -137,6 +150,7 @@ function searchPage() { const now = new Date(); this.searchTimestamp = now.toISOString().replace('T', ' ').substring(0, 19); + const router = getRouter(); fetch(`/api/collections/${this.selectedSearchCollection}/search`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -145,25 +159,14 @@ function searchPage() { max_results: maxResultsVal }) }) - .then(response => { - if (!response.ok) { - return response.text().then(text => { - try { - const data = JSON.parse(text); - throw new Error(data.error || data.message || 'Search failed with status: ' + response.status); - } catch (e) { - throw new Error(text || 'Search failed with status: ' + response.status); - } - }); - } - return response.json(); - }) + .then(response => router.handleAPIResponse(response)) .then(data => { - if (data.length === 0) { + const results = data.data?.results || []; + if (results.length === 0) { this.searchResults = ['No results found for query: "' + this.searchQuery + '"']; return; } - this.searchResults = data.map(item => JSON.stringify(item, null, 2)); + this.searchResults = results.map(item => JSON.stringify(item, null, 2)); }) .catch(error => { console.error('Error searching collection:', error); @@ -204,23 +207,21 @@ function collectionsPage() { } this.loading.create = true; + const router = getRouter(); fetch('/api/collections', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: this.newCollectionName }) }) - .then(response => { - if (!response.ok) throw new Error('Failed to create collection'); - return response.json(); - }) - .then(() => { - this.showToast('success', `Collection "${this.newCollectionName}" created successfully`); + .then(response => router.handleAPIResponse(response)) + .then(data => { + this.showToast('success', data.message || `Collection "${this.newCollectionName}" created successfully`); this.newCollectionName = ''; this.fetchCollections(); }) .catch(error => { console.error('Error creating collection:', error); - this.showToast('error', 'Failed to create collection'); + this.showToast('error', error.message || 'Failed to create collection'); }) .finally(() => { this.loading.create = false; @@ -262,18 +263,19 @@ function collectionsPage() { resetCollection(collectionName) { this.loading.reset = collectionName; + const router = getRouter(); fetch(`/api/collections/${collectionName}/reset`, { method: 'POST', headers: { 'Content-Type': 'application/json' } }) - .then(response => { - if (!response.ok) throw new Error('Reset failed'); - this.showToast('success', `Collection "${collectionName}" has been reset successfully`); + .then(response => router.handleAPIResponse(response)) + .then(data => { + this.showToast('success', data.message || `Collection "${collectionName}" has been reset successfully`); this.fetchCollections(); }) .catch(error => { console.error('Error resetting collection:', error); - this.showToast('error', `Failed to reset collection: ${error.message}`); + this.showToast('error', error.message || `Failed to reset collection`); }) .finally(() => { this.loading.reset = false; @@ -320,19 +322,20 @@ function uploadPage() { formData.append('file', fileInput.files[0]); this.loading.upload = true; + const router = getRouter(); fetch(`/api/collections/${this.selectedCollection}/upload`, { method: 'POST', body: formData }) - .then(response => { - if (!response.ok) throw new Error('Upload failed'); - this.showToast('success', 'File uploaded successfully'); + .then(response => router.handleAPIResponse(response)) + .then(data => { + this.showToast('success', data.message || 'File uploaded successfully'); fileInput.value = ''; this.fileName = ''; }) .catch(error => { console.error('Error uploading file:', error); - this.showToast('error', 'Failed to upload file'); + this.showToast('error', error.message || 'Failed to upload file'); }) .finally(() => { this.loading.upload = false; @@ -370,17 +373,15 @@ function sourcesPage() { this.loading.sources = true; this.sources = []; + const router = getRouter(); fetch(`/api/collections/${this.selectedSourceCollection}/sources`) - .then(response => { - if (!response.ok) throw new Error('Failed to list sources'); - return response.json(); - }) + .then(response => router.handleAPIResponse(response)) .then(data => { - this.sources = data; + this.sources = data.data?.sources || []; }) .catch(error => { console.error('Error listing sources:', error); - this.showToast('error', 'Failed to fetch sources'); + this.showToast('error', error.message || 'Failed to fetch sources'); }) .finally(() => { this.loading.sources = false; @@ -404,6 +405,7 @@ function sourcesPage() { } this.loading.addSource = true; + const router = getRouter(); fetch(`/api/collections/${this.selectedSourceCollection}/sources`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -412,19 +414,16 @@ function sourcesPage() { update_interval: interval }) }) - .then(response => { - if (!response.ok) throw new Error('Failed to add source'); - return response.json(); - }) - .then(() => { - this.showToast('success', 'Source added successfully'); + .then(response => router.handleAPIResponse(response)) + .then(data => { + this.showToast('success', data.message || 'Source added successfully'); this.newSourceURL = ''; this.newSourceInterval = 60; this.listSources(); }) .catch(error => { console.error('Error adding source:', error); - this.showToast('error', 'Failed to add source'); + this.showToast('error', error.message || 'Failed to add source'); }) .finally(() => { this.loading.addSource = false; @@ -438,22 +437,20 @@ function sourcesPage() { } this.loading.removeSource = url; + const router = getRouter(); fetch(`/api/collections/${this.selectedSourceCollection}/sources`, { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url: url }) }) - .then(response => { - if (!response.ok) throw new Error('Failed to remove source'); - return response.json(); - }) - .then(() => { - this.showToast('success', 'Source removed successfully'); + .then(response => router.handleAPIResponse(response)) + .then(data => { + this.showToast('success', data.message || 'Source removed successfully'); this.listSources(); }) .catch(error => { console.error('Error removing source:', error); - this.showToast('error', 'Failed to remove source'); + this.showToast('error', error.message || 'Failed to remove source'); }) .finally(() => { this.loading.removeSource = false; @@ -488,17 +485,15 @@ function entriesPage() { this.loading.entries = true; this.entries = []; + const router = getRouter(); fetch(`/api/collections/${this.selectedListCollection}/entries`) - .then(response => { - if (!response.ok) throw new Error('Failed to list entries'); - return response.json(); - }) + .then(response => router.handleAPIResponse(response)) .then(data => { - this.entries = data; + this.entries = data.data?.entries || []; }) .catch(error => { console.error('Error listing entries:', error); - this.showToast('error', 'Failed to fetch entries'); + this.showToast('error', error.message || 'Failed to fetch entries'); }) .finally(() => { this.loading.entries = false; @@ -512,19 +507,20 @@ function entriesPage() { } this.loading.delete = entry; + const router = getRouter(); fetch(`/api/collections/${this.selectedListCollection}/entry/delete`, { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ entry: entry }) }) - .then(response => { - if (!response.ok) throw new Error('Deletion failed'); - this.showToast('success', 'Entry deleted successfully'); + .then(response => router.handleAPIResponse(response)) + .then(data => { + this.showToast('success', data.message || 'Entry deleted successfully'); this.listEntries(); }) .catch(error => { console.error('Error deleting entry:', error); - this.showToast('error', 'Failed to delete entry'); + this.showToast('error', error.message || 'Failed to delete entry'); }) .finally(() => { this.loading.delete = false; @@ -556,20 +552,21 @@ function entriesPage() { resetCollection(collectionName) { this.loading.reset = collectionName; + const router = getRouter(); fetch(`/api/collections/${collectionName}/reset`, { method: 'POST', headers: { 'Content-Type': 'application/json' } }) - .then(response => { - if (!response.ok) throw new Error('Reset failed'); - this.showToast('success', `Collection "${collectionName}" has been reset successfully`); + .then(response => router.handleAPIResponse(response)) + .then(data => { + this.showToast('success', data.message || `Collection "${collectionName}" has been reset successfully`); if (collectionName === this.selectedListCollection) { this.listEntries(); } }) .catch(error => { console.error('Error resetting collection:', error); - this.showToast('error', `Failed to reset collection: ${error.message}`); + this.showToast('error', error.message || `Failed to reset collection`); }) .finally(() => { this.loading.reset = false; From a2e9d854c67056eef6124b2ed4a6adce1af70e2d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 12 Jan 2026 22:17:05 +0000 Subject: [PATCH 04/10] Remove unused errorMessage function Co-authored-by: mudler <2420543+mudler@users.noreply.github.com> --- routes.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/routes.go b/routes.go index 1d84641..a124fa1 100644 --- a/routes.go +++ b/routes.go @@ -253,10 +253,6 @@ func search(collections collectionList) func(c echo.Context) error { } } -func errorMessage(message string) map[string]string { - return map[string]string{"error": message} -} - func listFiles(collections collectionList) func(c echo.Context) error { return func(c echo.Context) error { name := c.Param("name") From 54b4f1d98186a83cb65295a46f7d708212500a1d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 12 Jan 2026 22:18:59 +0000 Subject: [PATCH 05/10] Make handleAPIResponse a global utility function for better code organization Co-authored-by: mudler <2420543+mudler@users.noreply.github.com> --- static/js/collectionManager.js | 75 ++++++++++++++++++++-------------- 1 file changed, 44 insertions(+), 31 deletions(-) diff --git a/static/js/collectionManager.js b/static/js/collectionManager.js index e85afba..5b3b296 100644 --- a/static/js/collectionManager.js +++ b/static/js/collectionManager.js @@ -117,6 +117,20 @@ function getRouter() { return routerElement ? Alpine.$data(routerElement) : null; } +// Utility function to handle API responses consistently +function handleAPIResponse(response) { + return response.json().then(data => { + if (!response.ok || (data.success === false)) { + // Extract error details + const error = new Error(data.error?.message || 'Operation failed'); + error.code = data.error?.code; + error.details = data.error?.details; + throw error; + } + return data; + }); +} + // Search Page Component function searchPage() { return { @@ -131,7 +145,7 @@ function searchPage() { }, get collections() { - const router = getRouter(); + return router ? router.collections : []; }, @@ -150,7 +164,6 @@ function searchPage() { const now = new Date(); this.searchTimestamp = now.toISOString().replace('T', ' ').substring(0, 19); - const router = getRouter(); fetch(`/api/collections/${this.selectedSearchCollection}/search`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -159,7 +172,7 @@ function searchPage() { max_results: maxResultsVal }) }) - .then(response => router.handleAPIResponse(response)) + .then(response => handleAPIResponse(response)) .then(data => { const results = data.data?.results || []; if (results.length === 0) { @@ -179,7 +192,7 @@ function searchPage() { }, showToast(type, message) { - const router = getRouter(); + if (router) router.showToast(type, message); } }; @@ -196,7 +209,7 @@ function collectionsPage() { }, get collections() { - const router = getRouter(); + return router ? router.collections : []; }, @@ -207,13 +220,13 @@ function collectionsPage() { } this.loading.create = true; - const router = getRouter(); + fetch('/api/collections', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: this.newCollectionName }) }) - .then(response => router.handleAPIResponse(response)) + .then(response => handleAPIResponse(response)) .then(data => { this.showToast('success', data.message || `Collection "${this.newCollectionName}" created successfully`); this.newCollectionName = ''; @@ -229,7 +242,7 @@ function collectionsPage() { }, fetchCollections() { - const router = getRouter(); + if (router) { this.loading.collections = true; router.fetchCollections().finally(() => { @@ -263,12 +276,12 @@ function collectionsPage() { resetCollection(collectionName) { this.loading.reset = collectionName; - const router = getRouter(); + fetch(`/api/collections/${collectionName}/reset`, { method: 'POST', headers: { 'Content-Type': 'application/json' } }) - .then(response => router.handleAPIResponse(response)) + .then(response => handleAPIResponse(response)) .then(data => { this.showToast('success', data.message || `Collection "${collectionName}" has been reset successfully`); this.fetchCollections(); @@ -283,7 +296,7 @@ function collectionsPage() { }, showToast(type, message) { - const router = getRouter(); + if (router) router.showToast(type, message); }, @@ -303,7 +316,7 @@ function uploadPage() { }, get collections() { - const router = getRouter(); + return router ? router.collections : []; }, @@ -322,12 +335,12 @@ function uploadPage() { formData.append('file', fileInput.files[0]); this.loading.upload = true; - const router = getRouter(); + fetch(`/api/collections/${this.selectedCollection}/upload`, { method: 'POST', body: formData }) - .then(response => router.handleAPIResponse(response)) + .then(response => handleAPIResponse(response)) .then(data => { this.showToast('success', data.message || 'File uploaded successfully'); fileInput.value = ''; @@ -343,7 +356,7 @@ function uploadPage() { }, showToast(type, message) { - const router = getRouter(); + if (router) router.showToast(type, message); } }; @@ -363,7 +376,7 @@ function sourcesPage() { }, get collections() { - const router = getRouter(); + return router ? router.collections : []; }, @@ -373,9 +386,9 @@ function sourcesPage() { this.loading.sources = true; this.sources = []; - const router = getRouter(); + fetch(`/api/collections/${this.selectedSourceCollection}/sources`) - .then(response => router.handleAPIResponse(response)) + .then(response => handleAPIResponse(response)) .then(data => { this.sources = data.data?.sources || []; }) @@ -405,7 +418,7 @@ function sourcesPage() { } this.loading.addSource = true; - const router = getRouter(); + fetch(`/api/collections/${this.selectedSourceCollection}/sources`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -414,7 +427,7 @@ function sourcesPage() { update_interval: interval }) }) - .then(response => router.handleAPIResponse(response)) + .then(response => handleAPIResponse(response)) .then(data => { this.showToast('success', data.message || 'Source added successfully'); this.newSourceURL = ''; @@ -437,13 +450,13 @@ function sourcesPage() { } this.loading.removeSource = url; - const router = getRouter(); + fetch(`/api/collections/${this.selectedSourceCollection}/sources`, { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url: url }) }) - .then(response => router.handleAPIResponse(response)) + .then(response => handleAPIResponse(response)) .then(data => { this.showToast('success', data.message || 'Source removed successfully'); this.listSources(); @@ -458,7 +471,7 @@ function sourcesPage() { }, showToast(type, message) { - const router = getRouter(); + if (router) router.showToast(type, message); } }; @@ -476,7 +489,7 @@ function entriesPage() { }, get collections() { - const router = getRouter(); + return router ? router.collections : []; }, @@ -485,9 +498,9 @@ function entriesPage() { this.loading.entries = true; this.entries = []; - const router = getRouter(); + fetch(`/api/collections/${this.selectedListCollection}/entries`) - .then(response => router.handleAPIResponse(response)) + .then(response => handleAPIResponse(response)) .then(data => { this.entries = data.data?.entries || []; }) @@ -507,13 +520,13 @@ function entriesPage() { } this.loading.delete = entry; - const router = getRouter(); + fetch(`/api/collections/${this.selectedListCollection}/entry/delete`, { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ entry: entry }) }) - .then(response => router.handleAPIResponse(response)) + .then(response => handleAPIResponse(response)) .then(data => { this.showToast('success', data.message || 'Entry deleted successfully'); this.listEntries(); @@ -552,12 +565,12 @@ function entriesPage() { resetCollection(collectionName) { this.loading.reset = collectionName; - const router = getRouter(); + fetch(`/api/collections/${collectionName}/reset`, { method: 'POST', headers: { 'Content-Type': 'application/json' } }) - .then(response => router.handleAPIResponse(response)) + .then(response => handleAPIResponse(response)) .then(data => { this.showToast('success', data.message || `Collection "${collectionName}" has been reset successfully`); if (collectionName === this.selectedListCollection) { @@ -574,7 +587,7 @@ function entriesPage() { }, showToast(type, message) { - const router = getRouter(); + if (router) router.showToast(type, message); } }; From b9eab15eb99d7316a290efa811e05478da6aa365 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 12 Jan 2026 22:20:38 +0000 Subject: [PATCH 06/10] Remove duplicate handleAPIResponse function and use only global version Co-authored-by: mudler <2420543+mudler@users.noreply.github.com> --- static/js/collectionManager.js | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/static/js/collectionManager.js b/static/js/collectionManager.js index 5b3b296..e13abe2 100644 --- a/static/js/collectionManager.js +++ b/static/js/collectionManager.js @@ -6,20 +6,6 @@ function appRouter() { mobileMenuOpen: false, collections: [], - // Utility function to handle API responses consistently - handleAPIResponse(response) { - return response.json().then(data => { - if (!response.ok || (data.success === false)) { - // Extract error details - const error = new Error(data.error?.message || 'Operation failed'); - error.code = data.error?.code; - error.details = data.error?.details; - throw error; - } - return data; - }); - }, - navigate(page) { this.currentPage = page; // Update URL hash for bookmarkable pages @@ -60,7 +46,7 @@ function appRouter() { fetchCollections() { return fetch('/api/collections') - .then(response => this.handleAPIResponse(response)) + .then(response => handleAPIResponse(response)) .then(data => { // Extract collections from the data field const collectionsList = data.data?.collections || []; From b2de410f68d8dcd1de78c89efd2b16be52e15ac6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 12 Jan 2026 22:22:05 +0000 Subject: [PATCH 07/10] Fix undefined router variable in showToast methods Co-authored-by: mudler <2420543+mudler@users.noreply.github.com> --- static/js/collectionManager.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/static/js/collectionManager.js b/static/js/collectionManager.js index e13abe2..0f39b4a 100644 --- a/static/js/collectionManager.js +++ b/static/js/collectionManager.js @@ -179,7 +179,7 @@ function searchPage() { showToast(type, message) { - if (router) router.showToast(type, message); + const router = getRouter(); if (router) router.showToast(type, message); } }; } @@ -283,7 +283,7 @@ function collectionsPage() { showToast(type, message) { - if (router) router.showToast(type, message); + const router = getRouter(); if (router) router.showToast(type, message); }, init() { @@ -343,7 +343,7 @@ function uploadPage() { showToast(type, message) { - if (router) router.showToast(type, message); + const router = getRouter(); if (router) router.showToast(type, message); } }; } @@ -458,7 +458,7 @@ function sourcesPage() { showToast(type, message) { - if (router) router.showToast(type, message); + const router = getRouter(); if (router) router.showToast(type, message); } }; } @@ -574,7 +574,7 @@ function entriesPage() { showToast(type, message) { - if (router) router.showToast(type, message); + const router = getRouter(); if (router) router.showToast(type, message); } }; } From c1d58f9955ef4189c0fd72e28d7ca5b5d61a9430 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 12 Jan 2026 22:23:17 +0000 Subject: [PATCH 08/10] Improve code readability by splitting multi-statement lines Co-authored-by: mudler <2420543+mudler@users.noreply.github.com> --- static/js/collectionManager.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/static/js/collectionManager.js b/static/js/collectionManager.js index 0f39b4a..265db9d 100644 --- a/static/js/collectionManager.js +++ b/static/js/collectionManager.js @@ -179,7 +179,8 @@ function searchPage() { showToast(type, message) { - const router = getRouter(); if (router) router.showToast(type, message); + const router = getRouter(); + if (router) router.showToast(type, message); } }; } @@ -283,7 +284,8 @@ function collectionsPage() { showToast(type, message) { - const router = getRouter(); if (router) router.showToast(type, message); + const router = getRouter(); + if (router) router.showToast(type, message); }, init() { @@ -343,7 +345,8 @@ function uploadPage() { showToast(type, message) { - const router = getRouter(); if (router) router.showToast(type, message); + const router = getRouter(); + if (router) router.showToast(type, message); } }; } @@ -458,7 +461,8 @@ function sourcesPage() { showToast(type, message) { - const router = getRouter(); if (router) router.showToast(type, message); + const router = getRouter(); + if (router) router.showToast(type, message); } }; } @@ -574,7 +578,8 @@ function entriesPage() { showToast(type, message) { - const router = getRouter(); if (router) router.showToast(type, message); + const router = getRouter(); + if (router) router.showToast(type, message); } }; } From c9bcd2e80ce13ada8dc9591c30f0da2537c32ad4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 12 Jan 2026 22:24:25 +0000 Subject: [PATCH 09/10] Fix undefined router variable in get collections() methods Co-authored-by: mudler <2420543+mudler@users.noreply.github.com> --- static/js/collectionManager.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/static/js/collectionManager.js b/static/js/collectionManager.js index 265db9d..ef2cee2 100644 --- a/static/js/collectionManager.js +++ b/static/js/collectionManager.js @@ -132,6 +132,7 @@ function searchPage() { get collections() { + const router = getRouter(); return router ? router.collections : []; }, @@ -197,6 +198,7 @@ function collectionsPage() { get collections() { + const router = getRouter(); return router ? router.collections : []; }, @@ -305,6 +307,7 @@ function uploadPage() { get collections() { + const router = getRouter(); return router ? router.collections : []; }, @@ -366,6 +369,7 @@ function sourcesPage() { get collections() { + const router = getRouter(); return router ? router.collections : []; }, @@ -480,6 +484,7 @@ function entriesPage() { get collections() { + const router = getRouter(); return router ? router.collections : []; }, From a325fb669fc697115be859f96646fa295ac1719e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 12 Jan 2026 22:25:27 +0000 Subject: [PATCH 10/10] Fix remaining undefined router variable in fetchCollections Co-authored-by: mudler <2420543+mudler@users.noreply.github.com> --- static/js/collectionManager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/js/collectionManager.js b/static/js/collectionManager.js index ef2cee2..4f3158c 100644 --- a/static/js/collectionManager.js +++ b/static/js/collectionManager.js @@ -231,7 +231,7 @@ function collectionsPage() { }, fetchCollections() { - + const router = getRouter(); if (router) { this.loading.collections = true; router.fetchCollections().finally(() => {