From 50bc85ff2ade9916519a1ce40b0e3f1c9c7db11d Mon Sep 17 00:00:00 2001 From: Yinyin Liu Date: Thu, 12 Mar 2026 14:39:24 +0100 Subject: [PATCH] unified daily report data from S3 and then db and fallback to xlsx and the container size is consistent among 4 tubs --- csharp/App/Backend/Controller.cs | 14 +- .../Backend/DataTypes/WeeklyReportResponse.cs | 5 + csharp/App/Backend/Program.cs | 4 +- .../Backend/Services/DailyIngestionService.cs | 76 ++++--- .../Services/ReportAggregationService.cs | 172 ++++++--------- .../Backend/Services/WeeklyReportService.cs | 44 +++- .../SodiohomeInstallations/DailySection.tsx | 200 ++++++------------ .../SodiohomeInstallations/Installation.tsx | 18 +- .../SodiohomeInstallations/WeeklyReport.tsx | 69 ++++-- .../SodiohomeInstallations/index.tsx | 15 +- typescript/frontend-marios2/src/lang/de.json | 4 +- typescript/frontend-marios2/src/lang/en.json | 4 +- typescript/frontend-marios2/src/lang/fr.json | 4 +- typescript/frontend-marios2/src/lang/it.json | 4 +- 14 files changed, 306 insertions(+), 327 deletions(-) diff --git a/csharp/App/Backend/Controller.cs b/csharp/App/Backend/Controller.cs index bf7ab8c6b..55b8a17e0 100644 --- a/csharp/App/Backend/Controller.cs +++ b/csharp/App/Backend/Controller.cs @@ -1309,20 +1309,14 @@ public class Controller : ControllerBase Int64 installationId, Installation installation, DateOnly fromDate, DateOnly toDate) { - var jsonDir = Path.Combine( - Environment.CurrentDirectory, "tmp_report", "aggregated", installationId.ToString()); - for (var date = fromDate; date <= toDate; date = date.AddDays(1)) { - var isoDate = date.ToString("yyyy-MM-dd"); - var fileName = AggregatedJsonParser.ToJsonFileName(date); + var isoDate = date.ToString("yyyy-MM-dd"); - // Try local file first - var localPath = Path.Combine(jsonDir, fileName); - String? content = System.IO.File.Exists(localPath) ? System.IO.File.ReadAllText(localPath) : null; + if (Db.DailyRecordExists(installationId, isoDate)) + continue; - // Try S3 if no local file - content ??= AggregatedJsonParser.TryReadFromS3(installation, isoDate) + var content = AggregatedJsonParser.TryReadFromS3(installation, isoDate) .GetAwaiter().GetResult(); if (content is null) continue; diff --git a/csharp/App/Backend/DataTypes/WeeklyReportResponse.cs b/csharp/App/Backend/DataTypes/WeeklyReportResponse.cs index 8e2660c98..7e871d189 100644 --- a/csharp/App/Backend/DataTypes/WeeklyReportResponse.cs +++ b/csharp/App/Backend/DataTypes/WeeklyReportResponse.cs @@ -28,6 +28,11 @@ public class WeeklyReportResponse public List DailyData { get; set; } = new(); public BehavioralPattern? Behavior { get; set; } public string AiInsight { get; set; } = ""; + + // Data availability — lets UI show which days are missing + public int DaysAvailable { get; set; } // how many of the 7 days have data + public int DaysExpected { get; set; } // 7 (Mon–Sun) + public List MissingDates { get; set; } = new(); // ISO dates with no data } public class WeeklySummary diff --git a/csharp/App/Backend/Program.cs b/csharp/App/Backend/Program.cs index 9183cc634..3034c1166 100644 --- a/csharp/App/Backend/Program.cs +++ b/csharp/App/Backend/Program.cs @@ -30,8 +30,8 @@ public static class Program TicketDiagnosticService.Initialize(); NetworkProviderService.Initialize(); AlarmReviewService.StartDailyScheduler(); - // DailyIngestionService.StartScheduler(); // Phase 2: enable when S3 auto-push is ready - // ReportAggregationService.StartScheduler(); // Phase 2: enable when scheduler should auto-run monthly/yearly + DailyIngestionService.StartScheduler(); + ReportAggregationService.StartScheduler(); var builder = WebApplication.CreateBuilder(args); RabbitMqManager.InitializeEnvironment(); diff --git a/csharp/App/Backend/Services/DailyIngestionService.cs b/csharp/App/Backend/Services/DailyIngestionService.cs index f82e46c56..28380c63f 100644 --- a/csharp/App/Backend/Services/DailyIngestionService.cs +++ b/csharp/App/Backend/Services/DailyIngestionService.cs @@ -5,7 +5,7 @@ namespace InnovEnergy.App.Backend.Services; /// /// Ingests daily energy totals into the DailyEnergyRecord SQLite table. -/// Data source priority: JSON (local) → JSON (S3) → xlsx fallback. +/// Data source priority: JSON (S3) → xlsx fallback. /// Runs automatically at 01:00 UTC daily. Can also be triggered manually via the /// IngestDailyData API endpoint. /// @@ -14,9 +14,6 @@ public static class DailyIngestionService private static readonly String TmpReportDir = Environment.CurrentDirectory + "/tmp_report/"; - private static readonly String JsonAggregatedDir = - Environment.CurrentDirectory + "/tmp_report/aggregated/"; - private static Timer? _dailyTimer; /// @@ -53,7 +50,7 @@ public static class DailyIngestionService Console.WriteLine($"[DailyIngestion] Starting ingestion for all SodioHome installations..."); var installations = Db.Installations - .Where(i => i.Product == (Int32)ProductType.SodioHome) + .Where(i => i.Product == (Int32)ProductType.SodioHome && i.Device != 3) // Skip Growatt (device=3) .ToList(); foreach (var installation in installations) @@ -72,7 +69,7 @@ public static class DailyIngestionService } /// - /// Ingests data for one installation. Tries JSON (local + S3) and xlsx. + /// Ingests data for one installation. Tries JSON (S3) and xlsx. /// Both sources are tried — idempotency checks prevent duplicates. /// JSON provides recent data; xlsx provides historical data. /// @@ -82,47 +79,60 @@ public static class DailyIngestionService IngestFromXlsx(installationId); } - private static async Task TryIngestFromJson(Int64 installationId) + /// + /// Ingests S3 JSON data for a specific date range. Used by report services + /// as a fallback when SQLite has no records for the requested period. + /// Idempotent — skips dates already in DB. + /// + public static async Task IngestDateRangeAsync(Int64 installationId, DateOnly fromDate, DateOnly toDate) { + var installation = Db.GetInstallationById(installationId); + if (installation is null) return; + var newDaily = 0; var newHourly = 0; - var jsonDir = Path.Combine(JsonAggregatedDir, installationId.ToString()); - // Collect JSON content from local files - var jsonFiles = Directory.Exists(jsonDir) - ? Directory.GetFiles(jsonDir, "*.json") - : Array.Empty(); - - foreach (var jsonPath in jsonFiles.OrderBy(f => f)) + for (var date = fromDate; date <= toDate; date = date.AddDays(1)) { - var content = File.ReadAllText(jsonPath); + var isoDate = date.ToString("yyyy-MM-dd"); + if (Db.DailyRecordExists(installationId, isoDate)) + continue; + + var content = await AggregatedJsonParser.TryReadFromS3(installation, isoDate); + if (content is null) continue; + var (d, h) = IngestJsonContent(installationId, content); newDaily += d; newHourly += h; } - // Also try S3 for recent days (yesterday + today) if no local files found - if (jsonFiles.Length == 0) + if (newDaily > 0 || newHourly > 0) + Console.WriteLine($"[DailyIngestion] Installation {installationId} (S3 date-range {fromDate:yyyy-MM-dd}–{toDate:yyyy-MM-dd}): {newDaily} day(s), {newHourly} hour(s) ingested."); + } + + private static async Task TryIngestFromJson(Int64 installationId) + { + var newDaily = 0; + var newHourly = 0; + + var installation = Db.GetInstallationById(installationId); + if (installation is null) return false; + + // Try S3 for recent days (yesterday + today), skip if already in DB + for (var daysBack = 0; daysBack <= 1; daysBack++) { - var installation = Db.GetInstallationById(installationId); - if (installation is not null) - { - for (var daysBack = 0; daysBack <= 1; daysBack++) - { - var date = DateOnly.FromDateTime(DateTime.UtcNow.AddDays(-daysBack)); - var isoDate = date.ToString("yyyy-MM-dd"); + var date = DateOnly.FromDateTime(DateTime.UtcNow.AddDays(-daysBack)); + var isoDate = date.ToString("yyyy-MM-dd"); - if (Db.DailyRecordExists(installationId, isoDate)) - continue; + if (Db.DailyRecordExists(installationId, isoDate)) + continue; - var content = await AggregatedJsonParser.TryReadFromS3(installation, isoDate); - if (content is null) continue; + var content = await AggregatedJsonParser.TryReadFromS3(installation, isoDate); + if (content is null) continue; - var (d, h) = IngestJsonContent(installationId, content); - newDaily += d; - newHourly += h; - } - } + var (d, h) = IngestJsonContent(installationId, content); + newDaily += d; + newHourly += h; } if (newDaily > 0 || newHourly > 0) diff --git a/csharp/App/Backend/Services/ReportAggregationService.cs b/csharp/App/Backend/Services/ReportAggregationService.cs index 824b9848b..6f135ca0d 100644 --- a/csharp/App/Backend/Services/ReportAggregationService.cs +++ b/csharp/App/Backend/Services/ReportAggregationService.cs @@ -9,7 +9,7 @@ public static class ReportAggregationService { private static Timer? _monthEndTimer; private static Timer? _yearEndTimer; - // private static Timer? _sundayReportTimer; + private static Timer? _weeklyReportTimer; private const Double ElectricityPriceCHF = 0.39; private static readonly String MistralUrl = "https://api.mistral.ai/v1/chat/completions"; @@ -18,17 +18,42 @@ public static class ReportAggregationService public static void StartScheduler() { - // ScheduleSundayWeeklyReport(); + ScheduleWeeklyReportJob(); ScheduleMonthEndJob(); ScheduleYearEndJob(); Console.WriteLine("[ReportAggregation] Scheduler started."); } + private static void ScheduleWeeklyReportJob() + { + // Run every Monday at 03:00 UTC — after DailyIngestionService (01:00 UTC) + var now = DateTime.UtcNow; + var daysUntil = ((Int32)DayOfWeek.Monday - (Int32)now.DayOfWeek + 7) % 7; + var nextMon = now.Date.AddDays(daysUntil == 0 && now.Hour >= 3 ? 7 : daysUntil).AddHours(3); + + _weeklyReportTimer = new Timer( + _ => + { + try + { + if (DateTime.UtcNow.DayOfWeek == DayOfWeek.Monday) + RunWeeklyReportGeneration().GetAwaiter().GetResult(); + } + catch (Exception ex) + { + Console.Error.WriteLine($"[ReportAggregation] Weekly report error: {ex.Message}"); + } + }, + null, nextMon - now, TimeSpan.FromDays(7)); + + Console.WriteLine($"[ReportAggregation] Weekly report scheduled (Monday 03:00 UTC). Next run: {nextMon:yyyy-MM-dd HH:mm} UTC"); + } + private static void ScheduleMonthEndJob() { - // Run daily at 02:00, but only act on the 1st of the month - var now = DateTime.Now; - var next = now.Date.AddHours(2); + // Run daily at 04:00 UTC, but only act on the 1st of the month + var now = DateTime.UtcNow; + var next = now.Date.AddHours(4); if (now >= next) next = next.AddDays(1); _monthEndTimer = new Timer( @@ -36,7 +61,7 @@ public static class ReportAggregationService { try { - if (DateTime.Now.Day == 1) + if (DateTime.UtcNow.Day == 1) RunMonthEndAggregation().GetAwaiter().GetResult(); } catch (Exception ex) @@ -46,14 +71,14 @@ public static class ReportAggregationService }, null, next - now, TimeSpan.FromDays(1)); - Console.WriteLine($"[ReportAggregation] Month-end timer scheduled (daily 02:00, acts on 1st). Next check: {next:yyyy-MM-dd HH:mm}"); + Console.WriteLine($"[ReportAggregation] Month-end timer scheduled (daily 04:00 UTC, acts on 1st). Next check: {next:yyyy-MM-dd HH:mm} UTC"); } private static void ScheduleYearEndJob() { - // Run daily at 03:00, but only act on Jan 2nd - var now = DateTime.Now; - var next = now.Date.AddHours(3); + // Run daily at 05:00 UTC, but only act on Jan 2nd + var now = DateTime.UtcNow; + var next = now.Date.AddHours(5); if (now >= next) next = next.AddDays(1); _yearEndTimer = new Timer( @@ -61,7 +86,7 @@ public static class ReportAggregationService { try { - if (DateTime.Now.Month == 1 && DateTime.Now.Day == 2) + if (DateTime.UtcNow.Month == 1 && DateTime.UtcNow.Day == 2) RunYearEndAggregation().GetAwaiter().GetResult(); } catch (Exception ex) @@ -71,99 +96,40 @@ public static class ReportAggregationService }, null, next - now, TimeSpan.FromDays(1)); - Console.WriteLine($"[ReportAggregation] Year-end timer scheduled (daily 03:00, acts on Jan 2). Next check: {next:yyyy-MM-dd HH:mm}"); + Console.WriteLine($"[ReportAggregation] Year-end timer scheduled (daily 05:00 UTC, acts on Jan 2). Next check: {next:yyyy-MM-dd HH:mm} UTC"); } - // ── Sunday Weekly Report Automation ───────────────────────────── - // Generates weekly reports (Sunday–Saturday) for all SodiStoreHome - // installations every Sunday at 06:00, saves summary to DB, and - // emails the report to all users who have access to the installation. - // - // TODO: uncomment ScheduleSundayWeeklyReport() in StartScheduler() to enable. + // ── Weekly Report Auto-Generation ───────────────────────────── - // private static void ScheduleSundayWeeklyReport() - // { - // // Calculate delay until next Sunday 06:00 - // var now = DateTime.Now; - // var daysUntil = ((int)DayOfWeek.Sunday - (int)now.DayOfWeek + 7) % 7; - // var nextSunday = now.Date.AddDays(daysUntil == 0 && now.Hour >= 6 ? 7 : daysUntil).AddHours(6); - // - // _sundayReportTimer = new Timer( - // _ => - // { - // try - // { - // if (DateTime.Now.DayOfWeek == DayOfWeek.Sunday) - // RunSundayWeeklyReports().GetAwaiter().GetResult(); - // } - // catch (Exception ex) - // { - // Console.Error.WriteLine($"[ReportAggregation] Sunday report error: {ex.Message}"); - // } - // }, - // null, nextSunday - now, TimeSpan.FromDays(7)); - // - // Console.WriteLine($"[ReportAggregation] Sunday weekly report scheduled. Next run: {nextSunday:yyyy-MM-dd HH:mm}"); - // } - // - // private static async Task RunSundayWeeklyReports() - // { - // Console.WriteLine("[ReportAggregation] Running Sunday weekly report generation..."); - // - // // Find all SodiStoreHome installations - // var installations = Db.Installations - // .Where(i => i.Product == (int)ProductType.SodioHome) - // .ToList(); - // - // foreach (var installation in installations) - // { - // try - // { - // // Generate the weekly report (covers last Sunday–Saturday) - // var report = await WeeklyReportService.GenerateReportAsync( - // installation.Id, installation.InstallationName, "en"); - // - // // Save summary to DB for future monthly aggregation - // SaveWeeklySummary(installation.Id, report); - // - // // Email the report to all users who have access to this installation - // var userIds = Db.InstallationAccess - // .Where(a => a.InstallationId == installation.Id) - // .Select(a => a.UserId) - // .ToList(); - // - // foreach (var userId in userIds) - // { - // var user = Db.GetUserById(userId); - // if (user == null || String.IsNullOrWhiteSpace(user.Email)) - // continue; - // - // try - // { - // var lang = user.Language ?? "en"; - // // Regenerate with user's language if different from "en" - // var localizedReport = lang == "en" - // ? report - // : await WeeklyReportService.GenerateReportAsync( - // installation.Id, installation.InstallationName, lang); - // - // await ReportEmailService.SendReportEmailAsync(localizedReport, user.Email, lang); - // Console.WriteLine($"[ReportAggregation] Weekly report emailed to {user.Email} for installation {installation.Id}"); - // } - // catch (Exception emailEx) - // { - // Console.Error.WriteLine($"[ReportAggregation] Failed to email {user.Email}: {emailEx.Message}"); - // } - // } - // } - // catch (Exception ex) - // { - // Console.Error.WriteLine($"[ReportAggregation] Failed weekly report for installation {installation.Id}: {ex.Message}"); - // } - // } - // - // Console.WriteLine("[ReportAggregation] Sunday weekly report generation complete."); - // } + private static async Task RunWeeklyReportGeneration() + { + Console.WriteLine("[ReportAggregation] Running Monday weekly report generation..."); + + var installations = Db.Installations + .Where(i => i.Product == (Int32)ProductType.SodioHome && i.Device != 3) // Skip Growatt (device=3) + .ToList(); + + var generated = 0; + foreach (var installation in installations) + { + try + { + var report = await WeeklyReportService.GenerateReportAsync( + installation.Id, installation.InstallationName, "en"); + + SaveWeeklySummary(installation.Id, report, "en"); + generated++; + + Console.WriteLine($"[ReportAggregation] Weekly report generated for installation {installation.Id} ({installation.InstallationName})"); + } + catch (Exception ex) + { + Console.Error.WriteLine($"[ReportAggregation] Failed weekly report for installation {installation.Id}: {ex.Message}"); + } + } + + Console.WriteLine($"[ReportAggregation] Weekly report generation complete. {generated}/{installations.Count} installations processed."); + } // ── Save Weekly Summary ─────────────────────────────────────────── @@ -335,7 +301,7 @@ public static class ReportAggregationService private static async Task RunMonthEndAggregation() { - var previousMonth = DateTime.Now.AddMonths(-1); + var previousMonth = DateTime.UtcNow.AddMonths(-1); var year = previousMonth.Year; var month = previousMonth.Month; var first = new DateOnly(year, month, 1); @@ -453,7 +419,7 @@ public static class ReportAggregationService private static async Task RunYearEndAggregation() { - var previousYear = DateTime.Now.Year - 1; + var previousYear = DateTime.UtcNow.Year - 1; Console.WriteLine($"[ReportAggregation] Running year-end aggregation for {previousYear}..."); diff --git a/csharp/App/Backend/Services/WeeklyReportService.cs b/csharp/App/Backend/Services/WeeklyReportService.cs index 4c841f470..2fe3e8047 100644 --- a/csharp/App/Backend/Services/WeeklyReportService.cs +++ b/csharp/App/Backend/Services/WeeklyReportService.cs @@ -91,7 +91,8 @@ public static class WeeklyReportService /// Generates a full weekly report for the given installation. /// Data source priority: /// 1. DailyEnergyRecord rows in SQLite (populated by DailyIngestionService) - /// 2. xlsx file fallback (if DB not yet populated for the target week) + /// 2. S3 fallback — ingests missing dates into SQLite via IngestDateRangeAsync, then retries + /// 3. xlsx file fallback (legacy, if both DB and S3 are empty) /// Cache is keyed to the calendar week — invalidated when the week changes. /// Pass weekStartOverride to generate a report for a specific historical week (disables cache). /// @@ -121,15 +122,25 @@ public static class WeeklyReportService var previousWeekDays = Db.GetDailyRecords(installationId, prevMon, prevSun) .Select(ToDailyEnergyData).ToList(); - // 2. Fallback: if DB empty, parse xlsx on the fly (pre-ingestion scenario) + // 2. S3 fallback: if DB empty, try ingesting from S3 into SQLite, then retry + if (currentWeekDays.Count == 0) + { + Console.WriteLine($"[WeeklyReportService] DB empty for week {curMon:yyyy-MM-dd}, trying S3 ingestion..."); + await DailyIngestionService.IngestDateRangeAsync(installationId, prevMon, curSun); + currentWeekDays = Db.GetDailyRecords(installationId, curMon, curSun) + .Select(ToDailyEnergyData).ToList(); + previousWeekDays = Db.GetDailyRecords(installationId, prevMon, prevSun) + .Select(ToDailyEnergyData).ToList(); + } + + // 3. xlsx fallback: if still empty after S3, parse xlsx on the fly (legacy) if (currentWeekDays.Count == 0) { - // Only parse xlsx files whose date range overlaps the needed weeks var relevantFiles = GetRelevantXlsxFiles(installationId, prevMon, curSun); if (relevantFiles.Count > 0) { - Console.WriteLine($"[WeeklyReportService] DB empty for week {curMon:yyyy-MM-dd}, falling back to {relevantFiles.Count} xlsx file(s)."); + Console.WriteLine($"[WeeklyReportService] S3 empty, falling back to {relevantFiles.Count} xlsx file(s)."); var allDaysParsed = relevantFiles.SelectMany(p => ExcelDataParser.Parse(p)).ToList(); currentWeekDays = allDaysParsed .Where(d => { var date = DateOnly.Parse(d.Date); return date >= curMon && date <= curSun; }) @@ -145,11 +156,12 @@ public static class WeeklyReportService $"No energy data available for week {curMon:yyyy-MM-dd}–{curSun:yyyy-MM-dd}. " + "Upload an xlsx file or wait for daily ingestion."); - // 3. Load hourly records from SQLite for behavioral analysis + // 4. Load hourly records from SQLite for behavioral analysis + // (S3 ingestion above already populated hourly records if available) var currentHourlyData = Db.GetHourlyRecords(installationId, curMon, curSun) .Select(ToHourlyEnergyData).ToList(); - // 3b. Fallback: if DB empty, parse hourly data from xlsx + // 4b. xlsx fallback for hourly data if (currentHourlyData.Count == 0) { var relevantFiles = GetRelevantXlsxFiles(installationId, curMon, curSun); @@ -264,11 +276,24 @@ public static class WeeklyReportService selfSufficiency, totalEnergySaved, totalSavingsCHF, behavior, installationName, language, location, country, region); + // Compute data availability — which days of the week are missing + var availableDates = currentWeekDays.Select(d => d.Date).ToHashSet(); + var missingDates = new List(); + if (weekStart.HasValue && weekEnd.HasValue) + { + for (var d = weekStart.Value; d <= weekEnd.Value; d = d.AddDays(1)) + { + var iso = d.ToString("yyyy-MM-dd"); + if (!availableDates.Contains(iso)) + missingDates.Add(iso); + } + } + return new WeeklyReportResponse { InstallationName = installationName, - PeriodStart = currentWeekDays.First().Date, - PeriodEnd = currentWeekDays.Last().Date, + PeriodStart = weekStart?.ToString("yyyy-MM-dd") ?? currentWeekDays.First().Date, + PeriodEnd = weekEnd?.ToString("yyyy-MM-dd") ?? currentWeekDays.Last().Date, CurrentWeek = currentSummary, PreviousWeek = previousSummary, TotalEnergySaved = totalEnergySaved, @@ -284,6 +309,9 @@ public static class WeeklyReportService DailyData = currentWeekDays, Behavior = behavior, AiInsight = aiInsight, + DaysAvailable = currentWeekDays.Count, + DaysExpected = 7, + MissingDates = missingDates, }; } diff --git a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/DailySection.tsx b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/DailySection.tsx index 75236921c..738167648 100644 --- a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/DailySection.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/DailySection.tsx @@ -4,10 +4,7 @@ import { Alert, Box, CircularProgress, - Container, - Grid, Paper, - TextField, Typography } from '@mui/material'; import { Line } from 'react-chartjs-2'; @@ -60,23 +57,16 @@ interface HourlyEnergyRecord { // ── Date Helpers ───────────────────────────────────────────── /** - * Anchor date for the 7-day strip. Returns last completed Sunday. - * To switch to live-data mode later, change to: () => new Date() + * Returns the Monday of the current week. */ -function getDataAnchorDate(): Date { +function getCurrentMonday(): Date { const today = new Date(); + today.setHours(0, 0, 0, 0); const dow = today.getDay(); // 0=Sun - const lastSunday = new Date(today); - lastSunday.setDate(today.getDate() - (dow === 0 ? 7 : dow)); - lastSunday.setHours(0, 0, 0, 0); - return lastSunday; -} - -function getWeekRange(anchor: Date): { monday: Date; sunday: Date } { - const sunday = new Date(anchor); - const monday = new Date(sunday); - monday.setDate(sunday.getDate() - 6); - return { monday, sunday }; + 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 { @@ -86,12 +76,21 @@ function formatDateISO(d: Date): string { return `${y}-${m}-${day}`; } -function getWeekDays(monday: Date): Date[] { - return Array.from({ length: 7 }, (_, i) => { - const d = new Date(monday); - d.setDate(monday.getDate() + i); - return d; - }); +/** + * Returns current week Mon→yesterday. Today excluded because + * S3 aggregated file is not available until end of day. + */ +function getCurrentWeekDays(currentMonday: Date): 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)); + } + + return days; } // ── Main Component ─────────────────────────────────────────── @@ -102,86 +101,72 @@ export default function DailySection({ installationId: number; }) { const intl = useIntl(); - const anchor = useMemo(() => getDataAnchorDate(), []); - const { monday, sunday } = useMemo(() => getWeekRange(anchor), [anchor]); - const weekDays = useMemo(() => getWeekDays(monday), [monday]); + const currentMonday = useMemo(() => getCurrentMonday(), []); + const yesterday = useMemo(() => { + const d = new Date(); + d.setHours(0, 0, 0, 0); + d.setDate(d.getDate() - 1); + return d; + }, []); - const [weekRecords, setWeekRecords] = useState([]); - const [weekHourlyRecords, setWeekHourlyRecords] = useState([]); - const [selectedDate, setSelectedDate] = useState(formatDateISO(sunday)); + const [allRecords, setAllRecords] = useState([]); + const [allHourlyRecords, setAllHourlyRecords] = useState([]); + const [selectedDate, setSelectedDate] = useState(formatDateISO(yesterday)); const [selectedDayRecord, setSelectedDayRecord] = useState(null); const [hourlyRecords, setHourlyRecords] = useState([]); const [loadingWeek, setLoadingWeek] = useState(false); const [noData, setNoData] = useState(false); - // Fetch week data (daily + hourly) via combined endpoint with xlsx fallback + cache + // Current week Mon→yesterday only + const weekDays = useMemo( + () => getCurrentWeekDays(currentMonday), + [currentMonday] + ); + + // Fetch data for current week days useEffect(() => { + if (weekDays.length === 0) return; + const from = formatDateISO(weekDays[0]); + const to = formatDateISO(weekDays[weekDays.length - 1]); setLoadingWeek(true); axiosConfig .get('/GetDailyDetailRecords', { - params: { - installationId, - from: formatDateISO(monday), - to: formatDateISO(sunday) - } + params: { installationId, from, to } }) .then((res) => { const daily = res.data?.dailyRecords?.records ?? []; const hourly = res.data?.hourlyRecords?.records ?? []; - setWeekRecords(Array.isArray(daily) ? daily : []); - setWeekHourlyRecords(Array.isArray(hourly) ? hourly : []); + setAllRecords(Array.isArray(daily) ? daily : []); + setAllHourlyRecords(Array.isArray(hourly) ? hourly : []); }) .catch(() => { - setWeekRecords([]); - setWeekHourlyRecords([]); + setAllRecords([]); + setAllHourlyRecords([]); }) .finally(() => setLoadingWeek(false)); - }, [installationId, monday, sunday]); + }, [installationId, weekDays]); - // When selected date changes, extract data from week cache or fetch + // When selected date changes, extract data from cache useEffect(() => { setNoData(false); setSelectedDayRecord(null); - // Try week cache first - const cachedDay = weekRecords.find((r) => r.date === selectedDate); - const cachedHours = weekHourlyRecords.filter((r) => r.date === selectedDate); + const cachedDay = allRecords.find((r) => r.date === selectedDate); + const cachedHours = allHourlyRecords.filter((r) => r.date === selectedDate); if (cachedDay) { setSelectedDayRecord(cachedDay); setHourlyRecords(cachedHours); - return; + } else if (!loadingWeek) { + setNoData(true); + setHourlyRecords([]); } - - // Not in cache (date picker outside strip) — fetch via combined endpoint - axiosConfig - .get('/GetDailyDetailRecords', { - params: { installationId, from: selectedDate, to: selectedDate } - }) - .then((res) => { - const daily = res.data?.dailyRecords?.records ?? []; - const hourly = res.data?.hourlyRecords?.records ?? []; - setHourlyRecords(Array.isArray(hourly) ? hourly : []); - if (Array.isArray(daily) && daily.length > 0) { - setSelectedDayRecord(daily[0]); - } else { - setNoData(true); - } - }) - .catch(() => { - setHourlyRecords([]); - setNoData(true); - }); - }, [installationId, selectedDate, weekRecords, weekHourlyRecords]); + }, [installationId, selectedDate, allRecords, allHourlyRecords, loadingWeek]); const record = selectedDayRecord; const kpis = useMemo(() => computeKPIs(record), [record]); - const handleDatePicker = (e: React.ChangeEvent) => { - setSelectedDate(e.target.value); - }; - const handleStripSelect = (date: string) => { setSelectedDate(date); setNoData(false); @@ -197,53 +182,37 @@ export default function DailySection({ return ( <> - {/* Date Picker */} - - - - - - - - {/* 7-Day Strip */} + {/* Day Strip — current week Mon→yesterday */} {/* Loading state */} {loadingWeek && !record && ( - - + )} {/* No data state */} {!loadingWeek && noData && !record && ( - - - + + + + + )} {/* Day detail */} @@ -312,25 +281,14 @@ function computeKPIs(record: DailyEnergyData | null) { function DayStrip({ weekDays, - weekRecords, selectedDate, onSelect, - sunday, - loading }: { weekDays: Date[]; - weekRecords: DailyEnergyData[]; selectedDate: string; onSelect: (date: string) => void; - sunday: Date; - loading: boolean; }) { const intl = useIntl(); - const sundayLabel = sunday.toLocaleDateString(intl.locale, { - year: 'numeric', - month: 'short', - day: 'numeric' - }); return ( @@ -339,19 +297,12 @@ function DayStrip({ display: 'flex', gap: 1, overflowX: 'auto', - pb: 1, - mb: 1 + pb: 1 }} > {weekDays.map((day) => { const dateStr = formatDateISO(day); const isSelected = dateStr === selectedDate; - const record = weekRecords.find((r) => r.date === dateStr); - const selfSuff = - record && record.loadConsumption > 0 - ? Math.min(100, (1 - record.gridImport / record.loadConsumption) * 100) - : null; - return ( {day.getDate()} - - {loading - ? '...' - : selfSuff != null - ? `${selfSuff.toFixed(0)}%` - : '—'} - ); })} diff --git a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/Installation.tsx b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/Installation.tsx index 1afca7643..6c8089500 100644 --- a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/Installation.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/Installation.tsx @@ -607,14 +607,16 @@ function SodioHomeInstallation(props: singleInstallationProps) { } /> - - } - /> + {props.current_installation.device !== 3 && ( + + } + /> + )} +