diff --git a/docs/CHATWOOT_PARITY_DEVELOPMENT_PLAN.md b/docs/CHATWOOT_PARITY_DEVELOPMENT_PLAN.md index 6a91947a..de7926c1 100644 --- a/docs/CHATWOOT_PARITY_DEVELOPMENT_PLAN.md +++ b/docs/CHATWOOT_PARITY_DEVELOPMENT_PLAN.md @@ -49,11 +49,11 @@ Hermes task landing checklist: ## Current Baseline -- Current tracking checkpoint: 2026-06-07 P3.88 global search type-scope parity, prepared as `fix(search): scope global result types`. -- Latest implementation checkpoint: this checkpoint, prepared as `fix(search): scope global result types`. -- Latest documentation/tooling checkpoint: this tracker update records Chatwoot global SearchAPI result-type scope parity. No route artifacts change. +- Current tracking checkpoint: 2026-06-07 P3.89 SearchAPI fixed page-size parity, prepared as `fix(search): align search page size`. +- Latest implementation checkpoint: this checkpoint, prepared as `fix(search): align search page size`. +- Latest documentation/tooling checkpoint: this tracker update records Chatwoot SearchAPI fixed `.per(15)` pagination parity. No route artifacts change. - Plan landing status: complete for the current known Hermes plans and user-confirmed scope. Future work should update this file directly instead of opening a parallel tracker. -- Worktree status at this implementation checkpoint: Global SearchAPI now ignores unsupported local `types` expansions and scopes the search backend to Chatwoot's four result families: conversations, contacts, messages, and articles. This retains P3.87 article payloads, P3.86 message payloads, P3.85 conversation payloads, P3.84 agent-sender filter parity, P3.83 conversation ordering, P3.82 contact-conversation reindex fanout, P3.81 conversation identity search, P3.80 search time-bound parity, P3.79 advanced-search feature gates, P3.78 message recency, P3.77 current-user inbox access, and prior checkpoints. Live API/browser/enterprise smoke still needs the full PostgreSQL/Redis/Meilisearch/GoChat/Vite/Chrome stack. +- Worktree status at this implementation checkpoint: SearchAPI handler filters now ignore caller `per_page` and force Chatwoot's fixed `.per(15)` page size before DB fallback or Meilisearch execution. This retains P3.88 global result type scoping, P3.87 article payloads, P3.86 message payloads, P3.85 conversation payloads, P3.84 agent-sender filter parity, P3.83 conversation ordering, P3.82 contact-conversation reindex fanout, P3.81 conversation identity search, P3.80 search time-bound parity, P3.79 advanced-search feature gates, P3.78 message recency, P3.77 current-user inbox access, and prior checkpoints. Live API/browser/enterprise smoke still needs the full PostgreSQL/Redis/Meilisearch/GoChat/Vite/Chrome stack. - Next executable implementation checkpoint: continue Phase 2/3 drift audit for the next reused-frontend mismatch, or run B12 live smoke when the full PostgreSQL/Redis/Meilisearch/GoChat/Vite/Chrome stack is available. Re-run Phase 6 placeholder audit after future route/smoke changes. - `go test ./...` passes when run outside the restricted socket sandbox for the latest implementation baseline; the latest docs/tooling checkpoint verified `scripts/parity_frontend_smoke.sh --check` with workspace-local temp/cache dirs after `/tmp` was full. - Route dump succeeds with `967` registered routes after profile MFA route tracking. @@ -156,6 +156,7 @@ This table is the shortest authoritative handoff view. If an older lower section | Priority | Workstream | Current state | Next checkpoint | Commit close rule | | --- | --- | --- | --- | --- | +| 0 | P3.89 SearchAPI fixed page-size parity | Implemented for reused SearchAPI endpoints: GoChat now ignores local `per_page` query params and forces Chatwoot's fixed `.per(15)` page size for global, conversation, message, contact, and article search before DB fallback or Meilisearch execution. | Keep in Review; reopen from B12 search smoke or fresh reference evidence for endpoint-specific pagination changes, Kaminari default drift, or frontend callers adding explicit per-page support upstream. | Focused SearchHandler pagination tests passed; full `go test ./...` passed outside the restricted socket sandbox; `git diff --check` passed. No route artifacts change. | | 0 | P3.88 global search type-scope parity | Implemented for reused global SearchAPI: `GET /search` now scopes backend searches to the same four result families rendered by Chatwoot `search/index.json.jbuilder` (`conversations`, `contacts`, `messages`, `articles`) and ignores unsupported local `types=company/help_center` expansion before DB fallback or Meilisearch execution. | Keep in Review; reopen from B12 global-search smoke or fresh reference/frontend evidence for additional rendered result families, company search UI integration, or Searchkick all-type behavior drift. | Focused SearchHandler global type-scope tests passed; full `go test ./...` passed outside the restricted socket sandbox; `git diff --check` passed. No route artifacts change. | | 0 | P3.87 search article payload parity | Implemented for reused SearchAPI article cards: DB-backed handlers and Meilisearch documents now carry Chatwoot `_article` `portal_slug` and `category_name` fields, so article search cards can route to `portals/:portalSlug/:locale/articles/edit/:id` and display category names without adapter code. Durable index replay and full reindex preload article portal/category relations. | Keep in Review; reopen from B12 search/help-center smoke or fresh reference evidence for article description payload drift, translated-article locale routing, or category/portal deletion edge cases. | Focused SearchHandler, SearchRepo, DurableSearchIndexer, reindex, and document-builder tests passed; full `go test ./...` passed outside the restricted socket sandbox; `git diff --check` passed. No route artifacts change. | | 0 | P3.86 search message payload parity | Implemented for reused SearchAPI message cards: DB-backed handlers now serialize message search hits through the Chatwoot `_message` contract with conversation display IDs, sender push data, and attachment push data; Meilisearch message documents and reindex/durable indexing paths now carry display-ID and attachment payload data when relations are loaded. | Keep in Review; reopen from B12 search smoke or fresh reference evidence for exact AgentBot sender push data, voice-call `call` payloads, or provider-specific attachment metadata beyond the inspected frontend card usage. | Focused SearchHandler, SearchRepo, DurableSearchIndexer, reindex, and document-builder tests passed; full `go test ./...` passed outside the restricted socket sandbox; `git diff --check` passed. No route artifacts change. | @@ -252,6 +253,7 @@ These rows are the executable development plan from this point forward. A checkp | ID | Owner files | Reference files | Work to land | Exit gate | | --- | --- | --- | --- | --- | +| P3.89 SearchAPI fixed page-size parity | `internal/handler/api/v1/search_handler.go`, `internal/handler/api/v1/search_handler_test.go` | `reference/chatwoot/app/services/search_service.rb`, `reference/chatwoot/app/controllers/api/v1/accounts/search_controller.rb`, `reference/chatwoot/app/javascript/dashboard/api/search.js` | SearchAPI handlers now match Chatwoot `SearchService` pagination by forcing `PerPage = 15` for global, conversation, message, contact, and article search regardless of local `per_page` query params. This keeps DB fallback and Meilisearch searches on the same fixed page-size contract used by every `page(params[:page]).per(15)` branch in the reference. | Review by `fix(search): align search page size`; focused SearchHandler tests prove explicit `per_page` on global/message search is ignored and the backend filter remains `15`; full `go test ./...` passed outside the restricted socket sandbox; `git diff --check` passed. No route artifacts change. | | P3.88 global search type-scope parity | `internal/handler/api/v1/search_handler.go`, `internal/handler/api/v1/search_handler_test.go` | `reference/chatwoot/app/controllers/api/v1/accounts/search_controller.rb`, `reference/chatwoot/app/services/search_service.rb#perform`, `reference/chatwoot/app/views/api/v1/accounts/search/index.json.jbuilder`, `reference/chatwoot/app/javascript/dashboard/api/search.js` | Global SearchAPI now follows Chatwoot's `search('all')` rendered surface by forcing backend execution to `conversations`, `contacts`, `messages`, and `articles` for `GET /search`, ignoring unsupported local `types=company/help_center` expansions before DB fallback or Meilisearch execution. | Review by `fix(search): scope global result types`; focused SearchHandler tests prove unsupported `types=company` does not call company search and the raw payload remains the four Chatwoot result families; full `go test ./...` passed outside the restricted socket sandbox; `git diff --check` passed. No route artifacts change. | | P3.87 search article payload parity | `internal/handler/api/v1/search_handler.go`, `internal/search/engine.go`, `internal/service/search_indexer_worker.go`, `cmd/reindex_search/main.go`, search handler/document/indexer tests | `reference/chatwoot/app/views/api/v1/accounts/search/_article.json.jbuilder`, `articles.json.jbuilder`, `reference/chatwoot/app/javascript/dashboard/modules/search/components/SearchResultArticlesList.vue`, `SearchResultArticleItem.vue` | Article SearchAPI results now match the reused frontend card route/display fields: `portal_slug` is hydrated from the article portal, `category_name` is hydrated from the optional article category, DB-backed handlers reload those relations for result rows and Meilisearch hits, and durable/full reindex paths preload the relations before document creation. | Review by `feat(search): align article result payloads`; focused SearchHandler, SearchRepo, DurableSearchIndexer, reindex, and document-builder tests prove DB-backed payload hydration plus Meilisearch nested payload shape; full `go test ./...` passed outside the restricted socket sandbox; `git diff --check` passed. No route artifacts change. | | P3.86 search message payload parity | `internal/handler/api/v1/search_handler.go`, `internal/search/engine.go`, `internal/service/search_indexer_worker.go`, `cmd/reindex_search/main.go`, search handler/document/indexer tests | `reference/chatwoot/app/views/api/v1/models/_message.json.jbuilder`, `reference/chatwoot/app/models/message.rb#push_event_data`, `reference/chatwoot/app/models/attachment.rb#push_event_data`, `reference/chatwoot/app/javascript/dashboard/modules/search/components/SearchResultMessagesList.vue`, `SearchResultMessageItem.vue` | Message SearchAPI results now match the reused frontend card needs from Chatwoot's `_message` serializer: `conversation_id` is the conversation display ID for navigation, `sender` is hydrated from persisted contact/user rows for DB-backed handlers, attachments use Chatwoot-style push data, and Meilisearch message documents carry display-ID/attachment payload data when relations are preloaded by durable indexing and full reindex. | Review by `feat(search): align message result payloads`; focused SearchHandler, SearchRepo, DurableSearchIndexer, reindex, and document-builder tests prove DB-backed payload hydration plus Meilisearch nested payload shape; full `go test ./...` passed outside the restricted socket sandbox; `git diff --check` passed. No route artifacts change. | @@ -364,6 +366,7 @@ This ledger records the committed parity checkpoints that future slices should b | Commit | Scope | Verification summary | Follow-up state | | --- | --- | --- | --- | +| `fix(search): align search page size` | Advances P3.89 with Chatwoot SearchAPI pagination parity. Global/entity search handlers now force the backend filter to the reference fixed page size of `15` and ignore local `per_page` query params before DB fallback or Meilisearch execution, matching every `page(params[:page]).per(15)` branch in `SearchService`. | `go test ./internal/handler/api/v1 -run 'TestSearchHandler_(GlobalSearch_WithFilterParams\|SearchMessages_Success)' -count=1` passed with workspace-local temp/cache dirs; full `go test ./...` passed outside the restricted socket sandbox; `git diff --check` passed. No route artifacts change. | Move P3.89 to Review; continue Phase 2/3 drift audit, Phase 6 placeholder audit, or B12 live smoke. | | `fix(search): scope global result types` | Advances P3.88 with Chatwoot global SearchAPI result-family parity. `GET /search` now forces the backend filter to conversations, contacts, messages, and articles before DB fallback or Meilisearch execution, matching `SearchService#perform` plus `search/index.json.jbuilder` and preventing local company/help-center document types from affecting reused frontend global-search results. | `go test ./internal/handler/api/v1 -run 'TestSearchHandler_GlobalSearch_(Success\|WithFilterParams\|UsesReferenceResultTypes)' -count=1` passed with workspace-local temp/cache dirs; full `go test ./...` passed outside the restricted socket sandbox; `git diff --check` passed. No route artifacts change. | Move P3.88 to Review; continue Phase 2/3 drift audit, Phase 6 placeholder audit, or B12 live smoke. | | `feat(search): align article result payloads` | Advances P3.87 with Chatwoot SearchAPI article result payload parity. Article search hits now include `portal_slug` and `category_name` from the same partial fields consumed by the reused article search card; DB-backed handlers reload portal/category relations for article results and Meilisearch hits, and durable/full reindex paths preload those relations before document creation. | `go test ./internal/handler/api/v1 ./internal/search ./internal/repository ./internal/service ./cmd/reindex_search -run 'TestSearchHandler_SearchArticles\|TestArticleDocumentCarriesSearchPartialData\|TestSearchRepo_SearchArticles\|TestDurableSearchIndexerPreloadsArticlePortalAndCategory' -count=1` passed with workspace-local temp/cache dirs; full `go test ./...` passed outside the restricted socket sandbox; `git diff --check` passed. No route artifacts change. | Move P3.87 to Review; continue Phase 2/3 drift audit, Phase 6 placeholder audit, or B12 live smoke. | | `feat(search): align message result payloads` | Advances P3.86 with Chatwoot SearchAPI message result payload parity. Message search hits now route to conversation display IDs instead of DB IDs, expose `sender` push data for DB-backed results, expose attachment push data for DB-backed and Meilisearch document results, and index/reindex message documents with preloaded conversation/attachment display data where available. | `go test ./internal/handler/api/v1 ./internal/search ./internal/repository ./internal/service ./cmd/reindex_search -run 'TestSearchHandler_SearchMessages\|TestDocumentBuildersSetStableUIDAndType\|TestMessageDocumentCarriesChatwootPayloadData\|TestSearchRepo_SearchMessages\|TestDurableSearchIndexer' -count=1` passed with workspace-local temp/cache dirs; full `go test ./...` passed outside the restricted socket sandbox; `git diff --check` passed. No route artifacts change. | Move P3.86 to Review; continue Phase 2/3 drift audit, Phase 6 placeholder audit, or B12 live smoke. | @@ -2732,3 +2735,4 @@ Verification milestone gates: - 2026-06-07: P3.86 search message payload checkpoint prepared as `feat(search): align message result payloads`; audited Chatwoot `_message.json.jbuilder`, `Message#push_event_data`, `Attachment#push_event_data`, and reused `SearchResultMessagesList.vue`/`SearchResultMessageItem.vue`. GoChat SearchAPI message results now hydrate DB-backed hits with conversation display IDs, sender push data, and attachment push data; Meilisearch hit serialization preserves nested sender/attachment payloads; message documents, durable index replay, and full `cmd/reindex_search` now preload or carry display/attachment data where available. Focused SearchHandler, SearchRepo, DurableSearchIndexer, reindex, and document-builder tests passed; full `go test ./...` passed outside the restricted socket sandbox; `git diff --check` passed. No route artifacts change. - 2026-06-07: P3.87 search article payload checkpoint prepared as `feat(search): align article result payloads`; audited Chatwoot search `_article.json.jbuilder`, `articles.json.jbuilder`, and reused `SearchResultArticlesList.vue`/`SearchResultArticleItem.vue`. GoChat SearchAPI article results now hydrate `portal_slug` and `category_name` from portal/category relations for DB-backed handlers and Meilisearch hits; article documents, durable index replay, and full `cmd/reindex_search` now preload or carry those route/display fields where available. Focused SearchHandler, SearchRepo, DurableSearchIndexer, reindex, and document-builder tests passed; full `go test ./...` passed outside the restricted socket sandbox; `git diff --check` passed. No route artifacts change. - 2026-06-07: P3.88 global search type-scope checkpoint prepared as `fix(search): scope global result types`; audited Chatwoot `SearchController#search`, `SearchService#perform`, `search/index.json.jbuilder`, and reused dashboard `SearchAPI.get`. GoChat global SearchAPI now forces backend execution to the four rendered Chatwoot result families (`conversations`, `contacts`, `messages`, `articles`) and ignores unsupported local `types=company/help_center` expansion before DB fallback or Meilisearch search, so hidden local document types cannot affect reused frontend global-search results. Focused SearchHandler tests passed; full `go test ./...` passed outside the restricted socket sandbox; `git diff --check` passed. No route artifacts change. +- 2026-06-07: P3.89 SearchAPI fixed page-size checkpoint prepared as `fix(search): align search page size`; audited Chatwoot `SearchService` pagination branches, `SearchController`, and reused dashboard `SearchAPI` callers. GoChat SearchAPI handlers now ignore local `per_page` query params and force the backend filter to Chatwoot's fixed `.per(15)` page size for global, conversation, message, contact, and article searches before DB fallback or Meilisearch execution. Focused SearchHandler pagination tests passed; full `go test ./...` passed outside the restricted socket sandbox; `git diff --check` passed. No route artifacts change. diff --git a/internal/handler/api/v1/search_handler.go b/internal/handler/api/v1/search_handler.go index 5311e72b..ea4c7e56 100644 --- a/internal/handler/api/v1/search_handler.go +++ b/internal/handler/api/v1/search_handler.go @@ -70,7 +70,7 @@ func NewSearchHandler(svc *search.SearchService, dbs ...*gorm.DB) *SearchHandler // @Param sort_by query string false "Sort field (created_at,last_activity_at,updated_at)" default(created_at) // @Param sort_order query string false "Sort order (asc,desc)" default(desc) // @Param page query int false "Page number" default(1) -// @Param per_page query int false "Items per page" default(25) +// @Param per_page query int false "Ignored for Chatwoot parity; search results are fixed at 15 per page" default(15) // @Success 200 {object} search.SearchResponse // @Failure 400 {object} model.ErrorResponse // @Failure 401 {object} model.ErrorResponse @@ -120,7 +120,7 @@ func (h *SearchHandler) GlobalSearch(c *gin.Context) { // @Param sort_by query string false "Sort field (created_at,last_activity_at,updated_at)" default(created_at) // @Param sort_order query string false "Sort order (asc,desc)" default(desc) // @Param page query int false "Page number" default(1) -// @Param per_page query int false "Items per page" default(25) +// @Param per_page query int false "Ignored for Chatwoot parity; search results are fixed at 15 per page" default(15) // @Success 200 {object} map[string]interface{} // @Failure 400 {object} model.ErrorResponse // @Failure 401 {object} model.ErrorResponse @@ -171,7 +171,7 @@ func (h *SearchHandler) SearchConversations(c *gin.Context) { // @Param sort_by query string false "Sort field (created_at,updated_at)" default(created_at) // @Param sort_order query string false "Sort order (asc,desc)" default(desc) // @Param page query int false "Page number" default(1) -// @Param per_page query int false "Items per page" default(25) +// @Param per_page query int false "Ignored for Chatwoot parity; search results are fixed at 15 per page" default(15) // @Success 200 {object} map[string]interface{} // @Failure 400 {object} model.ErrorResponse // @Failure 401 {object} model.ErrorResponse @@ -216,7 +216,7 @@ func (h *SearchHandler) SearchMessages(c *gin.Context) { // @Param sort_by query string false "Sort field (created_at,updated_at,name)" default(created_at) // @Param sort_order query string false "Sort order (asc,desc)" default(desc) // @Param page query int false "Page number" default(1) -// @Param per_page query int false "Items per page" default(25) +// @Param per_page query int false "Ignored for Chatwoot parity; search results are fixed at 15 per page" default(15) // @Success 200 {object} map[string]interface{} // @Failure 400 {object} model.ErrorResponse // @Failure 401 {object} model.ErrorResponse @@ -264,7 +264,7 @@ func (h *SearchHandler) SearchContacts(c *gin.Context) { // @Param sort_by query string false "Sort field (created_at,updated_at)" default(created_at) // @Param sort_order query string false "Sort order (asc,desc)" default(desc) // @Param page query int false "Page number" default(1) -// @Param per_page query int false "Items per page" default(25) +// @Param per_page query int false "Ignored for Chatwoot parity; search results are fixed at 15 per page" default(15) // @Success 200 {object} map[string]interface{} // @Failure 400 {object} model.ErrorResponse // @Failure 401 {object} model.ErrorResponse @@ -307,9 +307,7 @@ func parseChatwootSearchFilter(c *gin.Context) search.SearchFilter { if userID := getUserID(c); userID != 0 { filter.CurrentUserID = &userID } - if c.Query("per_page") == "" { - filter.PerPage = chatwootSearchPerPage - } + filter.PerPage = chatwootSearchPerPage return filter } diff --git a/internal/handler/api/v1/search_handler_test.go b/internal/handler/api/v1/search_handler_test.go index 79bd8060..33cc07a3 100644 --- a/internal/handler/api/v1/search_handler_test.go +++ b/internal/handler/api/v1/search_handler_test.go @@ -176,6 +176,8 @@ func TestSearchHandler_GlobalSearch_WithFilterParams(t *testing.T) { payload, ok := body["payload"].(map[string]interface{}) require.True(t, ok) assert.Len(t, payload["conversations"], 1) + require.NotNil(t, repo.msgFilter) + assert.Equal(t, 15, repo.msgFilter.PerPage) } func TestSearchHandler_GlobalSearch_UsesReferenceResultTypes(t *testing.T) { @@ -323,7 +325,7 @@ func TestSearchHandler_SearchMessages_Success(t *testing.T) { router := setupSearchHandlerRouter(handler) w := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/api/v1/accounts/1/search/messages?q=hello&page=1", nil) + req := httptest.NewRequest("GET", "/api/v1/accounts/1/search/messages?q=hello&page=1&per_page=99", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code)