From 5325e051438768800f1e13022f9968217bd74e20 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Wed, 29 Apr 2026 17:18:38 +0400 Subject: [PATCH 1/2] feat: Add platform-wide status banners for outage notifications (#13943) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a platform-wide status banner system to notify all users about external service outages. Super Admins can create, edit, and manage banners via the Super Admin console. Banners support markdown for links and are dismissible by users. image ## How to test 1. Set `ENABLE_PLATFORM_BANNERS=true` in your environment 2. Go to Super Admin → Platform Banners 3. Create a banner with a message like: `Elevated error rates from Meta APIs. [Check status](https://metastatus.com)` 4. Select a banner type: `info` (blue), `warning` (amber), or `error` (red) 5. Visit the dashboard — the banner should appear at the top 6. Click "Dismiss" — the banner hides and stays dismissed across page reloads 7. Deactivate the banner in Super Admin — it disappears on next page load ## What changed - New `PlatformBanner` model with `banner_message`, `banner_type` (info/warning/error), and `active` flag - Super Admin CRUD via Administrate (controller, dashboard, routes, sidebar icon) - `DashboardController` serves active banners via `globalConfig` - `StatusBanner.vue` component renders banners with markdown support and per-banner localStorage dismiss - Feature gated behind `ENABLE_PLATFORM_BANNERS` env var 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 (1M context) Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: iamsivin Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com> --- app/controllers/dashboard_controller.rb | 9 ++- .../platform_banners_controller.rb | 2 + app/dashboards/platform_banner_dashboard.rb | 20 ++++++ app/javascript/dashboard/App.vue | 3 + .../components-next/banner/Banner.vue | 14 ++--- .../dashboard/components/app/StatusBanner.vue | 63 +++++++++++++++++++ .../dashboard/components/ui/Banner.vue | 2 + .../dashboard/constants/localStorage.js | 1 + app/javascript/shared/store/globalConfig.js | 2 + app/models/platform_banner.rb | 18 ++++++ .../super_admin/application/_icons.html.erb | 4 ++ .../application/_navigation.html.erb | 1 + config/routes.rb | 1 + .../20260327065119_create_platform_banners.rb | 11 ++++ db/schema.rb | 8 +++ 15 files changed, 151 insertions(+), 8 deletions(-) create mode 100644 app/controllers/super_admin/platform_banners_controller.rb create mode 100644 app/dashboards/platform_banner_dashboard.rb create mode 100644 app/javascript/dashboard/components/app/StatusBanner.vue create mode 100644 app/models/platform_banner.rb create mode 100644 db/migrate/20260327065119_create_platform_banners.rb diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index d57ad0e53..b6df015f7 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -80,10 +80,17 @@ class DashboardController < ActionController::Base IS_ENTERPRISE: ChatwootApp.enterprise?, AZURE_APP_ID: GlobalConfigService.load('AZURE_APP_ID', ''), GIT_SHA: GIT_HASH, - ALLOWED_LOGIN_METHODS: allowed_login_methods + ALLOWED_LOGIN_METHODS: allowed_login_methods, + ACTIVE_PLATFORM_BANNERS: active_platform_banners } end + def active_platform_banners + return [] unless ChatwootApp.chatwoot_cloud? + + PlatformBanner.active.order(created_at: :desc).as_json(only: %i[id banner_message banner_type updated_at]) + end + def allowed_login_methods methods = ['email'] methods << 'google_oauth' if GlobalConfigService.load('ENABLE_GOOGLE_OAUTH_LOGIN', 'true').to_s != 'false' diff --git a/app/controllers/super_admin/platform_banners_controller.rb b/app/controllers/super_admin/platform_banners_controller.rb new file mode 100644 index 000000000..120dc3390 --- /dev/null +++ b/app/controllers/super_admin/platform_banners_controller.rb @@ -0,0 +1,2 @@ +class SuperAdmin::PlatformBannersController < SuperAdmin::ApplicationController +end diff --git a/app/dashboards/platform_banner_dashboard.rb b/app/dashboards/platform_banner_dashboard.rb new file mode 100644 index 000000000..41674406a --- /dev/null +++ b/app/dashboards/platform_banner_dashboard.rb @@ -0,0 +1,20 @@ +require 'administrate/base_dashboard' + +class PlatformBannerDashboard < Administrate::BaseDashboard + ATTRIBUTE_TYPES = { + id: Field::Number, + banner_message: Field::Text.with_options(truncate: 200), + banner_type: Field::Select.with_options(collection: %w[info warning error]), + active: Field::Boolean, + created_at: Field::DateTime, + updated_at: Field::DateTime + }.freeze + + COLLECTION_ATTRIBUTES = %i[id banner_message banner_type active created_at].freeze + SHOW_PAGE_ATTRIBUTES = %i[id banner_message banner_type active created_at updated_at].freeze + FORM_ATTRIBUTES = %i[banner_message banner_type active].freeze + + def display_resource(platform_banner) + "Banner ##{platform_banner.id} (#{platform_banner.banner_type})" + end +end diff --git a/app/javascript/dashboard/App.vue b/app/javascript/dashboard/App.vue index 988829407..99aebfd1a 100644 --- a/app/javascript/dashboard/App.vue +++ b/app/javascript/dashboard/App.vue @@ -3,6 +3,7 @@ import { mapGetters } from 'vuex'; import LoadingState from './components/widgets/LoadingState.vue'; import NetworkNotification from './components/NetworkNotification.vue'; import UpdateBanner from './components/app/UpdateBanner.vue'; +import StatusBanner from './components/app/StatusBanner.vue'; import PaymentPendingBanner from './components/app/PaymentPendingBanner.vue'; import PendingEmailVerificationBanner from './components/app/PendingEmailVerificationBanner.vue'; import vueActionCable from './helper/actionCable'; @@ -27,6 +28,7 @@ export default { LoadingState, NetworkNotification, UpdateBanner, + StatusBanner, PaymentPendingBanner, WootSnackbarBox, PendingEmailVerificationBanner, @@ -137,6 +139,7 @@ export default { :dir="isRTL ? 'rtl' : 'ltr'" > +