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}
+
+
+ }
+ onClick={() => fetchReport(true)}
+ sx={{ color: '#fff', borderColor: 'rgba(255,255,255,0.5)', '&:hover': { borderColor: '#fff' } }}
+ >
+
+
+
{/* Weekly Insights */}