feat: add locked new-year rollover wizard for season transitions

Promote returning students, assign officers, and clear last season's data after an automatic SQLite backup, with Docker volume path docs fixed for /app/Data.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-14 14:18:18 -04:00
co-authored by Cursor
parent c5c0f95f60
commit afdabd179a
16 changed files with 1677 additions and 50 deletions
+67
View File
@@ -0,0 +1,67 @@
using System.Text.Json;
using WebApp.Models;
namespace WebApp.Services;
/// <summary>
/// Persists chapter settings to <c>Data/appsettings.json</c>.
/// </summary>
public class ChapterSettingsWriter : IChapterSettingsWriter
{
private readonly IWebHostEnvironment _environment;
private readonly IConfiguration _configuration;
private readonly ILogger<ChapterSettingsWriter> _logger;
public ChapterSettingsWriter(
IWebHostEnvironment environment,
IConfiguration configuration,
ILogger<ChapterSettingsWriter> logger)
{
_environment = environment;
_configuration = configuration;
_logger = logger;
}
public async Task WriteAsync(ChapterSettings settings, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(settings);
var appSettingsPath = GetAppSettingsPath();
var dataDir = Path.GetDirectoryName(appSettingsPath);
if (dataDir != null && !Directory.Exists(dataDir))
{
Directory.CreateDirectory(dataDir);
}
Dictionary<string, object?> root;
if (File.Exists(appSettingsPath))
{
var existingJson = await File.ReadAllTextAsync(appSettingsPath, cancellationToken);
root = JsonSerializer.Deserialize<Dictionary<string, object?>>(existingJson)
?? [];
}
else
{
root = [];
}
root["ChapterSettings"] = settings;
var options = new JsonSerializerOptions { WriteIndented = true };
var json = JsonSerializer.Serialize(root, options);
await File.WriteAllTextAsync(appSettingsPath, json, cancellationToken);
_logger.LogInformation("Chapter settings written to {Path}", appSettingsPath);
}
public async Task UpdateCompetitionYearAsync(string competitionYear, CancellationToken cancellationToken = default)
{
var settings = _configuration.GetSection("ChapterSettings").Get<ChapterSettings>()
?? new ChapterSettings();
settings.CompetitionYear = competitionYear;
await WriteAsync(settings, cancellationToken);
}
private string GetAppSettingsPath() =>
Path.Combine(_environment.ContentRootPath, "Data", "appsettings.json");
}