diff --git a/pkg/console/console.go b/pkg/console/console.go
index 092e52a1bb..735802ae7c 100644
--- a/pkg/console/console.go
+++ b/pkg/console/console.go
@@ -106,6 +106,7 @@ func generateConsoleCSPString(config *Config, nonce string, others ...webui.Cont
ConnectionSource: append([]string{
"'self'",
config.UI.SentryDSN,
+ config.UI.StatusPage,
"gravatar.com",
"www.gravatar.com",
}, baseURLs...),
diff --git a/pkg/identityserver/identityserver.go b/pkg/identityserver/identityserver.go
index e26e762196..df39b5fbda 100644
--- a/pkg/identityserver/identityserver.go
+++ b/pkg/identityserver/identityserver.go
@@ -96,6 +96,7 @@ func GenerateCSPString(config *oauth.Config, nonce string) string {
ConnectionSource: append([]string{
"'self'",
config.UI.SentryDSN,
+ config.UI.StatusPage,
"gravatar.com",
"www.gravatar.com",
}, baseURLs...),
diff --git a/pkg/webui/components/alert-banner/alert-banner.styl b/pkg/webui/components/alert-banner/alert-banner.styl
new file mode 100644
index 0000000000..d00f1ba044
--- /dev/null
+++ b/pkg/webui/components/alert-banner/alert-banner.styl
@@ -0,0 +1,64 @@
+// Copyright © 2024 The Things Network Foundation, The Things Industries B.V.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+.alert-banner
+ position: absolute
+ right: 0
+ left: 0
+ z-index: $zi.slight
+ padding: $cs.m
+ opacity: 0
+ pointer-events: none
+ transition: opacity $ad.m
+ &.visible
+ opacity: 1
+ pointer-events: auto
+
+ .link > span
+ text-decoration: underline
+
+ .title
+ line-height: normal
+
+.error
+ background-color: var(--c-bg-error-light)
+ color: var(--c-text-error-bold)
+ .link, .closeIcon
+ color: var(--c-text-error-bold)
+ &:hover
+ color: var(--c-text-error-normal-hover)
+
+.success
+ background-color: var(--c-bg-success-light)
+ color: var(--c-text-success-bold)
+ .link, .closeIcon
+ color: var(--c-text-success-bold)
+ &:hover
+ color: var(--c-text-success-normal-hover)
+
+.warning
+ background-color: var(--c-bg-warning-light)
+ color: var(--c-text-warning-bold)
+ .link, .closeIcon
+ color: var(--c-text-warning-bold)
+ &:hover
+ color: var(--c-text-warning-normal-hover)
+
+.info
+ background-color: var(--c-bg-info-light)
+ color: var(--c-text-info-bold)
+ .link, .closeIcon
+ color: var(--c-text-info-bold)
+ &:hover
+ color: var(--c-text-info-normal-hover)
diff --git a/pkg/webui/components/alert-banner/context.js b/pkg/webui/components/alert-banner/context.js
new file mode 100644
index 0000000000..dcf50e57b0
--- /dev/null
+++ b/pkg/webui/components/alert-banner/context.js
@@ -0,0 +1,84 @@
+// Copyright © 2024 The Things Network Foundation, The Things Industries B.V.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import React, { useContext, useRef, useState } from 'react'
+
+import AlertBanner from '@ttn-lw/components/alert-banner/index'
+
+import PropTypes from '@ttn-lw/lib/prop-types'
+
+const AlertBannerContext = React.createContext()
+const { Provider } = AlertBannerContext
+
+const AlertBannerProvider = ({ children }) => {
+ const [type, setType] = useState('info')
+ const [open, setOpen] = useState(false)
+ const [title, setTitle] = useState('')
+ const [subtitle, setSubtitle] = useState(undefined)
+ const [titleValues, setTitleValues] = useState(undefined)
+ const [subtitleValues, setSubtitleValues] = useState(undefined)
+ const timeoutRef = useRef(null)
+ const showBanner = ({ type, title, duration, subtitle, titleValues, subtitleValues }) => {
+ if (window.innerWidth <= 768) {
+ clearTimeout(timeoutRef.current)
+ setType(type)
+ setTitle(title)
+ setSubtitle(subtitle)
+ setTitleValues(titleValues)
+ setSubtitleValues(subtitleValues)
+ setOpen(true)
+ if (duration) {
+ timeoutRef.current = setTimeout(() => {
+ setOpen(false)
+ }, duration)
+ }
+ }
+ }
+ const closeBanner = () => {
+ setOpen(false)
+ clearTimeout(timeoutRef.current)
+ }
+ const value = {
+ showBanner,
+ }
+
+ return (
+