// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using EpicGames.Horde.Server; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Diagnostics.HealthChecks; namespace HordeServer.Server; /// /// ASP.NET view model for server status updates /// public class ServerStatusUpdatesViewModel { /// /// Status for each subsystem /// public IReadOnlyList SubsystemStatuses { get; init; } = Array.Empty(); /// /// Format a date/time to human-readable relative date /// /// Date/time to convert /// Relative time public static string ToRelativeTime(DateTimeOffset dateTime) { TimeSpan timeSpan = DateTime.UtcNow - dateTime; if (timeSpan <= TimeSpan.FromSeconds(60)) { return $"{timeSpan.Seconds} seconds ago"; } if (timeSpan <= TimeSpan.FromMinutes(60)) { return $"{timeSpan.Minutes} minutes ago"; } if (timeSpan <= TimeSpan.FromHours(24)) { return $"{timeSpan.Hours} hours ago"; } if (timeSpan <= TimeSpan.FromDays(30)) { return $"{timeSpan.Days} days ago"; } if (timeSpan <= TimeSpan.FromDays(365)) { return $"{timeSpan.Days / 30} months ago"; } return $"{timeSpan.Days / 365} years ago"; } } /// /// Controller managing server status /// [ApiController] [Authorize] [Tags("Server")] public class ServerStatusController : Controller { private readonly ServerStatusService _serverStatus; /// /// Constructor /// public ServerStatusController(ServerStatusService serverStatus) { _serverStatus = serverStatus; } /// /// Get the server status of Horde's internal subsystems /// /// Http result [HttpGet] [Route("/api/v1/server/status")] [ProducesResponseType(typeof(ServerStatusResponse), 200)] public async Task> GetUpdatesAsync([FromQuery] string? format = null) { IReadOnlyList subsystemStatuses = await _serverStatus.GetSubsystemStatusesAsync(); if (format == "html") { return GetUpdatesHtml(subsystemStatuses); } return new ServerStatusResponse { Statuses = subsystemStatuses.Select(x => { return new ServerStatusSubsystem() { Name = x.Name, Updates = x.Updates.Select( u => new ServerStatusUpdate() { Result = ConvertSubsystemResult(u.Result), Message = u.Message ?? "Operating normally.", UpdatedAt = u.UpdatedAt }).ToArray() }; }).ToArray(), }; } private ActionResult GetUpdatesHtml(IReadOnlyList subsystemStatuses) { return View("~/Server/ServerStatusUpdates.cshtml", new ServerStatusUpdatesViewModel { SubsystemStatuses = subsystemStatuses }); } private static ServerStatusResult ConvertSubsystemResult(HealthStatus result) { return result switch { HealthStatus.Healthy => ServerStatusResult.Healthy, HealthStatus.Unhealthy => ServerStatusResult.Unhealthy, HealthStatus.Degraded => ServerStatusResult.Degraded, _ => throw new Exception($"Unknown result: {result}") }; } }