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 } = {}) => {