diff --git a/csharp/App/Backend/Controller.cs b/csharp/App/Backend/Controller.cs index 28f1e1b3a..ca7015c11 100644 --- a/csharp/App/Backend/Controller.cs +++ b/csharp/App/Backend/Controller.cs @@ -961,6 +961,46 @@ public class Controller : ControllerBase // ── Weekly Performance Report ────────────────────────────────────── + private async Task FetchWeeklyReportAsync( + Int64 installationId, String installationName, String lang, + DateOnly? weekStartDate = null, Boolean forceRegenerate = false) + { + DateOnly periodStart, periodEnd; + if (weekStartDate.HasValue) + { + periodStart = weekStartDate.Value; + periodEnd = weekStartDate.Value.AddDays(6); + } + else + { + (periodStart, periodEnd) = WeeklyReportService.LastCalendarWeek(); + } + + var periodStartStr = periodStart.ToString("yyyy-MM-dd"); + var periodEndStr = periodEnd.ToString("yyyy-MM-dd"); + + if (!forceRegenerate) + { + var cached = Db.GetWeeklyReportForWeek(installationId, periodStartStr, periodEndStr); + if (cached != null) + { + var cachedResponse = await ReportAggregationService.ToWeeklyReportResponseAsync(cached, lang); + if (cachedResponse != null) + { + Console.WriteLine($"[WeeklyReport] Serving cached report for installation {installationId}, period {periodStartStr}–{periodEndStr}, language={lang}"); + return cachedResponse; + } + } + } + + Console.WriteLine($"[WeeklyReport] Generating fresh report for installation {installationId}, period {periodStartStr}–{periodEndStr}"); + var report = await WeeklyReportService.GenerateReportAsync( + installationId, installationName, lang, weekStartDate); + + ReportAggregationService.SaveWeeklySummary(installationId, report, lang); + return report; + } + /// /// Returns a weekly performance report. Serves from cache if available; /// generates fresh on first request or when forceRegenerate is true. @@ -991,43 +1031,9 @@ public class Controller : ControllerBase { var lang = language ?? user.Language ?? "en"; - // Compute target week dates for cache lookup - DateOnly periodStart, periodEnd; - if (weekStartDate.HasValue) - { - periodStart = weekStartDate.Value; - periodEnd = weekStartDate.Value.AddDays(6); - } - else - { - (periodStart, periodEnd) = WeeklyReportService.LastCalendarWeek(); - } - - var periodStartStr = periodStart.ToString("yyyy-MM-dd"); - var periodEndStr = periodEnd.ToString("yyyy-MM-dd"); - - // Cache-first: check if a cached report exists for this week - if (!forceRegenerate) - { - var cached = Db.GetWeeklyReportForWeek(installationId, periodStartStr, periodEndStr); - if (cached != null) - { - var cachedResponse = await ReportAggregationService.ToWeeklyReportResponseAsync(cached, lang); - if (cachedResponse != null) - { - Console.WriteLine($"[GetWeeklyReport] Serving cached report for installation {installationId}, period {periodStartStr}–{periodEndStr}, language={lang}"); - return Ok(cachedResponse); - } - } - } - - // Cache miss or forceRegenerate: generate fresh - Console.WriteLine($"[GetWeeklyReport] Generating fresh report for installation {installationId}, period {periodStartStr}–{periodEndStr}"); - var report = await WeeklyReportService.GenerateReportAsync( - installationId, installation.Name, lang, weekStartDate); - - // Persist weekly summary and seed AiInsightCache for this language - ReportAggregationService.SaveWeeklySummary(installationId, report, lang); + var report = await FetchWeeklyReportAsync(installationId, installation.Name, lang, weekStartDate, forceRegenerate); + if (report == null) + return BadRequest("Failed to generate report."); return Ok(report); } @@ -1134,6 +1140,46 @@ public class Controller : ControllerBase return Ok(reports); } + [HttpGet(nameof(GetCurrentMonthPreview))] + public async Task> GetCurrentMonthPreview( + Int64 installationId, Token authToken, String? language = null) + { + var user = Db.GetSession(authToken)?.User; + if (user == null) + return Unauthorized(); + + var installation = Db.GetInstallationById(installationId); + if (installation is null || !user.HasAccessTo(installation)) + return Unauthorized(); + + var lang = language ?? user.Language ?? "en"; + var preview = await ReportAggregationService.GetCurrentMonthPreviewAsync(installationId, lang); + if (preview == null) + return NotFound("No daily data for the current month."); + + return Ok(preview); + } + + [HttpGet(nameof(GetCurrentYearPreview))] + public async Task> GetCurrentYearPreview( + Int64 installationId, Token authToken, String? language = null) + { + var user = Db.GetSession(authToken)?.User; + if (user == null) + return Unauthorized(); + + var installation = Db.GetInstallationById(installationId); + if (installation is null || !user.HasAccessTo(installation)) + return Unauthorized(); + + var lang = language ?? user.Language ?? "en"; + var preview = await ReportAggregationService.GetCurrentYearPreviewAsync(installationId, lang); + if (preview == null) + return NotFound("No monthly reports for the current year."); + + return Ok(preview); + } + /// /// Manually trigger monthly aggregation for an installation. /// Computes monthly report from daily records for the specified year/month. @@ -1514,7 +1560,9 @@ public class Controller : ControllerBase // ── Report HTML (for PDF download) ───────────────────────────── [HttpGet(nameof(GetWeeklyReportHtml))] - public async Task GetWeeklyReportHtml(Int64 installationId, Token authToken, String? language = null) + public async Task GetWeeklyReportHtml( + Int64 installationId, Token authToken, + String? language = null, String? weekStart = null, String source = "email") { var user = Db.GetSession(authToken)?.User; if (user == null) return Unauthorized(); @@ -1522,14 +1570,26 @@ public class Controller : ControllerBase var installation = Db.GetInstallationById(installationId); if (installation is null || !user.HasAccessTo(installation)) return Unauthorized(); - var lang = language ?? user.Language ?? "en"; - var report = await WeeklyReportService.GenerateReportAsync(installationId, installation.Name, lang); - var html = ReportEmailService.BuildHtmlEmail(report, lang); + var lang = language ?? user.Language ?? "en"; + + DateOnly? weekStartDate = null; + if (!String.IsNullOrEmpty(weekStart)) + { + if (!DateOnly.TryParseExact(weekStart, "yyyy-MM-dd", out var parsed)) + return BadRequest("weekStart must be in yyyy-MM-dd format."); + weekStartDate = parsed; + } + + var report = await FetchWeeklyReportAsync(installationId, installation.Name, lang, weekStartDate); + if (report == null) + return BadRequest("Failed to generate report."); + + var html = ReportEmailService.BuildHtmlEmail(report, lang, source: source); return Content(html, "text/html"); } [HttpGet(nameof(GetMonthlyReportHtml))] - public async Task GetMonthlyReportHtml(Int64 installationId, Int32 year, Int32 month, Token authToken, String? language = null) + public async Task GetMonthlyReportHtml(Int64 installationId, Int32 year, Int32 month, Token authToken, String? language = null, String source = "email") { var user = Db.GetSession(authToken)?.User; if (user == null) return Unauthorized(); @@ -1548,12 +1608,12 @@ public class Controller : ControllerBase report.TotalPvProduction, report.TotalConsumption, report.TotalGridImport, report.TotalGridExport, report.TotalBatteryCharged, report.TotalBatteryDischarged, report.TotalEnergySaved, report.TotalSavingsCHF, report.SelfSufficiencyPercent, report.BatteryEfficiencyPercent, report.AiInsight, - $"{report.WeekCount} {s.CountLabel}", s); + $"{report.WeekCount} {s.CountLabel}", s, source: source); return Content(html, "text/html"); } [HttpGet(nameof(GetYearlyReportHtml))] - public async Task GetYearlyReportHtml(Int64 installationId, Int32 year, Token authToken, String? language = null) + public async Task GetYearlyReportHtml(Int64 installationId, Int32 year, Token authToken, String? language = null, String source = "email") { var user = Db.GetSession(authToken)?.User; if (user == null) return Unauthorized(); @@ -1572,7 +1632,7 @@ public class Controller : ControllerBase report.TotalPvProduction, report.TotalConsumption, report.TotalGridImport, report.TotalGridExport, report.TotalBatteryCharged, report.TotalBatteryDischarged, report.TotalEnergySaved, report.TotalSavingsCHF, report.SelfSufficiencyPercent, report.BatteryEfficiencyPercent, report.AiInsight, - $"{report.MonthCount} {s.CountLabel}", s); + $"{report.MonthCount} {s.CountLabel}", s, source: source); return Content(html, "text/html"); } diff --git a/csharp/App/Backend/DataTypes/ReportSummary.cs b/csharp/App/Backend/DataTypes/ReportSummary.cs index 3a17c4117..cba7d1d97 100644 --- a/csharp/App/Backend/DataTypes/ReportSummary.cs +++ b/csharp/App/Backend/DataTypes/ReportSummary.cs @@ -94,6 +94,11 @@ public class MonthlyReportSummary public Int32 WeekCount { get; set; } public String AiInsight { get; set; } = ""; public String CreatedAt { get; set; } = ""; + + // Preview-only fields (not stored in DB) + [Ignore] public Boolean IsPreview { get; set; } + [Ignore] public Int32 DaysAvailable { get; set; } + [Ignore] public Int32 DaysInMonth { get; set; } } /// @@ -137,19 +142,22 @@ public class YearlyReportSummary public Int32 MonthCount { get; set; } public String AiInsight { get; set; } = ""; public String CreatedAt { get; set; } = ""; + + // Preview-only fields (not stored in DB) + [Ignore] public Boolean IsPreview { get; set; } } // ── DTOs for pending aggregation queries (not stored in DB) ── public class PendingMonth { - public Int32 Year { get; set; } - public Int32 Month { get; set; } - public Int32 WeekCount { get; set; } + public Int32 Year { get; set; } + public Int32 Month { get; set; } + public Int32 WeekCount { get; set; } } public class PendingYear { - public Int32 Year { get; set; } - public Int32 MonthCount { get; set; } + public Int32 Year { get; set; } + public Int32 MonthCount { get; set; } } diff --git a/csharp/App/Backend/Services/ReportAggregationService.cs b/csharp/App/Backend/Services/ReportAggregationService.cs index bec798b34..ec71be1b2 100644 --- a/csharp/App/Backend/Services/ReportAggregationService.cs +++ b/csharp/App/Backend/Services/ReportAggregationService.cs @@ -382,8 +382,8 @@ public static class ReportAggregationService InstallationId = installationId, Year = year, Month = month, - PeriodStart = first.ToString("yyyy-MM-dd"), - PeriodEnd = last.ToString("yyyy-MM-dd"), + PeriodStart = days.Min(d => d.Date), // actual first data day, not calendar month start + PeriodEnd = days.Max(d => d.Date), // actual last data day, not calendar month end TotalPvProduction = totalPv, TotalConsumption = totalConsump, TotalGridImport = totalGridIn, @@ -881,4 +881,132 @@ Rules: Write in {langName}. Write for a homeowner. No asterisks or formatting ma return "AI insight could not be generated at this time."; } + + // ── Current-Period Previews (not saved to DB) ───────────────────── + + public static async Task GetCurrentMonthPreviewAsync(Int64 installationId, String language = "en") + { + var now = DateTime.UtcNow; + var year = now.Year; + var month = now.Month; + var first = new DateOnly(year, month, 1); + var last = first.AddMonths(1).AddDays(-1); + var days = Db.GetDailyRecords(installationId, first, last); + + if (days.Count == 0) + return null; + + var totalPv = Math.Round(days.Sum(d => d.PvProduction), 1); + var totalConsump = Math.Round(days.Sum(d => d.LoadConsumption), 1); + var totalGridIn = Math.Round(days.Sum(d => d.GridImport), 1); + var totalGridOut = Math.Round(days.Sum(d => d.GridExport), 1); + var totalBattChg = Math.Round(days.Sum(d => d.BatteryCharged), 1); + var totalBattDis = Math.Round(days.Sum(d => d.BatteryDischarged), 1); + + var energySaved = Math.Round(totalConsump - totalGridIn, 1); + var savingsCHF = Math.Round(energySaved * ElectricityPriceCHF, 0); + var selfSufficiency = totalConsump > 0 ? Math.Round((totalConsump - totalGridIn) / totalConsump * 100, 1) : 0; + var selfConsumption = totalPv > 0 ? Math.Round((totalPv - totalGridOut) / totalPv * 100, 1) : 0; + var batteryEff = totalBattChg > 0 ? Math.Round(totalBattDis / totalBattChg * 100, 1) : 0; + var gridDependency = totalConsump > 0 ? Math.Round(totalGridIn / totalConsump * 100, 1) : 0; + + var installation = Db.GetInstallationById(installationId); + var installationName = installation?.Name ?? $"Installation {installationId}"; + var monthName = new DateTime(year, month, 1).ToString("MMMM yyyy"); + var weatherCity = !string.IsNullOrWhiteSpace(installation?.City) ? installation.City : installation?.Location; + var weatherRegion = !string.IsNullOrWhiteSpace(installation?.Canton) ? installation.Canton : installation?.Region; + + var aiInsight = await GenerateMonthlyAiInsightAsync( + installationName, monthName, days.Count, + totalPv, totalConsump, totalGridIn, totalGridOut, + totalBattChg, totalBattDis, energySaved, savingsCHF, + selfSufficiency, batteryEff, language, + weatherCity, installation?.Country, weatherRegion); + + var firstDataDay = days.Min(d => d.Date); + var lastDataDay = days.Max(d => d.Date); + + return new MonthlyReportSummary + { + InstallationId = installationId, + Year = year, + Month = month, + PeriodStart = firstDataDay, + PeriodEnd = lastDataDay, + TotalPvProduction = totalPv, + TotalConsumption = totalConsump, + TotalGridImport = totalGridIn, + TotalGridExport = totalGridOut, + TotalBatteryCharged = totalBattChg, + TotalBatteryDischarged = totalBattDis, + TotalEnergySaved = energySaved, + TotalSavingsCHF = savingsCHF, + SelfSufficiencyPercent = selfSufficiency, + SelfConsumptionPercent = selfConsumption, + BatteryEfficiencyPercent = batteryEff, + GridDependencyPercent = gridDependency, + WeekCount = days.Count, + AiInsight = aiInsight, + CreatedAt = DateTime.UtcNow.ToString("o"), + IsPreview = true, + DaysAvailable = days.Count, + DaysInMonth = last.Day, + }; + } + + public static async Task GetCurrentYearPreviewAsync(Int64 installationId, String language = "en") + { + var year = DateTime.UtcNow.Year; + var monthlies = Db.GetMonthlyReportsForYear(installationId, year); + + if (monthlies.Count == 0) + return null; + + var totalPv = Math.Round(monthlies.Sum(m => m.TotalPvProduction), 1); + var totalConsump = Math.Round(monthlies.Sum(m => m.TotalConsumption), 1); + var totalGridIn = Math.Round(monthlies.Sum(m => m.TotalGridImport), 1); + var totalGridOut = Math.Round(monthlies.Sum(m => m.TotalGridExport), 1); + var totalBattChg = Math.Round(monthlies.Sum(m => m.TotalBatteryCharged), 1); + var totalBattDis = Math.Round(monthlies.Sum(m => m.TotalBatteryDischarged), 1); + + var energySaved = Math.Round(totalConsump - totalGridIn, 1); + var savingsCHF = Math.Round(energySaved * ElectricityPriceCHF, 0); + var selfSufficiency = totalConsump > 0 ? Math.Round((totalConsump - totalGridIn) / totalConsump * 100, 1) : 0; + var selfConsumption = totalPv > 0 ? Math.Round((totalPv - totalGridOut) / totalPv * 100, 1) : 0; + var batteryEff = totalBattChg > 0 ? Math.Round(totalBattDis / totalBattChg * 100, 1) : 0; + var gridDependency = totalConsump > 0 ? Math.Round(totalGridIn / totalConsump * 100, 1) : 0; + + var installation = Db.GetInstallationById(installationId); + var installationName = installation?.Name ?? $"Installation {installationId}"; + + var aiInsight = await GenerateYearlyAiInsightAsync( + installationName, year, monthlies.Count, + totalPv, totalConsump, totalGridIn, totalGridOut, + totalBattChg, totalBattDis, energySaved, savingsCHF, + selfSufficiency, batteryEff, language); + + return new YearlyReportSummary + { + InstallationId = installationId, + Year = year, + PeriodStart = monthlies.Min(m => m.PeriodStart), + PeriodEnd = monthlies.Max(m => m.PeriodEnd), + TotalPvProduction = totalPv, + TotalConsumption = totalConsump, + TotalGridImport = totalGridIn, + TotalGridExport = totalGridOut, + TotalBatteryCharged = totalBattChg, + TotalBatteryDischarged = totalBattDis, + TotalEnergySaved = energySaved, + TotalSavingsCHF = savingsCHF, + SelfSufficiencyPercent = selfSufficiency, + SelfConsumptionPercent = selfConsumption, + BatteryEfficiencyPercent = batteryEff, + GridDependencyPercent = gridDependency, + MonthCount = monthlies.Count, + AiInsight = aiInsight, + CreatedAt = DateTime.UtcNow.ToString("o"), + IsPreview = true, + }; + } } diff --git a/csharp/App/Backend/Services/ReportEmailService.cs b/csharp/App/Backend/Services/ReportEmailService.cs index 1399b6a66..3826026fa 100644 --- a/csharp/App/Backend/Services/ReportEmailService.cs +++ b/csharp/App/Backend/Services/ReportEmailService.cs @@ -9,8 +9,10 @@ namespace InnovEnergy.App.Backend.Services; public static class ReportEmailService { - // inesco logo (dark background variant, SVG) embedded as base64 data URI for emails and PDF reports - private const string LogoBase64 = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIGlkPSJf0KHQu9C+0LlfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgNjY2LjMzIDIzNy44MyI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7fS5jbHMtMntmaWxsOiMwMGIwNTA7fTwvc3R5bGU+PC9kZWZzPjxwYXRoIGNsYXNzPSJjbHMtMiIgZD0ibTUuMDksMzIuMjJDMS43LDI4LjgzLDAsMjQuMzEsMCwxOC42NVMxLjcsOC40OCw1LjA5LDUuMDlDOC40OCwxLjcsMTMuMjQsMCwxOS4zOCwwczEwLjY2LDEuNywxNC4wNSw1LjA5YzMuMzksMy4zOSw1LjA5LDcuOTEsNS4wOSwxMy41NnMtMS43LDEwLjE3LTUuMDksMTMuNTdjLTMuMzksMy4zOS04LjA4LDUuMDktMTQuMDUsNS4wOXMtMTAuOS0xLjctMTQuMjktNS4wOVoiLz48cmVjdCBjbGFzcz0iY2xzLTEiIHg9IjIuNjYiIHk9IjUzLjc4IiB3aWR0aD0iMzMuNDMiIGhlaWdodD0iMTIzLjc4Ii8+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJtNjMuMTcsMTc3LjU2VjYxLjUzYzE2LjYzLTcuMTEsMzQuODgtMTAuNjYsNTQuNzUtMTAuNjYsMTcuNzYsMCwzMS40OSw0LjI4LDQxLjE4LDEyLjg0LDkuNjksOC41NiwxNC41NCwyMC44MywxNC41NCwzNi44MnY3Ny4wM2gtMzMuNDN2LTc0LjYxYzAtMTYuMzEtNy41MS0yNC40Ny0yMi41My0yNC40Ny03LjkxLDAtMTUuMDIsMS40NS0yMS4zMiw0LjM2djk0LjcyaC0zMy4xOVoiLz48cGF0aCBjbGFzcz0iY2xzLTEiIGQ9Im0zMDUuNDEsMTA1Ljg2YzAsNS44MS0uMzMsMTEuOTUtLjk3LDE4LjQxaC03Ni41NWMzLjA3LDE4Ljc0LDE0LjM3LDI4LjEsMzMuOTEsMjguMSwxMS4zLDAsMjIuNTMtMi4zNCwzMy42Ny03LjAzbDUuMzMsMjYuODljLTEyLjExLDUuNDktMjYuMjUsOC4yNC00Mi4zOSw4LjI0LTE5LjcsMC0zNS41Ny01Ljg1LTQ3LjYtMTcuNTYtMTIuMDMtMTEuNzEtMTguMDUtMjcuNDktMTguMDUtNDcuMzZzNS41Ny0zNC44LDE2LjcxLTQ2Ljc1YzExLjE0LTExLjk1LDI1LjU5LTE3LjkyLDQzLjM2LTE3LjkyLDE1LjgyLDAsMjguNTQsNS4wOSwzOC4xNSwxNS4yNiw5LjYxLDEwLjE3LDE0LjQxLDIzLjQyLDE0LjQxLDM5LjczWm0tMzEuOTgtMS45NHYtMi45MWMwLTYuOTQtMS44Mi0xMi42LTUuNDUtMTYuOTYtMy42My00LjM2LTguNjgtNi41NC0xNS4xNC02LjU0LTYuOTUsMC0xMi42NCwyLjM0LTE3LjA4LDcuMDItNC40NCw0LjY5LTcuMjMsMTEuMTQtOC4zNiwxOS4zOGg0Ni4wM1oiLz48cGF0aCBjbGFzcz0iY2xzLTEiIGQ9Im00MDMuNTYsODUuMDJjLTE0LjM3LTQuMzYtMjUuNi02LjU0LTMzLjY3LTYuNTQtMTEuMywwLTE2Ljk2LDMuMzEtMTYuOTYsOS45MywwLDMuMzksMS4zNyw1LjkzLDQuMTIsNy42MywyLjc0LDEuNyw4LjA3LDMuNjcsMTUuOTksNS45MywxNC42OSw0LjA0LDI1LjIzLDguOTIsMzEuNjEsMTQuNjYsNi4zOCw1Ljc0LDkuNTcsMTQuMDEsOS41NywyNC44MywwLDEyLjYtNC42NCwyMi4yNS0xMy45MywyOC45NS05LjI5LDYuNy0yMS42LDEwLjA1LTM2Ljk0LDEwLjA1LTE2Ljk2LDAtMzEuNTctMi42Ny00My44NS04bDQuNi0yNy4zN2MxMy40LDUuMTcsMjYuMjQsNy43NSwzOC41Miw3Ljc1LDUuNjUsMCwxMC4yMS0uOTMsMTMuNjktMi43OSwzLjQ3LTEuODUsNS4yMS00LjQ4LDUuMjEtNy44NywwLTMuNzEtMS42Ni02LjUtNC45Ny04LjM2LTMuMzEtMS44NS0xMC4yOS00LjMyLTIwLjk1LTcuMzktMTEuNzktMy4wNy0yMC42Ny03LjUxLTI2LjY1LTEzLjMyLTUuOTgtNS44MS04Ljk2LTEzLjgxLTguOTYtMjMuOTgsMC0xMi4yNyw0LjE2LTIxLjcyLDEyLjQ4LTI4LjM0LDguMzItNi42MiwxOS45LTkuOTMsMzQuNzYtOS45M3MyOC4xOCwyLjM0LDM5Ljk3LDcuMDJsLTMuNjMsMjcuMTNaIi8+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJtNTIzLjYzLDU4LjE0bC02LjMsMjYuNGMtOS41My00LjAzLTE4LjY1LTYuMDYtMjcuMzctNi4wNi05LjY5LDAtMTcuMTYsMy4yMy0yMi40MSw5LjY5LTUuMjUsNi40Ni03Ljg3LDE1LjQyLTcuODcsMjYuODksMCwyNS4xOSwxMC42NiwzNy43OSwzMS45OCwzNy43OSw4LjU2LDAsMTcuNTItMi4xLDI2Ljg5LTYuM2w1LjMzLDI2Ljg5Yy05LjM3LDQuNjgtMjEsNy4wMy0zNC44OCw3LjAzLTIwLjAzLDAtMzUuNjEtNS43Ny00Ni43NS0xNy4zMi0xMS4xNC0xMS41NC0xNi43MS0yNy4yNS0xNi43MS00Ny4xMnM1LjQ5LTM1LjY5LDE2LjQ3LTQ3LjQ4YzEwLjk4LTExLjc5LDI2LTE3LjY4LDQ1LjA2LTE3LjY4LDEzLjg5LDAsMjYuMDgsMi40MiwzNi41OCw3LjI3WiIvPjxwYXRoIGNsYXNzPSJjbHMtMSIgZD0ibTYxOS4yNiwxMzkuNTRjLTEyLjQ1LDkuNDQtMzAuMjcsOC40OS00MS42My0yLjg3LTExLjM2LTExLjM2LTEyLjMxLTI5LjE3LTIuODctNDEuNjNsLTI0LjUtMjQuNWMtMjIuODUsMjYuMDMtMjEuODcsNjUuNjksMi45OCw5MC41MywyNC44NCwyNC44NCw2NC41LDI1LjgzLDkwLjUzLDIuOThsLTI0LjUxLTI0LjUxWiIvPjxyZWN0IGNsYXNzPSJjbHMtMiIgeD0iNTYzLjI1IiB5PSI2Mi4yNyIgd2lkdGg9IjQuMzciIGhlaWdodD0iMzQuNTEiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEwOS4zNyA0MjMuMTEpIHJvdGF0ZSgtNDUpIi8+PHJlY3QgY2xhc3M9ImNscy0yIiB4PSI1NjkuOCIgeT0iNTYuNzgiIHdpZHRoPSI0LjM3IiBoZWlnaHQ9IjM0LjUxIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg2MS4wMSAzNDEuNTMpIHJvdGF0ZSgtMzUuMDEpIi8+PHJlY3QgY2xhc3M9ImNscy0yIiB4PSI1NzcuMiIgeT0iNTIuNTEiIHdpZHRoPSI0LjM3IiBoZWlnaHQ9IjM0LjUxIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgyNC44NyAyNTEuNjEpIHJvdGF0ZSgtMjUuMDIpIi8+PHJlY3QgY2xhc3M9ImNscy0yIiB4PSI1ODUuMjMiIHk9IjQ5LjU5IiB3aWR0aD0iNC4zNyIgaGVpZ2h0PSIzNC41MSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMi43MyAxNTQuNDIpIHJvdGF0ZSgtMTUuMDEpIi8+PHJlY3QgY2xhc3M9ImNscy0yIiB4PSI1OTMuNjUiIHk9IjQ4LjEiIHdpZHRoPSI0LjM3IiBoZWlnaHQ9IjM0LjUxIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMy40MyA1Mi4yMSkgcm90YXRlKC01KSIvPjxyZWN0IGNsYXNzPSJjbHMtMiIgeD0iNTg3LjEyIiB5PSI2My4xNyIgd2lkdGg9IjM0LjUxIiBoZWlnaHQ9IjQuMzciIHRyYW5zZm9ybT0idHJhbnNsYXRlKDQ4Ni41NSA2NjEuNzMpIHJvdGF0ZSgtODUpIi8+PHJlY3QgY2xhc3M9ImNscy0yIiB4PSI1OTUuNTQiIHk9IjY0LjY2IiB3aWR0aD0iMzQuNTEiIGhlaWdodD0iNC4zNyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMzg5LjY0IDY0MS40Nikgcm90YXRlKC03NSkiLz48cmVjdCBjbGFzcz0iY2xzLTIiIHg9IjYwMy41NyIgeT0iNjcuNTgiIHdpZHRoPSIzNC41MSIgaGVpZ2h0PSI0LjM3IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgyOTUuMjkgNjAyLjk4KSByb3RhdGUoLTY1LjAxKSIvPjxyZWN0IGNsYXNzPSJjbHMtMiIgeD0iNjEwLjk3IiB5PSI3MS44NSIgd2lkdGg9IjM0LjUxIiBoZWlnaHQ9IjQuMzciIHRyYW5zZm9ybT0idHJhbnNsYXRlKDIwNy4zIDU0Ni4yMykgcm90YXRlKC01NS4wMSkiLz48cmVjdCBjbGFzcz0iY2xzLTIiIHg9IjYxNy41MiIgeT0iNzcuMzUiIHdpZHRoPSIzNC41MSIgaGVpZ2h0PSI0LjM3IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxMjkuNjEgNDcyLjA1KSByb3RhdGUoLTQ0Ljk5KSIvPjxyZWN0IGNsYXNzPSJjbHMtMiIgeD0iNjIzLjAxIiB5PSI4My44OSIgd2lkdGg9IjM0LjUxIiBoZWlnaHQ9IjQuMzciIHRyYW5zZm9ybT0idHJhbnNsYXRlKDY2LjM0IDM4Mi42NSkgcm90YXRlKC0zNC45OCkiLz48cmVjdCBjbGFzcz0iY2xzLTIiIHg9IjYyNy4yOCIgeT0iOTEuMjkiIHdpZHRoPSIzNC41MSIgaGVpZ2h0PSI0LjM2IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgyMC45MyAyODEuMykgcm90YXRlKC0yNS4wMSkiLz48cmVjdCBjbGFzcz0iY2xzLTIiIHg9IjYzMC4yMSIgeT0iOTkuMzMiIHdpZHRoPSIzNC41MSIgaGVpZ2h0PSI0LjM2IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtNC4yMSAxNzEuMDgpIHJvdGF0ZSgtMTUpIi8+PHJlY3QgY2xhc3M9ImNscy0yIiB4PSI2MzEuNjkiIHk9IjEwNy43NCIgd2lkdGg9IjM0LjUxIiBoZWlnaHQ9IjQuMzciIHRyYW5zZm9ybT0idHJhbnNsYXRlKC03LjEgNTYuODYpIHJvdGF0ZSgtNC45OSkiLz48cmVjdCBjbGFzcz0iY2xzLTIiIHg9IjY0Ni43NiIgeT0iMTAxLjIyIiB3aWR0aD0iNC4zNyIgaGVpZ2h0PSIzNC41MSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNDc0LjQ3IDc1NC42NSkgcm90YXRlKC04NS4wMSkiLz48cmVjdCBjbGFzcz0iY2xzLTIiIHg9IjY0NS4yOCIgeT0iMTA5LjYzIiB3aWR0aD0iNC4zNyIgaGVpZ2h0PSIzNC41MSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMzU3LjMgNzE5LjQzKSByb3RhdGUoLTc1KSIvPjxyZWN0IGNsYXNzPSJjbHMtMiIgeD0iNjQyLjM2IiB5PSIxMTcuNjYiIHdpZHRoPSI0LjM2IiBoZWlnaHQ9IjM0LjUxIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgyNDkuNzIgNjYxLjk0KSByb3RhdGUoLTY0Ljk4KSIvPjxwb2x5Z29uIGNsYXNzPSJjbHMtMiIgcG9pbnRzPSI2MjcuMzkgMTMwLjYzIDYyNC44OCAxMzQuMjEgNjUzLjE1IDE1NCA2NTUuNjUgMTUwLjQzIDYyNy4zOSAxMzAuNjMiLz48cG9seWdvbiBjbGFzcz0iY2xzLTIiIHBvaW50cz0iNjI0LjEyIDEzNS4xMiA2MjEuMDMgMTM4LjIxIDY0NS40MyAxNjIuNjEgNjQ4LjUxIDE1OS41MyA2MjQuMTIgMTM1LjEyIi8+PHBvbHlnb24gY2xhc3M9ImNscy0xIiBwb2ludHM9IjkuMTEgMjI0LjY3IDIzLjc4IDIyNC42NyAyMy43OCAyMTcuNzkgOS4xMSAyMTcuNzkgOS4xMSAyMTIuODEgMzQuMjggMjEyLjgxIDM0LjI4IDIwNS43OCAwIDIwNS43OCAwIDIzNy44MyAzNC42NCAyMzcuODMgMzQuNjQgMjMwLjU5IDkuMTEgMjMwLjU5IDkuMTEgMjI0LjY3Ii8+PHBvbHlnb24gY2xhc3M9ImNscy0xIiBwb2ludHM9IjE1Mi4xNiAyMjUuMTIgMTI3LjQ1IDIwNS43OCAxMjEuMjUgMjA1Ljc4IDEyMS4yNSAyMzcuODMgMTI5LjU3IDIzNy44MyAxMjkuNTcgMjE4LjIxIDE1NC4wNCAyMzcuNjUgMTU0LjI2IDIzNy44MyAxNjAuNDggMjM3LjgzIDE2MC40OCAyMDUuNzggMTUyLjE2IDIwNS43OCAxNTIuMTYgMjI1LjEyIi8+PHBvbHlnb24gY2xhc3M9ImNscy0xIiBwb2ludHM9IjI1Ni4yMSAyMjQuNjcgMjcwLjg3IDIyNC42NyAyNzAuODcgMjE3Ljc5IDI1Ni4yMSAyMTcuNzkgMjU2LjIxIDIxMi44MSAyODEuMzcgMjEyLjgxIDI4MS4zNyAyMDUuNzggMjQ3LjEgMjA1Ljc4IDI0Ny4xIDIzNy44MyAyODEuNzMgMjM3LjgzIDI4MS43MyAyMzAuNTkgMjU2LjIxIDIzMC41OSAyNTYuMjEgMjI0LjY3Ii8+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJtMzk2LjU1LDIyNi4zMmMyLjY2LDAsNC41OC0uNDksNS44OS0xLjUsMS4zNi0xLjA0LDIuMDQtMi43OCwyLjA0LTUuMTR2LTcuMTljMC0yLjQxLS42OS00LjE3LTIuMDQtNS4yMS0xLjMxLTEuMDEtMy4yNC0xLjUtNS44OS0xLjVoLTI4LjIxdjMyLjA1aDguOTd2LTExLjVoNS44NGwxMi43MywxMS41aDEzLjI5bC0xNC40NS0xMS41aDEuODNabS0xOS4yNC0xMy42NmgxNi4wOGMxLjE5LDAsMS44Mi4yLDIuMTMuMzcuMjkuMTYuNDQuNTguNDQsMS4yNnYzLjUyYzAsLjY4LS4xNSwxLjEtLjQ0LDEuMjYtLjMxLjE3LS45My4zNy0yLjEzLjM3aC0xNi4wOHYtNi43OFoiLz48cGF0aCBjbGFzcz0iY2xzLTEiIGQ9Im01MzMuMTIsMjA3LjMxYy0xLjM2LTEuMDMtMy41Mi0xLjUzLTYuNjEtMS41M2gtMjJjLTEuNTMsMC0yLjg1LjExLTMuOTEuMzQtMS4xNC4yNC0yLjA4LjY4LTIuOCwxLjI5LS43NS42My0xLjI4LDEuNTEtMS41OSwyLjU5LS4yOSwxLjAxLS40MywyLjI1LS40MywzLjc4djE2LjAzYzAsMS41My4xNCwyLjc2LjQzLDMuNzUuMywxLjA1LjgyLDEuOTIsMS41MiwyLjU3LjcxLjY1LDEuNjUsMS4xLDIuNzksMS4zNSwxLjA2LjIzLDIuNC4zNCwzLjk4LjM0aDIyYzEuNTMsMCwyLjg2LS4xMSwzLjk0LS4zNCwxLjE3LS4yNCwyLjEyLS43LDIuODMtMS4zNS43MS0uNjUsMS4yMi0xLjUyLDEuNTItMi41Ny4yOC0uOTguNDMtMi4yNC40My0zLjc1di0xMS4zMWgtMjAuOTd2Ny4wM2gxMi4wOHY0Ljg0aC0yMS41MnYtMTcuMzVoMjEuNTJ2My4zOGw4Ljg5LTEuNXYtMS40OGMwLTMuMDMtLjY5LTUuMDMtMi4xMi02LjExWiIvPjxwb2x5Z29uIGNsYXNzPSJjbHMtMSIgcG9pbnRzPSI2NTUuMzIgMjA1Ljc4IDY0NC42NiAyMTYuNzggNjM0LjE1IDIwNS43OCA2MjEuODUgMjA1Ljc4IDYzOS42IDIyMy42OSA2MzkuNiAyMzcuODMgNjQ4LjY0IDIzNy44MyA2NDguNjQgMjIzLjU0IDY2Ni4zMyAyMDUuNzggNjU1LjMyIDIwNS43OCIvPjwvc3ZnPg=="; + // inesco logo (dark background variant, PNG) embedded as base64 for CID attachment in emails + private static readonly byte[] LogoPngBytes = Convert.FromBase64String("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"); + private const string LogoCid = "inesco-logo"; + private const string LogoDataUri = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZAAAACPCAYAAADUSI02AAAABmJLR0QA/wD/AP+gvaeTAAAgAElEQVR4nO2deXxcVfn/389kKa0sraUtVAQEkV0WWRWQYgEF2kxaUr78BMStdQEVEeGLihVEKCK4gLLIV76sXxPbTBoWkbIJsoMgi6AIKEs3oLSFliaZ+fz+ODfJpE0y27kzk/S8X69pZuae+9wztzP3ueec5/k8xkDMnb4NNZmjgINBO2G8H/F+4B3gDcSLJPgzicQtTJn3+IB2AoFAIDAssXXeaZ++HZn0jxAzgJq8rIjbSWTOpGH+o577FwgEAoEqpa8DaZt2JMpcB4wuwlYX6Psk2+Z46VkgEAgEqppeB9KaTGLMBRIlWjyZhtQlJfYrEAgEAlWOcyDt03cgnX4Y2MiDzU4ydgjTWu/zYCsQCAQCVYobbaTT5+HHeQDUkdDPUT/rK4FAIBAYNhjzp+xMpuYp+ltQLwl9mmTbbX5tBgKBQKBaSJBJNODdeQDYVP82A4FAIFAtJMAOicn2p2KyGwgEAoEqIAFsG5PtbcI6SCAQCAxfEsDYmGzXkUpuEpPtQCAQCFSYBFAfm/X6ug1isx0IBAKBipIA3ozJtnh1TFy2A4FAIFBhEsDimGy/yawrOmOyHQgEAoEKk0D8JSbbIRM9EAgEhjEJEpnb4zGtmOwGAoFAoBpIsGbEzcA/PNt9G9kNnm0GAoFAoIpIMKMljfixX7M2h8bU235tBgKBQKCacGKKjalrMa7yZHMBHbU/9WQrEAgEAlVKb+2PjUefBKRKM6e/0FF3DDNa0qXZCQQCgUC101dqRBip5GkYPwA2LMBOJ9glLB53egjdDQQCgfWD/rWq5jWOJcHXQY3ARxm4SuELoFtI11zE9Hn/jquTgUAgEKg+cosd3nTkGNIjdoKuscjGIluB7A1k/2L6vFfL0MdAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQqF5CydlAIBCoFtqnjEJMYOHmr/bJqUs1fBBTmkWbLa2mXLuB8jsCgUAgEBfNTRvS3NQ3WTvVcCbpmnfJ1LzIxDe37ruD/RIlXmPCknWL9N3ctFmMPR2U2koduJJIqgXGAWOA90d/RwMjoyYbRM87gXeADLAcV71xEfC6mQWxyAhJBmwBfAiYQO/52wRYA7yHO38rgJeBl8zsvYp0tgxIqsOdj81x37PxuNH+Rrjf3EqgA3dO3gNWR+8tBV4xs6q5w4wLSROArYEPACNw52Yk7ruzAnde3sl6vgz3vVlZif56Y17jR0hwJXTuDzoZuLxnm+yNnjmhjMYD/8zac3z0t28BQGG0dT5OqjEDXEGy9ez4Or8uw9aBSBoB7ADsBOyC+7JuBWwJTARqSrS/AngGeCp63Ac8aWYqxW61EzmLHYEDgQOAjwHb4C4Chdh5BXgEd97+AjxqZhm/vY2f6GZkT+AgYD/cudkOqCvSZFrSq0SONvr7DPCAmb1Wan/LTXR+dgU+jjs/e+C+LyMH228Qe4tw5SdewF1gn8adm+osny0Mo/eaYFqCOxe1kJhMtgOBJVk7TljL0oR12wCtjTuT0OYgwDbqs2327ASzZ8f6m6oFkPQZ4Bse7f7ZzM7zaG9QJNUAO+O+oPsB+wIfIV4HuTGwf/ToZqmkO4FWYL6ZrY7x+GVF0j7A/wNm4O6sS+WD0WNa9PpVSc3AjWb2qAf7sRGNMD4NNAFTcKNXX9TgbnS2Aj651nH/g3O2DwD3A0+YWdUJl0Y3b4fhzs9U3EjUF5tFj4P6HlLPA3cANwN3V/y319w0jrrOr9PGUdz19n5MursLgMbU26QaHgXbD/QpmptqesRnTUuylqXHr2Uxem19RyCJzKE9+2itIn67P/lVUsnPg35JR/31cYjcdl9gt8T9IHwR+zBT0lbA4dHjU/j9khbLOOCY6LFc0lzgV2b2RGW7VRySNgBOBE7BOeQ42QL4NvBtSfcBc4Cbq2lEJ+n9wCzg67ipl3KzZfQ4Nnr9hqSbgDbgVjNbU4E+9SDpQ7jvygmU9/douNmGHXD/N+9IagNuBG4zs64y9sUxovN0xKkArBhzNPB/vb212xH7AWMYsWYPwN0wyRb3+o+sEUhz00jo7B5d9B2BYJOjJx2MWtVbnvyug2tZoW8jtgG7hFGr2nHTgF4ZUovoknaWNFvS07ih/eW4O9hqcB5rswnwBeBxSe2S9q10h/JF0gaSvgO8CPyG+J3H2hwAtAOPSto/V+O4kVQv6VTctMlPqIzz6I9NcQ6+FVgo6TJJHy53JyTtKun3uCmlk6n873FD4LPATcC/JJ0hyecoMTeJrotx61yAzkDZEa+ZBVkND+19mul1DtK4nuc16d7RiJsCczQ31dM9EhP3cfif3u3ZtnyTGc55ANjlHHWzd+cBQ8CBSJoo6fuSnsHNd/4QN101VDDgKOABSb+TtGmlOzQYkg7Fren8FD9TVaWwJ/AXSb8t+wUgQtLuwBPAhbhgi2plDG509LFyHVDS+ySdDzyOm9osaV0xJrYEzsMFeMRD+/QdaEvO6eMkptz0Gtj1AIjdaGs4rGfbmhEP4gIEIMPknvcb5q8EVrkXid4RSCKTvR7S60Bqu/anu+yGWd/pK+w70ZNOyPyyz6a2qZNINZzS16kVR1U6EEkm6UhJKeDfwDm4xfChjOHuFp+TdHSF+7IOkkZLug74E1D2u9hBMOCLuNHI7uU8sKSvAQ/iFsaHCi+W4yCSpuFGZKdT/cE4y4G/xWK5rXEa6fRjiO/S1jCzzzZLz8FFcAKJ03ven9HSAbrHteEAbjvsfVl7dTuI3lGHrPd5JsuBmHqdj6V7RzVtjZ/GBSuAcQ3Jtld6trUmR6PE1WAXkUrexl0Hl/R/V5UOBBiLG342UP1fzkIZC7RIujxajK04knbFRUR9ttJ9GYRtgfslHRf3gaIbmNnApRQYXVYF/CtO45Jqo1HHH3CL2UOB+2MLNuhKPAFEtu0i2qZu37OtYf7zwHz3QpOYP22/nm2y7gt+PatHHpBlMXIQWWsglvVcWYvovQ5kGWtG/LW3jbqdlZAu6tPfBL/GjcoAnupZ3C+SanUg6wMzgXZJRYUz+kLSf+Gieqpp1DEQI4FrJH095uNcjJsqHWosN7O34jIuaTPgHtyoYyipWNwXm+Xpc1/E7NTo1SiUuJ7LZ/beGIreaNRM5js9z2vSWVNOiayRRD8jkESfiCy3vTU5Gmyv6L0FPRFW86ftDRwcHbuVZNuzPXu2JqejngCMZ+ms+36+H3MgggOpLIcDf5RUSPlgb0j6KnAD8L5cbasIAy6RdFocxiV9C/hmHLbLQGyjj8h5LMDlMAw1/uzNUvuULZnX2DeopKH1SrA/RK8+xoTFvRfmxtTDGPd0v2Jeo5sOndr+DER5PaZDs6x1jzBGR4vkkO1MumoXR/tMont2RvROX2X03z3PZRf29vuoD2BcEb1aQ8b+HzNa+oY6z288ZOAP3j/BgVSeg4DfSyrr/0XkPC5laN1JZjNH0vE+DUraE7jAp80yE8v6h6QtgXsZWsEr3ayhO0y2VFINHyRdcycJ/ZlUQ9812Zqur4IWuRd2JvMae6MHM8yJniVI8O3enXSn+8NHe+RIetc4jJGrx0Xbux1IF00tUTSV9Y5aMjXOgbRN3R7UEL17N9NaHwBcQmFX7TU41Q0Q32Na65NrfbbzyOgOUo3fy/NsRB8oUA0cAZRNgiCaAvo1Q9d5gOv7lZIO9GFMUj1wLcVnkFcD3h2IXDjp3QyNKc7+eNibbI7ZN3FrcRPAbu8zEpnS/gYuSEZALTW6jrapLnejMXUrEK1R6HOkGj7ontI9jWV0dnTf/WfledR3r330ZqF3Z7WL7lHLv5g+N/p/T5xG9zXdrNtpwW5PnILRbf9eOut+3udzpRovAjsj6t9phWhrBQdSPZwp6ci4DyLpCOAXcR+nTIzABSSMy9kyN19i6Ef6eXUgkVNtJs4Q2Pi5t1QDkkZIOo41dacjrovenrjOSCTZdhvISZOIbVBN1gK2uqeT6nC5MlBfdzt0y5xE6yDZeR5ODwt6p7DctvYpW2Js596KFuPbj/oAIhqR29+Y2nobAPOn7Izpx9H+y0knju+TkZ5qOA90Ss92dDhHtizK89QEB1JFGPCbONdDJO2EW/Ooxnj9YpkAXFmKgUh640w/3akovkcgv6V7QXbo4mMB/VfAtWpqPpPOuhOznMgEsDv7OJGazKnA8+6FvkSqwYXsd9T/np41KvsKrcnR7kJt3YvcLk9EfaRKukcefYUUu2qy1kyipMR03bcAt2YinY8hbvnMCFRzPdgGUX++xvR5/+7ZNdVwXu/II3IeybaHCjgvwYFUGR8ESo6M6I/IMc2n8lnCcdBQYnjvVKonu7wUvC2iS/o84HWNqQKkcZphRSNpFvDl6OXZOZ3IlPZViM/ilLwB+w03N23m7vrt4mifjTB9zW3u1q/SB2ifvkOfEQiMZ/bsBC70H7pHINYzfZWmo/5Omps2AXX38SVGv90CQOeIcxG7Re/PJdl2Q4/lVOP5OZ3HbYe9z0V7DUxwINXHtyTFkQF+IW7+drgyR1Kx0WQneO1JZegCXsnZKg8kTQR+5sNWhfmbmS0vdmdJe7DudO/Zamo+fVAn0ph6DKx72mhTOjuvRhibbHJV1kL7N53GVSIrgio9mUyidwQixvPRJzelJxfOFkfqvodE2x9nRstb1Hd8nZ4bQ/sZk+7uItVwIOJb0Xuv0VHXm+SYajwfenJF+nce86ftzepRj2H8brBzFBxI9TGC7jlST0RqyzNzNvTDK8DtwHW4fIrLgBTwMD0JV7EwkSLCbyPByMk5G1Y///FYR+Qqqlu2JV+KXv+IZOivpP9E0nNzjkQ2WfYTXH4VwOGkkl9h0tXvYXZp9N546jpPZIN37sZFioHsULpq36T3dzKePhnpWsr8qbsj3Jqf2e3c8pkRYCdFLd5k5LtXuxGJXYubqs6Q4ARmtLj8oFzO466Da2lL/pBM5n5geyBJW+N/DXSehluWd6H8Cxcd8Q/cvOXrOMXK1bgiNqNww8exwO7AJGAfuuca4+Orks41s3dzNx2caH4/7oir53EXnVvM7JlB+rIpLuLsFNz59M13JV1iZisK2Gc/XBGjOOgCHgLuAp7D1fdYgdM7MpwM/Gjc3eNEnGTK9jhV2UKn1Lysf0iail9l7lz8HXeh/yvu97gUd47AnZuNo8dEXF2fHXH1RfIJnChlAf27DK4rdraamrGWGSdS1wnGcfQ6kUOY1PYsc6cfR036CWAjjAtpm3onNZ2X0lX33ei906jvuJLV73sIdBBoEsvGJKKqg+MhMx6zLE0sW0wmcWjPL9nSC1gz4gtYpFln/JzD//QuqeT1uHIAIH7O1FYXLpzLecyf8iFW1FyDyM6Mv5Y1tTcNdBLWNwfShasX0A7cYWYvF7DvPABJG+PC9f6b+KQcRuMEGH/vwdZJuGJacfAGcBpwTT7FoMzsDVwm+fW4qKcLcBcHX2wCfA636Jkvcaj9CqdiPMfM/lOUAWkTYC/gEzgntz+D1x0p2YFEuUg/ztmwdFbgRqZXm9nfizEQ5aYcBhyKG0G+v59mRS2gR4rGP8ij6dlqak4P6ESSc58l1fBtsCuBUWQSN7Bos/0Yv+RKjG8DH2LFmKORFmAcBGzEhMV7gy0BxoNNIHsEktESeuWGVpHQQ6gnOfBd0vYbWpPTcXV7IDvbPJfzSDWcQMYupUeckaWYfZmprW2DnQCLTtgs3H+oL1rMbEaxO0d3qks99ucdnAz31Wa20IdBSRvhEvHiWmica2YliS5KGoMTvOvvx1UqzwJHFuiE+xAl7t2GkyX3xT+B7fOtIyLpKpzsvi/SwHQzG/SHVyjRxX1HXFTU4cAh9FUQOMOyY/+LO8ZxuFyYuMgAlwA/8im5EhWU2x9389CEu5H4h5ltP+iOA9u7ERhw2qYfzrKWGT+hrvPqyIkALAYdQrLtWVKNLRAJqBrnkOi6nHTti0A9xpMk7KukdT8gsJOBsaBNwV7A0vdCohFpM2pqLyKdvgM3GrsN9DswV2dEXERt10Wka/+G+72vIWP7Mq31yUGdhyt8dSVGdwIiwG1Y5gs0zH891wdfXxzI82a2g0d7PUj6MVBQ9maerALGlpIEJelHwFn+utTDn4EGH3XhJe2Gi5QZVXKvejnQzPK6+5R0D32r25XKeWYWe0hwNDV5AG666XDgHDNrKdHmM8SXC7MUmGFmd8dkH4BIW24aMMbMLili/11x8v2Frg8P7ERqMktIJ54C2wzIYJlJqOZE0OeBDDWJvenKbI/sDqa1LhnwCADNTTXUd+xFosZIZ86ILvydpBPbkcj8T9YC+3doTP1sUOeRajgc7Hf0lm1YjfHfTE39sk8ZXnDZ7B998uNMa+3zuwoOxAOSrgXiUIn9hJkVFYYYKf2+jLtb8cnLwF4+a1BLOhn4Zc6G+XOhmeWllSXpKdzcui+2GKK1yw/Ep2ZUX14CJptZWaTmS0HSfFyZ4mL4nrXMmNOvE4EPgt2Ku+a+hGVmIJtJJnEh01r/UdTR7jq4lhVjjiGjbUjYu0jdkXP3ssnbh/D26AuxnsCSXufR3DSS+o7zo9GOW1ERjyA7bp2+XD6zjgmLj4XEd0E7I/ZyUWaOEIXlh5MAL1Nja7Ff7iYDMg3/zmMVkPTpPCIuwdX69kVD7iY9+BSSfGsoOo+IWTHZXQocPkScx/a4tcdiGTg6C17Bem7SXwVeJ9k2s2jnATDp7i4aWq+nMXUOqAP0HvA2dR3HD+g85k/bm/rOJ8C+gXMeXWBz6Kw7oE9fmps2JNVwChOW/Avsf0FOBy3BqdldWN8W0WPBzJZL+gVwvmfT+5Sw71e89aKXc8zsydzNCsPMJOkCXG1vH2wnaVszyyexzmcEVvlrb3sgWiubHoPpDHCsmf0zBttxMJPSoxUHjs5KpI8gXfsMHbWX9ZET8UFD6hLmNd5NLVvTUX9KH+chDmP08sdJNZxOJnMOvXpvL5GxE/pMS7VP2ZSumpOwzpPAxmYdYQ3QTKLmnOzDBgfij6uA2fi9IBUlYBdpQ3kRGczidfxOM63NTbhIom082duH/DKz13g6HsA4SZuUkrxWIT5NPKHMF5vZHTHY9U60puQrIKbXiYzorIlqcIylq2ZLGlsvzbl3sUxrfZr2KS9iNd1rcG+TSBwGnW+wYvRdsFZ4bkfd15jR4krrzp22FTXpb5O2L2F91iNXgK4mXXM+0+etM8sSHIgnzOwNSTcBPsvVbl3kfkfhX+/qx2a2yrPNHswsI6kNlyPig32AG/No5/MzGS5653KPNstBKdM2A7GYMipMe6CB/HJL8qU7xPd46ju7QPNpbEt5tN8/U9pXcctnPk3niP/DEj8kk94RagYOz0017Ap2GmSOBcvyB1qE7HI66y5mRsuAN0TBgfjlfvw6kLGSNjKzlQXuN9VjH8BdZOMM7+zmfvw5kMGSwLJZiN/Io3Ml3VZKeHM5iUJgD4/B9E8KTOisNIWsm+XLuWpqrjGz8krlHHHrCuAIUg2XgGVX72wnbV9iWusS5jUeQI1ORxxJ32m7FzAuYeMxlzPp6pwRoMGB+OXhGGyOAfJ2IFG+QMGVxXJws5m949lmfzzo0Va+EuT/zt2kIMYCD0g60cxu82w7DvagV6zPF8uA//FsMzYi2ZK4su/jSFTNj0ziehL6CtARhef+iraGI0klzwB9vE+grngc0y/oqL++3/WZ5qYa6jp3d3XYbTLoFpJtFwcH4pe/xWCz0Cih7fGb3Q3wf57tDcRCXBa3D9mVzSXV5aEP9ZSHY63NZrhSxXfhRAlvzSdTv0LslbtJwfxfmW44fPEJ4km2XQ58MQa7+TGt9QFak7PQiPtJvLc3bcmnWGe0rb8Ac2hsa19n/7nTtyGRnowxGTonA2N6f5pmQHAgPjGzlZLW0L8AW7EUmmAXxwXhntxNSsfM0pKWM7hkR77UAFvgchAGI45RYzeTosfLUXbzPOCxfLPky8SeMdhsjsFmnPhMJM3mXF/KF0XTmLqKVPIRsOzrQhfo95guoGF+701vc9M4RnR9iowOdU4jveUAVt/GnLR8cCD+WU62fk3pFBod49uBvBxD3sdgvI0fBwJuaiaXA3kIp+nlU05lbbbGaaf9N/CKpFtxUWd/9KigWyz5rhXlyzv4zekpB77PAcBbOBHTKsDmgvaiOxS3puYcpsz9J81NI0k1THbVEDUZOvdAJPoZ/3cBT7rqh5kFLJ5wD7Ou6ITgQOLgbfw6kEKnc3b0eGyAx3I38YrPXIqco7do1HMTTiCzHHwQl28wE1gSZT63AQu81e7OE0mG/+/LfVXgFAsljlHYZT7UtL3QUfsbRnSO4N2RlzFy9Raku6aRSk6GjgNctcJ+BsTGi8ACsAWsqf3TQJFYwYH4p9I/ni082/uwpHKGpfp0vvlO/11K+RxINuNxqsRfAlZLugOnFN1m1qe0aVxsCoz0bPNRz/ZiRdJYnFP3zfUx2CyOGS3LaUu+yajVzwBjs9Yxslu9DrYA6Xbq6xbkWxc9OJDhx0DzlsWyW/QYiuTlQMzsUUl/Jr658HwYicvHOAq4VNIfgStwNVbiKsQVx4WzKHn2CuL79wLwkllPrfMqwVaCsqPtVgH3gxYgW0Ay9fg6AooD0dxUz8jV42DEqOBAhhHR3ZRPbaehTiEX3m/h7p6rQR+ull5n8nok1vkbM/MdchxHHfgXYrAZJ3E40UdisFki6dsh4RwGLGDxhAe71zEAaG4aSduaMWQSm2OaiEsf2ByYCJb1nDHQOYF0bQIyzwQHMrzwHc8/1MlbpsTM/irpYugrFlcFTAROB06VNA+nR/a0J9txFETLa+qjiojDiZZ73TA3DfNfp63xKqQdkH2RCUvOIJWcAEwAxkHnCJSIZrXyXHY1jQ8OZHjhez57qFOoztWZuMSvj8fQl1KpBWYAR0tqwRWQerlEm3F8XwavZ1F9xFH73ZeD94s0C2yfIrKsVgNLQAvBliKWYraIjBYFBzK8CA6kLwU5EDPrkDQFVxdj53i6VDIJ4BhgqqSf4nINOoq05bOIF0Cm3JFkHqjL3aRgYtOMKwljSe8qh97D7HWkhZgtA14nw0ISLAN7nTQLqe1ahmzZYJUJgwMZXgQH0peCL6xm9pakg3F5Gvt675E/RuKqTTZIOq7IaS3f3xefysblwrfoKBTxvSsLaTsV2Uze/9ZSJt3tJVw+OJDhRRx3U0OZosT8ImXlQ3CJYJ/z2yXv7Eav9tbcAvf1qZgA/SYUVD1xOJDqrAuT0IWgKSwfvYxUciHIjTbQQmTL1hl91Ha9EgkzDkhwIMOLoTZ9EDdFl0WOpOtPlPQn4EJ660ZXIxsCLZJmmdmVBeznO2ep3rO9chCHZleVflc0LlogH0NfXSv3VLh/EkCmBjpqIJVcCSwELQVbCloELMFsKRkWBgcyvFhd6Q5UEWmcrExJmNkNUab6D4BvUL0XSQMuk7TSzPIVv/T9famVVBNj3kocvBGDze1jsOmDdsQLwATMJjiHwjgGH0hs5B72EfcycjpO8vTN4ECGF9W5eFcZ3vSlgBvVtThN0m+BH+HqzVfjdGEC+K2kp8zsmTzax3HDMYZ4LspxEYfO20disFkabVMngO2C2QISXQuY0v6fnm3NTSOpyWyOZSaSyIxB5nI+TGPANgd154J054NEWIjCGmZUh/ZOdfCWb4Nm9jzwX5I2w62NnEw8eQSl8D7gakn75uFA43AgExhaDiQOyZh9YrBZIjWTkY4FHUu6BtqSvVpXNasWcNTNL+JKSg9O+5RRdNSNo47NyaQTwYEMLxbhr55GN8uB33u0Vy5y/xiKxMwWAXMk/Rw3GpmGK0i0YVzHLJC9cPW9/zdHuzjuvicC+Yx+qoU4pFd2lbSlmf0nd9MykdF2GBm6lRbENsBM0Ey66rpIJR/BcFpYa2epZzOlfRWuCNu/ISyiDyvM7D1Ji/C7iDcKOCXOeuhDFTNbg6u7fqOkDXCVIBuAKVR+IfUMSdfkqD3ySgzH/Qhwewx2YyEK216I//+v44FzPdssjls+M4I1vEBH3TjqO/YEpmB2VOREwPmB/RH7g/2ACUtWkUrmpZNVDbo/Ab/41kuqA/b2bHPYYWbvmdktZjYLp4i8P3AB8I8KdWkHYHKONnHcIe8Qg824iSNz/MuSqmOdrKP+eIxrqe94BtkBdNSfRUNqW9I12yJmAS30nfIdBUwGOx/jUdqSi0glm2lNziTV0Ec7LDiQ4cfLMdj8RAw2hy1mljGzB83sdDPbHtgWJ9b4F8qbK9GUY/si/Ce9VeH8f07iqEq5FZUpEdAP9rXo72YYP6S+80VSDT8inVhJY+oKkqkZdNSNJ5HYB+NMsLvomxQ6HmjCuBzsP6SST9GWnAPBgQxHnozB5idjsLneYGYvmtkvzOwAnHz4N4G/luHQR+boVwb/a0V7ShpqitB3xGT3HElxaG3lT6rhCLpqjkH8iN5RxvvBzqK+8xVSyWton74dM1rSTJ33CA2p80i2HkJH3RjQoWBzcOKQ2QEZuxCpNAQHMvx4MAabh0jyWehpvcXMXjWzX5rZnsAewJXEJwEyUVKuehe+C0DVAod6thk39xNPCPwE4KIY7OZHqmEnsD9Qm76XROIROuq2wvgWvWtfI4DjSaefoy3ZTtvU3nLYM1pWk2xbQLL1DJKpveio2wyzGRhX4KbJF0BwIMORRymsDkY+1AKf9WxzvcfMnjCzmcA2QHNMh9kjx/Y4pMePjsFmbETBEHfHZP5ESV+IyXYO7Ayc3tkElGmnvuNc1tRdweLx24I+B3QXvUogjkKJR0g13EeqYco6pma0LKWhtYWG1CySqa1ZU3eB2zEwrDCzd4CnYjD9TUkhai8GzOx1MzsGJ47omw/l2B6HA2mMipsNJW6M0fZlkj4To/2+NDfV05b8LdSdH01dpXF6Jd+gvvMxJr6xK8m2a2hI7QKaihuBRdgnwObTmpOBZfwAABE4SURBVHyMVMMJNDf1rxU2o6UDggMZrtwcg82tgJkx2A308mPgNs82c1XcewT/CaijgC97thk3rcDKmGzXAX+QdERM9ntpbqqnvqsF8UXovBNTMxk7EPhX1GJH0pkHaU3OpqUpQbKtnWTqE2TsQIyb6A7yMPYE+1/qO5+jLflN7jpxg/4OFxzI8GReTHZnSxoXk+31nihn4+eezW6U45jvEU/exneH0ijEzN4F/hDjIUYBKUnfiOsA0YJ9GqlbQXcC2J0kMsupX7NntH4BUBdFY91La3JbAKa13kdDagpoN+BaehWFP4z4OcuXvURrcjbNTZtkHzM4kGGImT0OvBSD6XE4rSWfme6BvvgOKc2naFS752OC0036cQx24+TXMduvA34hKSVpC19GJZmkLwEvq6n5dDrrTkRcF212TqSjfgsaUrMQR9OrQLA/xuO0JntnFpJtT5FMnUAi/RHQpfTI3fSEAF+dfezgQIYv+SqyFspUYHZMtgNOOsaLCGREPgEVN+fZrlBmSUrGYDcWzOxR/E8h9kcD8HdJZ5cS5hs5jsNxNx1XAhsD5w7oRFINO9GYmotldgZ1T3NvjHE5qcYW5jX2jhintr9Esu0katJb9gkBNuvjZIMDGb78Bv/1Hro5S9IZMdle39kKv7/LnPP6ZrYYV4HRN4YTdtwrZ8vq4SdlOs6GuBIBr0j6naTDJOVV4EvStpK+gwuW+SNO+yybwZ1Iw/zFNLRNibLQo/BlHU0i8zSphr7rNFPa36AxNZuOuq1AX6Chtc90p0UdmgVcVsinz0GLmc0odmdJm1JCMaB+eN7MyiKxIOlp/NbTPtDM7iuyLzcAx3rsy9pchtPJqkghqyg35VjgZTNrK9HWN4DtcMKR9/uSgi+iH2fhJON9cZ6ZnZnHcQ/HXYzi4E1gqpndn7NlFSDpDpyuWblZBTwHPI8TeXyT3jWs0cDHosf787T3PWuZMYe6zqsxjoveWww6hGSbC+Gd17gjpuvcojkAwriSRPqUSDhxUMIIZHhzccz2vwI8LOmAmI/Tg6QNJDVJagdexS06N3gw/UngJOBe4N+SfiaprLIckvYEvuPZbL5rYbcDL3g+djdjgbsknSQp1muOpDpJ0yTdWkL+xTeIb/Q+GKOAPXE3RWcDlwLnR48zcAma+ToPcCORM/sdicxr3BGAaa1/Z/Tb+/YJ9xUzSdc8QtvUXDlEwYEMZ8zsEaDQOtmFsivwZ0nzJR0UxwK7pJ0knSwphavf0AwcRW9Rp495OMxOWc+3AL4NPCTp5WiK4XOStvJwnH6RNB03/z5o1FQR5CXmGI24LvB87GzqgV8B90k60KfhaC3gY5LOxwlEzsXJ6xclAhoV4/qlxy5WkrP7dSKJTK8TmXR3F42p2WuF++6EEg9FkVcD1o0PU1ieqaYprKg/H8JlnPYbxx0DL+LCIe8CHjSzt/PdMVIv3QbYEVcW9KPAJHJLbXcBG5tZUQWSornnd8ivvMHLwD04YcTngOfMrKjvqqSRwBG4kc/BxdjIQQYYZ2Z5FdeKEkWfojyKug/iwkXnRfVVCkLSRJzI52dw53BCP80eNrN9i+mcpI1wdU1y5dEMFfqZztIiMolDmNbaWxPlls9sTMcGvwKd0POe2XE0tF7fn9HgQDxTbQ4EQNI5wPc99adQ/oPT3nkbWBb9XYO70x4d/d0Qp/i5DcWXit3PzB4qZkdJH6U0EcpluDv954DXgBW4aKqVOMf0Lu63Nho3nbMdsBvuAhinY3/UzAq6C4+iplpj6s9A/AOX0PgCzkGviB7gwoFHA5vgLua74m4s8skxeQ93Y1HUdFQ0UrqT4VM36SxrmfGTnE4EoDU53anv8ixP7H4ws2f3uyYYHIhnqtSBjMSF+u3ip0tVydfM7DfF7CjpWOAGz/2pBs41s4JvHCT9iaEniDgQu5tZ0TcHkr7H0MtnGYz8nUj7Ua5c85SbXhvIWFgDWQ+IpnaOIR7F0WqhlHUQnw6/WhBwTZH7fp4YaspXiFLXx84D/uSjI1XC2euG+NpmfdZEuply02uDOQ8IDmS9wcyexdWhGK7smbvJgAxHB3KXmRVVDdHMXmP4fFdKciBRcEET5anfUi76ic4awInkIDiQ9Qgz+y3wi0r3IyZ2yTcRqx+GowMpKZfEzK4D/sdTXypJKTcWAJjZClxU1z9L707V0BudZd1KxLYZCW6nbWp/AQn9EhzI+scpDI8Lw9rU4RZYC0LSBrjF++HEPDP7swc7XyEeocVysruP2uRmtgRX4bHgiLEqxk1nrak7vmckIv2BqfOX5GsgOJD1jEjxdRYwv9J9iYFipit2AAaMcx+CvIW7SSiZKHqpCXjah70KsQGewpLN7J/Ax8kzt2aIkD2ddQLJ1ClYJOmeB8GBrIeYWRcwHbi80n3xTDHTFcNp+krA8Wb2H18GzWw5MJmhvQbgI9EUADN7CTgIeNyXzSrgLDU170Jj6tpCnAcEB7LeYmZdZvYVnERCQV+aKqaYC8VwCW0WcLKZ3eLbcCS2eBBwh2/bZaLkdZBsovNxMPHV3SknS4DJxYY6BweynmNmc3DaO3lnjFcxu0iqL3CfnXI3qXoEnGpml8Z1gKhU8hSGZr6MtxFIN2a20sym43Sz1vi2XyYeAj5mZvcUayA4kABm9ntcZnTRX6QqYQSFjyiG+gjkXaDJzOIWzsTMVpvZZ4HP4TLshwq7RzIt3jGzX+EUBZ6Nw35MvAecCRxgZq+WYig4kAAA0bz5p4BTGdqjkbzvNiWNAraOryux8xhOwiVuwcw+mNk1OKHCe8t53BIYhdNWiwUzewzYHTgd//XlfXMfLjv/vGgttCSCAwn0YGZpM7sI+DBOObUSktal8Azw7wLab0xpGliVYiXwXZzzqEiElJk9Z2YH4SpUvliJPhSI92msbMys08wuwAmBXk88FR5L4VngaOAgM3vel9HgQALrYGZvmtk3cOsDl5BHVbsKshK4DjjYzHYxs7xlJ8xskZntibubvhx4I6Y++uI9XI2Xbc3spz7uIEvFzNpxkWwnU53hrStx5+zOchzMzF4xs+NwocNXAR3lOO4gPAucAOxqZnOjMH6/SJolvzSX2J9NPffnOV/nKo++P+2572Ur1jTIZ9pY0jclPeH5sxXLG5JulCssNdLj56yVNFnSZZJerODnW5sXJJ0mJzJatUhKSDpC0s2S1lTwfGUk3Sfpy5I2qfA52ULS9yU9X8bPv0bSDZI+Gffn61bjPQD4rEe7j0WyGUUhaUPgZx77s9jMzvJob0Ak/RCY6NHkT80srkpxBSNXXySJqwK4H27hOm7ewNWPeAAXSvqomcU+RSBpW9y60P7APri7ynKM2oW7c2zHhYo+GsudY4xIGo2r05HEKfuOjvmQb+Lm928Fbi51cTgOJO2HS8ycjFNN8Fl8bTmuIFk7cKuZvenR9oB4rx4XWH+QC5ndDXdx3Qu3drIVzoEWmt29Blei9hVcTYjncBfRZ8ysKubY5YoM7Yyb5/4Irq7HB3B1KjajuIz21TiNpedxn/lh4IFyXQDKgVyVyo/gpgr3xp3DDwJbUng9lDdxdUP+iZsyewG3jvX3oeRkJU3A1V3fA+dMdsT9dvJhJW7d6XFcIMVjuJv2sq9ZBgcS8I6c9tDmwDhcKdP34YpG1eHm8Vfj7rKXdT8KqVxYjUiqwd1lZz8MN0IbFTVbgVtcXYWrd7Mwyq9Yb5G0Gc751uGKRtXhvisrcd+VldFjNfC2mVXzelxJyEUFjgU2xRVYG0Xv+mMaV875leF8DgKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgEKg+TNLpnm0+bma3e7ZZMSRNBI7PemsRcE0xtQckTcXp/mfTZmZlq5gYN5IacIWXfPGumV3i0V7FkfRVXD32UliNq53yuJkVUgd+yCKpFtgXVzdjHIXXEgEnpb8QeAn4q5ll/PWwOogKVw1UjXAlcFm+nzuq5fIFnMT82rxKDOUUf1Xcx65OJO3bz2dsjyquFWrrxn5szYij35VC0u89f58WVvoz+UbSvz2en4ykX8vVIxm2SNpT0ksez5sk/V3S2jd0Qx5J9XLXqIH4nZxjyMfW+QPYeEXSh8tRnnM4chTwiKRdK92RwHqPAV8FTqxwP2JDzjm2AFt7Nr0DcINnmxXHzDqA6cBNAzQ5EfhFLjuSfgz0N0O1CDjMzF4IDqR4Pgw8JOnESnckEMDVHh+u7AlsE5Pt3SV9KCbbFSNyIkcDNw/Q5OTIQfSLpB8A3+tn0yLgEDP7O0DtAPu/gKtLXQzPF7nfUGQk8DtJ+wMnR/9pgXVZCTxU5L5v+exIFfMs8Ewe7eqB/XElT7PZ3HuPqoeBnMdLuNLI+WJAf85i68jWsMLM1khqAuYDk/tp8j1Jq8zsJ9lvSvoWcHY/7ReT5Ty6G/fHGR4/x5BG/a+B9Me9kgb9EWv9XQN5rNL9qibU/xrI7AL2P6af/f8aY5criqQTB/jNva9AO6MGsHNUXH2vBiSNlLRggM8uSd/JanvSAG0WSdppbdsDjUAChXMA8KSk/zKzOyvdmcCwZnWlO1Bm8lrwzQMBf8WN9p7O+vuyJ/tViZmtlosAbQcO6afJBZJWAp30vzbSPfJ4du0NAzmQD0vqb8iTzV/N7M0cbdY3xgG3Sfq+mc2pdGeqiI3y+D691mdoHBgMXxfU9QozW41bT1nvMLNVkRO5mXVDfA34dfR87XXxhTjn0W+qwUAO5IvRYzAOB/6Uo81w5VHgDvqPUKgFzpe0G/BlM3u3rD2rTrYDcuUGXQnMLENfqpVt8nCy4HIfjou7M0MVSQcDVxW425fXh1kDM3tX0hG46KxJa23uL6BqMTB5sDy1MIVVHF1mdoakJ4HfAqP6aXMssIekaeHOOpAHx9M3YbVQVvjqyBBnFIVHbBW0ljKUiUYiR+FGIgcP0nQJ8Kn+pq2yCWG8JWBmNwIfB14coMkOuFDf6eXrVWA95eFKdyAwNDCzVbhctnsGaLIEN22VMypwoBHIW8DbOfZdlcv4+oCZPSlpb+BG4LB+mmwEtEi6ABjW2cKDsAZ4LUebN8rRkWHKq8DFle5EYOgQTWfNwE1Trc3383EeMLAD+amZnV9079YzzOytaG7xXOC7rLvIabj1knS5+1YlPGNmH6t0J6qcZdGjm60Y+IbjJWA5znE8ClxqZsEBO54CZg2wrR4YVlJLJTLQIKArXwNhDcQTZpYGzpB0P3ANsEk/zdbXEUggN780s9ndL+REKW/EJauuzbPAcWaWa5ZgvcPMXgGu6G+bpFEEB+KVsAbiGTObj1MMDQvngaIxszZczH5/ofJHAg9L2rm8vQoE+jLQCKRRxevDvGxm5xXboeGAmT0vJ6l8NdBY4e5UA1tKuryE/c9cH3OOzOxBSZ8E/ghssdbm7YAHJX3ezP5Q/t6VlTUDvN8uqZBp4YFmAN4rsD+BbgZJby+WYRUNov6lTB7Ic1+TdLqk9CDna32QMimVrSv9uXyiAqVMJG0lJz3eHxk5ye1hOz0qaVIM36lsdqn0Z6wEkjYc4Hx8Pl8bYQorRsxMUUb6VHJHtQUC/RIVjPoE8Jf+NuMCNG6SNKasHSsfjxBfnsvrwLAp6FZuggMpA2Z2M7AP+amtBgLrYGZv4cLEB5Ln/jTDtEaNmb0DnIr/KMZOnIp23lFHgb7UAr6VUoebN3+Xdc9RwZ/RzP4pJ/t+KZCtarlsgF2GKi/i/zs13GTy72NdOfaBklF7iLKIG4GfAgMtoJ8j6VQz+1eJfawqzOy3cqrOxwAfwOnOFTNt1wUsxQko3jCcykkXQYb+f6t5h4T/f7rCUsONcr+VAAAAAElFTkSuQmCC"; // NOTE: old PNG constant removed — using SVG dark-bg variant (data:image/png;base64,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"; @@ -23,7 +25,7 @@ public static class ReportEmailService var strings = GetStrings(language); var installSegment = !string.IsNullOrWhiteSpace(report.InstallationName) ? $" — {report.InstallationName}" : ""; var subject = $"{strings.Title}{installSegment} ({report.PeriodStart} to {report.PeriodEnd})"; - var html = BuildHtmlEmail(report, strings, customerName); + var html = BuildHtmlEmail(report, strings, customerName, $"cid:{LogoCid}"); var config = await ReadMailerConfig(); @@ -35,9 +37,10 @@ public static class ReportEmailService From = { from }, To = { to }, Subject = subject, - Body = new TextPart("html") { Text = html } }; + msg.Body = BuildMultipartBodyWithLogo(html); + Console.WriteLine($"[ReportEmailService] SMTP: {config.SmtpUsername}@{config.SmtpServerUrl}:{config.SmtpPort}"); using var smtp = new SmtpClient(); @@ -220,11 +223,12 @@ public static class ReportEmailService // ── HTML email template ───────────────────────────────────────────── - public static string BuildHtmlEmail(WeeklyReportResponse r, string language = "en", string customerName = null) - => BuildHtmlEmail(r, GetStrings(language), customerName); + public static string BuildHtmlEmail(WeeklyReportResponse r, string language = "en", string customerName = null, string logoSrc = null, string source = "email") + => BuildHtmlEmail(r, GetStrings(language), customerName, logoSrc, source); - private static string BuildHtmlEmail(WeeklyReportResponse r, EmailStrings s, string customerName = null) + private static string BuildHtmlEmail(WeeklyReportResponse r, EmailStrings s, string customerName = null, string logoSrc = null, string source = "email") { + logoSrc ??= LogoDataUri; var cur = r.CurrentWeek; var prev = r.PreviousWeek; @@ -328,19 +332,30 @@ public static class ReportEmailService {s.Change}" : $@"{s.ThisWeek}"; + var footerHtml = source == "web" ? "" : $@" + + + + {s.Footer} + + + "; + + var bgColor = source == "web" ? "transparent" : "#f4f4f4"; + return $@" - - + +
@@ -480,7 +489,7 @@ public static class ReportEmailService report.TotalPvProduction, report.TotalConsumption, report.TotalGridImport, report.TotalGridExport, report.TotalBatteryCharged, report.TotalBatteryDischarged, report.TotalEnergySaved, report.TotalSavingsCHF, report.SelfSufficiencyPercent, report.BatteryEfficiencyPercent, report.AiInsight, - $"{report.WeekCount} {s.CountLabel}", s, customerName); + $"{report.WeekCount} {s.CountLabel}", s, customerName, $"cid:{LogoCid}"); await SendHtmlEmailAsync(subject, html, recipientEmail); } @@ -499,7 +508,7 @@ public static class ReportEmailService report.TotalPvProduction, report.TotalConsumption, report.TotalGridImport, report.TotalGridExport, report.TotalBatteryCharged, report.TotalBatteryDischarged, report.TotalEnergySaved, report.TotalSavingsCHF, report.SelfSufficiencyPercent, report.BatteryEfficiencyPercent, report.AiInsight, - $"{report.MonthCount} {s.CountLabel}", s, customerName); + $"{report.MonthCount} {s.CountLabel}", s, customerName, $"cid:{LogoCid}"); await SendHtmlEmailAsync(subject, html, recipientEmail); } @@ -515,9 +524,10 @@ public static class ReportEmailService From = { from }, To = { to }, Subject = subject, - Body = new TextPart("html") { Text = html } }; + msg.Body = BuildMultipartBodyWithLogo(html); + using var smtp = new SmtpClient(); await smtp.ConnectAsync(config.SmtpServerUrl, config.SmtpPort, SecureSocketOptions.StartTls); await smtp.AuthenticateAsync(config.SmtpUsername, config.SmtpPassword); @@ -527,6 +537,25 @@ public static class ReportEmailService Console.WriteLine($"[ReportEmailService] Report sent to {recipientEmail}"); } + private static MimeEntity BuildMultipartBodyWithLogo(string html) + { + var htmlPart = new TextPart("html") { Text = html }; + + var logoPart = new MimePart("image", "png") + { + Content = new MimeContent(new MemoryStream(LogoPngBytes)), + ContentId = LogoCid, + ContentDisposition = new ContentDisposition(ContentDisposition.Inline), + ContentTransferEncoding = ContentEncoding.Base64, + }; + + var related = new MultipartRelated(); + related.Add(htmlPart); + related.Add(logoPart); + + return related; + } + // ── Aggregated report translation strings ───────────────────────────── public record AggregatedEmailStrings( @@ -604,8 +633,9 @@ public static class ReportEmailService // ── Daily Report HTML ──────────────────────────────────────────── public static string BuildDailyHtmlEmail( - DailyEnergyRecord record, string installationName, string language = "en") + DailyEnergyRecord record, string installationName, string language = "en", string logoSrc = null) { + logoSrc ??= LogoDataUri; var s = GetAggregatedStrings(language, "monthly"); // reuse monthly strings for metric labels var dailyTitle = language switch @@ -637,7 +667,7 @@ public static class ReportEmailService + + "; + + var bgColor = source == "web" ? "transparent" : "#f4f4f4"; + return $@" - -
- +
- - - - + {footerHtml}
- +
{s.Title}
{r.InstallationName}
{r.PeriodStart} — {r.PeriodEnd}
@@ -404,13 +419,7 @@ public static class ReportEmailService
- {s.Footer} - -
- +
{dailyTitle}
{installationName}
{record.Date}
@@ -697,8 +727,9 @@ public static class ReportEmailService double pvProduction, double consumption, double gridImport, double gridExport, double batteryCharged, double batteryDischarged, double energySaved, double savingsCHF, double selfSufficiency, double batteryEfficiency, string aiInsight, - string countLabel, AggregatedEmailStrings s, string customerName = null) + string countLabel, AggregatedEmailStrings s, string customerName = null, string logoSrc = null, string source = "email") { + logoSrc ??= LogoDataUri; var insightLines = aiInsight .Split('\n', StringSplitOptions.RemoveEmptyEntries) .Select(l => System.Text.RegularExpressions.Regex.Replace(l.Trim(), @"^[\d]+[.)]\s*|^[-*]\s*", "").Replace("**", "")) @@ -711,19 +742,30 @@ public static class ReportEmailService "" : $"

{FormatInsightLine(aiInsight)}

"; + var footerHtml = source == "web" ? "" : $@" + +
+ {s.Footer} + +
+ +
diff --git a/csharp/App/Backend/Services/WeatherService.cs b/csharp/App/Backend/Services/WeatherService.cs index d90e7c3fb..1c393ff7b 100644 --- a/csharp/App/Backend/Services/WeatherService.cs +++ b/csharp/App/Backend/Services/WeatherService.cs @@ -40,6 +40,33 @@ public static class WeatherService } } + /// + /// Returns historical weather for a date range, or null on any failure. + /// Uses Open-Meteo's archive API for past weather data. + /// + public static async Task?> GetHistoricalAsync( + string? city, string? country, string? region, + DateOnly startDate, DateOnly endDate) + { + if (string.IsNullOrWhiteSpace(city)) + return null; + + try + { + var coords = await GeocodeAsync(city, region); + if (coords == null) + return null; + + var (lat, lon) = coords.Value; + return await FetchHistoricalAsync(lat, lon, startDate, endDate); + } + catch (Exception ex) + { + Console.Error.WriteLine($"[WeatherService] Error fetching historical weather for '{city}': {ex.Message}"); + return null; + } + } + /// /// Formats a forecast list into a compact text block for AI prompt injection. /// @@ -52,7 +79,22 @@ public static class WeatherService return $"- {dayName}: {d.TempMin:F0}–{d.TempMax:F0}°C, {d.Description}, {d.SunshineHours:F1}h sunshine, {d.PrecipitationMm:F0}mm rain"; }); - return "WEATHER FORECAST (next 7 days):\n" + string.Join("\n", lines); + return "WEATHER FORECAST (coming 7 days):\n" + string.Join("\n", lines); + } + + /// + /// Formats historical weather into a compact text block for AI prompt injection. + /// + public static string FormatHistoricalForPrompt(List historical) + { + var lines = historical.Select(d => + { + var date = DateTime.Parse(d.Date); + var dayName = date.ToString("ddd dd MMM"); + return $"- {dayName}: {d.TempMin:F0}–{d.TempMax:F0}°C, {d.Description}, {d.SunshineHours:F1}h sunshine, {d.PrecipitationMm:F0}mm rain"; + }); + + return "ACTUAL WEATHER (during reporting week):\n" + string.Join("\n", lines); } /// @@ -145,6 +187,44 @@ public static class WeatherService return forecast; } + private static async Task?> FetchHistoricalAsync(double lat, double lon, DateOnly startDate, DateOnly endDate) + { + var url = $"https://archive-api.open-meteo.com/v1/archive" + + $"?latitude={lat}&longitude={lon}" + + $"&start_date={startDate:yyyy-MM-dd}&end_date={endDate:yyyy-MM-dd}" + + "&daily=temperature_2m_max,temperature_2m_min,sunshine_duration,precipitation_sum,weathercode" + + "&timezone=Europe/Zurich"; + + var json = await url.GetStringAsync(); + var data = JsonConvert.DeserializeObject(json); + + if (data?.daily == null) + return null; + + var dates = data.daily.time; + var tempMax = data.daily.temperature_2m_max; + var tempMin = data.daily.temperature_2m_min; + var sun = data.daily.sunshine_duration; + var precip = data.daily.precipitation_sum; + var codes = data.daily.weathercode; + + var historical = new List(); + for (int i = 0; i < dates.Count; i++) + { + historical.Add(new DailyWeather( + Date: (string)dates[i], + TempMin: (double)tempMin[i], + TempMax: (double)tempMax[i], + SunshineHours: Math.Round((double)sun[i] / 3600.0, 1), + PrecipitationMm: (double)precip[i], + Description: WeatherCodeToDescription((int)codes[i]) + )); + } + + Console.WriteLine($"[WeatherService] Fetched {historical.Count}-day historical weather ({startDate:yyyy-MM-dd} to {endDate:yyyy-MM-dd})."); + return historical; + } + private static string WeatherCodeToDescription(int code) => code switch { 0 => "Clear sky", diff --git a/csharp/App/Backend/Services/WeeklyReportService.cs b/csharp/App/Backend/Services/WeeklyReportService.cs index 67c1b07b3..9d141ddac 100644 --- a/csharp/App/Backend/Services/WeeklyReportService.cs +++ b/csharp/App/Backend/Services/WeeklyReportService.cs @@ -274,7 +274,8 @@ public static class WeeklyReportService var aiInsight = await GetAiInsightAsync( currentWeekDays, currentSummary, previousSummary, selfSufficiency, totalEnergySaved, totalSavingsCHF, - behavior, installationName, language, location, country, region); + behavior, installationName, language, + weekStart, weekEnd, location, country, region); // Compute data availability — which days of the week are missing var availableDates = currentWeekDays.Select(d => d.Date).ToHashSet(); @@ -356,6 +357,8 @@ public static class WeeklyReportService BehavioralPattern behavior, string installationName, string language = "en", + DateOnly? periodStart = null, + DateOnly? periodEnd = null, string? location = null, string? country = null, string? region = null) @@ -367,7 +370,23 @@ public static class WeeklyReportService return "AI insight unavailable (API key not configured)."; } - // Fetch weather forecast for the installation's location + // Date labels for prompt clarity + var today = DateOnly.FromDateTime(DateTime.UtcNow); + var periodLabel = periodStart.HasValue && periodEnd.HasValue + ? $"{periodStart.Value:MMM dd}–{periodEnd.Value:MMM dd}" + : "the reporting week"; + + // Fetch historical weather for the report week (actual conditions) + List? historical = null; + var historicalBlock = ""; + if (periodStart.HasValue && periodEnd.HasValue) + { + historical = await WeatherService.GetHistoricalAsync(location, country, region, periodStart.Value, periodEnd.Value); + historicalBlock = historical != null ? "\n" + WeatherService.FormatHistoricalForPrompt(historical) + "\n" : ""; + Console.WriteLine($"[WeeklyReportService] Historical weather: {(historical != null ? $"{historical.Count} days fetched" : "SKIPPED (no location or API error)")}"); + } + + // Fetch weather forecast for the coming week var forecast = await WeatherService.GetForecastAsync(location, country, region); var weatherBlock = forecast != null ? "\n" + WeatherService.FormatForPrompt(forecast) + "\n" : ""; Console.WriteLine($"[WeeklyReportService] Weather forecast: {(forecast != null ? $"{forecast.Count} days fetched" : "SKIPPED (no location or API error)")}"); @@ -399,22 +418,29 @@ public static class WeeklyReportService var battDepleteLine = hasBattery ? (behavior.AvgBatteryDepletedHour >= 0 ? $"Battery typically depletes below 20% during {FormatHourSlot(behavior.AvgBatteryDepletedHour)}." - : "Battery stayed above 20% SoC every night this week.") + : $"Battery stayed above 20% SoC every night during {periodLabel}.") : ""; var weekdayWeekendLine = behavior.WeekendAvgDailyLoad > 0 ? $"Weekday avg load: {behavior.WeekdayAvgDailyLoad} kWh/day. Weekend avg: {behavior.WeekendAvgDailyLoad} kWh/day." : $"Weekday avg load: {behavior.WeekdayAvgDailyLoad} kWh/day."; + // Look up actual weather for best/worst solar days (if historical data available) + var bestDayWeather = historical?.FirstOrDefault(w => w.Date == bestDay.Date); + var worstDayWeather = historical?.FirstOrDefault(w => w.Date == worstDay.Date); + + var bestDayWeatherNote = bestDayWeather != null ? $" (actual weather: {bestDayWeather.Description}, {bestDayWeather.SunshineHours:F1}h sunshine)" : ""; + var worstDayWeatherNote = worstDayWeather != null ? $" (actual weather: {worstDayWeather.Description}, {worstDayWeather.SunshineHours:F1}h sunshine)" : ""; + // Build conditional fact lines var pvDailyFact = hasPv - ? $"- PV: total {current.TotalPvProduction:F1} kWh this week. Best day: {bestDayName} ({bestDay.PvProduction:F1} kWh), worst: {worstDayName} ({worstDay.PvProduction:F1} kWh). Solar covered {selfSufficiency}% of consumption." + ? $"- PV: total {current.TotalPvProduction:F1} kWh for {periodLabel}. Best day: {bestDayName} ({bestDay.PvProduction:F1} kWh){bestDayWeatherNote}, worst: {worstDayName} ({worstDay.PvProduction:F1} kWh){worstDayWeatherNote}. Solar covered {selfSufficiency}% of consumption." : ""; var battDailyFact = hasBattery ? $"- Battery: {current.TotalBatteryCharged:F1} kWh charged, {current.TotalBatteryDischarged:F1} kWh discharged. Most active day: {topBattDayName} ({topBattDay.BatteryCharged:F1} kWh charged)." : ""; var gridDailyFact = hasGrid - ? $"- Grid import: {current.TotalGridImport:F1} kWh total this week." + ? $"- Grid import: {current.TotalGridImport:F1} kWh total for {periodLabel}." : ""; // Behavioral section — only include when hourly data exists @@ -432,7 +458,7 @@ public static class WeeklyReportService var battBehaviorLine = !string.IsNullOrEmpty(battDepleteLine) ? $"- {battDepleteLine}" : ""; behavioralSection = $@" -BEHAVIORAL PATTERN (from hourly data this week): +BEHAVIORAL PATTERN (from hourly data for {periodLabel}): - Peak household load: {FormatHourSlot(behavior.PeakLoadHour)}, avg {behavior.AvgPeakLoadKwh} kWh during that hour - {weekdayWeekendLine}{pvBehaviorLines} {gridBehaviorLine} @@ -440,12 +466,15 @@ BEHAVIORAL PATTERN (from hourly data this week): } // Build conditional instructions - var instruction1 = $"1. Energy savings: Write 1–2 sentences. Say that this week, thanks to sodistore home, the customer avoided buying {totalEnergySaved} kWh from the grid, saving {totalSavingsCHF} CHF (at {ElectricityPriceCHF} CHF/kWh). Use these exact numbers — do not recalculate or change them."; + var instruction1 = $"1. Energy savings: Write 1–2 sentences. Say that during {periodLabel}, thanks to sodistore home, the customer avoided buying {totalEnergySaved} kWh from the grid, saving {totalSavingsCHF} CHF (at {ElectricityPriceCHF} CHF/kWh). Use these exact numbers — do not recalculate or change them."; + var hasHistorical = historical != null && historical.Count > 0; var instruction2 = hasPv - ? $"2. Solar performance: Comment on the best and worst solar day this week and the likely weather reason." + ? hasHistorical + ? $"2. Solar performance: Comment on the best and worst solar day during {periodLabel}. Use the ACTUAL WEATHER data provided above to explain why — do NOT guess the weather. Reference the real conditions (sunshine hours, weather description) from the historical weather data." + : $"2. Solar performance: Comment on the best and worst solar day during {periodLabel}. Only state the production numbers — do NOT speculate about weather reasons if no weather data is provided." : hasGrid - ? $"2. Grid usage: Comment on the {current.TotalGridImport:F1} kWh drawn from the grid this week." + ? $"2. Grid usage: Comment on the {current.TotalGridImport:F1} kWh drawn from the grid during {periodLabel}." : "2. Consumption pattern: Comment on the weekday vs weekend load pattern."; var instruction3 = hasBattery @@ -455,19 +484,24 @@ BEHAVIORAL PATTERN (from hourly data this week): // Instruction 4 — adapts based on whether we have behavioral data string instruction4; if (hasBehavior && hasPv) - instruction4 = $"4. Smart action for next week: Write exactly 2 sentences. Sentence 1: point out the timing mismatch using exact numbers — peak household load is during {FormatHourSlot(behavior.PeakLoadHour)} ({behavior.AvgPeakLoadKwh} kWh) but solar peaks during {FormatHourSlot(behavior.PeakSolarHour)} ({behavior.AvgPeakSolarKwh} kWh), with solar active from {peakSolarWindow}. Sentence 2: suggest shifting energy-intensive appliances (such as washing machine, dishwasher, heat pump, or EV charger if applicable) to run during the solar window {peakSolarWindow} — do not assume which specific device the customer has."; + instruction4 = $"4. Smart action for the coming week: Write exactly 2 sentences. Sentence 1: point out the timing mismatch using exact numbers — peak household load is during {FormatHourSlot(behavior.PeakLoadHour)} ({behavior.AvgPeakLoadKwh} kWh) but solar peaks during {FormatHourSlot(behavior.PeakSolarHour)} ({behavior.AvgPeakSolarKwh} kWh), with solar active from {peakSolarWindow}. Sentence 2: suggest shifting energy-intensive appliances (such as washing machine, dishwasher, heat pump, or EV charger if applicable) to run during the solar window {peakSolarWindow} — do not assume which specific device the customer has."; else if (hasBehavior && hasGrid) - instruction4 = $"4. Smart action for next week: Write exactly 2 sentences. Sentence 1: state that the peak grid-import hour is {FormatHourSlot(behavior.HighestGridImportHour)} ({behavior.AvgGridImportAtPeakHour} kWh avg). Sentence 2: suggest one action to reduce grid use during that hour — shifting energy-intensive appliances (washing machine, dishwasher, heat pump, EV charger) away from that time."; + instruction4 = $"4. Smart action for the coming week: Write exactly 2 sentences. Sentence 1: state that the peak grid-import hour is {FormatHourSlot(behavior.HighestGridImportHour)} ({behavior.AvgGridImportAtPeakHour} kWh avg). Sentence 2: suggest one action to reduce grid use during that hour — shifting energy-intensive appliances (washing machine, dishwasher, heat pump, EV charger) away from that time."; else - instruction4 = "4. Smart action for next week: Based on this week's daily energy patterns, suggest one practical tip to maximize self-consumption and reduce grid dependency."; + instruction4 = $"4. Smart action for the coming week: Based on the energy patterns from {periodLabel}, suggest one practical tip to maximize self-consumption and reduce grid dependency."; // Instruction 5 — weather outlook with pattern-based predictions var hasWeather = forecast != null; var bulletCount = hasWeather ? 5 : 4; + // Forecast date range label for prompt + var forecastLabel = forecast != null && forecast.Count > 0 + ? $"{DateTime.Parse(forecast.First().Date):MMM dd}–{DateTime.Parse(forecast.Last().Date):MMM dd}" + : "the coming days"; + var instruction5 = ""; if (hasWeather && hasPv) { - // Compute avg daily PV production this week for reference + // Compute avg daily PV production for the reporting week as reference var avgDailyPv = currentWeek.Count > 0 ? Math.Round(currentWeek.Average(d => d.PvProduction), 1) : 0; var bestDayPv = Math.Round(bestDay.PvProduction, 1); var worstDayPv = Math.Round(worstDay.PvProduction, 1); @@ -477,36 +511,39 @@ BEHAVIORAL PATTERN (from hourly data this week): var cloudyDays = forecast.Where(d => d.SunshineHours < 3).Select(d => DateTime.Parse(d.Date).ToString("dddd")).ToList(); var totalForecastSunshine = Math.Round(forecast.Sum(d => d.SunshineHours), 1); - var patternContext = $"This week the installation averaged {avgDailyPv} kWh/day PV (best: {bestDayPv} kWh on {bestDayName}, worst: {worstDayPv} kWh on {worstDayName}). "; + var patternContext = $"During {periodLabel} the installation averaged {avgDailyPv} kWh/day PV (best: {bestDayPv} kWh on {bestDayName}, worst: {worstDayPv} kWh on {worstDayName}). "; if (sunnyDays.Count > 0) - patternContext += $"Next week, sunny days ({string.Join(", ", sunnyDays)}) should produce above average (~{avgDailyPv}+ kWh/day). "; + patternContext += $"In the coming days ({forecastLabel}), sunny days ({string.Join(", ", sunnyDays)}) should produce above average (~{avgDailyPv}+ kWh/day). "; if (cloudyDays.Count > 0) patternContext += $"Cloudy/rainy days ({string.Join(", ", cloudyDays)}) will likely produce below average (~{worstDayPv} kWh/day or less). "; - patternContext += $"Total forecast sunshine next week: {totalForecastSunshine}h."; + patternContext += $"Total forecast sunshine for {forecastLabel}: {totalForecastSunshine}h."; - instruction5 = $@"5. Weather outlook: {patternContext} Write 2-3 concise sentences. (1) Name the best solar days next week and estimate production based on this week's pattern. (2) Recommend which specific days are best for running energy-heavy appliances (washing machine, dishwasher, EV charging) to maximize free solar energy. (3) If rainy days follow sunny days, suggest front-loading heavy usage onto the sunny days before the rain arrives. IMPORTANT: Do NOT suggest ""reducing consumption"" in the evening — people need electricity for cooking, lighting, and daily life. Instead, focus on SHIFTING discretionary loads (laundry, dishwasher, EV) to the best solar days. The battery system handles grid charging automatically — do not tell the customer to manage battery charging."; + instruction5 = $@"5. Weather outlook: {patternContext} Write 2-3 concise sentences. (1) Name the best solar days in the coming days ({forecastLabel}) and estimate production based on the reporting week's pattern. (2) Recommend which specific days are best for running energy-heavy appliances (washing machine, dishwasher, EV charging) to maximize free solar energy. (3) If rainy days follow sunny days, suggest front-loading heavy usage onto the sunny days before the rain arrives. IMPORTANT: Do NOT suggest ""reducing consumption"" in the evening — people need electricity for cooking, lighting, and daily life. Instead, focus on SHIFTING discretionary loads (laundry, dishwasher, EV) to the best solar days. The battery system handles grid charging automatically — do not tell the customer to manage battery charging."; } else if (hasWeather) { - instruction5 = @"5. Weather outlook: Summarize the coming week's weather in 1-2 sentences. Mention which days have the most sunshine and suggest those as the best days to run energy-heavy appliances (laundry, dishwasher). Do NOT suggest reducing evening consumption — focus on shifting discretionary loads to sunny days."; + instruction5 = $@"5. Weather outlook: Summarize the weather for the coming days ({forecastLabel}) in 1-2 sentences. Mention which days have the most sunshine and suggest those as the best days to run energy-heavy appliances (laundry, dishwasher). Do NOT suggest reducing evening consumption — focus on shifting discretionary loads to sunny days."; } var prompt = $@"You are an energy advisor for a sodistore home installation: ""{installationName}"". +Today is {today:yyyy-MM-dd} ({today:dddd}). This report covers the week of {periodLabel}. Write {bulletCount} bullet points (each on its own line starting with ""- ""). No bold markers, no asterisks, no markdown — plain text only. IMPORTANT FORMAT RULE: Each bullet MUST start with a short title followed by a colon, then the description. Example: ""- Title label: Description text here."" Translate the title label into {LanguageName(language)} but always keep the ""Title: description"" structure. CRITICAL: All numbers below are pre-calculated. Use these values as-is — do not recalculate, round differently, or change any number. +CRITICAL: Use explicit date references. Say ""during {periodLabel}"" for the reporting week. Say ""the coming days ({forecastLabel})"" for the forecast period. NEVER use ambiguous terms like ""this week"" or ""next week"". SYSTEM COMPONENTS: PV={hasPv}, Battery={hasBattery}, Grid={hasGrid} -DAILY FACTS: -- Total consumption: {current.TotalConsumption:F1} kWh this week. Self-sufficiency: {selfSufficiency}%. +DAILY FACTS (for {periodLabel}): +- Total consumption: {current.TotalConsumption:F1} kWh. Self-sufficiency: {selfSufficiency}%. {pvDailyFact} {battDailyFact} {gridDailyFact} {behavioralSection} +{historicalBlock} {weatherBlock} INSTRUCTIONS: {instruction1} diff --git a/typescript/frontend-marios2/src/Resources/images/inesco_logo_for_dark_bg.png b/typescript/frontend-marios2/src/Resources/images/inesco_logo_for_dark_bg.png new file mode 100644 index 000000000..acfffcd03 Binary files /dev/null and b/typescript/frontend-marios2/src/Resources/images/inesco_logo_for_dark_bg.png differ diff --git a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/DailySection.tsx b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/DailySection.tsx index 6fd5d28d5..c71ed26a8 100644 --- a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/DailySection.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/DailySection.tsx @@ -56,19 +56,6 @@ interface HourlyEnergyRecord { // ── Date Helpers ───────────────────────────────────────────── -/** - * Returns the Monday of the current week. - */ -function getCurrentMonday(): Date { - const today = new Date(); - today.setHours(0, 0, 0, 0); - const dow = today.getDay(); // 0=Sun - const offset = dow === 0 ? 6 : dow - 1; // Mon=0 offset - const monday = new Date(today); - monday.setDate(today.getDate() - offset); - return monday; -} - function formatDateISO(d: Date): string { const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, '0'); @@ -77,19 +64,20 @@ function formatDateISO(d: Date): string { } /** - * Returns current week Mon→yesterday. Today excluded because - * S3 aggregated file is not available until end of day. + * Returns the last 7 days ending yesterday. + * Today is excluded because S3 aggregated file is not available until ~01:00 UTC the next day. */ -function getCurrentWeekDays(currentMonday: Date): Date[] { +function getLast7Days(): Date[] { const yesterday = new Date(); yesterday.setHours(0, 0, 0, 0); yesterday.setDate(yesterday.getDate() - 1); + const days: Date[] = []; - - for (let d = new Date(currentMonday); d <= yesterday; d.setDate(d.getDate() + 1)) { - days.push(new Date(d)); + for (let i = 6; i >= 0; i--) { + const d = new Date(yesterday); + d.setDate(yesterday.getDate() - i); + days.push(d); } - return days; } @@ -105,7 +93,6 @@ export default function DailySection({ onPeriodChange?: (date: string) => void; }) { const intl = useIntl(); - const currentMonday = useMemo(() => getCurrentMonday(), []); const yesterday = useMemo(() => { const d = new Date(); d.setHours(0, 0, 0, 0); @@ -125,11 +112,8 @@ export default function DailySection({ const [loadingWeek, setLoadingWeek] = useState(false); const [noData, setNoData] = useState(false); - // Current week Mon→yesterday only - const weekDays = useMemo( - () => getCurrentWeekDays(currentMonday), - [currentMonday] - ); + // Rolling 7-day window ending yesterday + const weekDays = useMemo(() => getLast7Days(), []); // Fetch data for current week days useEffect(() => { @@ -193,7 +177,7 @@ export default function DailySection({ return ( <> - {/* Day Strip — current week Mon→yesterday */} + {/* Day Strip — last 7 days ending yesterday */} @@ -388,13 +372,14 @@ function IntradayChart({ const hourMap = new Map(hourlyData.map((h) => [h.hour, h])); - const pvData = HOUR_LABELS.map((_, i) => hourMap.get(i)?.pvKwh ?? null); - const loadData = HOUR_LABELS.map((_, i) => hourMap.get(i)?.loadKwh ?? null); + const getHour = (i: number) => hourMap.get(i === 24 ? 23 : i); + const pvData = HOUR_LABELS.map((_, i) => getHour(i)?.pvKwh ?? null); + const loadData = HOUR_LABELS.map((_, i) => getHour(i)?.loadKwh ?? null); const batteryData = HOUR_LABELS.map((_, i) => { - const h = hourMap.get(i); + const h = getHour(i); return h ? h.batteryDischargedKwh - h.batteryChargedKwh : null; }); - const socData = HOUR_LABELS.map((_, i) => hourMap.get(i)?.battSoC ?? null); + const socData = HOUR_LABELS.map((_, i) => getHour(i)?.battSoC ?? null); const chartData = { labels: HOUR_LABELS, diff --git a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/WeeklyReport.tsx b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/WeeklyReport.tsx index ff662d40f..289f0514f 100644 --- a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/WeeklyReport.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/WeeklyReport.tsx @@ -26,6 +26,7 @@ import SendIcon from '@mui/icons-material/Send'; import DownloadIcon from '@mui/icons-material/Download'; import SaveIcon from '@mui/icons-material/Save'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import RefreshIcon from '@mui/icons-material/Refresh'; import axiosConfig from 'src/Resources/axiosConfig'; import DailySection from './DailySection'; @@ -104,6 +105,9 @@ interface MonthlyReport extends ReportSummary { avgPeakSolarHour: number; avgWeekdayDailyLoad: number; avgWeekendDailyLoad: number; + isPreview?: boolean; + daysAvailable?: number; + daysInMonth?: number; } interface YearlyReport extends ReportSummary { @@ -113,17 +117,7 @@ interface YearlyReport extends ReportSummary { avgPeakSolarHour: number; avgWeekdayDailyLoad: number; avgWeekendDailyLoad: number; -} - -interface PendingMonth { - year: number; - month: number; - weekCount: number; -} - -interface PendingYear { - year: number; - monthCount: number; + isPreview?: boolean; } interface WeeklyReportSummaryRecord { @@ -151,6 +145,49 @@ interface WeeklyReportSummaryRecord { createdAt: string; } +function ReportHtmlFrame({ html }: { html: string }) { + const iframeRef = useRef(null); + const [height, setHeight] = useState(600); + + useEffect(() => { + const iframe = iframeRef.current; + if (!iframe) return; + + const updateHeight = () => { + try { + const doc = iframe.contentDocument || iframe.contentWindow?.document; + if (doc?.body) { + const newHeight = doc.body.scrollHeight + 20; + if (newHeight > 50) setHeight(newHeight); + } + } catch { /* cross-origin safety */ } + }; + + iframe.addEventListener('load', updateHeight); + const timers = [300, 800, 1500].map(ms => setTimeout(updateHeight, ms)); + + return () => { + iframe.removeEventListener('load', updateHeight); + timers.forEach(clearTimeout); + }; + }, [html]); + + return ( +
- +
- - - - + {footerHtml}
- +
{s.Title}
{installationName}
{periodStart} — {periodEnd}
@@ -774,13 +816,7 @@ public static class ReportEmailService
- {s.Footer} - -