diff --git a/docs/CHATWOOT_PARITY_DEVELOPMENT_PLAN.md b/docs/CHATWOOT_PARITY_DEVELOPMENT_PLAN.md index 25762258..b4d523cd 100644 --- a/docs/CHATWOOT_PARITY_DEVELOPMENT_PLAN.md +++ b/docs/CHATWOOT_PARITY_DEVELOPMENT_PLAN.md @@ -49,16 +49,16 @@ Hermes task landing checklist: ## Current Baseline -- Current tracking checkpoint: 2026-06-07 P3.99 conversation destroy async parity, prepared as `feat(conversations): align destroy job`. -- Latest implementation checkpoint: this checkpoint, prepared as `feat(conversations): align destroy job`. -- Latest documentation/tooling checkpoint: this tracker update records Chatwoot conversation destroy `head :ok` and `DeleteObjectJob` parity. No route artifacts change. +- Current tracking checkpoint: 2026-06-07 P3.100 dashboard app route parameter parity, prepared as `fix(routes): align dashboard app ids`. +- Latest implementation checkpoint: this checkpoint, prepared as `fix(routes): align dashboard app ids`. +- Latest documentation/tooling checkpoint: this tracker update records exact Chatwoot `dashboard_apps/:id` route-parameter parity and regenerated route artifacts. - 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: account conversation destroy now returns empty `200 OK` like Chatwoot `head :ok` and, when a WorkerPool is configured, enqueues a low-priority durable `conversation:delete_object` job matching the inspected `DeleteObjectJob` path before the worker performs the actual soft delete and event/search cleanup. No-worker tests keep a synchronous fallback. This retains P3.98 conversation transcript delivery parity, P3.97 conversation typing event parity, P3.96 conversation last-seen read parity, P3.95 conversation mute contact-block parity, P3.94 conversation toggle-priority empty-body parity, P3.93 conversation toggle-status fallback parity, 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: dashboard app member routes now register the same `:id` path parameter used by Chatwoot `resources :dashboard_apps`, while the handler still accepts the legacy local `:dashboard_app_id` name for focused tests and extension routes. Route parity artifacts are regenerated and the tracked frontend-critical audit improves to 425 exact routes. This retains P3.99 conversation destroy async parity, P3.98 conversation transcript delivery parity, P3.97 conversation typing event parity, 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. +- Route dump succeeds with `967` registered routes after dashboard app route-parameter tracking. - Route parity artifacts now exist under `docs/parity/` and are generated by `cmd/route_parity`. -- Tracked frontend-critical route audit covers 439 Chatwoot routes: 421 exact, 0 method-compatible, 18 parameter-compatible, 0 missing. The 18 parameter-compatible routes are Gin-internal parameter-name differences for nested AgentCapacityPolicy users/inbox limits, dashboard app member `:id` names, WhatsApp call `:id` route names, plus the public article `.md`/`.png` suffixes served through the same external article route dispatcher. +- Tracked frontend-critical route audit covers 439 Chatwoot routes: 425 exact, 0 method-compatible, 14 parameter-compatible, 0 missing. The 14 parameter-compatible routes are Gin-internal parameter-name differences for nested AgentCapacityPolicy users/inbox limits, WhatsApp call `:id` route names, plus the public article `.md`/`.png` suffixes served through the same external article route dispatcher. - `/api/v1/widget` stubs are burned down and public inbox/contact/conversation/message core flows are backed by real handlers. - Handler test stability fixes are committed into the baseline before feature parity work continues. - `.codegraph/` is generated indexing output and is not part of tracked product code. @@ -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.100 dashboard app route parameter parity | Implemented for reused dashboard app settings routes: GoChat now registers standard dashboard app member routes with Chatwoot's `:id` parameter name for `show/update/destroy`, keeps `PATCH` and `PUT`, and preserves legacy handler compatibility for local focused tests and widget extension routes. Route parity improves from `421 exact / 18 parameter-compatible` to `425 exact / 14 parameter-compatible` with no missing critical routes. | Keep in Review; reopen from B12 dashboard app settings smoke or fresh reference evidence for serializer/request-permit drift beyond the inspected controller/Jbuilder/frontend store contract. | Focused DashboardAppHandler and router tests passed; route dump/parity artifacts regenerated; full `go test ./...` passed outside the restricted socket sandbox; `git diff --check` passed. | | 0 | P3.99 conversation destroy async parity | Implemented for reused conversation delete actions: `DELETE /conversations/:conversation_id` now returns empty `200 OK` like Chatwoot and queues a low-priority durable `conversation:delete_object` job when workers are configured. The job performs the existing soft delete, dispatches `conversation.deleted`, and removes the search index entry; no-worker construction keeps synchronous focused-test behavior. | Keep in Review; reopen from B12 conversation delete smoke or fresh reference evidence for enterprise `DeleteObjectJob#process_post_deletion_tasks`, exact Pundit destroy permission drift, or heavy-association purge behavior beyond conversation objects. | Focused ConversationService delete/job tests and ConversationHandler delete response tests passed; full `go test ./...` passed outside the restricted socket sandbox; `git diff --check` passed. No route artifacts change. | | 0 | P3.98 conversation transcript delivery parity | Implemented for reused conversation transcript and automation transcript actions: `POST /conversations/:conversation_id/transcript` now checks Chatwoot-style email transcript availability, enforces configured daily email limits, delivers through the SMTP transcript boundary, increments account outbound-email counters, and scopes transcript content to public incoming/outgoing chat messages. Automation `send_email_transcript` no-ops when disabled and stops recipient fan-out at the rate limit before sync delivery or durable enqueue. | Keep in Review; reopen from B12 transcript smoke or fresh reference evidence for exact Redis TTL behavior, cloud-only global default limit handling, mailer template HTML body drift, or provider-specific SMTP availability checks beyond the inspected mailer gate. | Focused ConversationService, ConversationHandler, ActionService, and Account model transcript tests passed; full `go test ./...` passed outside the restricted socket sandbox; `git diff --check` passed. `accounts.limits` migration is included; no route artifacts change. | | 0 | P3.97 conversation typing event parity | Implemented for reused conversation typing action: `POST /conversations/:conversation_id/toggle_typing_status` now accepts empty bodies and `{}` as Chatwoot no-op `200 OK`, rejects malformed JSON, and emits `conversation.typing_on` / `conversation.typing_off` events with conversation/contact/user context and `is_private` metadata for valid statuses. | Keep in Review; reopen from B12 conversation typing smoke or fresh reference evidence for ActionCable event payload fields beyond the inspected `TypingStatusManager` contract. | Focused ConversationService and ConversationHandler toggle-typing tests passed; full `go test ./...` must pass outside the restricted socket sandbox; `git diff --check` must pass. No route artifacts change. | @@ -263,6 +264,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.100 dashboard app route parameter parity | `internal/router/router.go`, `internal/handler/api/v1/dashboard_app_handler.go`, dashboard app handler/router tests, `docs/parity/gochat_routes.txt`, `docs/parity/route_parity.md` | `reference/chatwoot/config/routes.rb:130`, `reference/chatwoot/app/controllers/api/v1/accounts/dashboard_apps_controller.rb`, dashboard apps Jbuilder views, reused dashboard `api/dashboardApps.js` and `store/modules/dashboardApps.js` | Dashboard app standard member routes now use Chatwoot's external `:id` route shape for `GET/PATCH/PUT/DELETE /dashboard_apps/:id`, while handler parsing keeps the prior `:dashboard_app_id` compatibility path for tests and GoChat-only widget extensions. Route parity artifacts are regenerated and the dashboard app rows move from parameter-compatible to exact. | Review by `fix(routes): align dashboard app ids`; focused tests prove handler support for `:id` routes; route parity proves `425 exact, 14 parameter-compatible, 0 missing out of 439`; full `go test ./...` passed outside the restricted socket sandbox; `git diff --check` passed. | | P3.99 conversation destroy async parity | `internal/handler/api/v1/conversation_handler.go`, `internal/service/conversation_service.go`, `internal/service/conversation_delete_worker.go`, `internal/app/bootstrap.go`, conversation handler/service tests | `reference/chatwoot/app/controllers/api/v1/accounts/conversations_controller.rb#destroy`, `reference/chatwoot/app/jobs/delete_object_job.rb`, reused dashboard `ConversationApi.delete` and `conversations/actions.js#deleteConversation` | Account conversation destroy now matches the inspected Chatwoot controller/job path: the handler returns empty `200 OK`, production service wiring enqueues a low-priority durable delete-object job, and the worker performs the existing soft-delete side effects plus event dispatch/search-index deletion. The service keeps synchronous behavior when no WorkerPool is set so focused tests and non-worker local callers still delete immediately. | Review by `feat(conversations): align destroy job`; focused tests prove handler `200` response shape, not-found behavior, synchronous fallback deletion, durable job enqueue, delayed visibility before worker execution, missing conversations do not enqueue phantom jobs, and worker replay deletion; full `go test ./...` passed outside the restricted socket sandbox; `git diff --check` passed. No route artifacts change. | | P3.98 conversation transcript delivery parity | `internal/handler/api/v1/conversation_handler.go`, `internal/service/conversation_service.go`, `internal/model/account.go`, `internal/model/account_email.go`, `internal/automation/action_service.go`, migration `000037_add_accounts_limits`, conversation handler/service/action/model tests | `reference/chatwoot/app/controllers/api/v1/accounts/conversations_controller.rb#transcript`, `reference/chatwoot/app/models/concerns/account_email_rate_limitable.rb`, `reference/chatwoot/app/mailers/conversation_reply_mailer.rb#conversation_transcript`, `reference/chatwoot/app/models/message.rb#chat`, `reference/chatwoot/app/models/concerns/message_filter_helpers.rb#conversation_transcriptable?`, `reference/chatwoot/app/services/action_service.rb#send_email_transcript`, reused dashboard transcript modal/API/i18n payment-required behavior | Account conversation transcript delivery now matches the inspected Chatwoot controller and mailer contract: missing email returns raw `422`, disabled accounts return raw `402`, configured email limits return empty `429`, success returns empty `200 OK` after delivering through a fakeable SMTP transcript boundary and incrementing account outbound-email count. Transcript bodies include only public incoming/outgoing chat messages, and automation transcript actions respect the same enabled/rate behavior while splitting comma recipients and stopping at the first rate-limit miss. | Review by `feat(conversations): align transcript delivery`; focused tests prove account gate/rate helpers, handler `402/429` shape, service delivery/count/message-scope behavior, and automation disabled/rate/filter behavior; full `go test ./...` passed outside the restricted socket sandbox; `git diff --check` passed. No route artifacts change. | | P3.97 conversation typing event parity | `internal/handler/api/v1/conversation_handler.go`, `internal/service/conversation_service.go`, `internal/channel/event.go`, conversation handler/service tests | `reference/chatwoot/app/controllers/api/v1/accounts/conversations_controller.rb#toggle_typing_status`, `reference/chatwoot/app/services/conversations/typing_status_manager.rb`, `reference/chatwoot/app/javascript/dashboard/api/inbox/conversation.js`, `reference/chatwoot/app/javascript/dashboard/store/modules/conversationTypingStatus.js`, reused dashboard reply-box typing callers | Account conversation `toggle_typing_status` now matches Chatwoot's permissive member action: missing body or missing `typing_status` resolves the conversation and returns empty `200 OK` without dispatching an event, malformed JSON still fails, and `on`/`off` statuses dispatch `conversation.typing_on` / `conversation.typing_off` events with conversation/contact/user context plus `is_private`. | Review by `feat(conversations): align typing events`; focused tests prove service event type/data behavior, blank-status no-op after conversation lookup, handler empty-body and `{}` no-op success, and invalid JSON failure; full `go test ./...` must pass outside the restricted socket sandbox; `git diff --check` must pass. No route artifacts change. | @@ -386,6 +388,7 @@ This ledger records the committed parity checkpoints that future slices should b | Commit | Scope | Verification summary | Follow-up state | | --- | --- | --- | --- | +| `fix(routes): align dashboard app ids` | Advances P3.100 with exact Chatwoot dashboard app member route parameter parity. GoChat now registers `GET/PATCH/PUT/DELETE /api/v1/accounts/:account_id/dashboard_apps/:id`, keeps handler compatibility with legacy local `:dashboard_app_id`, and regenerates route parity artifacts. | Focused DashboardAppHandler and router tests passed; route dump/parity regenerated to `TOTAL: 967` and `425 exact, 0 method-compatible, 14 parameter-compatible, 0 missing out of 439`; full `go test ./...` passed outside the restricted socket sandbox; `git diff --check` passed. | Move P3.100 to Review; continue reducing remaining parameter-compatible rows or run B12 live smoke. | | `feat(conversations): align destroy job` | Advances P3.99 with Chatwoot conversation destroy parity. GoChat now returns empty `200 OK` for `DELETE /conversations/:conversation_id`, wires a durable low-priority `conversation:delete_object` job into the WorkerPool, and lets that job perform the existing soft-delete/event/search cleanup while retaining a synchronous fallback without workers. | Focused ConversationService delete/job tests and ConversationHandler delete response tests passed; full `go test ./...` passed outside the restricted socket sandbox; `git diff --check` passed. No route artifacts change. | Move P3.99 to Review; continue Phase 2/3 drift audit, Phase 6 placeholder audit, or B12 live smoke. | | `feat(conversations): align transcript delivery` | Advances P3.98 with Chatwoot conversation transcript delivery parity. GoChat now adds the Chatwoot `accounts.limits` field, uses account email transcript availability/rate helpers, maps dashboard transcript disabled/rate-limited responses to raw `402`/empty `429`, delivers transcript emails through the fakeable SMTP boundary, increments outbound-email counters, and filters transcript content to public incoming/outgoing chat messages. Automation `send_email_transcript` now no-ops when disabled and stops recipient fan-out at the configured rate limit. | Focused Account model, ConversationService, ConversationHandler, and ActionService transcript tests passed; full `go test ./...` passed outside the restricted socket sandbox; `git diff --check` passed. No route artifacts change. | Move P3.98 to Review; continue Phase 2/3 drift audit, Phase 6 placeholder audit, or B12 live smoke. | | `feat(conversations): align typing events` | Advances P3.97 with Chatwoot conversation `toggle_typing_status` parity. GoChat now treats empty or missing `typing_status` as a no-op `200 OK`, keeps malformed JSON as a validation error, and dispatches Chatwoot-style `conversation.typing_on` / `conversation.typing_off` events with user/contact/conversation context and `is_private` metadata. | Focused ConversationService and ConversationHandler toggle-typing tests passed; full `go test ./...` passed outside the restricted socket sandbox; `git diff --check` passed. No route artifacts change. | Move P3.97 to Review; continue Phase 2/3 drift audit, Phase 6 placeholder audit, or B12 live smoke. | @@ -2778,3 +2781,4 @@ Verification milestone gates: - 2026-06-07: P3.97 conversation typing checkpoint prepared as `feat(conversations): align typing events`; audited Chatwoot `ConversationsController#toggle_typing_status`, `Conversations::TypingStatusManager`, and reused dashboard conversation typing API/store/reply-box callers. GoChat account conversation `toggle_typing_status` now accepts empty bodies and missing `typing_status` as no-op `200 OK`, still rejects malformed JSON, and dispatches Chatwoot-style `conversation.typing_on` / `conversation.typing_off` events with current user, contact, conversation, and `is_private` metadata for valid `on`/`off` statuses. Focused ConversationService and ConversationHandler toggle-typing tests passed; full `go test ./...` passed outside the restricted socket sandbox; `git diff --check` passed. No route artifacts change. - 2026-06-07: P3.98 conversation transcript checkpoint prepared as `feat(conversations): align transcript delivery`; audited Chatwoot `ConversationsController#transcript`, `AccountEmailRateLimitable`, `ConversationReplyMailer#conversation_transcript`, `Message.chat`, `MessageFilterHelpers#conversation_transcriptable?`, and automation `ActionService#send_email_transcript`. GoChat account conversation transcript now honors email transcript availability, configured email limits, raw `402`/empty `429` response shapes, SMTP transcript delivery, outbound-email count increments, and public incoming/outgoing chat-message scoping; automation `send_email_transcript` no-ops when disabled and stops recipient fan-out at the rate limit before sync delivery or durable enqueue. Focused Account model, ConversationService, ConversationHandler, and ActionService transcript tests passed; full `go test ./...` passed outside the restricted socket sandbox; `git diff --check` passed. No route artifacts change. - 2026-06-07: P3.99 conversation destroy checkpoint prepared as `feat(conversations): align destroy job`; audited Chatwoot `ConversationsController#destroy`, `DeleteObjectJob`, and reused dashboard `ConversationApi.delete` / `deleteConversation` action. GoChat account conversation delete now returns empty `200 OK` instead of local `204`, queues a low-priority durable `conversation:delete_object` job when WorkerPool is configured, and the worker performs the existing soft delete, `conversation.deleted` dispatch, and search-index deletion; no-worker construction keeps synchronous deletion for focused tests/local callers. Focused ConversationService delete/job tests and ConversationHandler delete tests passed; full `go test ./...` passed outside the restricted socket sandbox; `git diff --check` passed. No route artifacts change. +- 2026-06-07: P3.100 dashboard app route-parameter checkpoint prepared as `fix(routes): align dashboard app ids`; audited Chatwoot `routes.rb:130`, `DashboardAppsController`, dashboard app Jbuilder views, and reused dashboard `dashboardApps.js` API/store calls. GoChat now registers dashboard app member CRUD routes with Chatwoot's `:id` parameter name, keeps `PATCH` and `PUT`, and preserves legacy local `:dashboard_app_id` parsing for focused tests and GoChat-only widget extensions. Focused DashboardAppHandler and router tests passed; route dump/parity regenerated to `TOTAL: 967` and `425 exact, 0 method-compatible, 14 parameter-compatible, 0 missing out of 439`; full `go test ./...` passed outside the restricted socket sandbox; `git diff --check` passed. diff --git a/docs/parity/gochat_routes.txt b/docs/parity/gochat_routes.txt index 1760dc68..baf2d546 100644 --- a/docs/parity/gochat_routes.txt +++ b/docs/parity/gochat_routes.txt @@ -48,8 +48,8 @@ DELETE /api/v1/accounts/:account_id/conversations/:conversation_id/whatsapp_call DELETE /api/v1/accounts/:account_id/custom_attribute_definitions/:id DELETE /api/v1/accounts/:account_id/custom_filters/:id DELETE /api/v1/accounts/:account_id/custom_roles/:id -DELETE /api/v1/accounts/:account_id/dashboard_apps/:dashboard_app_id -DELETE /api/v1/accounts/:account_id/dashboard_apps/:dashboard_app_id/widgets/:widget_index +DELETE /api/v1/accounts/:account_id/dashboard_apps/:id +DELETE /api/v1/accounts/:account_id/dashboard_apps/:id/widgets/:widget_index DELETE /api/v1/accounts/:account_id/google_channels/:google_id DELETE /api/v1/accounts/:account_id/hooks/:id DELETE /api/v1/accounts/:account_id/inbox_members/ @@ -249,8 +249,8 @@ GET /api/v1/accounts/:account_id/custom_filters/:id GET /api/v1/accounts/:account_id/custom_roles/ GET /api/v1/accounts/:account_id/custom_roles/:id GET /api/v1/accounts/:account_id/dashboard_apps -GET /api/v1/accounts/:account_id/dashboard_apps/:dashboard_app_id -GET /api/v1/accounts/:account_id/dashboard_apps/:dashboard_app_id/widgets +GET /api/v1/accounts/:account_id/dashboard_apps/:id +GET /api/v1/accounts/:account_id/dashboard_apps/:id/widgets GET /api/v1/accounts/:account_id/dashboard_apps/search GET /api/v1/accounts/:account_id/draft_messages/count GET /api/v1/accounts/:account_id/draft_messages/search @@ -528,7 +528,7 @@ PATCH /api/v1/accounts/:account_id/csat_survey_responses/:id PATCH /api/v1/accounts/:account_id/custom_attribute_definitions/:id PATCH /api/v1/accounts/:account_id/custom_filters/:id PATCH /api/v1/accounts/:account_id/custom_roles/:id -PATCH /api/v1/accounts/:account_id/dashboard_apps/:dashboard_app_id +PATCH /api/v1/accounts/:account_id/dashboard_apps/:id PATCH /api/v1/accounts/:account_id/inbox_members/ PATCH /api/v1/accounts/:account_id/inboxes/:inbox_id PATCH /api/v1/accounts/:account_id/inboxes/:inbox_id/email_channels/:em_id @@ -685,7 +685,7 @@ POST /api/v1/accounts/:account_id/custom_attribute_definitions/ POST /api/v1/accounts/:account_id/custom_filters/ POST /api/v1/accounts/:account_id/custom_roles/ POST /api/v1/accounts/:account_id/dashboard_apps -POST /api/v1/accounts/:account_id/dashboard_apps/:dashboard_app_id/widgets +POST /api/v1/accounts/:account_id/dashboard_apps/:id/widgets POST /api/v1/accounts/:account_id/direct_uploads POST /api/v1/accounts/:account_id/email_channel_migrations/ POST /api/v1/accounts/:account_id/email_channels/ @@ -914,8 +914,8 @@ PUT /api/v1/accounts/:account_id/conversations/:conversation_id/whatsapp_calls/: PUT /api/v1/accounts/:account_id/custom_attribute_definitions/:id PUT /api/v1/accounts/:account_id/custom_filters/:id PUT /api/v1/accounts/:account_id/custom_roles/:id -PUT /api/v1/accounts/:account_id/dashboard_apps/:dashboard_app_id -PUT /api/v1/accounts/:account_id/dashboard_apps/:dashboard_app_id/widgets/:widget_index +PUT /api/v1/accounts/:account_id/dashboard_apps/:id +PUT /api/v1/accounts/:account_id/dashboard_apps/:id/widgets/:widget_index PUT /api/v1/accounts/:account_id/hooks/:id PUT /api/v1/accounts/:account_id/inboxes/:inbox_id PUT /api/v1/accounts/:account_id/inboxes/:inbox_id/assignment_policy/:policy_id diff --git a/docs/parity/route_parity.md b/docs/parity/route_parity.md index 933b4d3a..da2ba323 100644 --- a/docs/parity/route_parity.md +++ b/docs/parity/route_parity.md @@ -7,7 +7,7 @@ Generated from: This report covers tracked frontend-critical Chatwoot routes from `reference/chatwoot/config/routes.rb`, including API v1 account routes, Captain/Copilot, assignment policies, widget/public APIs, and API v2 reports. Ruby is not installed in the workspace, so Chatwoot routes are sourced from static route declarations instead of `bin/rails routes`. -Summary: 421 exact, 0 method-compatible, 18 parameter-compatible, 0 missing out of 439 tracked critical routes. +Summary: 425 exact, 0 method-compatible, 14 parameter-compatible, 0 missing out of 439 tracked critical routes. ## Missing Critical Routes @@ -31,14 +31,11 @@ These routes exist with equivalent method and path shape but different parameter | --- | --- | --- | --- | --- | --- | | DELETE | `/api/v1/accounts/:account_id/agent_capacity_policies/:agent_capacity_policy_id/inbox_limits/:id` | `/api/v1/accounts/:account_id/agent_capacity_policies/:id/inbox_limits/:limit_id` | `api/v1/accounts/agent_capacity_policies/inbox_limits#destroy` | `routes.rb:126` | parameter-compatible | | DELETE | `/api/v1/accounts/:account_id/agent_capacity_policies/:agent_capacity_policy_id/users/:id` | `/api/v1/accounts/:account_id/agent_capacity_policies/:id/users/:user_id` | `api/v1/accounts/agent_capacity_policies/users#destroy` | `routes.rb:125` | parameter-compatible | -| DELETE | `/api/v1/accounts/:account_id/dashboard_apps/:id` | `/api/v1/accounts/:account_id/dashboard_apps/:dashboard_app_id` | `api/v1/accounts/dashboard_apps#destroy` | `routes.rb:130` | parameter-compatible | | GET | `/api/v1/accounts/:account_id/agent_capacity_policies/:agent_capacity_policy_id/users` | `/api/v1/accounts/:account_id/agent_capacity_policies/:id/users` | `api/v1/accounts/agent_capacity_policies/users#index` | `routes.rb:125` | parameter-compatible | -| GET | `/api/v1/accounts/:account_id/dashboard_apps/:id` | `/api/v1/accounts/:account_id/dashboard_apps/:dashboard_app_id` | `api/v1/accounts/dashboard_apps#show` | `routes.rb:130` | parameter-compatible | | GET | `/api/v1/accounts/:account_id/whatsapp_calls/:id` | `/api/v1/accounts/:account_id/whatsapp_calls/:call_id` | `api/v1/accounts/whatsapp_calls#show` | `routes.rb:237` | parameter-compatible | | GET | `/hc/:slug/articles/:article_slug.md` | `/hc/:slug/articles/:article_slug` | `public/api/v1/portals/articles#show_markdown` | `routes.rb:599` | parameter-compatible | | GET | `/hc/:slug/articles/:article_slug.png` | `/hc/:slug/articles/:article_slug` | `public/api/v1/portals/articles#tracking_pixel` | `routes.rb:598` | parameter-compatible | | PATCH | `/api/v1/accounts/:account_id/agent_capacity_policies/:agent_capacity_policy_id/inbox_limits/:id` | `/api/v1/accounts/:account_id/agent_capacity_policies/:id/inbox_limits/:limit_id` | `api/v1/accounts/agent_capacity_policies/inbox_limits#update` | `routes.rb:126` | parameter-compatible | -| PATCH | `/api/v1/accounts/:account_id/dashboard_apps/:id` | `/api/v1/accounts/:account_id/dashboard_apps/:dashboard_app_id` | `api/v1/accounts/dashboard_apps#update` | `routes.rb:130` | parameter-compatible | | POST | `/api/v1/accounts/:account_id/agent_capacity_policies/:agent_capacity_policy_id/inbox_limits` | `/api/v1/accounts/:account_id/agent_capacity_policies/:id/inbox_limits` | `api/v1/accounts/agent_capacity_policies/inbox_limits#create` | `routes.rb:126` | parameter-compatible | | POST | `/api/v1/accounts/:account_id/agent_capacity_policies/:agent_capacity_policy_id/users` | `/api/v1/accounts/:account_id/agent_capacity_policies/:id/users` | `api/v1/accounts/agent_capacity_policies/users#create` | `routes.rb:125` | parameter-compatible | | POST | `/api/v1/accounts/:account_id/whatsapp_calls/:id/accept` | `/api/v1/accounts/:account_id/whatsapp_calls/:call_id/accept` | `api/v1/accounts/whatsapp_calls#accept` | `routes.rb:239` | parameter-compatible | @@ -46,7 +43,6 @@ These routes exist with equivalent method and path shape but different parameter | POST | `/api/v1/accounts/:account_id/whatsapp_calls/:id/terminate` | `/api/v1/accounts/:account_id/whatsapp_calls/:call_id/terminate` | `api/v1/accounts/whatsapp_calls#terminate` | `routes.rb:241` | parameter-compatible | | POST | `/api/v1/accounts/:account_id/whatsapp_calls/:id/upload_recording` | `/api/v1/accounts/:account_id/whatsapp_calls/:call_id/upload_recording` | `api/v1/accounts/whatsapp_calls#upload_recording` | `routes.rb:242` | parameter-compatible | | PUT | `/api/v1/accounts/:account_id/agent_capacity_policies/:agent_capacity_policy_id/inbox_limits/:id` | `/api/v1/accounts/:account_id/agent_capacity_policies/:id/inbox_limits/:limit_id` | `api/v1/accounts/agent_capacity_policies/inbox_limits#update` | `routes.rb:126` | parameter-compatible | -| PUT | `/api/v1/accounts/:account_id/dashboard_apps/:id` | `/api/v1/accounts/:account_id/dashboard_apps/:dashboard_app_id` | `api/v1/accounts/dashboard_apps#update` | `routes.rb:130` | parameter-compatible | ## Exact Critical Routes @@ -77,6 +73,7 @@ These routes exist with equivalent method and path shape but different parameter | DELETE | `/api/v1/accounts/:account_id/conversations/:conversation_id/participants` | `/api/v1/accounts/:account_id/conversations/:conversation_id/participants` | `api/v1/accounts/conversations/participants#destroy` | `routes.rb:150` | exact | | DELETE | `/api/v1/accounts/:account_id/custom_attribute_definitions/:id` | `/api/v1/accounts/:account_id/custom_attribute_definitions/:id` | `api/v1/accounts/custom_attribute_definitions#destroy` | `routes.rb:250` | exact | | DELETE | `/api/v1/accounts/:account_id/custom_filters/:id` | `/api/v1/accounts/:account_id/custom_filters/:id` | `api/v1/accounts/custom_filters#destroy` | `routes.rb:251` | exact | +| DELETE | `/api/v1/accounts/:account_id/dashboard_apps/:id` | `/api/v1/accounts/:account_id/dashboard_apps/:id` | `api/v1/accounts/dashboard_apps#destroy` | `routes.rb:130` | exact | | DELETE | `/api/v1/accounts/:account_id/inbox_members/` | `/api/v1/accounts/:account_id/inbox_members/` | `api/v1/accounts/inbox_members#destroy` | `routes.rb:277` | exact | | DELETE | `/api/v1/accounts/:account_id/inboxes/:inbox_id` | `/api/v1/accounts/:account_id/inboxes/:inbox_id` | `api/v1/accounts/inboxes#destroy` | `routes.rb:252` | exact | | DELETE | `/api/v1/accounts/:account_id/inboxes/:inbox_id/assignment_policy/` | `/api/v1/accounts/:account_id/inboxes/:inbox_id/assignment_policy/` | `api/v1/accounts/inboxes/assignment_policy#destroy` | `routes.rb:311` | exact | @@ -174,6 +171,7 @@ These routes exist with equivalent method and path shape but different parameter | GET | `/api/v1/accounts/:account_id/custom_filters/:id` | `/api/v1/accounts/:account_id/custom_filters/:id` | `api/v1/accounts/custom_filters#show` | `routes.rb:251` | exact | | GET | `/api/v1/accounts/:account_id/custom_roles/` | `/api/v1/accounts/:account_id/custom_roles/` | `api/v1/accounts/custom_roles#index` | `routes.rb:122` | exact | | GET | `/api/v1/accounts/:account_id/dashboard_apps` | `/api/v1/accounts/:account_id/dashboard_apps` | `api/v1/accounts/dashboard_apps#index` | `routes.rb:130` | exact | +| GET | `/api/v1/accounts/:account_id/dashboard_apps/:id` | `/api/v1/accounts/:account_id/dashboard_apps/:id` | `api/v1/accounts/dashboard_apps#show` | `routes.rb:130` | exact | | GET | `/api/v1/accounts/:account_id/inbox_members/:inbox_id` | `/api/v1/accounts/:account_id/inbox_members/:inbox_id` | `api/v1/accounts/inbox_members#show` | `routes.rb:275` | exact | | GET | `/api/v1/accounts/:account_id/inboxes/` | `/api/v1/accounts/:account_id/inboxes/` | `api/v1/accounts/inboxes#index` | `routes.rb:252` | exact | | GET | `/api/v1/accounts/:account_id/inboxes/:inbox_id` | `/api/v1/accounts/:account_id/inboxes/:inbox_id` | `api/v1/accounts/inboxes#show` | `routes.rb:252` | exact | @@ -279,6 +277,7 @@ These routes exist with equivalent method and path shape but different parameter | PATCH | `/api/v1/accounts/:account_id/canned_responses/:id` | `/api/v1/accounts/:account_id/canned_responses/:id` | `api/v1/accounts/canned_responses#update` | `routes.rb:114` | exact | | PATCH | `/api/v1/accounts/:account_id/conversations/:conversation_id/draft_messages` | `/api/v1/accounts/:account_id/conversations/:conversation_id/draft_messages` | `api/v1/accounts/conversations/draft_messages#update` | `routes.rb:152` | exact | | PATCH | `/api/v1/accounts/:account_id/conversations/:conversation_id/participants` | `/api/v1/accounts/:account_id/conversations/:conversation_id/participants` | `api/v1/accounts/conversations/participants#update` | `routes.rb:150` | exact | +| PATCH | `/api/v1/accounts/:account_id/dashboard_apps/:id` | `/api/v1/accounts/:account_id/dashboard_apps/:id` | `api/v1/accounts/dashboard_apps#update` | `routes.rb:130` | exact | | PATCH | `/api/v1/accounts/:account_id/inbox_members/` | `/api/v1/accounts/:account_id/inbox_members/` | `api/v1/accounts/inbox_members#update` | `routes.rb:278` | exact | | PATCH | `/api/v1/accounts/:account_id/integrations/hooks/:id` | `/api/v1/accounts/:account_id/integrations/hooks/:id` | `api/v1/accounts/integrations/hooks#update` | `routes.rb:346` | exact | | PATCH | `/api/v1/accounts/:account_id/integrations/slack` | `/api/v1/accounts/:account_id/integrations/slack` | `api/v1/accounts/integrations/slack#update` | `routes.rb:350` | exact | @@ -453,6 +452,7 @@ These routes exist with equivalent method and path shape but different parameter | PUT | `/api/v1/accounts/:account_id/conversations/:conversation_id/participants` | `/api/v1/accounts/:account_id/conversations/:conversation_id/participants` | `api/v1/accounts/conversations/participants#update` | `routes.rb:150` | exact | | PUT | `/api/v1/accounts/:account_id/custom_attribute_definitions/:id` | `/api/v1/accounts/:account_id/custom_attribute_definitions/:id` | `api/v1/accounts/custom_attribute_definitions#update` | `routes.rb:250` | exact | | PUT | `/api/v1/accounts/:account_id/custom_filters/:id` | `/api/v1/accounts/:account_id/custom_filters/:id` | `api/v1/accounts/custom_filters#update` | `routes.rb:251` | exact | +| PUT | `/api/v1/accounts/:account_id/dashboard_apps/:id` | `/api/v1/accounts/:account_id/dashboard_apps/:id` | `api/v1/accounts/dashboard_apps#update` | `routes.rb:130` | exact | | PUT | `/api/v1/accounts/:account_id/inboxes/:inbox_id` | `/api/v1/accounts/:account_id/inboxes/:inbox_id` | `api/v1/accounts/inboxes#update` | `routes.rb:252` | exact | | PUT | `/api/v1/accounts/:account_id/integrations/hooks/:id` | `/api/v1/accounts/:account_id/integrations/hooks/:id` | `api/v1/accounts/integrations/hooks#update` | `routes.rb:346` | exact | | PUT | `/api/v1/accounts/:account_id/integrations/slack` | `/api/v1/accounts/:account_id/integrations/slack` | `api/v1/accounts/integrations/slack#update` | `routes.rb:350` | exact | diff --git a/internal/handler/api/v1/dashboard_app_handler.go b/internal/handler/api/v1/dashboard_app_handler.go index 04e14a74..bf4f7d31 100644 --- a/internal/handler/api/v1/dashboard_app_handler.go +++ b/internal/handler/api/v1/dashboard_app_handler.go @@ -70,9 +70,9 @@ func (h *DashboardAppHandler) Create(c *gin.Context) { } // Get retrieves a dashboard app by ID. -// GET /api/v1/accounts/:account_id/dashboard_apps/:dashboard_app_id +// GET /api/v1/accounts/:account_id/dashboard_apps/:id func (h *DashboardAppHandler) Get(c *gin.Context) { - id, err := parseUintParam(c, "dashboard_app_id") + id, err := parseDashboardAppIDParam(c) if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid dashboard_app_id") return @@ -95,10 +95,10 @@ func (h *DashboardAppHandler) Get(c *gin.Context) { } // Update modifies an existing dashboard app. -// PUT /api/v1/accounts/:account_id/dashboard_apps/:dashboard_app_id +// PUT /api/v1/accounts/:account_id/dashboard_apps/:id // Chatwoot: @dashboard_app.update!(permitted_payload) func (h *DashboardAppHandler) Update(c *gin.Context) { - id, err := parseUintParam(c, "dashboard_app_id") + id, err := parseDashboardAppIDParam(c) if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid dashboard_app_id") return @@ -127,10 +127,10 @@ func (h *DashboardAppHandler) Update(c *gin.Context) { } // Delete removes a dashboard app. -// DELETE /api/v1/accounts/:account_id/dashboard_apps/:dashboard_app_id +// DELETE /api/v1/accounts/:account_id/dashboard_apps/:id // Chatwoot: @dashboard_app.destroy!; head :no_content (204) func (h *DashboardAppHandler) Delete(c *gin.Context) { - id, err := parseUintParam(c, "dashboard_app_id") + id, err := parseDashboardAppIDParam(c) if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid dashboard_app_id") return @@ -155,10 +155,10 @@ func (h *DashboardAppHandler) Delete(c *gin.Context) { response.NoContent(c) } -// Patch partially updates a dashboard app (GoChat extension — Chatwoot has no PATCH). -// PATCH /api/v1/accounts/:account_id/dashboard_apps/:dashboard_app_id +// Patch partially updates a dashboard app through Rails resource update. +// PATCH /api/v1/accounts/:account_id/dashboard_apps/:id func (h *DashboardAppHandler) Patch(c *gin.Context) { - id, err := parseUintParam(c, "dashboard_app_id") + id, err := parseDashboardAppIDParam(c) if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid dashboard_app_id") return @@ -230,6 +230,13 @@ func bindDashboardAppCreate(c *gin.Context) (*service.CreateDashboardAppRequest, return &req, nil } +func parseDashboardAppIDParam(c *gin.Context) (uint, error) { + if c.Param("id") != "" { + return parseUintParam(c, "id") + } + return parseUintParam(c, "dashboard_app_id") +} + func bindDashboardAppUpdate(c *gin.Context) (*service.UpdateDashboardAppRequest, error) { var raw map[string]json.RawMessage if err := c.ShouldBindJSON(&raw); err != nil { @@ -294,9 +301,9 @@ func (h *DashboardAppHandler) Search(c *gin.Context) { } // GetWidgets returns all widgets in a dashboard app's content. -// GET /api/v1/accounts/:account_id/dashboard_apps/:dashboard_app_id/widgets +// GET /api/v1/accounts/:account_id/dashboard_apps/:id/widgets func (h *DashboardAppHandler) GetWidgets(c *gin.Context) { - id, err := parseUintParam(c, "dashboard_app_id") + id, err := parseDashboardAppIDParam(c) if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid dashboard_app_id") return @@ -313,9 +320,9 @@ func (h *DashboardAppHandler) GetWidgets(c *gin.Context) { } // AddWidget adds a widget to a dashboard app's content array. -// POST /api/v1/accounts/:account_id/dashboard_apps/:dashboard_app_id/widgets +// POST /api/v1/accounts/:account_id/dashboard_apps/:id/widgets func (h *DashboardAppHandler) AddWidget(c *gin.Context) { - id, err := parseUintParam(c, "dashboard_app_id") + id, err := parseDashboardAppIDParam(c) if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid dashboard_app_id") return @@ -338,9 +345,9 @@ func (h *DashboardAppHandler) AddWidget(c *gin.Context) { } // RemoveWidget removes a widget from a dashboard app's content array by index. -// DELETE /api/v1/accounts/:account_id/dashboard_apps/:dashboard_app_id/widgets/:widget_index +// DELETE /api/v1/accounts/:account_id/dashboard_apps/:id/widgets/:widget_index func (h *DashboardAppHandler) RemoveWidget(c *gin.Context) { - id, err := parseUintParam(c, "dashboard_app_id") + id, err := parseDashboardAppIDParam(c) if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid dashboard_app_id") return @@ -363,10 +370,10 @@ func (h *DashboardAppHandler) RemoveWidget(c *gin.Context) { } // UpdateWidget updates a widget in a dashboard app's content array by index. -// PUT /api/v1/accounts/:account_id/dashboard_apps/:dashboard_app_id/widgets/:widget_index +// PUT /api/v1/accounts/:account_id/dashboard_apps/:id/widgets/:widget_index // GoChat extension — Chatwoot has no widget sub-resource CRUD. func (h *DashboardAppHandler) UpdateWidget(c *gin.Context) { - id, err := parseUintParam(c, "dashboard_app_id") + id, err := parseDashboardAppIDParam(c) if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid dashboard_app_id") return diff --git a/internal/handler/api/v1/dashboard_app_handler_test.go b/internal/handler/api/v1/dashboard_app_handler_test.go index ee6b96b8..160d6667 100644 --- a/internal/handler/api/v1/dashboard_app_handler_test.go +++ b/internal/handler/api/v1/dashboard_app_handler_test.go @@ -137,7 +137,7 @@ func (s *DashboardAppHandlerTestSuite) TestCreate_Success() { func (s *DashboardAppHandlerTestSuite) TestGet_Success() { r := gin.New() - r.GET("/api/v1/accounts/:account_id/dashboard_apps/:dashboard_app_id", s.handler.Get) + r.GET("/api/v1/accounts/:account_id/dashboard_apps/:id", s.handler.Get) // Seed via service (same DB path as handler) seedApp, err := s.handler.svc.Create(context.Background(), s.account.ID, &s.user.ID, &service.CreateDashboardAppRequest{ @@ -163,7 +163,7 @@ func (s *DashboardAppHandlerTestSuite) TestGet_Success() { func (s *DashboardAppHandlerTestSuite) TestPatch_RawPayloadAndAccountScope() { r := gin.New() - r.PATCH("/api/v1/accounts/:account_id/dashboard_apps/:dashboard_app_id", s.handler.Patch) + r.PATCH("/api/v1/accounts/:account_id/dashboard_apps/:id", s.handler.Patch) seedApp, err := s.handler.svc.Create(context.Background(), s.account.ID, &s.user.ID, &service.CreateDashboardAppRequest{ Title: "Before", @@ -192,7 +192,7 @@ func (s *DashboardAppHandlerTestSuite) TestPatch_RawPayloadAndAccountScope() { func (s *DashboardAppHandlerTestSuite) TestDelete_Success() { r := gin.New() - r.DELETE("/api/v1/accounts/:account_id/dashboard_apps/:dashboard_app_id", s.handler.Delete) + r.DELETE("/api/v1/accounts/:account_id/dashboard_apps/:id", s.handler.Delete) // Seed via service seedApp, err := s.handler.svc.Create(context.Background(), s.account.ID, &s.user.ID, &service.CreateDashboardAppRequest{ diff --git a/internal/router/router.go b/internal/router/router.go index e0299822..2b506f9b 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -1490,15 +1490,15 @@ func registerV1Routes(g *gin.RouterGroup, h *Handlers) { dashboardApps.GET("", h.DashboardApp.List) dashboardApps.POST("", h.DashboardApp.Create) dashboardApps.GET("/search", h.DashboardApp.Search) - dashboardApps.GET("/:dashboard_app_id", h.DashboardApp.Get) - dashboardApps.PUT("/:dashboard_app_id", h.DashboardApp.Update) - dashboardApps.PATCH("/:dashboard_app_id", h.DashboardApp.Patch) - dashboardApps.DELETE("/:dashboard_app_id", h.DashboardApp.Delete) + dashboardApps.GET("/:id", h.DashboardApp.Get) + dashboardApps.PUT("/:id", h.DashboardApp.Update) + dashboardApps.PATCH("/:id", h.DashboardApp.Patch) + dashboardApps.DELETE("/:id", h.DashboardApp.Delete) // Widget management (CRUD on content jsonb array) - dashboardApps.GET("/:dashboard_app_id/widgets", h.DashboardApp.GetWidgets) - dashboardApps.POST("/:dashboard_app_id/widgets", h.DashboardApp.AddWidget) - dashboardApps.PUT("/:dashboard_app_id/widgets/:widget_index", h.DashboardApp.UpdateWidget) - dashboardApps.DELETE("/:dashboard_app_id/widgets/:widget_index", h.DashboardApp.RemoveWidget) + dashboardApps.GET("/:id/widgets", h.DashboardApp.GetWidgets) + dashboardApps.POST("/:id/widgets", h.DashboardApp.AddWidget) + dashboardApps.PUT("/:id/widgets/:widget_index", h.DashboardApp.UpdateWidget) + dashboardApps.DELETE("/:id/widgets/:widget_index", h.DashboardApp.RemoveWidget) } // Knowledge Base / Help Center routes (M9)