From f5d8d85e5613c3aa316461ecde98b6bc174922c5 Mon Sep 17 00:00:00 2001 From: Vadim S <20091002+va-deem@users.noreply.github.com> Date: Sat, 11 Apr 2026 00:21:42 +1200 Subject: [PATCH 1/4] Use enum for GtfsImportStatus --- Models/Entities/GtfsImportRun.cs | 9 ++++++++- Persistence/AppDbContext.cs | 6 ++++++ Services/ActiveImportRunResolver.cs | 3 ++- Services/GtfsImportService.cs | 6 +++--- 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/Models/Entities/GtfsImportRun.cs b/Models/Entities/GtfsImportRun.cs index 6b0d9ff..389b37a 100644 --- a/Models/Entities/GtfsImportRun.cs +++ b/Models/Entities/GtfsImportRun.cs @@ -1,5 +1,12 @@ namespace TransitAnalyticsAPI.Models.Entities; +public enum GtfsImportStatus +{ + Running, + Completed, + Failed +} + public class GtfsImportRun { public long Id { get; set; } @@ -10,7 +17,7 @@ public class GtfsImportRun public DateTime? CompletedAtUtc { get; set; } - public string Status { get; set; } = string.Empty; + public GtfsImportStatus Status { get; set; } = GtfsImportStatus.Running; public string? Notes { get; set; } diff --git a/Persistence/AppDbContext.cs b/Persistence/AppDbContext.cs index 4f8e921..8cdd07c 100644 --- a/Persistence/AppDbContext.cs +++ b/Persistence/AppDbContext.cs @@ -29,6 +29,12 @@ public AppDbContext(DbContextOptions options) : base(options) protected override void OnModelCreating(ModelBuilder modelBuilder) { + modelBuilder.Entity() + .Property(importRun => importRun.Status) + .HasConversion( + status => status.ToString().ToLowerInvariant(), + value => Enum.Parse(value, true)); + modelBuilder.Entity() .HasIndex(vehiclePosition => new { vehiclePosition.VehicleId, vehiclePosition.RecordedAtUtc }); diff --git a/Services/ActiveImportRunResolver.cs b/Services/ActiveImportRunResolver.cs index f99ac29..68c8ad1 100644 --- a/Services/ActiveImportRunResolver.cs +++ b/Services/ActiveImportRunResolver.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore; +using TransitAnalyticsAPI.Models.Entities; using TransitAnalyticsAPI.Persistence; namespace TransitAnalyticsAPI.Services; @@ -16,7 +17,7 @@ public ActiveImportRunResolver(AppDbContext appDbContext) { return await _appDbContext.GtfsImportRuns .AsNoTracking() - .Where(importRun => importRun.IsActive && importRun.Status == "completed") + .Where(importRun => importRun.IsActive && importRun.Status == GtfsImportStatus.Completed) .OrderByDescending(importRun => importRun.CompletedAtUtc) .Select(importRun => (long?)importRun.Id) .FirstOrDefaultAsync(cancellationToken); diff --git a/Services/GtfsImportService.cs b/Services/GtfsImportService.cs index 9fbcc4a..2c1edbe 100644 --- a/Services/GtfsImportService.cs +++ b/Services/GtfsImportService.cs @@ -34,7 +34,7 @@ public async Task ImportRoutesAndTripsAsync( { SourceVersion = sourceVersion, StartedAtUtc = DateTime.UtcNow, - Status = "running", + Status = GtfsImportStatus.Running, IsActive = false }; @@ -68,7 +68,7 @@ public async Task ImportRoutesAndTripsAsync( .SingleAsync(run => run.Id == importRun.Id, cancellationToken); importRunToFinalize.IsActive = true; - importRunToFinalize.Status = "completed"; + importRunToFinalize.Status = GtfsImportStatus.Completed; importRunToFinalize.CompletedAtUtc = DateTime.UtcNow; await _appDbContext.SaveChangesAsync(cancellationToken); @@ -90,7 +90,7 @@ public async Task ImportRoutesAndTripsAsync( var importRunToFail = await _appDbContext.GtfsImportRuns .SingleAsync(run => run.Id == importRun.Id, cancellationToken); - importRunToFail.Status = "failed"; + importRunToFail.Status = GtfsImportStatus.Failed; importRunToFail.Notes = exception.Message; importRunToFail.CompletedAtUtc = DateTime.UtcNow; await _appDbContext.SaveChangesAsync(cancellationToken); From 90c37e7ca4f704f9b667afbb92561b3143cf3155 Mon Sep 17 00:00:00 2001 From: Vadim S <20091002+va-deem@users.noreply.github.com> Date: Sat, 11 Apr 2026 01:00:04 +1200 Subject: [PATCH 2/4] Move websockets endpoint from Program.cs & update README.md --- Program.cs | 66 +--------------------- README.md | 14 +++++ Websockets/VehicleWebSocketEndpoint.cs | 77 ++++++++++++++++++++++++++ 3 files changed, 93 insertions(+), 64 deletions(-) create mode 100644 Websockets/VehicleWebSocketEndpoint.cs diff --git a/Program.cs b/Program.cs index 0d20df1..c28aed2 100644 --- a/Program.cs +++ b/Program.cs @@ -8,6 +8,7 @@ using TransitAnalyticsAPI.Background; using TransitAnalyticsAPI.Clients.AucklandTransport; using TransitAnalyticsAPI.Configuration; +using TransitAnalyticsAPI.Websockets; using TransitAnalyticsAPI.Middleware; using TransitAnalyticsAPI.Persistence; using TransitAnalyticsAPI.Services; @@ -133,72 +134,9 @@ app.UseAuthorization(); app.UseHttpsRedirection(); -app.Map("/ws/vehicles", async context => -{ - var webSocketOptions = context.RequestServices - .GetRequiredService>() - .Value; - var requestLogger = context.RequestServices - .GetRequiredService() - .CreateLogger("VehicleWebSocketEndpoint"); - - var adminSettingsService = context.RequestServices.GetRequiredService(); - if (await adminSettingsService.IsMaintenanceModeEnabledAsync(context.RequestAborted)) - { - context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable; - await context.Response.WriteAsJsonAsync(new - { - error = "service_unavailable", - message = "The service is in maintenance mode." - }); - return; - } - - if (!context.WebSockets.IsWebSocketRequest) - { - context.Response.StatusCode = StatusCodes.Status400BadRequest; - return; - } - - if (!IsAllowedWebSocketOrigin(context, webSocketOptions)) - { - requestLogger.LogWarning( - "Rejected websocket request from unexpected origin {Origin}.", - context.Request.Headers.Origin.ToString()); - context.Response.StatusCode = StatusCodes.Status403Forbidden; - await context.Response.WriteAsJsonAsync(new - { - error = "forbidden", - message = "The request origin is not allowed." - }); - return; - } - - var socket = await context.WebSockets.AcceptWebSocketAsync(); - var ipAddress = context.Connection.RemoteIpAddress?.ToString() ?? "unknown"; - using var scope = app.Services.CreateScope(); - var webSocketService = scope.ServiceProvider.GetRequiredService(); - - await webSocketService.HandleConnectionAsync(socket, ipAddress, context.RequestAborted); -}); +app.MapVehicleWebSocket(); app.MapRazorPages(); app.MapControllers(); app.Run(); - -static bool IsAllowedWebSocketOrigin(HttpContext context, VehicleWebSocketOptions options) -{ - if (options.AllowedOrigins.Length == 0) - { - return true; - } - - if (!context.Request.Headers.TryGetValue("Origin", out var originValues)) - { - return false; - } - - var origin = originValues.ToString(); - return options.AllowedOrigins.Contains(origin, StringComparer.OrdinalIgnoreCase); -} diff --git a/README.md b/README.md index c20687b..3559c6e 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,20 @@ The app: - Entity Framework Core - Hosted background services +## Project Structure + +- `Controllers/` — HTTP API endpoints +- `Websockets/` — WebSocket transport endpoints +- `Services/` — application and business logic +- `Persistence/` — EF Core DbContext and database access +- `Models/` — entities and DTOs +- `Configuration/` — strongly-typed option classes +- `Middleware/` — request pipeline middleware +- `Background/` — hosted background services +- `Clients/` — external API integrations +- `Admin/` — admin security and services +- `Areas/Admin/Pages/` — Razor Pages for the admin UI + ## Features ### Public/backend API diff --git a/Websockets/VehicleWebSocketEndpoint.cs b/Websockets/VehicleWebSocketEndpoint.cs new file mode 100644 index 0000000..33ef2ba --- /dev/null +++ b/Websockets/VehicleWebSocketEndpoint.cs @@ -0,0 +1,77 @@ +using Microsoft.Extensions.Options; +using TransitAnalyticsAPI.Admin.Services; +using TransitAnalyticsAPI.Configuration; +using TransitAnalyticsAPI.Services; + +namespace TransitAnalyticsAPI.Websockets; + +public static class VehicleWebSocketEndpoint +{ + public static void MapVehicleWebSocket(this WebApplication app) + { + app.Map("/ws/vehicles", async context => + { + var webSocketOptions = context.RequestServices + .GetRequiredService>() + .Value; + var logger = context.RequestServices + .GetRequiredService() + .CreateLogger("VehicleWebSocketEndpoint"); + + var adminSettingsService = context.RequestServices.GetRequiredService(); + if (await adminSettingsService.IsMaintenanceModeEnabledAsync(context.RequestAborted)) + { + context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable; + await context.Response.WriteAsJsonAsync(new + { + error = "service_unavailable", + message = "The service is in maintenance mode." + }); + return; + } + + if (!context.WebSockets.IsWebSocketRequest) + { + context.Response.StatusCode = StatusCodes.Status400BadRequest; + return; + } + + if (!IsAllowedOrigin(context, webSocketOptions)) + { + logger.LogWarning( + "Rejected websocket request from unexpected origin {Origin}.", + context.Request.Headers.Origin.ToString()); + context.Response.StatusCode = StatusCodes.Status403Forbidden; + await context.Response.WriteAsJsonAsync(new + { + error = "forbidden", + message = "The request origin is not allowed." + }); + return; + } + + var socket = await context.WebSockets.AcceptWebSocketAsync(); + var ipAddress = context.Connection.RemoteIpAddress?.ToString() ?? "unknown"; + using var scope = app.Services.CreateScope(); + var webSocketService = scope.ServiceProvider.GetRequiredService(); + + await webSocketService.HandleConnectionAsync(socket, ipAddress, context.RequestAborted); + }); + } + + private static bool IsAllowedOrigin(HttpContext context, VehicleWebSocketOptions options) + { + if (options.AllowedOrigins.Length == 0) + { + return true; + } + + if (!context.Request.Headers.TryGetValue("Origin", out var originValues)) + { + return false; + } + + var origin = originValues.ToString(); + return options.AllowedOrigins.Contains(origin, StringComparer.OrdinalIgnoreCase); + } +} From 147eb2654cd5c39db51a49e8b3e298d2456bee1b Mon Sep 17 00:00:00 2001 From: Vadim S <20091002+va-deem@users.noreply.github.com> Date: Sat, 11 Apr 2026 01:11:37 +1200 Subject: [PATCH 3/4] Use TimeProvider for UtcNow --- Admin/Services/AdminSettingsService.cs | 11 ++++++++--- Program.cs | 1 + Services/GtfsImportService.cs | 10 ++++++---- Services/SystemLogService.cs | 6 ++++-- Services/VehicleLatestQueryService.cs | 5 ++++- Services/VehiclePositionMapper.cs | 9 ++++++++- Services/VehicleRetentionService.cs | 5 ++++- .../Services/VehicleLatestQueryServiceTests.cs | 1 + .../Services/VehiclePositionMapperTests.cs | 2 +- 9 files changed, 37 insertions(+), 13 deletions(-) diff --git a/Admin/Services/AdminSettingsService.cs b/Admin/Services/AdminSettingsService.cs index 999d35c..ce624d8 100644 --- a/Admin/Services/AdminSettingsService.cs +++ b/Admin/Services/AdminSettingsService.cs @@ -9,11 +9,16 @@ public class AdminSettingsService : IAdminSettingsService private const int SettingsRowId = 1; private readonly AppDbContext _appDbContext; + private readonly TimeProvider _timeProvider; private readonly IPollingRuntimeState _pollingRuntimeState; - public AdminSettingsService(AppDbContext appDbContext, IPollingRuntimeState pollingRuntimeState) + public AdminSettingsService( + AppDbContext appDbContext, + TimeProvider timeProvider, + IPollingRuntimeState pollingRuntimeState) { _appDbContext = appDbContext; + _timeProvider = timeProvider; _pollingRuntimeState = pollingRuntimeState; } @@ -57,7 +62,7 @@ public async Task UpdateGtfsUploadStatusAsync( CancellationToken cancellationToken = default) { var settings = await GetOrCreateAsync(cancellationToken); - settings.LastGtfsUploadAtUtc = DateTime.UtcNow; + settings.LastGtfsUploadAtUtc = _timeProvider.GetUtcNow().UtcDateTime; settings.LastGtfsUploadFileName = fileName; settings.LastGtfsImportStatus = status; settings.LastGtfsImportError = error; @@ -73,7 +78,7 @@ public async Task RecordGtfsUploadResultAsync( CancellationToken cancellationToken = default) { var settings = await GetOrCreateAsync(cancellationToken); - settings.LastGtfsUploadAtUtc = DateTime.UtcNow; + settings.LastGtfsUploadAtUtc = _timeProvider.GetUtcNow().UtcDateTime; settings.LastGtfsUploadFileName = fileName; settings.LastGtfsImportStatus = isSuccessful ? "completed" : "failed"; settings.LastGtfsImportError = error; diff --git a/Program.cs b/Program.cs index c28aed2..b5afbf4 100644 --- a/Program.cs +++ b/Program.cs @@ -34,6 +34,7 @@ SingleReader = true, SingleWriter = false })); +builder.Services.AddSingleton(TimeProvider.System); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddScoped(); diff --git a/Services/GtfsImportService.cs b/Services/GtfsImportService.cs index 2c1edbe..2a9ef39 100644 --- a/Services/GtfsImportService.cs +++ b/Services/GtfsImportService.cs @@ -12,10 +12,12 @@ public class GtfsImportService : IGtfsImportService private const int BatchSize = 5000; private readonly AppDbContext _appDbContext; + private readonly TimeProvider _timeProvider; - public GtfsImportService(AppDbContext appDbContext) + public GtfsImportService(AppDbContext appDbContext, TimeProvider timeProvider) { _appDbContext = appDbContext; + _timeProvider = timeProvider; } public async Task ImportRoutesAndTripsAsync( @@ -33,7 +35,7 @@ public async Task ImportRoutesAndTripsAsync( var importRun = new GtfsImportRun { SourceVersion = sourceVersion, - StartedAtUtc = DateTime.UtcNow, + StartedAtUtc = _timeProvider.GetUtcNow().UtcDateTime, Status = GtfsImportStatus.Running, IsActive = false }; @@ -69,7 +71,7 @@ public async Task ImportRoutesAndTripsAsync( importRunToFinalize.IsActive = true; importRunToFinalize.Status = GtfsImportStatus.Completed; - importRunToFinalize.CompletedAtUtc = DateTime.UtcNow; + importRunToFinalize.CompletedAtUtc = _timeProvider.GetUtcNow().UtcDateTime; await _appDbContext.SaveChangesAsync(cancellationToken); await DeleteInactiveImportRunsAsync(importRun.Id, cancellationToken); @@ -92,7 +94,7 @@ public async Task ImportRoutesAndTripsAsync( importRunToFail.Status = GtfsImportStatus.Failed; importRunToFail.Notes = exception.Message; - importRunToFail.CompletedAtUtc = DateTime.UtcNow; + importRunToFail.CompletedAtUtc = _timeProvider.GetUtcNow().UtcDateTime; await _appDbContext.SaveChangesAsync(cancellationToken); throw; } diff --git a/Services/SystemLogService.cs b/Services/SystemLogService.cs index e8ba38e..9db3183 100644 --- a/Services/SystemLogService.cs +++ b/Services/SystemLogService.cs @@ -8,10 +8,12 @@ public class SystemLogService : ISystemLogService private static readonly string Source = typeof(T).Name; private readonly AppDbContext _appDbContext; + private readonly TimeProvider _timeProvider; - public SystemLogService(AppDbContext appDbContext) + public SystemLogService(AppDbContext appDbContext, TimeProvider timeProvider) { _appDbContext = appDbContext; + _timeProvider = timeProvider; } public async Task LogAsync(SystemLogType type, string description, string? details = null, @@ -19,7 +21,7 @@ public async Task LogAsync(SystemLogType type, string description, string? detai { _appDbContext.SystemLogs.Add(new SystemLog { - CreatedAtUtc = DateTime.UtcNow, + CreatedAtUtc = _timeProvider.GetUtcNow().UtcDateTime, Type = type, Source = Source, Description = description, diff --git a/Services/VehicleLatestQueryService.cs b/Services/VehicleLatestQueryService.cs index f63025d..b2e8b72 100644 --- a/Services/VehicleLatestQueryService.cs +++ b/Services/VehicleLatestQueryService.cs @@ -10,21 +10,24 @@ public class VehicleLatestQueryService : IVehicleLatestQueryService { private readonly AppDbContext _appDbContext; private readonly IVehicleMetadataLookupService _vehicleMetadataLookupService; + private readonly TimeProvider _timeProvider; private readonly TimeSpan _latestPositionMaxAge; public VehicleLatestQueryService( AppDbContext appDbContext, IVehicleMetadataLookupService vehicleMetadataLookupService, + TimeProvider timeProvider, IOptions vehicleOptions) { _appDbContext = appDbContext; _vehicleMetadataLookupService = vehicleMetadataLookupService; + _timeProvider = timeProvider; _latestPositionMaxAge = TimeSpan.FromMinutes(Math.Max(1, vehicleOptions.Value.LatestPositionMaxAgeMinutes)); } public async Task> GetLatestAsync(CancellationToken cancellationToken = default) { - var cutoffUtc = DateTime.UtcNow - _latestPositionMaxAge; + var cutoffUtc = _timeProvider.GetUtcNow().UtcDateTime - _latestPositionMaxAge; var latestPositions = await _appDbContext.VehiclePositions .AsNoTracking() diff --git a/Services/VehiclePositionMapper.cs b/Services/VehiclePositionMapper.cs index 11bfc0d..2bb1780 100644 --- a/Services/VehiclePositionMapper.cs +++ b/Services/VehiclePositionMapper.cs @@ -5,9 +5,16 @@ namespace TransitAnalyticsAPI.Services; public class VehiclePositionMapper : IVehiclePositionMapper { + private readonly TimeProvider _timeProvider; + + public VehiclePositionMapper(TimeProvider timeProvider) + { + _timeProvider = timeProvider; + } + public List Map(IEnumerable entities) { - var ingestedAtUtc = DateTime.UtcNow; + var ingestedAtUtc = _timeProvider.GetUtcNow().UtcDateTime; return entities .Where(entity => diff --git a/Services/VehicleRetentionService.cs b/Services/VehicleRetentionService.cs index 216a3af..e043299 100644 --- a/Services/VehicleRetentionService.cs +++ b/Services/VehicleRetentionService.cs @@ -9,22 +9,25 @@ namespace TransitAnalyticsAPI.Services; public class VehicleRetentionService : IVehicleRetentionService { private readonly AppDbContext _appDbContext; + private readonly TimeProvider _timeProvider; private readonly TimeSpan _historyRetention; private readonly ISystemLogService _systemLog; public VehicleRetentionService( AppDbContext appDbContext, + TimeProvider timeProvider, IOptions vehicleOptions, ISystemLogService systemLogService) { _appDbContext = appDbContext; + _timeProvider = timeProvider; _historyRetention = TimeSpan.FromDays(Math.Max(1, vehicleOptions.Value.HistoryRetentionDays)); _systemLog = systemLogService; } public async Task DeleteExpiredAsync(CancellationToken cancellationToken = default) { - var cutoffUtc = DateTime.UtcNow - _historyRetention; + var cutoffUtc = _timeProvider.GetUtcNow().UtcDateTime - _historyRetention; var dbSize = await _appDbContext.Database .SqlQueryRaw("SELECT pg_size_pretty(pg_database_size(current_database())) AS \"Value\"") diff --git a/TransitAnalyticsAPI.Tests/Services/VehicleLatestQueryServiceTests.cs b/TransitAnalyticsAPI.Tests/Services/VehicleLatestQueryServiceTests.cs index ee57179..2177d7a 100644 --- a/TransitAnalyticsAPI.Tests/Services/VehicleLatestQueryServiceTests.cs +++ b/TransitAnalyticsAPI.Tests/Services/VehicleLatestQueryServiceTests.cs @@ -45,6 +45,7 @@ public async Task GetLatestAsync_ReturnsNewestFreshRowPerVehicle() var service = new VehicleLatestQueryService( dbContext, new FakeVehicleMetadataLookupService(), + TimeProvider.System, Options.Create(new VehicleOptions { LatestPositionMaxAgeMinutes = 5 diff --git a/TransitAnalyticsAPI.Tests/Services/VehiclePositionMapperTests.cs b/TransitAnalyticsAPI.Tests/Services/VehiclePositionMapperTests.cs index fcbb4c8..33efbbc 100644 --- a/TransitAnalyticsAPI.Tests/Services/VehiclePositionMapperTests.cs +++ b/TransitAnalyticsAPI.Tests/Services/VehiclePositionMapperTests.cs @@ -9,7 +9,7 @@ public class VehiclePositionMapperTests [Fact] public void Map_SkipsDeletedAndIncompleteEntities_AndMapsValidOnes() { - var mapper = new VehiclePositionMapper(); + var mapper = new VehiclePositionMapper(TimeProvider.System); var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); var result = mapper.Map( From 931bd2cdd1c9d37e83fa0aa6e8f394a06488babc Mon Sep 17 00:00:00 2001 From: Vadim S <20091002+va-deem@users.noreply.github.com> Date: Thu, 16 Apr 2026 22:49:35 +1200 Subject: [PATCH 4/4] Move maintenance mode check from DB to in-memory flag --- Admin/Services/AdminSettingsBootstrapService.cs | 3 ++- Admin/Services/AdminSettingsService.cs | 5 +++-- Admin/Services/IPollingRuntimeState.cs | 4 ++++ Admin/Services/PollingRuntimeState.cs | 7 +++++++ 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/Admin/Services/AdminSettingsBootstrapService.cs b/Admin/Services/AdminSettingsBootstrapService.cs index 5758bf4..b737438 100644 --- a/Admin/Services/AdminSettingsBootstrapService.cs +++ b/Admin/Services/AdminSettingsBootstrapService.cs @@ -21,9 +21,10 @@ public async Task StartAsync(CancellationToken cancellationToken) using var scope = _serviceScopeFactory.CreateScope(); var adminSettingsService = scope.ServiceProvider.GetRequiredService(); var settings = await adminSettingsService.GetAsync(cancellationToken); + _pollingRuntimeState.SetMaintenanceModeEnabled(settings.IsMaintenanceMode); _pollingRuntimeState.SetPollingEnabled(settings.IsPollingEnabled); var startupMessage = - $"Admin settings initialized.\nmaintenance: db={settings.IsMaintenanceMode}, memory=n/a\npolling: db={settings.IsPollingEnabled}, memory={_pollingRuntimeState.IsPollingEnabled}"; + $"Admin settings initialized.\nmaintenance: db={settings.IsMaintenanceMode}, memory={_pollingRuntimeState.IsMaintenanceModeEnabled}\npolling: db={settings.IsPollingEnabled}, memory={_pollingRuntimeState.IsPollingEnabled}"; WriteHighlightedMessage(startupMessage); diff --git a/Admin/Services/AdminSettingsService.cs b/Admin/Services/AdminSettingsService.cs index ce624d8..285464c 100644 --- a/Admin/Services/AdminSettingsService.cs +++ b/Admin/Services/AdminSettingsService.cs @@ -29,8 +29,8 @@ public async Task GetAsync(CancellationToken cancellationToken = public async Task IsMaintenanceModeEnabledAsync(CancellationToken cancellationToken = default) { - var settings = await GetOrCreateAsync(cancellationToken); - return settings.IsMaintenanceMode; + await Task.CompletedTask; + return _pollingRuntimeState.IsMaintenanceModeEnabled; } public async Task IsPollingEnabledAsync(CancellationToken cancellationToken = default) @@ -44,6 +44,7 @@ public async Task SetMaintenanceModeAsync(bool isEnabled, CancellationToken canc var settings = await GetOrCreateAsync(cancellationToken); settings.IsMaintenanceMode = isEnabled; await _appDbContext.SaveChangesAsync(cancellationToken); + _pollingRuntimeState.SetMaintenanceModeEnabled(isEnabled); } public async Task SetPollingEnabledAsync(bool isEnabled, CancellationToken cancellationToken = default) diff --git a/Admin/Services/IPollingRuntimeState.cs b/Admin/Services/IPollingRuntimeState.cs index dd92e96..4f9278b 100644 --- a/Admin/Services/IPollingRuntimeState.cs +++ b/Admin/Services/IPollingRuntimeState.cs @@ -4,8 +4,12 @@ public interface IPollingRuntimeState { bool IsPollingEnabled { get; } + bool IsMaintenanceModeEnabled { get; } + void SetPollingEnabled(bool isEnabled); + void SetMaintenanceModeEnabled(bool isEnabled); + Task WaitUntilPollingEnabledAsync(CancellationToken cancellationToken); Task WaitForPollingStateChangeOrTimeoutAsync(TimeSpan timeout, CancellationToken cancellationToken); diff --git a/Admin/Services/PollingRuntimeState.cs b/Admin/Services/PollingRuntimeState.cs index 8bb5536..52f49b6 100644 --- a/Admin/Services/PollingRuntimeState.cs +++ b/Admin/Services/PollingRuntimeState.cs @@ -7,6 +7,13 @@ public class PollingRuntimeState : IPollingRuntimeState public bool IsPollingEnabled { get; private set; } + public bool IsMaintenanceModeEnabled { get; private set; } + + public void SetMaintenanceModeEnabled(bool isEnabled) + { + IsMaintenanceModeEnabled = isEnabled; + } + public void SetPollingEnabled(bool isEnabled) { lock (_sync)