From 1b3e89f4bfeaffaaed0959c0c5257ce209d03cbe Mon Sep 17 00:00:00 2001 From: Shawn Jackson Date: Mon, 17 Aug 2026 14:24:05 -0700 Subject: [PATCH] RG-T132 Fixing app dispatch issue, also TTS temp cleanup task --- Core/Resgrid.Config/NoticeConfig.cs | 9 ++ Core/Resgrid.Config/SystemBehaviorConfig.cs | 4 +- Core/Resgrid.Config/TtsConfig.cs | 8 + .../Web/Services/DispatchListHelperTests.cs | 74 +++++++++ .../TempDirectorySweepHostedServiceTests.cs | 147 ++++++++++++++++++ .../Controllers/v4/CallsController.cs | 20 ++- .../Helpers/DispatchListHelper.cs | 58 +++++++ .../Resgrid.Web.Services.xml | 17 ++ .../ServiceCollectionExtensions.cs | 1 + .../Configuration/TtsOptions.cs | 10 ++ Web/Resgrid.Web.Tts/Program.cs | 1 + .../TempDirectorySweepHostedService.cs | 120 ++++++++++++++ Web/Resgrid.Web.Tts/k8s/deployment.yaml | 1 + .../Controllers/AccountController.cs | 2 +- 14 files changed, 463 insertions(+), 9 deletions(-) create mode 100644 Tests/Resgrid.Tests/Web/Services/DispatchListHelperTests.cs create mode 100644 Tests/Resgrid.Tests/Web/Tts/TempDirectorySweepHostedServiceTests.cs create mode 100644 Web/Resgrid.Web.Services/Helpers/DispatchListHelper.cs create mode 100644 Web/Resgrid.Web.Tts/Services/TempDirectorySweepHostedService.cs diff --git a/Core/Resgrid.Config/NoticeConfig.cs b/Core/Resgrid.Config/NoticeConfig.cs index 2a6f71c23..569428296 100644 --- a/Core/Resgrid.Config/NoticeConfig.cs +++ b/Core/Resgrid.Config/NoticeConfig.cs @@ -10,6 +10,15 @@ public static class NoticeConfig /// public static string LoginPageNotice = ""; + /// + /// The login page notice to display, preferring and falling back to the + /// legacy key so existing on-prem configs keep working. + /// + public static string EffectiveLoginPageNotice => + !string.IsNullOrWhiteSpace(LoginPageNotice) + ? LoginPageNotice + : SystemBehaviorConfig.LoginPageNotice; + /// /// The dashboard toast notice /// diff --git a/Core/Resgrid.Config/SystemBehaviorConfig.cs b/Core/Resgrid.Config/SystemBehaviorConfig.cs index a886eb1aa..9ab85cb6b 100644 --- a/Core/Resgrid.Config/SystemBehaviorConfig.cs +++ b/Core/Resgrid.Config/SystemBehaviorConfig.cs @@ -195,7 +195,9 @@ public static class SystemBehaviorConfig public static string SiteKey = ""; /// - /// A notice to display on the login page + /// A notice to display on the login page. + /// Legacy key, kept so existing on-prem configs keep working. New configs should set + /// NoticeConfig.LoginPageNotice instead; read through NoticeConfig.EffectiveLoginPageNotice. /// public static string LoginPageNotice = ""; diff --git a/Core/Resgrid.Config/TtsConfig.cs b/Core/Resgrid.Config/TtsConfig.cs index eeeb27f94..6c9b8feb5 100644 --- a/Core/Resgrid.Config/TtsConfig.cs +++ b/Core/Resgrid.Config/TtsConfig.cs @@ -47,6 +47,14 @@ public static class TtsConfig public static string PiperModelDirectory = "/usr/local/share/piper-voices"; public static string FfmpegExecutable = "ffmpeg"; public static string TempDirectory = ""; + + /// + /// Age, in hours, at which an orphaned entry under is + /// swept, and the interval the sweep runs on. Only entries left behind by a hard + /// kill or a failed delete get this old; keep it well above + /// so in-flight jobs are never collected. + /// + public static int TempDirectorySweepHours = 6; public static string CachePrefix = "tts2"; public static int NormalizedSampleRate = 8000; public static int NormalizedChannels = 1; diff --git a/Tests/Resgrid.Tests/Web/Services/DispatchListHelperTests.cs b/Tests/Resgrid.Tests/Web/Services/DispatchListHelperTests.cs new file mode 100644 index 000000000..e52c895bf --- /dev/null +++ b/Tests/Resgrid.Tests/Web/Services/DispatchListHelperTests.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using FluentAssertions; +using NUnit.Framework; +using Resgrid.Web.Helpers; + +namespace Resgrid.Tests.Web.Services +{ + /// + /// The DispatchList is client supplied text, and a call that reaches the API with one bad entry still + /// has to dispatch everything else in it. Prod hit this with a Spanish department whose client sent + /// role names ("R:PARAMÉDICO") instead of role ids, which threw and dropped every role on the call. + /// + [TestFixture] + public class DispatchListHelperTests + { + private static readonly Dictionary Roles = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "Paramédico", 12 }, + { "Comandante", 34 } + }; + + private static int? ResolveRole(string name) => Roles.TryGetValue(name, out var id) ? id : (int?)null; + + private static string[] Split(string dispatchList) => dispatchList.Split('|'); + + [Test] + public void Numeric_entries_are_returned_for_their_prefix_only() + { + var ids = DispatchListHelper.ResolveIds(Split("P:abc|G:1|R:12|U:7|R:34"), "R:", ResolveRole); + + ids.Should().Equal(12, 34); + } + + [Test] + public void A_name_is_resolved_when_the_entry_isnt_numeric() + { + var ids = DispatchListHelper.ResolveIds(Split("R:PARAMÉDICO"), "R:", ResolveRole); + + ids.Should().Equal(12); + } + + [Test] + public void One_unresolvable_entry_does_not_drop_the_rest() + { + var ids = DispatchListHelper.ResolveIds(Split("R:12|R:NOT A ROLE|R:34"), "R:", ResolveRole); + + ids.Should().Equal(12, 34); + } + + [Test] + public void Duplicates_and_empty_entries_are_ignored() + { + var ids = DispatchListHelper.ResolveIds(Split("R:12|R:|R:PARAMÉDICO|R: 12 ||R:34"), "R:", ResolveRole); + + ids.Should().Equal(12, 34); + } + + [Test] + public void An_id_wins_over_a_name_lookup() + { + var ids = DispatchListHelper.ResolveIds(Split("R:99"), "R:", _ => 12); + + ids.Should().Equal(99); + } + + [Test] + public void No_matching_prefix_returns_an_empty_list() + { + DispatchListHelper.ResolveIds(Split("G:1|U:2"), "R:", ResolveRole).Should().BeEmpty(); + DispatchListHelper.ResolveIds(null, "R:", ResolveRole).Should().BeEmpty(); + } + } +} diff --git a/Tests/Resgrid.Tests/Web/Tts/TempDirectorySweepHostedServiceTests.cs b/Tests/Resgrid.Tests/Web/Tts/TempDirectorySweepHostedServiceTests.cs new file mode 100644 index 000000000..483011b19 --- /dev/null +++ b/Tests/Resgrid.Tests/Web/Tts/TempDirectorySweepHostedServiceTests.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using System.IO; +using FluentAssertions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using NUnit.Framework; +using Resgrid.Web.Tts.Configuration; +using Resgrid.Web.Tts.Services; + +namespace Resgrid.Tests.Web.Tts +{ + [TestFixture] + public class TempDirectorySweepHostedServiceTests + { + private string _tempRoot; + + [SetUp] + public void SetUp() + { + _tempRoot = Path.Combine(Path.GetTempPath(), $"resgrid-tts-sweep-tests-{Guid.NewGuid():N}"); + Directory.CreateDirectory(_tempRoot); + } + + [TearDown] + public void TearDown() + { + if (Directory.Exists(_tempRoot)) + { + Directory.Delete(_tempRoot, recursive: true); + } + } + + [Test] + public void sweep_once_should_delete_orphaned_working_directories_older_than_the_max_age() + { + var orphan = CreateWorkingDirectory("aged", DateTime.UtcNow.AddHours(-7)); + + var removed = CreateService().SweepOnce(); + + removed.Should().Be(1); + Directory.Exists(orphan).Should().BeFalse(); + } + + [Test] + public void sweep_once_should_leave_directories_newer_than_the_max_age_alone() + { + // An in-flight synthesis writes into a directory whose mtime is minutes old at + // most; the sweep must never collect one out from under a running job. + var inFlight = CreateWorkingDirectory("in-flight", DateTime.UtcNow.AddMinutes(-5)); + + var removed = CreateService().SweepOnce(); + + removed.Should().Be(0); + Directory.Exists(inFlight).Should().BeTrue(); + } + + [Test] + public void sweep_once_should_delete_stale_loose_files() + { + var stalePath = Path.Combine(_tempRoot, "stale.wav"); + File.WriteAllBytes(stalePath, new byte[] { 1 }); + File.SetLastWriteTimeUtc(stalePath, DateTime.UtcNow.AddHours(-7)); + + var removed = CreateService().SweepOnce(); + + removed.Should().Be(1); + File.Exists(stalePath).Should().BeFalse(); + } + + [Test] + public void sweep_once_should_return_zero_when_the_temp_root_does_not_exist() + { + Directory.Delete(_tempRoot, recursive: true); + + CreateService().SweepOnce().Should().Be(0); + } + + [Test] + public void sweep_once_should_log_the_number_of_entries_it_removed() + { + CreateWorkingDirectory("aged-one", DateTime.UtcNow.AddHours(-7)); + CreateWorkingDirectory("aged-two", DateTime.UtcNow.AddHours(-8)); + CreateWorkingDirectory("fresh", DateTime.UtcNow); + var logger = new RecordingLogger(); + + var removed = CreateService(logger).SweepOnce(); + + removed.Should().Be(2); + logger.Entries.Should().ContainSingle(x => + x.Level == LogLevel.Information && + x.Message.Contains("Swept 2 orphaned TTS temp entries")); + } + + private string CreateWorkingDirectory(string name, DateTime lastWriteUtc) + { + var path = Path.Combine(_tempRoot, name); + Directory.CreateDirectory(path); + File.WriteAllBytes(Path.Combine(path, "raw.wav"), new byte[] { 1 }); + Directory.SetLastWriteTimeUtc(path, lastWriteUtc); + return path; + } + + private TempDirectorySweepHostedService CreateService(ILogger logger = null) + { + return new TempDirectorySweepHostedService( + Options.Create(new TtsOptions + { + TempDirectory = _tempRoot, + TempDirectorySweepHours = 6 + }), + logger ?? NullLogger.Instance); + } + + private sealed class RecordingLogger : ILogger + { + public List Entries { get; } = new(); + + public IDisposable BeginScope(TState state) + { + return NullScope.Instance; + } + + public bool IsEnabled(LogLevel logLevel) + { + return true; + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + Entries.Add(new LogEntry(logLevel, exception, formatter(state, exception))); + } + + public sealed record LogEntry(LogLevel Level, Exception Exception, string Message); + + private sealed class NullScope : IDisposable + { + public static readonly NullScope Instance = new(); + + public void Dispose() + { + } + } + } + } +} diff --git a/Web/Resgrid.Web.Services/Controllers/v4/CallsController.cs b/Web/Resgrid.Web.Services/Controllers/v4/CallsController.cs index a16d1d8d2..766253aa4 100644 --- a/Web/Resgrid.Web.Services/Controllers/v4/CallsController.cs +++ b/Web/Resgrid.Web.Services/Controllers/v4/CallsController.cs @@ -765,7 +765,8 @@ public async Task> SaveCall([FromBody] NewCallInput try { - var groupsToDispatch = dispatch.Where(x => x.StartsWith("G:")).Select(y => int.Parse(y.Replace("G:", ""))); + var groupsToDispatch = DispatchListHelper.ResolveIds(dispatch,"G:", + name => groups.FirstOrDefault(x => string.Equals(x.Name?.Trim(), name, StringComparison.OrdinalIgnoreCase))?.DepartmentGroupId); foreach (var group in groupsToDispatch) { if (groups.Any(x => x.DepartmentGroupId == group)) @@ -782,7 +783,8 @@ public async Task> SaveCall([FromBody] NewCallInput try { - var rolesToDispatch = dispatch.Where(x => x.StartsWith("R:")).Select(y => int.Parse(y.Replace("R:", ""))); + var rolesToDispatch = DispatchListHelper.ResolveIds(dispatch,"R:", + name => roles.FirstOrDefault(x => string.Equals(x.Name?.Trim(), name, StringComparison.OrdinalIgnoreCase))?.PersonnelRoleId); foreach (var role in rolesToDispatch) { if (roles.Any(x => x.PersonnelRoleId == role)) @@ -799,7 +801,8 @@ public async Task> SaveCall([FromBody] NewCallInput try { - var unitsToDispatch = dispatch.Where(x => x.StartsWith("U:")).Select(y => int.Parse(y.Replace("U:", ""))); + var unitsToDispatch = DispatchListHelper.ResolveIds(dispatch,"U:", + name => units.FirstOrDefault(x => string.Equals(x.Name?.Trim(), name, StringComparison.OrdinalIgnoreCase))?.UnitId); foreach (var unit in unitsToDispatch) { if (units.Any(x => x.UnitId == unit)) @@ -1063,10 +1066,13 @@ public async Task> EditCall([FromBody] EditCallInpu else { var dispatch = editCallInput.DispatchList.Split(char.Parse("|")); - var usersToDispatch = dispatch.Where(x => x.StartsWith("P:")).Select(y => y.Replace("P:", "")); - var groupsToDispatch = dispatch.Where(x => x.StartsWith("G:")).Select(y => int.Parse(y.Replace("G:", ""))); - var rolesToDispatch = dispatch.Where(x => x.StartsWith("R:")).Select(y => int.Parse(y.Replace("R:", ""))); - var unitsToDispatch = dispatch.Where(x => x.StartsWith("U:")).Select(y => int.Parse(y.Replace("U:", ""))); + var usersToDispatch = dispatch.Where(x => x.StartsWith("P:")).Select(y => y.Replace("P:", "")).ToList(); + var groupsToDispatch = DispatchListHelper.ResolveIds(dispatch,"G:", + name => groups.FirstOrDefault(x => string.Equals(x.Name?.Trim(), name, StringComparison.OrdinalIgnoreCase))?.DepartmentGroupId); + var rolesToDispatch = DispatchListHelper.ResolveIds(dispatch,"R:", + name => roles.FirstOrDefault(x => string.Equals(x.Name?.Trim(), name, StringComparison.OrdinalIgnoreCase))?.PersonnelRoleId); + var unitsToDispatch = DispatchListHelper.ResolveIds(dispatch,"U:", + name => units.FirstOrDefault(x => string.Equals(x.Name?.Trim(), name, StringComparison.OrdinalIgnoreCase))?.UnitId); try { diff --git a/Web/Resgrid.Web.Services/Helpers/DispatchListHelper.cs b/Web/Resgrid.Web.Services/Helpers/DispatchListHelper.cs new file mode 100644 index 000000000..50760eb7a --- /dev/null +++ b/Web/Resgrid.Web.Services/Helpers/DispatchListHelper.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Resgrid.Framework; + +namespace Resgrid.Web.Helpers +{ + /// + /// Parsing for the pipe delimited DispatchList string clients send when creating or editing a call. + /// + public static class DispatchListHelper + { + /// + /// Pulls the ids out of a DispatchList for a single prefix ("G:", "R:" or "U:"). Clients are supposed + /// to send ids, but some send the display name instead (i.e. "R:PARAMÉDICO"), and one unparsable entry + /// used to throw out of the int.Parse projection and silently drop every id sharing that prefix. Each + /// entry is parsed on its own now and falls back to a name lookup before it's discarded. + /// + /// The already split DispatchList entries. + /// The entry prefix to collect, i.e. "G:", "R:" or "U:". + /// Looks up an id for an entry that isn't numeric, null when there's no match. + /// The distinct ids found for that prefix, in the order they appeared. + public static List ResolveIds(IEnumerable dispatchList, string prefix, Func resolveByName) + { + var ids = new List(); + + if (dispatchList == null) + return ids; + + foreach (var entry in dispatchList.Where(x => !string.IsNullOrWhiteSpace(x) && x.StartsWith(prefix))) + { + var value = entry.Substring(prefix.Length).Trim(); + + if (string.IsNullOrWhiteSpace(value)) + continue; + + if (!int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var id)) + { + var resolved = resolveByName?.Invoke(value); + + if (!resolved.HasValue) + { + Logging.LogWarning($"Discarding dispatch list entry '{entry}', it's neither an id nor a known name."); + continue; + } + + id = resolved.Value; + } + + if (!ids.Contains(id)) + ids.Add(id); + } + + return ids; + } + } +} diff --git a/Web/Resgrid.Web.Services/Resgrid.Web.Services.xml b/Web/Resgrid.Web.Services/Resgrid.Web.Services.xml index 69fd4ac7c..d58d201bf 100644 --- a/Web/Resgrid.Web.Services/Resgrid.Web.Services.xml +++ b/Web/Resgrid.Web.Services/Resgrid.Web.Services.xml @@ -13453,5 +13453,22 @@ Respects the provided cancellation token for timeout control. + + + Parsing for the pipe delimited DispatchList string clients send when creating or editing a call. + + + + + Pulls the ids out of a DispatchList for a single prefix ("G:", "R:" or "U:"). Clients are supposed + to send ids, but some send the display name instead (i.e. "R:PARAMÉDICO"), and one unparsable entry + used to throw out of the int.Parse projection and silently drop every id sharing that prefix. Each + entry is parsed on its own now and falls back to a name lookup before it's discarded. + + The already split DispatchList entries. + The entry prefix to collect, i.e. "G:", "R:" or "U:". + Looks up an id for an entry that isn't numeric, null when there's no match. + The distinct ids found for that prefix, in the order they appeared. + diff --git a/Web/Resgrid.Web.Tts/Configuration/ServiceCollectionExtensions.cs b/Web/Resgrid.Web.Tts/Configuration/ServiceCollectionExtensions.cs index 18fd58c84..9a9be64da 100644 --- a/Web/Resgrid.Web.Tts/Configuration/ServiceCollectionExtensions.cs +++ b/Web/Resgrid.Web.Tts/Configuration/ServiceCollectionExtensions.cs @@ -58,6 +58,7 @@ private static void ApplyTtsOptions(TtsOptions options) options.PiperModelDirectory = string.IsNullOrWhiteSpace(TtsConfig.PiperModelDirectory) ? options.PiperModelDirectory : TtsConfig.PiperModelDirectory; options.FfmpegExecutable = string.IsNullOrWhiteSpace(TtsConfig.FfmpegExecutable) ? options.FfmpegExecutable : TtsConfig.FfmpegExecutable; options.TempDirectory = string.IsNullOrWhiteSpace(TtsConfig.TempDirectory) ? options.TempDirectory : TtsConfig.TempDirectory; + options.TempDirectorySweepHours = TtsConfig.TempDirectorySweepHours; options.CachePrefix = string.IsNullOrWhiteSpace(TtsConfig.CachePrefix) ? options.CachePrefix : TtsConfig.CachePrefix; options.NormalizedSampleRate = TtsConfig.NormalizedSampleRate; options.NormalizedChannels = TtsConfig.NormalizedChannels; diff --git a/Web/Resgrid.Web.Tts/Configuration/TtsOptions.cs b/Web/Resgrid.Web.Tts/Configuration/TtsOptions.cs index 4360d8525..00ea49a99 100644 --- a/Web/Resgrid.Web.Tts/Configuration/TtsOptions.cs +++ b/Web/Resgrid.Web.Tts/Configuration/TtsOptions.cs @@ -38,6 +38,16 @@ public sealed class TtsOptions [Required] public string TempDirectory { get; set; } = Path.Combine(Path.GetTempPath(), "resgrid-tts"); + /// + /// Age, in hours, at which an orphaned entry under is + /// deleted, and the interval the sweep runs on. Synthesis working directories are + /// removed in a finally block, so anything this old was left by a hard kill or a + /// delete that failed. Must stay well above + /// so an in-flight job is never collected. + /// + [Range(1, 168)] + public int TempDirectorySweepHours { get; set; } = 6; + [Required] public string CachePrefix { get; set; } = "tts2"; diff --git a/Web/Resgrid.Web.Tts/Program.cs b/Web/Resgrid.Web.Tts/Program.cs index f805197f4..4db77e554 100644 --- a/Web/Resgrid.Web.Tts/Program.cs +++ b/Web/Resgrid.Web.Tts/Program.cs @@ -116,6 +116,7 @@ await context.HttpContext.Response.WriteAsJsonAsync( builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddHostedService(); +builder.Services.AddHostedService(); var app = builder.Build(); diff --git a/Web/Resgrid.Web.Tts/Services/TempDirectorySweepHostedService.cs b/Web/Resgrid.Web.Tts/Services/TempDirectorySweepHostedService.cs new file mode 100644 index 000000000..6f3694155 --- /dev/null +++ b/Web/Resgrid.Web.Tts/Services/TempDirectorySweepHostedService.cs @@ -0,0 +1,120 @@ +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using Resgrid.Web.Tts.Configuration; + +namespace Resgrid.Web.Tts.Services +{ + /// + /// Deletes orphaned synthesis working directories under the TTS temp directory. + /// removes its own per-job directory in a finally + /// block, so leftovers only appear when the process is hard-killed mid-synthesis + /// (OOMKill, SIGKILL) or when the delete itself failed and was swallowed as a warning. + /// Nothing else reclaims those, and the temp volume is a fixed-size emptyDir, so they + /// are swept at startup and then on a schedule. + /// + public sealed class TempDirectorySweepHostedService : BackgroundService + { + private readonly TtsOptions _options; + private readonly ILogger _logger; + + public TempDirectorySweepHostedService( + IOptions options, + ILogger logger) + { + _options = options.Value; + _logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + // Yield before the first sweep so host startup isn't blocked on directory IO. + await Task.Yield(); + + SweepOnce(); + + using var timer = new PeriodicTimer(TimeSpan.FromHours(_options.TempDirectorySweepHours)); + + try + { + while (await timer.WaitForNextTickAsync(stoppingToken)) + { + SweepOnce(); + } + } + catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) + { + _logger.LogInformation("TTS temp directory sweep stopped."); + } + } + + /// + /// Removes every entry directly under the temp root last written more than + /// TempDirectorySweepHours ago, and returns how many were deleted. Individual + /// failures are logged and skipped so one undeletable entry can't stop the sweep. + /// + public int SweepOnce() + { + var tempRoot = Path.GetFullPath(string.IsNullOrWhiteSpace(_options.TempDirectory) + ? Path.GetTempPath() + : _options.TempDirectory); + + if (!Directory.Exists(tempRoot)) + { + return 0; + } + + var cutoff = DateTime.UtcNow - TimeSpan.FromHours(_options.TempDirectorySweepHours); + FileSystemInfo[] entries; + + try + { + // Materialized rather than streamed: the loop below deletes as it goes. + entries = new DirectoryInfo(tempRoot).GetFileSystemInfos(); + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) + { + _logger.LogWarning(ex, "Could not enumerate the TTS temp directory {TempRoot} for sweeping.", tempRoot); + return 0; + } + + var removed = 0; + + foreach (var entry in entries) + { + if (entry.LastWriteTimeUtc >= cutoff) + { + continue; + } + + try + { + if (entry is DirectoryInfo directory) + { + directory.Delete(recursive: true); + } + else + { + entry.Delete(); + } + + removed++; + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) + { + _logger.LogWarning(ex, "Failed to sweep orphaned TTS temp entry {Entry}.", entry.FullName); + } + } + + if (removed > 0) + { + _logger.LogInformation( + "Swept {RemovedCount} orphaned TTS temp entries older than {MaxAgeHours}h from {TempRoot}.", + removed, + _options.TempDirectorySweepHours, + tempRoot); + } + + return removed; + } + } +} diff --git a/Web/Resgrid.Web.Tts/k8s/deployment.yaml b/Web/Resgrid.Web.Tts/k8s/deployment.yaml index bec1f6172..4ad2515b8 100644 --- a/Web/Resgrid.Web.Tts/k8s/deployment.yaml +++ b/Web/Resgrid.Web.Tts/k8s/deployment.yaml @@ -25,6 +25,7 @@ data: RESGRID__TtsConfig__PiperModelDirectory: /usr/local/share/piper-voices RESGRID__TtsConfig__FfmpegExecutable: /usr/bin/ffmpeg RESGRID__TtsConfig__TempDirectory: /tmp/resgrid-tts + RESGRID__TtsConfig__TempDirectorySweepHours: "6" RESGRID__TtsConfig__CachePrefix: tts RESGRID__TtsConfig__PlaybackMemoryCacheMinutes: "60" RESGRID__TtsConfig__PlaybackCacheControlSeconds: "86400" diff --git a/Web/Resgrid.Web/Controllers/AccountController.cs b/Web/Resgrid.Web/Controllers/AccountController.cs index 0199932c2..08617c4a8 100644 --- a/Web/Resgrid.Web/Controllers/AccountController.cs +++ b/Web/Resgrid.Web/Controllers/AccountController.cs @@ -101,7 +101,7 @@ public async Task LogOn(string returnUrl = null) //RemoveCookies(); ViewData["ReturnUrl"] = returnUrl; - ViewData["LoginNotice"] = SystemBehaviorConfig.LoginPageNotice; + ViewData["LoginNotice"] = NoticeConfig.EffectiveLoginPageNotice; return View(); }