From f4e4abd9786fedf5c91c45d41b0ed49adddf23ad Mon Sep 17 00:00:00 2001 From: Peter Meier Date: Mon, 4 May 2026 23:35:13 +0200 Subject: [PATCH] fix(strommix): pick price at "now" + surface cross-border timestamp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audit against ground truth at 22:15 lokal exposed three real bugs: 1. Day-ahead price was wrong (widget 125, real 153 €/MWh). The CMS' `last_non_null` transform always picks the last cell of the 96-slot array — typically a slot in the future, not the slot covering "now". Bypass the CMS for prices: aggregator fetches energy-charts /price directly, walks unix_seconds, and picks the largest start ≤ now. `priceMeasuredAtUnix` is surfaced so the widget can show the slot timestamp as a sanity check. 2. Cross-border saldo (cbpf) typically lags 1.5–2 h. Widget labelled it as "jetzt" even when the upstream's last datapoint was 90 min old. Now the cross-border timestamp travels through the aggregator and the widget renders "Stand 20:30 Uhr · verzögert" when the gap to the production snapshot exceeds 30 min. 3. Bucket label "Dunkelflaute" misuses the term — the formal definition needs ≥24 h history, not a single threshold crossing. Updated the strommix-default content entry to use "Sehr niedrige Einspeisung" / "Very low feed-in" instead, and explained the reasoning in a comment so editors don't re-introduce the misnomer. Aggregator response shape gained `crossborderMeasuredAtUnix` + `priceMeasuredAtUnix`; the widget renders both as small timestamps under their values, plus a "verzögert" hint when cross-border data is older than the 30-min tolerance window. Cache TTLs on the price external collections lowered from 30 min to 15 min so the admin-side cache flips at the same cadence as the day-ahead slots (since 30.09.2025 prices come in 15-min buckets). --- .../components/blocks/StrommixBlock.svelte | 21 ++++++ src/lib/strommix.ts | 13 +++- src/routes/api/strommix/+server.ts | 65 ++++++++++++++++--- 3 files changed, 89 insertions(+), 10 deletions(-) diff --git a/src/lib/components/blocks/StrommixBlock.svelte b/src/lib/components/blocks/StrommixBlock.svelte index c63ddfd..efc0435 100644 --- a/src/lib/components/blocks/StrommixBlock.svelte +++ b/src/lib/components/blocks/StrommixBlock.svelte @@ -99,6 +99,15 @@ return d.toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" }); } + /** Cross-border data is considered stale (and worth flagging) when its + * timestamp is more than 30 min behind the production-snapshot. cbpf + * typically lags 1.5–2 h, so this is the rule rather than the + * exception — surfacing the stamp keeps the widget honest. */ + const cbStale = $derived.by(() => { + if (!live?.measuredAtUnix || !live?.crossborderMeasuredAtUnix) return false; + return live.measuredAtUnix - live.crossborderMeasuredAtUnix > 30 * 60; + }); + const bucketAccent = $derived.by(() => { if (!bucket) return "neutral"; return bucket === "dunkelflaute" @@ -198,6 +207,7 @@
Konventionell jetzt
{fmtMw(live.conventionalMw)}
+
Braunkohle + Steinkohle + Erdgas + Kernkraft
@@ -208,6 +218,12 @@ {#if importDirection === "Export"}↑{/if} {fmtGw(live.netFlowGw)}
+ {#if live.crossborderMeasuredAtUnix} +
+ Stand: {fmtTime(live.crossborderMeasuredAtUnix)} Uhr + {#if cbStale} · verzögert{/if} +
+ {/if}
@@ -218,6 +234,11 @@ {#if enableFr && live.priceFrEurMwh != null} (Frankreich: {fmtPrice(live.priceFrEurMwh)}) {/if} + {#if live.priceMeasuredAtUnix} + + · Slot {fmtTime(live.priceMeasuredAtUnix)} + + {/if} {#if (live.priceDeEurMwh ?? 0) < 0}