From ea80d136cabd1331747a0cb7b87421e051501d7c Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Mon, 10 Aug 2026 18:00:10 +0200 Subject: [PATCH] History UI: add prefetch and caching (#32699) --- assets/js/views/History.vue | 59 ++++++++++++++++++++++------------ server/http_history_handler.go | 6 ++++ 2 files changed, 44 insertions(+), 21 deletions(-) diff --git a/assets/js/views/History.vue b/assets/js/views/History.vue index 377ac7dc5..4ae2eb3f9 100644 --- a/assets/js/views/History.vue +++ b/assets/js/views/History.vue @@ -120,6 +120,18 @@ function daysInMonth(year: number, month: number) { return new Date(year, month, 0).getDate(); } +function shiftPeriod(period: PERIODS, from: Date, offset: number): Date { + switch (period) { + case PERIODS.DAY: + return new Date(from.getFullYear(), from.getMonth(), from.getDate() + offset); + case PERIODS.YEAR: + return new Date(from.getFullYear() + offset, 0, 1); + case PERIODS.MONTH: + default: + return new Date(from.getFullYear(), from.getMonth() + offset, 1); + } +} + export default defineComponent({ name: "History", components: { @@ -204,18 +216,7 @@ export default defineComponent({ } }, to(): Date { - switch (this.effectivePeriod) { - case PERIODS.DAY: { - const d = new Date(this.from); - d.setDate(d.getDate() + 1); - return d; - } - case PERIODS.YEAR: - return new Date(this.effectiveYear + 1, 0, 1); - case PERIODS.MONTH: - default: - return new Date(this.effectiveYear, this.effectiveMonth, 1); - } + return shiftPeriod(this.effectivePeriod, this.from, 1); }, aggregate(): string { switch (this.effectivePeriod) { @@ -466,27 +467,43 @@ export default defineComponent({ }, async fetchData() { this.loading = true; + const requestKey = this.fetchKey; const requestFrom = this.from; const requestTo = this.to; const requestPeriod = this.effectivePeriod; try { - const res = await api.get("history/energy", { - params: { - from: requestFrom.toISOString(), - to: requestTo.toISOString(), - aggregate: this.aggregate, - grouped: false, - }, - }); + const res = await this.fetchEnergy(requestFrom, requestTo); + // a newer request superseded this one + if (requestKey !== this.fetchKey) return; this.rawSeries = res.data || []; this.displayFrom = requestFrom; this.displayTo = requestTo; this.displayPeriod = requestPeriod; + this.prefetchAdjacent(); } catch (e) { console.error("Failed to load energy history", e); // keep previous data on error } finally { - this.loading = false; + if (requestKey === this.fetchKey) this.loading = false; + } + }, + fetchEnergy(from: Date, to: Date) { + return api.get("history/energy", { + params: { + from: from.toISOString(), + to: to.toISOString(), + aggregate: this.aggregate, + grouped: false, + }, + }); + }, + // warm the browser http cache for the prev/next period + prefetchAdjacent() { + for (const offset of [-1, 1]) { + const from = shiftPeriod(this.effectivePeriod, this.from, offset); + const to = shiftPeriod(this.effectivePeriod, this.from, offset + 1); + if (from > new Date() || to <= this.startDate) continue; + this.fetchEnergy(from, to).catch(() => {}); } }, buildBaseQuery(): Record { diff --git a/server/http_history_handler.go b/server/http_history_handler.go index bcb91953e..5d513f8e0 100644 --- a/server/http_history_handler.go +++ b/server/http_history_handler.go @@ -3,11 +3,13 @@ package server import ( "context" "errors" + "fmt" "net/http" "time" "github.com/evcc-io/evcc/core/metrics" "github.com/evcc-io/evcc/server/db" + "github.com/evcc-io/evcc/tariff" "github.com/evcc-io/evcc/util/locale" "golang.org/x/text/language" ) @@ -77,6 +79,10 @@ func energyHistoryHandler(w http.ResponseWriter, r *http.Request) { return } + // data only changes at the next slot boundary + maxAge := time.Until(time.Now().Truncate(tariff.SlotDuration).Add(tariff.SlotDuration)) + w.Header().Set("Cache-Control", fmt.Sprintf("private, max-age=%d", int(maxAge.Seconds()))) + jsonWrite(w, res) }