fix(strommix): show sparkline without history backend
Deploy / verify (push) Successful in 55s
Deploy / deploy (push) Successful in 57s

The strommix_history_de collection source URL 404s (Energy-Charts needs
start/end params), so the CMS history backend stays empty and todayHourly
was always null — making the Wind & Solar sparkline never appear.

Extend fetchEnergyChartsPublicPower to start at midnight (was: -2h) and
return the raw series alongside the snapshot values. Build todayHourly
from it when the history backend has no items. No extra API call.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Peter Meier
2026-05-13 07:58:34 +02:00
parent 10fb2a3ef6
commit 81e419aa4d
+49 -17
View File
@@ -46,13 +46,6 @@ type StrommixRaw = {
measured_at_unix?: number | null;
};
/**
* Fetch Energy-Charts /public_power directly with an explicit time
* window. The CMS-cached `strommix_de` schema cannot do this because
* external-collections v1 has no runtime-vars; without start/end the
* upstream returns 404 ("no content available"). 2-hour window covers
* latest 8 quarter-hour slots; we pick the latest non-null per series.
*/
type PublicPowerSeries = { name: string; data: (number | null)[] };
type PublicPowerResp = {
unix_seconds?: number[];
@@ -67,25 +60,33 @@ function lastNonNull(arr: (number | null)[]): number | null {
return null;
}
/**
* Fetch Energy-Charts /public_power with an explicit time window.
* `fromSec` is the window start (pass `dayStartSec` to cover all of today).
* Returns both the current-snapshot values (for live numbers) and the raw
* series (for the today-hourly sparkline when the history backend is empty).
*/
async function fetchEnergyChartsPublicPower(
fetcher: typeof fetch
): Promise<{ data: StrommixRaw | null; stale: boolean }> {
fetcher: typeof fetch,
fromSec: number
): Promise<{ data: StrommixRaw | null; rawSeries: PublicPowerResp | null; stale: boolean }> {
try {
const nowSec = Math.floor(Date.now() / 1000);
const fmt = (sec: number) =>
new Date(sec * 1000).toISOString().slice(0, 16) + "Z";
const start = fmt(nowSec - 2 * 60 * 60);
const start = fmt(fromSec);
const end = fmt(nowSec);
const url =
`https://api.energy-charts.info/public_power?country=de` +
`&start=${encodeURIComponent(start)}&end=${encodeURIComponent(end)}`;
const res = await fetcher(url, { signal: AbortSignal.timeout(8_000) });
if (!res.ok) return { data: null, stale: false };
if (!res.ok) return { data: null, rawSeries: null, stale: false };
const d = (await res.json()) as PublicPowerResp;
const find = (n: string) =>
d.production_types?.find((p) => p.name === n)?.data ?? [];
const ts = d.unix_seconds ?? [];
return {
rawSeries: d,
data: {
wind_onshore_mw: lastNonNull(find("Wind onshore")),
wind_offshore_mw: lastNonNull(find("Wind offshore")),
@@ -102,10 +103,36 @@ async function fetchEnergyChartsPublicPower(
stale: false,
};
} catch {
return { data: null, stale: true };
return { data: null, rawSeries: null, stale: true };
}
}
/** Build today's sparkline series directly from an Energy-Charts response.
* Used as fallback when the CMS history backend has no data yet. */
function buildTodayHourlyFromEC(
d: PublicPowerResp,
dayStartSec: number
): ResidualLoadPoint[] {
const ts = d.unix_seconds ?? [];
const find = (n: string) =>
d.production_types?.find((p) => p.name === n)?.data ?? [];
const load = find("Load");
const wOn = find("Wind onshore");
const wOff = find("Wind offshore");
const sol = find("Solar");
const result: ResidualLoadPoint[] = [];
for (let i = 0; i < ts.length; i++) {
if (ts[i] < dayStartSec) continue;
if (load[i] == null) continue;
result.push({
unix: ts[i],
loadMw: num(load[i]),
weatherRenewableMw: num(wOn[i]) + num(wOff[i]) + num(sol[i]),
});
}
return result.sort((a, b) => a.unix - b.unix);
}
type CrossborderRaw = {
flow_at_gw?: number | null;
flow_be_gw?: number | null;
@@ -501,10 +528,10 @@ export const GET: RequestHandler = async ({ fetch }) => {
] = await Promise.all([
fetchSmardSnapshot(fetch),
// Energy-Charts /public_power direct (with explicit time range —
// upstream now returns 404 without start/end). Used as per-field
// fallback when SMARD's load filter (id 410) lags 12+ hours behind
// the production filters and the SMARD snapshot gets discarded.
fetchEnergyChartsPublicPower(fetch),
// upstream returns 404 without start/end). Window starts at midnight so
// the full-day series doubles as the sparkline fallback when the CMS
// history backend is empty. Used as per-field fallback when SMARD lags.
fetchEnergyChartsPublicPower(fetch, dayStartSec),
fetchOne<CrossborderRaw>("crossborder_de", fetch),
fetchPriceAtNow("DE-LU", fetch),
fetchPriceAtNow("FR", fetch),
@@ -599,10 +626,15 @@ export const GET: RequestHandler = async ({ fetch }) => {
// Historical aggregates derived from CMS `_history`. `null` when the
// backing collection is unavailable (e.g. CMS not yet upgraded with
// history-mode schemas) — widget renders without the affected panels.
// Build today's sparkline series. Prefer the CMS history backend (full
// deduped timeline); fall back to the Energy-Charts direct response when
// history is empty (e.g. backfill not yet run on a fresh install).
const todayHourly =
histStrommix && histStrommix.items.length > 0
? buildTodayHourly(histStrommix.items)
: null;
: energyCharts.rawSeries
? buildTodayHourlyFromEC(energyCharts.rawSeries, dayStartSec)
: null;
const negPriceHoursYtd = histPriceDe ? countNegPriceSlots(histPriceDe.items) : null;
const crossborderYtd = histCrossborder
? aggregateCrossborderRange(histCrossborder.items, yearStartSec)