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 <noreply@anthropic.com>
This commit is contained in:
@@ -34,7 +34,23 @@ describe('AvailabilityStatusBadge', () => {
|
|||||||
expect(badge.querySelector('.bg-red-100')).toBeTruthy();
|
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(<AvailabilityStatusBadge status="Available" message="ignored" />);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('availability-message')).toHaveTextContent(
|
||||||
|
'System is running normally.',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Available shows the default translated message when no message is provided', () => {
|
||||||
|
renderWithProviders(<AvailabilityStatusBadge status="Available" />);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('availability-message')).toHaveTextContent(
|
||||||
|
'System is running normally.',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Maintenance shows a custom reason when provided', () => {
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<AvailabilityStatusBadge status="Maintenance" message="Scheduled downtime until 18:00" />,
|
<AvailabilityStatusBadge status="Maintenance" message="Scheduled downtime until 18:00" />,
|
||||||
);
|
);
|
||||||
@@ -44,16 +60,30 @@ describe('AvailabilityStatusBadge', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('hides message element when message is empty string', () => {
|
it('Maintenance falls back to default translated message when no reason is provided', () => {
|
||||||
renderWithProviders(<AvailabilityStatusBadge status="Available" message="" />);
|
renderWithProviders(<AvailabilityStatusBadge status="Maintenance" />);
|
||||||
|
|
||||||
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', () => {
|
it('Unavailable shows a custom reason when provided', () => {
|
||||||
renderWithProviders(<AvailabilityStatusBadge status="Available" />);
|
renderWithProviders(
|
||||||
|
<AvailabilityStatusBadge status="Unavailable" message="Emergency shutdown in progress" />,
|
||||||
|
);
|
||||||
|
|
||||||
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(<AvailabilityStatusBadge status="Unavailable" />);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('availability-message')).toHaveTextContent(
|
||||||
|
'System is currently unavailable.',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows stale indicator when stale={true}', () => {
|
it('shows stale indicator when stale={true}', () => {
|
||||||
|
|||||||
@@ -10,24 +10,34 @@ interface AvailabilityStatusBadgeProps {
|
|||||||
|
|
||||||
const STATUS_CONFIG: Record<
|
const STATUS_CONFIG: Record<
|
||||||
AvailabilityStatus,
|
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: {
|
Available: {
|
||||||
labelKey: 'availability.available',
|
labelKey: 'availability.available',
|
||||||
colorClass:
|
defaultMessageKey: 'availability.messageAvailable',
|
||||||
'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
|
alwaysDefault: true,
|
||||||
|
colorClass: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
|
||||||
Icon: CheckCircle,
|
Icon: CheckCircle,
|
||||||
},
|
},
|
||||||
Maintenance: {
|
Maintenance: {
|
||||||
labelKey: 'availability.maintenance',
|
labelKey: 'availability.maintenance',
|
||||||
colorClass:
|
defaultMessageKey: 'availability.messageMaintenance',
|
||||||
'bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200',
|
alwaysDefault: false,
|
||||||
|
colorClass: 'bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200',
|
||||||
Icon: AlertTriangle,
|
Icon: AlertTriangle,
|
||||||
},
|
},
|
||||||
Unavailable: {
|
Unavailable: {
|
||||||
labelKey: 'availability.unavailable',
|
labelKey: 'availability.unavailable',
|
||||||
colorClass:
|
defaultMessageKey: 'availability.messageUnavailable',
|
||||||
'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
|
alwaysDefault: false,
|
||||||
|
colorClass: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
|
||||||
Icon: XCircle,
|
Icon: XCircle,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -38,7 +48,8 @@ export function AvailabilityStatusBadge({
|
|||||||
stale = false,
|
stale = false,
|
||||||
}: AvailabilityStatusBadgeProps) {
|
}: AvailabilityStatusBadgeProps) {
|
||||||
const { t } = useTranslation();
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -50,14 +61,12 @@ export function AvailabilityStatusBadge({
|
|||||||
<span data-testid="availability-status-label">{t(labelKey)}</span>
|
<span data-testid="availability-status-label">{t(labelKey)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{message && message.length > 0 && (
|
|
||||||
<p
|
<p
|
||||||
data-testid="availability-message"
|
data-testid="availability-message"
|
||||||
className="mt-2 text-sm text-muted-foreground"
|
className="mt-2 text-sm text-muted-foreground"
|
||||||
>
|
>
|
||||||
{message}
|
{displayMessage}
|
||||||
</p>
|
</p>
|
||||||
)}
|
|
||||||
|
|
||||||
{stale && (
|
{stale && (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -66,6 +66,9 @@
|
|||||||
"available": "Available",
|
"available": "Available",
|
||||||
"maintenance": "Maintenance",
|
"maintenance": "Maintenance",
|
||||||
"unavailable": "Unavailable",
|
"unavailable": "Unavailable",
|
||||||
|
"messageAvailable": "System is running normally.",
|
||||||
|
"messageMaintenance": "System is undergoing scheduled maintenance.",
|
||||||
|
"messageUnavailable": "System is currently unavailable.",
|
||||||
"errorTitle": "Could not load status",
|
"errorTitle": "Could not load status",
|
||||||
"staleLabel": "Last known status",
|
"staleLabel": "Last known status",
|
||||||
"retry": "Retry"
|
"retry": "Retry"
|
||||||
|
|||||||
@@ -66,6 +66,9 @@
|
|||||||
"available": "Beschikbaar",
|
"available": "Beschikbaar",
|
||||||
"maintenance": "Onderhoud",
|
"maintenance": "Onderhoud",
|
||||||
"unavailable": "Niet beschikbaar",
|
"unavailable": "Niet beschikbaar",
|
||||||
|
"messageAvailable": "Systeem werkt normaal.",
|
||||||
|
"messageMaintenance": "Het systeem ondergaat gepland onderhoud.",
|
||||||
|
"messageUnavailable": "Het systeem is momenteel niet beschikbaar.",
|
||||||
"errorTitle": "Status kon niet worden geladen",
|
"errorTitle": "Status kon niet worden geladen",
|
||||||
"staleLabel": "Laatste bekende status",
|
"staleLabel": "Laatste bekende status",
|
||||||
"retry": "Opnieuw proberen"
|
"retry": "Opnieuw proberen"
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace SlpModularCms.Core.Availability;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Status en optionele admin-melding van de systeembeschikbaarheid.
|
||||||
|
/// </summary>
|
||||||
|
public record AvailabilityStatusDetails(AvailabilityStatus Status, string? Message);
|
||||||
@@ -10,4 +10,9 @@ public interface IAvailabilityService
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>De huidige <see cref="AvailabilityStatus"/>.</returns>
|
/// <returns>De huidige <see cref="AvailabilityStatus"/>.</returns>
|
||||||
Task<AvailabilityStatus> IsAvailableAsync();
|
Task<AvailabilityStatus> IsAvailableAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Geeft de huidige status inclusief de optionele admin-melding terug.
|
||||||
|
/// </summary>
|
||||||
|
Task<AvailabilityStatusDetails> GetStatusDetailsAsync();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,12 +20,12 @@ public class AvailabilityController : ControllerBase
|
|||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
public async Task<IActionResult> GetStatus()
|
public async Task<IActionResult> GetStatus()
|
||||||
{
|
{
|
||||||
var status = await _availabilityService.IsAvailableAsync();
|
var details = await _availabilityService.GetStatusDetailsAsync();
|
||||||
return Ok(new
|
return Ok(new
|
||||||
{
|
{
|
||||||
Status = status.ToString(),
|
Status = details.Status.ToString(),
|
||||||
CheckedAt = DateTimeOffset.UtcNow,
|
CheckedAt = DateTimeOffset.UtcNow,
|
||||||
Message = string.Empty,
|
Message = details.Message ?? string.Empty,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,24 @@ public class PersistentAvailabilityService : IAvailabilityService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<AvailabilityStatusDetails> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Update de globale systeemstatus.
|
/// Update de globale systeemstatus.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
Reference in New Issue
Block a user