cahched weekly report

This commit is contained in:
Yinyin Liu 2026-03-10 10:50:17 +01:00
parent 7f972d13c3
commit f7ee347fc5
5 changed files with 124 additions and 15 deletions

View File

@ -922,12 +922,13 @@ public class Controller : ControllerBase
// ── Weekly Performance Report ──────────────────────────────────────
/// <summary>
/// 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.
/// </summary>
[HttpGet(nameof(GetWeeklyReport))]
public async Task<ActionResult<WeeklyReportResponse>> 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)
@ -949,6 +950,39 @@ public class Controller : ControllerBase
try
{
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);

View File

@ -43,6 +43,12 @@ public class WeeklyReportSummary
// AI insight for this week
public String AiInsight { get; set; } = "";
/// <summary>
/// Full serialized WeeklyReportResponse (with AiInsight cleared).
/// Used for cache-first serving — avoids regenerating numeric data + Mistral call.
/// </summary>
public String ResponseJson { get; set; } = "";
public String CreatedAt { get; set; } = "";
}

View File

@ -77,6 +77,18 @@ public static partial class Db
.ToList();
}
/// <summary>
/// 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.
/// </summary>
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<MonthlyReportSummary> GetMonthlyReports(Int64 installationId)
=> MonthlyReports
.Where(r => r.InstallationId == installationId)

View File

@ -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));
}
/// <summary>
/// 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.
/// </summary>
public static async Task<WeeklyReportResponse?> ToWeeklyReportResponseAsync(
WeeklyReportSummary summary, String language)
{
if (String.IsNullOrEmpty(summary.ResponseJson))
return null;
var response = JsonConvert.DeserializeObject<WeeklyReportResponse>(summary.ResponseJson);
if (response == null)
return null;
response.AiInsight = await GetOrGenerateWeeklyInsightAsync(summary, language);
return response;
}
/// <summary>Cached-or-generated AI insight for a stored MonthlyReportSummary.</summary>
public static Task<String> GetOrGenerateMonthlyInsightAsync(
MonthlyReportSummary report, String language)

View File

@ -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,6 +615,8 @@ function WeeklySection({ installationId, latestMonthlyPeriodEnd }: { installatio
borderRadius: 2
}}
>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<Box>
<Typography variant="h5" fontWeight="bold">
<FormattedMessage id="reportTitle" defaultMessage="Weekly Performance Report" />
</Typography>
@ -623,6 +626,17 @@ function WeeklySection({ installationId, latestMonthlyPeriodEnd }: { installatio
<Typography variant="body2" sx={{ opacity: 0.7 }}>
{report.periodStart} {report.periodEnd}
</Typography>
</Box>
<Button
variant="outlined"
size="small"
startIcon={<RefreshIcon />}
onClick={() => fetchReport(true)}
sx={{ color: '#fff', borderColor: 'rgba(255,255,255,0.5)', '&:hover': { borderColor: '#fff' } }}
>
<FormattedMessage id="regenerateReport" defaultMessage="Regenerate" />
</Button>
</Box>
</Paper>
{/* Weekly Insights */}