From f7ee347fc5a7e3fd2eebec81170e28d7221e5832 Mon Sep 17 00:00:00 2001 From: Yinyin Liu Date: Tue, 10 Mar 2026 10:50:17 +0100 Subject: [PATCH] cahched weekly report --- csharp/App/Backend/Controller.cs | 42 ++++++++++++++++-- csharp/App/Backend/DataTypes/ReportSummary.cs | 6 +++ csharp/App/Backend/Database/Read.cs | 12 ++++++ .../Services/ReportAggregationService.cs | 43 +++++++++++++++++++ .../SodiohomeInstallations/WeeklyReport.tsx | 36 +++++++++++----- 5 files changed, 124 insertions(+), 15 deletions(-) diff --git a/csharp/App/Backend/Controller.cs b/csharp/App/Backend/Controller.cs index 9de49dbe5..28430c240 100644 --- a/csharp/App/Backend/Controller.cs +++ b/csharp/App/Backend/Controller.cs @@ -922,12 +922,13 @@ public class Controller : ControllerBase // ── Weekly Performance Report ────────────────────────────────────── /// - /// Generates a weekly performance report from an Excel file in tmp_report/{installationId}.xlsx - /// Returns JSON with daily data, weekly totals, ratios, and AI insight. + /// Returns a weekly performance report. Serves from cache if available; + /// generates fresh on first request or when forceRegenerate is true. /// [HttpGet(nameof(GetWeeklyReport))] public async Task> GetWeeklyReport( - Int64 installationId, Token authToken, String? language = null, String? weekStart = null) + Int64 installationId, Token authToken, String? language = null, + String? weekStart = null, Boolean forceRegenerate = false) { var user = Db.GetSession(authToken)?.User; if (user == null) @@ -948,7 +949,40 @@ public class Controller : ControllerBase try { - var lang = language ?? user.Language ?? "en"; + 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.InstallationName, lang, weekStartDate); diff --git a/csharp/App/Backend/DataTypes/ReportSummary.cs b/csharp/App/Backend/DataTypes/ReportSummary.cs index 04cc93c76..3a17c4117 100644 --- a/csharp/App/Backend/DataTypes/ReportSummary.cs +++ b/csharp/App/Backend/DataTypes/ReportSummary.cs @@ -43,6 +43,12 @@ public class WeeklyReportSummary // AI insight for this week public String AiInsight { get; set; } = ""; + /// + /// Full serialized WeeklyReportResponse (with AiInsight cleared). + /// Used for cache-first serving — avoids regenerating numeric data + Mistral call. + /// + public String ResponseJson { get; set; } = ""; + public String CreatedAt { get; set; } = ""; } diff --git a/csharp/App/Backend/Database/Read.cs b/csharp/App/Backend/Database/Read.cs index d67ab0554..49fb192dc 100644 --- a/csharp/App/Backend/Database/Read.cs +++ b/csharp/App/Backend/Database/Read.cs @@ -77,6 +77,18 @@ public static partial class Db .ToList(); } + /// + /// Finds a cached weekly report whose period overlaps with the given date range. + /// Uses overlap logic (not exact match) because PeriodStart may be offset + /// if the first day of the week has no data. + /// + public static WeeklyReportSummary? GetWeeklyReportForWeek(Int64 installationId, String periodStart, String periodEnd) + => WeeklyReports + .Where(r => r.InstallationId == installationId) + .ToList() + .FirstOrDefault(r => String.Compare(r.PeriodStart, periodEnd, StringComparison.Ordinal) <= 0 + && String.Compare(r.PeriodEnd, periodStart, StringComparison.Ordinal) >= 0); + public static List GetMonthlyReports(Int64 installationId) => MonthlyReports .Where(r => r.InstallationId == installationId) diff --git a/csharp/App/Backend/Services/ReportAggregationService.cs b/csharp/App/Backend/Services/ReportAggregationService.cs index 7ca43b156..e29265c5c 100644 --- a/csharp/App/Backend/Services/ReportAggregationService.cs +++ b/csharp/App/Backend/Services/ReportAggregationService.cs @@ -185,6 +185,29 @@ public static class ReportAggregationService foreach (var old in overlapping) Db.WeeklyReports.Delete(r => r.Id == old.Id); + // Serialize full response (minus AI insight) for cache-first serving + var reportForCache = new WeeklyReportResponse + { + InstallationName = report.InstallationName, + PeriodStart = report.PeriodStart, + PeriodEnd = report.PeriodEnd, + CurrentWeek = report.CurrentWeek, + PreviousWeek = report.PreviousWeek, + TotalEnergySaved = report.TotalEnergySaved, + TotalSavingsCHF = report.TotalSavingsCHF, + DaysEquivalent = report.DaysEquivalent, + SelfSufficiencyPercent = report.SelfSufficiencyPercent, + SelfConsumptionPercent = report.SelfConsumptionPercent, + BatteryEfficiencyPercent = report.BatteryEfficiencyPercent, + GridDependencyPercent = report.GridDependencyPercent, + PvChangePercent = report.PvChangePercent, + ConsumptionChangePercent = report.ConsumptionChangePercent, + GridImportChangePercent = report.GridImportChangePercent, + DailyData = report.DailyData, + Behavior = report.Behavior, + AiInsight = "", // Language-dependent; stored in AiInsightCache + }; + var summary = new WeeklyReportSummary { InstallationId = installationId, @@ -207,6 +230,7 @@ public static class ReportAggregationService WeekdayAvgDailyLoad = report.Behavior?.WeekdayAvgDailyLoad ?? 0, WeekendAvgDailyLoad = report.Behavior?.WeekendAvgDailyLoad ?? 0, AiInsight = report.AiInsight, + ResponseJson = JsonConvert.SerializeObject(reportForCache), CreatedAt = DateTime.UtcNow.ToString("o"), }; @@ -574,6 +598,25 @@ public static class ReportAggregationService () => GenerateWeeklySummaryAiInsightAsync(report, installationName, language)); } + /// + /// Reconstructs a full WeeklyReportResponse from a cached WeeklyReportSummary. + /// Returns null if ResponseJson is empty (old records without cache data). + /// AI insight is fetched/generated per-language via AiInsightCache. + /// + public static async Task ToWeeklyReportResponseAsync( + WeeklyReportSummary summary, String language) + { + if (String.IsNullOrEmpty(summary.ResponseJson)) + return null; + + var response = JsonConvert.DeserializeObject(summary.ResponseJson); + if (response == null) + return null; + + response.AiInsight = await GetOrGenerateWeeklyInsightAsync(summary, language); + return response; + } + /// Cached-or-generated AI insight for a stored MonthlyReportSummary. public static Task GetOrGenerateMonthlyInsightAsync( MonthlyReportSummary report, String language) diff --git a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/WeeklyReport.tsx b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/WeeklyReport.tsx index 0f4722d34..c2e2c5f8c 100644 --- a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/WeeklyReport.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/WeeklyReport.tsx @@ -20,6 +20,7 @@ import { } from '@mui/material'; import SendIcon from '@mui/icons-material/Send'; import DownloadIcon from '@mui/icons-material/Download'; +import RefreshIcon from '@mui/icons-material/Refresh'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import axiosConfig from 'src/Resources/axiosConfig'; @@ -514,12 +515,12 @@ function WeeklySection({ installationId, latestMonthlyPeriodEnd }: { installatio fetchReport(); }, [installationId, intl.locale]); - const fetchReport = async () => { + const fetchReport = async (forceRegenerate = false) => { setLoading(true); setError(null); try { const res = await axiosConfig.get('/GetWeeklyReport', { - params: { installationId, language: intl.locale } + params: { installationId, language: intl.locale, forceRegenerate } }); setReport(res.data); } catch (err: any) { @@ -614,15 +615,28 @@ function WeeklySection({ installationId, latestMonthlyPeriodEnd }: { installatio borderRadius: 2 }} > - - - - - {report.installationName} - - - {report.periodStart} — {report.periodEnd} - + + + + + + + {report.installationName} + + + {report.periodStart} — {report.periodEnd} + + + + {/* Weekly Insights */}