using System.Text.Json; using Server.WatchBody.Core.Metrics; using Server.WatchBody.Core.Programs; namespace Server.WatchBody.Web.Config; /// Атлет из конфига (Api.Athletes). public class ConfigAthlete { public string Name { get; set; } = string.Empty; public string ApiKey { get; set; } = string.Empty; } /// /// Конфигурация сервера (appsettings.json; tech-requirements 4.2). /// Ошибка валидации — фатальна: лог + stderr, код возврата 1. /// public class ServerConfig { public string ApiKey { get; set; } = string.Empty; public List Athletes { get; set; } = []; public string DbConnectionString { get; set; } = string.Empty; /// Каталог миграций (--mode migrate, З-36); по умолчанию — /// migrations рядом с исполняемым файлом. public string MigrationsDir { get; set; } = Path.Combine(AppContext.BaseDirectory, "migrations"); /// Папка импорта (--mode import, m06; подпапки — атлеты). public string ImportDir { get; set; } = Path.Combine(AppContext.BaseDirectory, "data", "import"); /// Папка обработанных файлов импорта. public string ImportArchiveDir { get; set; } = Path.Combine(AppContext.BaseDirectory, "data", "import_done"); /// Учётка Garmin Connect владельца (--mode fetch, m07). public string GarminLogin { get; set; } = string.Empty; public string GarminPassword { get; set; } = string.Empty; /// Атлет, на которого пишутся факты fetch (изоляция 🛡1). public string GarminAthleteName { get; set; } = string.Empty; /// Папка скачанных .fit (кэш по activityId). public string GarminDownloadDir { get; set; } = Path.Combine(AppContext.BaseDirectory, "data", "fit"); /// Точки входа Garmin (замена в тестах/прокси). public string GarminBaseUrl { get; set; } = "https://connect.garmin.com"; /// Пороги красных флагов (Safety.*, Р3-А; MetricsEngine). public MetricsConfig Safety { get; set; } = new(); /// Пороги правил прогрессии/делоада/перерывов (Progression.*, m05). public ProgressionConfig Progression { get; set; } = new(); /// Загрузка из json-файла (путь — из WATCHBODY_APPSETTINGS либо appsettings.json рядом). public static ServerConfig Load(string path) { var json = File.ReadAllText(path); using var doc = JsonDocument.Parse(json); var root = doc.RootElement; var result = new ServerConfig(); if (root.TryGetProperty("Api", out var api)) { if (api.TryGetProperty("Key", out var key)) result.ApiKey = key.GetString() ?? string.Empty; if (api.TryGetProperty("Athletes", out var athletes) && athletes.ValueKind == JsonValueKind.Array) { foreach (var a in athletes.EnumerateArray()) { result.Athletes.Add(new ConfigAthlete { Name = a.TryGetProperty("name", out var n) ? n.GetString() ?? string.Empty : string.Empty, ApiKey = a.TryGetProperty("apiKey", out var k) ? k.GetString() ?? string.Empty : string.Empty, }); } } } if (root.TryGetProperty("Db", out var db)) { if (db.TryGetProperty("ConnectionString", out var cs)) result.DbConnectionString = cs.GetString() ?? string.Empty; if (db.TryGetProperty("MigrationsDir", out var md) && md.ValueKind == JsonValueKind.String) result.MigrationsDir = md.GetString() ?? result.MigrationsDir; if (db.TryGetProperty("ImportDir", out var idir) && idir.ValueKind == JsonValueKind.String) result.ImportDir = idir.GetString() ?? result.ImportDir; if (db.TryGetProperty("ImportArchiveDir", out var adir) && adir.ValueKind == JsonValueKind.String) result.ImportArchiveDir = adir.GetString() ?? result.ImportArchiveDir; if (db.TryGetProperty("Garmin", out var garmin) && garmin.ValueKind == JsonValueKind.Object) { if (garmin.TryGetProperty("Login", out var gl)) result.GarminLogin = gl.GetString() ?? ""; if (garmin.TryGetProperty("Password", out var gp)) result.GarminPassword = gp.GetString() ?? ""; if (garmin.TryGetProperty("AthleteName", out var ga)) result.GarminAthleteName = ga.GetString() ?? ""; if (garmin.TryGetProperty("DownloadDir", out var gd) && gd.ValueKind == JsonValueKind.String) result.GarminDownloadDir = gd.GetString() ?? result.GarminDownloadDir; if (garmin.TryGetProperty("BaseUrl", out var gb) && gb.ValueKind == JsonValueKind.String) result.GarminBaseUrl = gb.GetString() ?? result.GarminBaseUrl; } } result.Safety = MetricsConfig.Load(root); result.Progression = ProgressionConfig.Load(root); return result; } /// Список ошибок валидации (spec 01-config-bootstrap). public List Validate() { var errors = new List(); if (string.IsNullOrWhiteSpace(ApiKey)) errors.Add("Api.Key пуст"); var seen = new HashSet(StringComparer.Ordinal); for (var i = 0; i < Athletes.Count; i++) { var a = Athletes[i]; if (string.IsNullOrWhiteSpace(a.Name)) errors.Add($"Api.Athletes[{i}].name пуст"); if (string.IsNullOrWhiteSpace(a.ApiKey)) errors.Add($"Api.Athletes[{i}].apiKey пуст"); else if (a.ApiKey == ApiKey) errors.Add($"Api.Athletes[{i}].apiKey совпадает с Api.Key"); else if (!seen.Add(a.ApiKey)) errors.Add($"дубликат apiKey у Api.Athletes[{i}]"); } if (string.IsNullOrWhiteSpace(DbConnectionString)) errors.Add("Db.ConnectionString пуст"); errors.AddRange(Safety.Validate()); errors.AddRange(Progression.Validate()); return errors; } }