From 291475f0ec37c462c7d14748e19d5432c659948e Mon Sep 17 00:00:00 2001
From: Hamza Alqurneh
Date: Thu, 27 Aug 2026 16:12:04 +0300
Subject: [PATCH 1/3] fix: don't 500 or hang the ops pages when RabbitMQ
management isn't configured
Ops endpoints could 500 when management API calls failed; the frontend
also had no way to tell "not configured" from "still loading". Dashboard,
Queue health, and Work groups now check isRabbitMqManagementConfigured
and show a clear message instead of hanging or silently zeroing out.
---
.../Resources/Ops/UnattendedQueues.cs | 15 ++++++++--
SW.Bitween.Api/Resources/WorkGroups/Search.cs | 2 +-
.../ClientApp/src/api/http/dashboard.ts | 6 ++--
SW.Bitween.Web/ClientApp/src/api/types.ts | 3 +-
.../ClientApp/src/components/ui/basics.tsx | 12 +++++++-
SW.Bitween.Web/ClientApp/src/lib/appConfig.ts | 13 ++++++++
.../src/pages/dashboard/DashboardPage.tsx | 18 ++++++++---
.../pages/queue-health/QueueHealthPage.tsx | 30 +++++++++++++++++--
.../src/pages/work-groups/LiveQueueStats.tsx | 11 +++++++
.../src/pages/work-groups/WorkGroupsPage.tsx | 13 ++++++--
10 files changed, 107 insertions(+), 16 deletions(-)
create mode 100644 SW.Bitween.Web/ClientApp/src/lib/appConfig.ts
diff --git a/SW.Bitween.Api/Resources/Ops/UnattendedQueues.cs b/SW.Bitween.Api/Resources/Ops/UnattendedQueues.cs
index d231c86..d35e743 100644
--- a/SW.Bitween.Api/Resources/Ops/UnattendedQueues.cs
+++ b/SW.Bitween.Api/Resources/Ops/UnattendedQueues.cs
@@ -4,6 +4,7 @@
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using EasyNetQ.Management.Client;
+using EasyNetQ.Management.Client.Model;
using Microsoft.Extensions.Caching.Memory;
using SW.Bitween.Domain;
using SW.Bus;
@@ -62,9 +63,17 @@ public async Task Handle()
var queues = await memoryCache.GetOrCreateAsync("bitween-all-queues", async entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(busOptions.MonitoringCacheSeconds);
- var client = new ManagementClient(new Uri(busOptions.ManagementUrl),
- busOptions.ManagementUsername, busOptions.ManagementPassword);
- return await client.GetQueuesAsync(busOptions.VirtualHost);
+ try
+ {
+ var client = new ManagementClient(new Uri(busOptions.ManagementUrl),
+ busOptions.ManagementUsername, busOptions.ManagementPassword);
+ return await client.GetQueuesAsync(busOptions.VirtualHost);
+ }
+ catch
+ {
+ // Management API unreachable or misconfigured - degrade to "no data" instead of 500ing.
+ return Array.Empty();
+ }
});
var orphans = queues
diff --git a/SW.Bitween.Api/Resources/WorkGroups/Search.cs b/SW.Bitween.Api/Resources/WorkGroups/Search.cs
index d5c85e7..391098c 100644
--- a/SW.Bitween.Api/Resources/WorkGroups/Search.cs
+++ b/SW.Bitween.Api/Resources/WorkGroups/Search.cs
@@ -41,7 +41,7 @@ public async Task Handle(SearchWorkGroupModel request)
{
consumerCounts = await consumerReader.GetConsumerCount();
}
- catch (Exception ex) when (ex is TaskCanceledException or TimeoutException or System.Net.Http.HttpRequestException)
+ catch (Exception ex)
{
logger.LogWarning(ex, "Unable to load RabbitMQ consumer metrics for work groups.");
}
diff --git a/SW.Bitween.Web/ClientApp/src/api/http/dashboard.ts b/SW.Bitween.Web/ClientApp/src/api/http/dashboard.ts
index 426e230..ffd0158 100644
--- a/SW.Bitween.Web/ClientApp/src/api/http/dashboard.ts
+++ b/SW.Bitween.Web/ClientApp/src/api/http/dashboard.ts
@@ -45,7 +45,9 @@ export const dashboardMethods = {
`/xchanges?filter=${encodeURIComponent(`StartedOn:6:${new Date(windowStart).toISOString()}`)}&size=1000&sort=StartedOn:1`,
),
get>("/delayedretries?size=1"),
- get("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/ops/alerts"),
+ // Depends on RabbitMQ management being configured on the backend — don't let it take the
+ // rest of the dashboard down when it isn't; the "Queue alerts" tile flags it instead.
+ get("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/ops/alerts").catch(() => null),
subscriptionMethods.listSubscriptionRows(),
]);
@@ -117,7 +119,7 @@ export const dashboardMethods = {
yesterdayTotal: yesterdayRows.length,
successRate7d,
pendingRetries: delayedRes.totalCount,
- queueAlerts: alertsRaw.filter((a) => a.severity !== "Info").length,
+ queueAlerts: alertsRaw === null ? null : alertsRaw.filter((a) => a.severity !== "Info").length,
trafficByDay,
busiest,
latestFailures,
diff --git a/SW.Bitween.Web/ClientApp/src/api/types.ts b/SW.Bitween.Web/ClientApp/src/api/types.ts
index 3ee6743..3337a7f 100644
--- a/SW.Bitween.Web/ClientApp/src/api/types.ts
+++ b/SW.Bitween.Web/ClientApp/src/api/types.ts
@@ -1002,7 +1002,8 @@ export interface DashboardData {
/** Percentage 0–100 across the last 7 days of finished exchanges. */
successRate7d: number;
pendingRetries: number;
- queueAlerts: number;
+ /** Null when the RabbitMQ management API is unavailable — never counts as zero. */
+ queueAlerts: number | null;
/** Last 14 days, oldest first; today is the final entry. */
trafficByDay: { date: string; success: number; failed: number }[];
/** Top subscriptions by 7-day traffic, busiest first. */
diff --git a/SW.Bitween.Web/ClientApp/src/components/ui/basics.tsx b/SW.Bitween.Web/ClientApp/src/components/ui/basics.tsx
index 3a5e851..ed84d77 100644
--- a/SW.Bitween.Web/ClientApp/src/components/ui/basics.tsx
+++ b/SW.Bitween.Web/ClientApp/src/components/ui/basics.tsx
@@ -1,5 +1,5 @@
import type { ButtonHTMLAttributes, ReactNode } from "react";
-import { Loader2 } from "lucide-react";
+import { Loader2, TriangleAlert } from "lucide-react";
type ButtonVariant = "primary" | "secondary" | "ghost" | "danger";
@@ -108,6 +108,16 @@ export function EmptyState({
);
}
+/** Slim inline banner for a known, expected degraded state (e.g. an optional integration not configured). */
+export function InlineNotice({ children }: { children: ReactNode }) {
+ return (
+
+
+ {children}
+
+ );
+}
+
/** Inline error strip for failed form submissions. */
export function FormError({ children }: { children: ReactNode }) {
if (!children) return null;
diff --git a/SW.Bitween.Web/ClientApp/src/lib/appConfig.ts b/SW.Bitween.Web/ClientApp/src/lib/appConfig.ts
new file mode 100644
index 0000000..ceb6d9f
--- /dev/null
+++ b/SW.Bitween.Web/ClientApp/src/lib/appConfig.ts
@@ -0,0 +1,13 @@
+import { useQuery } from "@tanstack/react-query";
+import { getAppConfig } from "../api";
+
+/** Same query key as `useBranding`, so this shares its cache rather than firing a second fetch. */
+export function useAppConfig() {
+ return useQuery({ queryKey: ["appConfig"], queryFn: getAppConfig });
+}
+
+/** Defaults to true while the config is still loading, so pages don't flash a "not configured" state. */
+export function useRabbitMqManagementConfigured(): boolean {
+ const { data } = useAppConfig();
+ return data?.isRabbitMqManagementConfigured ?? true;
+}
diff --git a/SW.Bitween.Web/ClientApp/src/pages/dashboard/DashboardPage.tsx b/SW.Bitween.Web/ClientApp/src/pages/dashboard/DashboardPage.tsx
index e15c125..7614241 100644
--- a/SW.Bitween.Web/ClientApp/src/pages/dashboard/DashboardPage.tsx
+++ b/SW.Bitween.Web/ClientApp/src/pages/dashboard/DashboardPage.tsx
@@ -5,6 +5,7 @@ import { api } from "../../api";
import { PageHeader } from "../../components/layout/PageHeader";
import { Badge, EmptyState, LoadingBlock } from "../../components/ui/basics";
import { Panel } from "../../components/ui/Panel";
+import { useRabbitMqManagementConfigured } from "../../lib/appConfig";
import { timeAgo } from "../../lib/dates";
import { StatusBadge, XchangeId } from "../exchanges/shared";
@@ -50,15 +51,24 @@ function StatTile({
* what needs a human — everything links into the page that can act on it.
*/
export function DashboardPage() {
- const { data, isLoading } = useQuery({
+ const rabbitMqConfigured = useRabbitMqManagementConfigured();
+ const { data, isLoading, isError } = useQuery({
queryKey: ["dashboard"],
queryFn: () => api.getDashboard(),
refetchInterval: 60_000,
placeholderData: keepPreviousData,
});
+ if (isError && !data)
+ return (
+
+ Something went wrong fetching dashboard data. It'll keep retrying in the background — try
+ refreshing the page.
+
+ );
if (isLoading || !data) return ;
+ const queueAlerts = rabbitMqConfigured ? data.queueAlerts : null;
const delta = data.today.total - data.yesterdayTotal;
const maxDay = Math.max(1, ...data.trafficByDay.map((d) => d.success + d.failed));
const needsAttention =
@@ -107,10 +117,10 @@ export function DashboardPage() {
/>
0 ? "warn" : undefined}
+ accent={queueAlerts !== null && queueAlerts > 0 ? "warn" : undefined}
/>
diff --git a/SW.Bitween.Web/ClientApp/src/pages/queue-health/QueueHealthPage.tsx b/SW.Bitween.Web/ClientApp/src/pages/queue-health/QueueHealthPage.tsx
index 873d1e0..c4bf38e 100644
--- a/SW.Bitween.Web/ClientApp/src/pages/queue-health/QueueHealthPage.tsx
+++ b/SW.Bitween.Web/ClientApp/src/pages/queue-health/QueueHealthPage.tsx
@@ -1,13 +1,14 @@
import type { ReactNode } from "react";
import { Link } from "react-router";
import { keepPreviousData, useQuery } from "@tanstack/react-query";
-import { AlertTriangle, OctagonAlert } from "lucide-react";
+import { AlertTriangle, OctagonAlert, Unplug } from "lucide-react";
import { api, type ConsumerHealth, type QueueLane, type QueueSeverity } from "../../api";
import { PageHeader } from "../../components/layout/PageHeader";
import { Badge, EmptyState, LoadingBlock } from "../../components/ui/basics";
import { queueHealthTitle } from "../../components/config/shared";
import { Panel } from "../../components/ui/Panel";
import { timeAgo } from "../../lib/dates";
+import { useRabbitMqManagementConfigured } from "../../lib/appConfig";
const POLL_MS = 5_000;
@@ -89,13 +90,38 @@ function StatTile({ label, value, sub }: { label: string; value: ReactNode; sub?
* stays on screen while the next one loads, so nothing flashes.
*/
export function QueueHealthPage() {
- const { data, isLoading, dataUpdatedAt } = useQuery({
+ const rabbitMqConfigured = useRabbitMqManagementConfigured();
+ const { data, isLoading, isError, dataUpdatedAt } = useQuery({
queryKey: ["queue-health"],
queryFn: () => api.getQueueHealth(),
refetchInterval: POLL_MS,
placeholderData: keepPreviousData,
+ enabled: rabbitMqConfigured,
});
+ if (!rabbitMqConfigured)
+ return (
+
+
+
} title="RabbitMQ management isn't configured">
+ This page reads live queue stats from the RabbitMQ management API, which the backend
+ isn't configured to reach. Ask an admin to set the management URL, username and
+ password, then reload.
+
+
+ );
+
+ if (isError && !data)
+ return (
+
+ Something went wrong reaching the RabbitMQ management API. It'll keep retrying in the
+ background — try refreshing the page.
+
+ );
+
if (isLoading || !data) return ;
const { summary, consumers, retryBacklog, deadLetters, unattended, alerts } = data;
diff --git a/SW.Bitween.Web/ClientApp/src/pages/work-groups/LiveQueueStats.tsx b/SW.Bitween.Web/ClientApp/src/pages/work-groups/LiveQueueStats.tsx
index edd0f65..035ce64 100644
--- a/SW.Bitween.Web/ClientApp/src/pages/work-groups/LiveQueueStats.tsx
+++ b/SW.Bitween.Web/ClientApp/src/pages/work-groups/LiveQueueStats.tsx
@@ -3,6 +3,7 @@ import { keepPreviousData, useQuery } from "@tanstack/react-query";
import { api } from "../../api";
import { Badge } from "../../components/ui/basics";
import { queueHealthTitle } from "../../components/config/shared";
+import { useRabbitMqManagementConfigured } from "../../lib/appConfig";
function LiveStat({ label, value, tone }: { label: string; value: ReactNode; tone?: "warn" | "danger" }) {
return (
@@ -25,12 +26,22 @@ function LiveStat({ label, value, tone }: { label: string; value: ReactNode; ton
* these as columns instead, off the same shared query.
*/
export function LiveQueueStats({ groupId }: { groupId: number }) {
+ const rabbitMqConfigured = useRabbitMqManagementConfigured();
const { data } = useQuery({
queryKey: ["queue-health"],
queryFn: () => api.getQueueHealth(),
refetchInterval: 5_000,
placeholderData: keepPreviousData,
+ enabled: rabbitMqConfigured,
});
+
+ if (!rabbitMqConfigured)
+ return (
+
+ Live queue stats need RabbitMQ management configured on the backend.
+
+ );
+
const consumer = data?.consumers.find((c) => c.workGroupId === groupId);
if (!consumer)
diff --git a/SW.Bitween.Web/ClientApp/src/pages/work-groups/WorkGroupsPage.tsx b/SW.Bitween.Web/ClientApp/src/pages/work-groups/WorkGroupsPage.tsx
index 2f4814d..ad5c8d3 100644
--- a/SW.Bitween.Web/ClientApp/src/pages/work-groups/WorkGroupsPage.tsx
+++ b/SW.Bitween.Web/ClientApp/src/pages/work-groups/WorkGroupsPage.tsx
@@ -6,10 +6,11 @@ import { api, type QueueHealthSnapshot, type WorkGroupRow } from "../../api";
import { Can, useSessionCan } from "../../auth/guards";
import { WorkGroupDialog } from "../../components/config/WorkGroupDialog";
import { PageHeader } from "../../components/layout/PageHeader";
-import { Badge, Button, EmptyState, LoadingBlock } from "../../components/ui/basics";
+import { Badge, Button, EmptyState, InlineNotice, LoadingBlock } from "../../components/ui/basics";
import { Pagination } from "../../components/ui/Pagination";
import { Table, type Column } from "../../components/ui/Table";
import { UsedByCell, queueHealthTitle, useSubscriptionsCache } from "../../components/config/shared";
+import { useRabbitMqManagementConfigured } from "../../lib/appConfig";
/**
* The live RabbitMQ numbers, as columns rather than a per-row drill-down.
@@ -56,6 +57,7 @@ export function WorkGroupsPage() {
const q = searchParams.get("q") ?? "";
const offset = searchParams.get("offset") ? Number(searchParams.get("offset")) : 0;
const canMonitor = useSessionCan("monitoring.view");
+ const rabbitMqConfigured = useRabbitMqManagementConfigured();
const groups = useQuery({
queryKey: ["work-groups-search", q, offset],
@@ -68,7 +70,7 @@ export function WorkGroupsPage() {
queryFn: () => api.getQueueHealth(),
refetchInterval: 5_000,
placeholderData: keepPreviousData,
- enabled: canMonitor,
+ enabled: canMonitor && rabbitMqConfigured,
});
const setParam = (key: string, value: string | null, resetOffset = true) =>
@@ -126,6 +128,13 @@ export function WorkGroupsPage() {
/>
+ {canMonitor && !rabbitMqConfigured && (
+
+ Live queue stats (Health, Nodes, Queued, …) need RabbitMQ management configured on the
+ backend — those columns will stay blank until then.
+
+ )}
+
{groups.isPending ? (
) : filtered.length === 0 ? (
From b921e8149c1a3631ff2d549fa00501266cc670f7 Mon Sep 17 00:00:00 2001
From: Hamza Alqurneh
Date: Thu, 27 Aug 2026 16:14:33 +0300
Subject: [PATCH 2/3] chore: bump Bus, Serverless and CloudFiles packages to
latest
SimplyWorks.Bus/.RabbitMqExtensions 8.1.11 -> 8.1.18 (includes today's
ConsumerReader fixes), Serverless(.Sdk) 8.1.5/8.1.1 -> 8.1.11,
CloudFiles.* 8.1.1/8.1.6 -> 8.1.12. Bumped Azure.Identity to the minimum
CloudFiles.AS now requires, and migrated NativeS3Receiver off the removed
CloudFilesOptions.CreateClient() to the new S3CloudFilesOptions type.
---
SW.Bitween.Api/SW.Bitween.Api.csproj | 4 ++--
.../SW.Bitween.IntegrationTests.csproj | 6 +++---
.../S3Receiver/NativeS3Receiver.cs | 2 +-
.../SW.Bitween.NativeAdapters.csproj | 2 +-
.../SW.Bitween.SampleConfigurableAdapter.csproj | 2 +-
.../SW.Bitween.SampleHandler.csproj | 2 +-
.../SW.Bitween.SampleMapper.csproj | 2 +-
.../SW.Bitween.SampleValidator.csproj | 2 +-
SW.Bitween.UnitTests/SW.Bitween.UnitTests.csproj | 2 +-
SW.Bitween.Web/SW.Bitween.Web.csproj | 14 +++++++-------
10 files changed, 19 insertions(+), 19 deletions(-)
diff --git a/SW.Bitween.Api/SW.Bitween.Api.csproj b/SW.Bitween.Api/SW.Bitween.Api.csproj
index 6ec8358..da390ed 100644
--- a/SW.Bitween.Api/SW.Bitween.Api.csproj
+++ b/SW.Bitween.Api/SW.Bitween.Api.csproj
@@ -32,8 +32,8 @@
them to ask the broker what exists; without this it would have to rebuild each of them
from configuration and get them subtly wrong. Already in the app through SW.Bitween.Web —
keep the version in step with it. -->
-
-
+
+
diff --git a/SW.Bitween.IntegrationTests/SW.Bitween.IntegrationTests.csproj b/SW.Bitween.IntegrationTests/SW.Bitween.IntegrationTests.csproj
index 53c7635..7415050 100644
--- a/SW.Bitween.IntegrationTests/SW.Bitween.IntegrationTests.csproj
+++ b/SW.Bitween.IntegrationTests/SW.Bitween.IntegrationTests.csproj
@@ -15,7 +15,7 @@
-
+
all
@@ -27,10 +27,10 @@
-
+
-
+
diff --git a/SW.Bitween.NativeAdapters/S3Receiver/NativeS3Receiver.cs b/SW.Bitween.NativeAdapters/S3Receiver/NativeS3Receiver.cs
index 0514890..102f24d 100644
--- a/SW.Bitween.NativeAdapters/S3Receiver/NativeS3Receiver.cs
+++ b/SW.Bitween.NativeAdapters/S3Receiver/NativeS3Receiver.cs
@@ -13,7 +13,7 @@ public class NativeS3Receiver : INativeInfolinkReceiver, IDisposable
public Task Initialize()
{
- var options = new CloudFilesOptions
+ var options = new S3CloudFilesOptions
{
AccessKeyId = _options.AccessKeyId,
SecretAccessKey = _options.SecretAccessKey,
diff --git a/SW.Bitween.NativeAdapters/SW.Bitween.NativeAdapters.csproj b/SW.Bitween.NativeAdapters/SW.Bitween.NativeAdapters.csproj
index 007c67b..b12b1a8 100644
--- a/SW.Bitween.NativeAdapters/SW.Bitween.NativeAdapters.csproj
+++ b/SW.Bitween.NativeAdapters/SW.Bitween.NativeAdapters.csproj
@@ -23,7 +23,7 @@
-
+
diff --git a/SW.Bitween.SampleConfigurableAdapter/SW.Bitween.SampleConfigurableAdapter.csproj b/SW.Bitween.SampleConfigurableAdapter/SW.Bitween.SampleConfigurableAdapter.csproj
index 574f696..ca017b8 100644
--- a/SW.Bitween.SampleConfigurableAdapter/SW.Bitween.SampleConfigurableAdapter.csproj
+++ b/SW.Bitween.SampleConfigurableAdapter/SW.Bitween.SampleConfigurableAdapter.csproj
@@ -5,6 +5,6 @@
SW.Bitween.SampleConfigurableAdapter
-
+
diff --git a/SW.Bitween.SampleHandler/SW.Bitween.SampleHandler.csproj b/SW.Bitween.SampleHandler/SW.Bitween.SampleHandler.csproj
index 0a1f988..cb83ae7 100644
--- a/SW.Bitween.SampleHandler/SW.Bitween.SampleHandler.csproj
+++ b/SW.Bitween.SampleHandler/SW.Bitween.SampleHandler.csproj
@@ -7,7 +7,7 @@
-
+
diff --git a/SW.Bitween.SampleMapper/SW.Bitween.SampleMapper.csproj b/SW.Bitween.SampleMapper/SW.Bitween.SampleMapper.csproj
index b98f76c..bc31167 100644
--- a/SW.Bitween.SampleMapper/SW.Bitween.SampleMapper.csproj
+++ b/SW.Bitween.SampleMapper/SW.Bitween.SampleMapper.csproj
@@ -7,7 +7,7 @@
-
+
diff --git a/SW.Bitween.SampleValidator/SW.Bitween.SampleValidator.csproj b/SW.Bitween.SampleValidator/SW.Bitween.SampleValidator.csproj
index b9dc71a..a6ba465 100644
--- a/SW.Bitween.SampleValidator/SW.Bitween.SampleValidator.csproj
+++ b/SW.Bitween.SampleValidator/SW.Bitween.SampleValidator.csproj
@@ -8,7 +8,7 @@
-
+
diff --git a/SW.Bitween.UnitTests/SW.Bitween.UnitTests.csproj b/SW.Bitween.UnitTests/SW.Bitween.UnitTests.csproj
index 188a86d..eec835e 100644
--- a/SW.Bitween.UnitTests/SW.Bitween.UnitTests.csproj
+++ b/SW.Bitween.UnitTests/SW.Bitween.UnitTests.csproj
@@ -19,7 +19,7 @@
-
+
diff --git a/SW.Bitween.Web/SW.Bitween.Web.csproj b/SW.Bitween.Web/SW.Bitween.Web.csproj
index dede759..9efbfd9 100644
--- a/SW.Bitween.Web/SW.Bitween.Web.csproj
+++ b/SW.Bitween.Web/SW.Bitween.Web.csproj
@@ -36,19 +36,19 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
-
-
-
-
-
+
+
+
+
+
-
+
From 7d0c9bdf1555188286fa3d968b43633bd0eb08d5 Mon Sep 17 00:00:00 2001
From: Hamza Alqurneh
Date: Thu, 27 Aug 2026 16:29:51 +0300
Subject: [PATCH 3/3] fix: address CodeRabbit findings on the
ops-not-configured PR
- Don't cache a failed unattended-queues fetch (same bug as ConsumerReader)
- Narrow WorkGroups/Search's catch back to network exceptions only, so a
real ConsumerDiscovery bug surfaces instead of rendering as empty metrics
- Distinguish "not configured" from "configured but unreachable" in the
dashboard's Queue alerts tile and LiveQueueStats
- Stop stale cached queue-health data from rendering in Work groups' live
columns after RabbitMQ management becomes unconfigured mid-session
---
SW.Bitween.Api/Resources/Ops/UnattendedQueues.cs | 12 +++++++-----
SW.Bitween.Api/Resources/WorkGroups/Search.cs | 6 +++++-
.../ClientApp/src/pages/dashboard/DashboardPage.tsx | 7 ++++++-
.../src/pages/work-groups/LiveQueueStats.tsx | 5 ++++-
.../src/pages/work-groups/WorkGroupsPage.tsx | 5 ++++-
5 files changed, 26 insertions(+), 9 deletions(-)
diff --git a/SW.Bitween.Api/Resources/Ops/UnattendedQueues.cs b/SW.Bitween.Api/Resources/Ops/UnattendedQueues.cs
index d35e743..0c9af0c 100644
--- a/SW.Bitween.Api/Resources/Ops/UnattendedQueues.cs
+++ b/SW.Bitween.Api/Resources/Ops/UnattendedQueues.cs
@@ -60,21 +60,23 @@ public async Task Handle()
}
// Cached on the same clock as the bus's own management call, because this page polls.
- var queues = await memoryCache.GetOrCreateAsync("bitween-all-queues", async entry =>
+ // A failed fetch is deliberately not cached: caching it would report "no queues" for the
+ // rest of the cache window even after the management API recovers.
+ if (!memoryCache.TryGetValue("bitween-all-queues", out IReadOnlyList queues))
{
- entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(busOptions.MonitoringCacheSeconds);
try
{
var client = new ManagementClient(new Uri(busOptions.ManagementUrl),
busOptions.ManagementUsername, busOptions.ManagementPassword);
- return await client.GetQueuesAsync(busOptions.VirtualHost);
+ queues = await client.GetQueuesAsync(busOptions.VirtualHost);
+ memoryCache.Set("bitween-all-queues", queues, TimeSpan.FromSeconds(busOptions.MonitoringCacheSeconds));
}
catch
{
// Management API unreachable or misconfigured - degrade to "no data" instead of 500ing.
- return Array.Empty();
+ queues = Array.Empty();
}
- });
+ }
var orphans = queues
.Where(q => q.Name.StartsWith($"{prefix}.", StringComparison.OrdinalIgnoreCase))
diff --git a/SW.Bitween.Api/Resources/WorkGroups/Search.cs b/SW.Bitween.Api/Resources/WorkGroups/Search.cs
index 391098c..43a5a5f 100644
--- a/SW.Bitween.Api/Resources/WorkGroups/Search.cs
+++ b/SW.Bitween.Api/Resources/WorkGroups/Search.cs
@@ -41,8 +41,12 @@ public async Task Handle(SearchWorkGroupModel request)
{
consumerCounts = await consumerReader.GetConsumerCount();
}
- catch (Exception ex)
+ catch (Exception ex) when (ex is TaskCanceledException or TimeoutException or System.Net.Http.HttpRequestException)
{
+ // The RabbitMQ management API call itself already degrades to empty data on failure
+ // (ConsumerReader.GetConsumerCounts) - this only guards the network-facing edge of
+ // that call. Anything else (e.g. ConsumerDiscovery.Load() throwing) is a real bug and
+ // should surface as a 500, not silently render as "no metrics".
logger.LogWarning(ex, "Unable to load RabbitMQ consumer metrics for work groups.");
}
diff --git a/SW.Bitween.Web/ClientApp/src/pages/dashboard/DashboardPage.tsx b/SW.Bitween.Web/ClientApp/src/pages/dashboard/DashboardPage.tsx
index 7614241..1f5f20b 100644
--- a/SW.Bitween.Web/ClientApp/src/pages/dashboard/DashboardPage.tsx
+++ b/SW.Bitween.Web/ClientApp/src/pages/dashboard/DashboardPage.tsx
@@ -69,6 +69,11 @@ export function DashboardPage() {
if (isLoading || !data) return ;
const queueAlerts = rabbitMqConfigured ? data.queueAlerts : null;
+ const queueAlertsSub = !rabbitMqConfigured
+ ? "needs RabbitMQ management configured"
+ : queueAlerts === null
+ ? "unavailable right now"
+ : "live consumer health";
const delta = data.today.total - data.yesterdayTotal;
const maxDay = Math.max(1, ...data.trafficByDay.map((d) => d.success + d.failed));
const needsAttention =
@@ -118,7 +123,7 @@ export function DashboardPage() {
0 ? "warn" : undefined}
/>
diff --git a/SW.Bitween.Web/ClientApp/src/pages/work-groups/LiveQueueStats.tsx b/SW.Bitween.Web/ClientApp/src/pages/work-groups/LiveQueueStats.tsx
index 035ce64..93001d3 100644
--- a/SW.Bitween.Web/ClientApp/src/pages/work-groups/LiveQueueStats.tsx
+++ b/SW.Bitween.Web/ClientApp/src/pages/work-groups/LiveQueueStats.tsx
@@ -27,7 +27,7 @@ function LiveStat({ label, value, tone }: { label: string; value: ReactNode; ton
*/
export function LiveQueueStats({ groupId }: { groupId: number }) {
const rabbitMqConfigured = useRabbitMqManagementConfigured();
- const { data } = useQuery({
+ const { data, isError } = useQuery({
queryKey: ["queue-health"],
queryFn: () => api.getQueueHealth(),
refetchInterval: 5_000,
@@ -42,6 +42,9 @@ export function LiveQueueStats({ groupId }: { groupId: number }) {
);
+ if (isError && !data)
+ return Couldn't reach RabbitMQ management for live queue stats.
;
+
const consumer = data?.consumers.find((c) => c.workGroupId === groupId);
if (!consumer)
diff --git a/SW.Bitween.Web/ClientApp/src/pages/work-groups/WorkGroupsPage.tsx b/SW.Bitween.Web/ClientApp/src/pages/work-groups/WorkGroupsPage.tsx
index ad5c8d3..54ab37d 100644
--- a/SW.Bitween.Web/ClientApp/src/pages/work-groups/WorkGroupsPage.tsx
+++ b/SW.Bitween.Web/ClientApp/src/pages/work-groups/WorkGroupsPage.tsx
@@ -166,7 +166,10 @@ export function WorkGroupsPage() {
truncate: true,
cell: (g) => s.workGroupId === g.id)} />,
},
- ...(canMonitor ? liveColumns(live.data) : []),
+ // `enabled: false` on the `live` query only stops it refetching — it doesn't clear a
+ // result already cached from before RabbitMQ management was disabled. Pass undefined
+ // explicitly so the columns actually go blank rather than showing stale numbers.
+ ...(canMonitor ? liveColumns(rabbitMqConfigured ? live.data : undefined) : []),
{
header: "",
align: "right",