From d8b048480396ae73a3f90e73893be5d2c023687a Mon Sep 17 00:00:00 2001 From: Sluijsens Date: Mon, 22 Jun 2026 14:53:11 +0200 Subject: [PATCH] fix(availability): translate status messages, return stored admin reason from API - Add AvailabilityStatusDetails record and GetStatusDetailsAsync() to IAvailabilityService; implement in PersistentAvailabilityService so the admin-set reason stored in the database is returned alongside the status - AvailabilityController.GetStatus() now returns the stored message instead of hardcoded English strings - Add messageAvailable / messageMaintenance / messageUnavailable translation keys in en + nl so default messages are fully translatable - AvailabilityStatusBadge: Available always shows the translated default; Maintenance and Unavailable show the custom admin reason when set, otherwise fall back to the translated default Co-Authored-By: Claude Haiku 4.5 --- .../shared/AvailabilityStatusBadge.test.tsx | 44 ++++++++++++++++--- .../shared/AvailabilityStatusBadge.tsx | 41 ++++++++++------- frontend/src/i18n/locales/en/translation.json | 3 ++ frontend/src/i18n/locales/nl/translation.json | 3 ++ .../Availability/AvailabilityStatusDetails.cs | 6 +++ .../Availability/IAvailabilityService.cs | 5 +++ .../Controllers/AvailabilityController.cs | 6 +-- .../Services/PersistentAvailabilityService.cs | 18 ++++++++ 8 files changed, 100 insertions(+), 26 deletions(-) create mode 100644 src/SlpModularCms.Core/Availability/AvailabilityStatusDetails.cs diff --git a/frontend/src/components/shared/AvailabilityStatusBadge.test.tsx b/frontend/src/components/shared/AvailabilityStatusBadge.test.tsx index 6ce0f47..f903cc2 100644 --- a/frontend/src/components/shared/AvailabilityStatusBadge.test.tsx +++ b/frontend/src/components/shared/AvailabilityStatusBadge.test.tsx @@ -34,7 +34,23 @@ describe('AvailabilityStatusBadge', () => { expect(badge.querySelector('.bg-red-100')).toBeTruthy(); }); - it('shows message subtitle when message is non-empty', () => { + it('Available always shows the default translated message, ignoring any backend message', () => { + renderWithProviders(); + + expect(screen.getByTestId('availability-message')).toHaveTextContent( + 'System is running normally.', + ); + }); + + it('Available shows the default translated message when no message is provided', () => { + renderWithProviders(); + + expect(screen.getByTestId('availability-message')).toHaveTextContent( + 'System is running normally.', + ); + }); + + it('Maintenance shows a custom reason when provided', () => { renderWithProviders( , ); @@ -44,16 +60,30 @@ describe('AvailabilityStatusBadge', () => { ); }); - it('hides message element when message is empty string', () => { - renderWithProviders(); + it('Maintenance falls back to default translated message when no reason is provided', () => { + renderWithProviders(); - expect(screen.queryByTestId('availability-message')).toBeNull(); + expect(screen.getByTestId('availability-message')).toHaveTextContent( + 'System is undergoing scheduled maintenance.', + ); }); - it('hides message element when message prop is omitted', () => { - renderWithProviders(); + it('Unavailable shows a custom reason when provided', () => { + renderWithProviders( + , + ); - expect(screen.queryByTestId('availability-message')).toBeNull(); + expect(screen.getByTestId('availability-message')).toHaveTextContent( + 'Emergency shutdown in progress', + ); + }); + + it('Unavailable falls back to default translated message when no reason is provided', () => { + renderWithProviders(); + + expect(screen.getByTestId('availability-message')).toHaveTextContent( + 'System is currently unavailable.', + ); }); it('shows stale indicator when stale={true}', () => { diff --git a/frontend/src/components/shared/AvailabilityStatusBadge.tsx b/frontend/src/components/shared/AvailabilityStatusBadge.tsx index cae3f66..c43f0b7 100644 --- a/frontend/src/components/shared/AvailabilityStatusBadge.tsx +++ b/frontend/src/components/shared/AvailabilityStatusBadge.tsx @@ -10,24 +10,34 @@ interface AvailabilityStatusBadgeProps { const STATUS_CONFIG: Record< AvailabilityStatus, - { labelKey: string; colorClass: string; Icon: React.ComponentType<{ className?: string }> } + { + labelKey: string; + defaultMessageKey: string; + /** When true the default translated message is always shown, ignoring any backend message. */ + alwaysDefault: boolean; + colorClass: string; + Icon: React.ComponentType<{ className?: string }>; + } > = { Available: { labelKey: 'availability.available', - colorClass: - 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200', + defaultMessageKey: 'availability.messageAvailable', + alwaysDefault: true, + colorClass: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200', Icon: CheckCircle, }, Maintenance: { labelKey: 'availability.maintenance', - colorClass: - 'bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200', + defaultMessageKey: 'availability.messageMaintenance', + alwaysDefault: false, + colorClass: 'bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200', Icon: AlertTriangle, }, Unavailable: { labelKey: 'availability.unavailable', - colorClass: - 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200', + defaultMessageKey: 'availability.messageUnavailable', + alwaysDefault: false, + colorClass: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200', Icon: XCircle, }, }; @@ -38,7 +48,8 @@ export function AvailabilityStatusBadge({ stale = false, }: AvailabilityStatusBadgeProps) { const { t } = useTranslation(); - const { labelKey, colorClass, Icon } = STATUS_CONFIG[status]; + const { labelKey, defaultMessageKey, alwaysDefault, colorClass, Icon } = STATUS_CONFIG[status]; + const displayMessage = alwaysDefault ? t(defaultMessageKey) : (message || t(defaultMessageKey)); return (
{t(labelKey)}
- {message && message.length > 0 && ( -

- {message} -

- )} +

+ {displayMessage} +

{stale && (
+/// Status en optionele admin-melding van de systeembeschikbaarheid. +/// +public record AvailabilityStatusDetails(AvailabilityStatus Status, string? Message); diff --git a/src/SlpModularCms.Core/Availability/IAvailabilityService.cs b/src/SlpModularCms.Core/Availability/IAvailabilityService.cs index 3860c2c..149fe0e 100644 --- a/src/SlpModularCms.Core/Availability/IAvailabilityService.cs +++ b/src/SlpModularCms.Core/Availability/IAvailabilityService.cs @@ -10,4 +10,9 @@ public interface IAvailabilityService /// /// De huidige . Task IsAvailableAsync(); + + /// + /// Geeft de huidige status inclusief de optionele admin-melding terug. + /// + Task GetStatusDetailsAsync(); } diff --git a/src/SlpModularCms.Modules.Availability/Controllers/AvailabilityController.cs b/src/SlpModularCms.Modules.Availability/Controllers/AvailabilityController.cs index 71284c0..533ca6b 100644 --- a/src/SlpModularCms.Modules.Availability/Controllers/AvailabilityController.cs +++ b/src/SlpModularCms.Modules.Availability/Controllers/AvailabilityController.cs @@ -20,12 +20,12 @@ public class AvailabilityController : ControllerBase [AllowAnonymous] public async Task GetStatus() { - var status = await _availabilityService.IsAvailableAsync(); + var details = await _availabilityService.GetStatusDetailsAsync(); return Ok(new { - Status = status.ToString(), + Status = details.Status.ToString(), CheckedAt = DateTimeOffset.UtcNow, - Message = string.Empty, + Message = details.Message ?? string.Empty, }); } diff --git a/src/SlpModularCms.Modules.Availability/Services/PersistentAvailabilityService.cs b/src/SlpModularCms.Modules.Availability/Services/PersistentAvailabilityService.cs index 57ec202..4ec129e 100644 --- a/src/SlpModularCms.Modules.Availability/Services/PersistentAvailabilityService.cs +++ b/src/SlpModularCms.Modules.Availability/Services/PersistentAvailabilityService.cs @@ -54,6 +54,24 @@ public class PersistentAvailabilityService : IAvailabilityService } } + public async Task GetStatusDetailsAsync() + { + try + { + var state = await _context.AvailabilityStates.FirstOrDefaultAsync(); + if (state == null) + return new AvailabilityStatusDetails(AvailabilityStatus.Available, null); + + return new AvailabilityStatusDetails(state.Status, state.Message); + } + catch (Exception) + { + _lastErrorTime = DateTimeOffset.UtcNow; + _cachedStatus = AvailabilityStatus.Available; + return new AvailabilityStatusDetails(_cachedStatus, null); + } + } + /// /// Update de globale systeemstatus. ///