From 10cc300f3115da522fc5e749a7155cf26a9d5546 Mon Sep 17 00:00:00 2001 From: Rogee Date: Wed, 8 Jul 2026 11:43:34 +0800 Subject: [PATCH] Fix canned_responses 500: add missing SQL migration The canned_responses table was registered in autoMigrate() (app.go) but autoMigrate() is never called on the serve path (Bootstrap -> NewDatabase -> RunMigrations only). With no SQL migration file, the table never existed, so POST /api/v1/accounts/:id/canned_responses failed with 'relation does not exist' -> 500. Add migration 000049 to create the table matching the GORM model (id, account_id, content, short_code, timestamps) with the account+ short_code partial unique index and deleted_at index. Also include two related fixes staged in the working tree: - router.go: register notifications.GET("") alongside GET("/") to avoid Gin 301 trailing-slash redirect that the Vite proxy doesn't follow - notifications/actions.js: stop the infinite loader on fetch error to prevent IntersectionObserver re-fire loop --- backend/internal/router/router.go | 5 +++++ .../000049_add_canned_responses_table.down.sql | 1 + .../000049_add_canned_responses_table.up.sql | 12 ++++++++++++ .../dashboard/store/modules/notifications/actions.js | 4 ++++ 4 files changed, 22 insertions(+) create mode 100644 backend/migrations/000049_add_canned_responses_table.down.sql create mode 100644 backend/migrations/000049_add_canned_responses_table.up.sql diff --git a/backend/internal/router/router.go b/backend/internal/router/router.go index 00570038..751d39f5 100644 --- a/backend/internal/router/router.go +++ b/backend/internal/router/router.go @@ -1732,6 +1732,11 @@ func registerV1Routes(g *gin.RouterGroup, h *Handlers) { // Notifications — account-scoped Chatwoot routes used by the dashboard frontend. notifications := accountScoped.Group("/notifications") { + // Register both "" and "/" so Gin does not 301-redirect between them. + // The dashboard frontend requests /notifications (no trailing slash); + // without the "" route Gin's RedirectTrailingSlash emits a 301 that + // the Vite dev proxy does not follow, causing an infinite-load loop. + notifications.GET("", h.Notification.List) notifications.GET("/", h.Notification.List) notifications.POST("/read_all", h.Notification.MarkAllRead) notifications.GET("/unread_count", h.Notification.UnreadCount) diff --git a/backend/migrations/000049_add_canned_responses_table.down.sql b/backend/migrations/000049_add_canned_responses_table.down.sql new file mode 100644 index 00000000..ac122622 --- /dev/null +++ b/backend/migrations/000049_add_canned_responses_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS canned_responses; diff --git a/backend/migrations/000049_add_canned_responses_table.up.sql b/backend/migrations/000049_add_canned_responses_table.up.sql new file mode 100644 index 00000000..7e12066d --- /dev/null +++ b/backend/migrations/000049_add_canned_responses_table.up.sql @@ -0,0 +1,12 @@ +CREATE TABLE IF NOT EXISTS canned_responses ( + id SERIAL PRIMARY KEY, + account_id INTEGER NOT NULL, + content TEXT NOT NULL, + short_code VARCHAR(255) NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + deleted_at TIMESTAMPTZ +); +CREATE INDEX IF NOT EXISTS idx_canned_responses_account_id ON canned_responses(account_id); +CREATE UNIQUE INDEX IF NOT EXISTS idx_canned_short_code ON canned_responses(account_id, short_code) WHERE deleted_at IS NULL; +CREATE INDEX IF NOT EXISTS idx_canned_responses_deleted_at ON canned_responses(deleted_at); diff --git a/frontend/app/javascript/dashboard/store/modules/notifications/actions.js b/frontend/app/javascript/dashboard/store/modules/notifications/actions.js index 36e953c5..ed1518a2 100644 --- a/frontend/app/javascript/dashboard/store/modules/notifications/actions.js +++ b/frontend/app/javascript/dashboard/store/modules/notifications/actions.js @@ -39,6 +39,10 @@ export const actions = { } } catch (error) { commit(types.SET_NOTIFICATIONS_UI_FLAG, { isFetching: false }); + // On error, stop the infinite loader — otherwise IntersectionObserver + // keeps firing loadMoreNotifications, incrementing page forever and + // re-issuing the failing request in a tight loop. + commit(types.SET_ALL_NOTIFICATIONS_LOADED); } }, unReadCount: async ({ commit } = {}) => {