Files
windwiderstand/src/lib/strommix.ts
T
Peter Meier 4790d571fd feat(strommix): hero variant, 24h sparkline, trend, polishing
- New "hero" variant between compact and full: pct + status +
  sparkline + trend, no sub-cards or accordion
- 24h Wind+Solar sparkline below hero pct, accent-colored
- Trend arrow vs previous hour with delta percentage points
- Polling pauses on tab hidden, refetches on visibility return
  if last fetch >5min old
- Skeleton loader replaces "Lade…" text (matches final layout)
- Retry button on error state (silent vs explicit fetch)
- Stale-flag now prominent red badge in header bar
- Stand-time wrapped in <time datetime title> for full-date hover
- loadMw=0 falls back to last hourly slot instead of red error
- Negative-price banner replaces standalone price card (dedup)
- Production breakdown grid: clean auto/1fr label-value alignment
- Print CSS: details auto-open, chevrons hidden
- All German strings moved to translation keys
- STROMMIX_DEFAULT_THRESHOLDS const as single source for buckets

Includes data-block="LiveStrommix" attribute for the new
identifier scheme + new RenewableSparkline.svelte component.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 15:44:57 +02:00

122 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Shared types for the Strommix-Live-Widget.
*
* The aggregator route at `/api/strommix` builds this payload from the
* four CMS external collections; the `StrommixBlock.svelte` component
* consumes it.
*/
/** Default percentage thresholds (Wind+Solar / Last × 100) for bucket
* classification. Single source of truth — both the CMS schema
* (`live_strommix.json5`) and the block (`StrommixBlock.svelte`) read
* these. Editor can override per-instance via `threshold_*` fields. */
export const STROMMIX_DEFAULT_THRESHOLDS = {
dunkelflaute: 15,
niedrig: 35,
mittel: 60,
} as const;
export type StrommixResponse = {
// Production-snapshot timestamp (Unix sec) — used as the "anchor" for
// the rest of the values. Cross-border + prices may have their own,
// earlier or later, timestamps.
measuredAtUnix: number | null;
/** Where production + load came from. `smard` = pure SMARD (canonical);
* `smard+energy-charts` = SMARD with Energy-Charts as per-field
* fallback for filters SMARD couldn't resolve at the snapshot's slot
* (typically the load filter when SMARD's pipeline lags more than
* the production filters); `energy-charts` = SMARD entirely
* unreachable, full fallback. */
dataSource: "smard" | "smard+energy-charts" | "energy-charts";
// Production (MW)
windOnshoreMw: number;
windOffshoreMw: number;
solarMw: number;
biomassMw: number;
hydroMw: number;
ligniteMw: number;
hardCoalMw: number;
gasMw: number;
nuclearMw: number;
// Aggregates (MW)
loadMw: number;
weatherRenewableMw: number;
conventionalMw: number;
// Cross-border (GW; positive = net export from DE, negative = import)
netFlowGw: number;
/** When was the cross-border saldo last reported by upstream? cbpf
* typically lags 12 h behind real time. Widget compares to
* `measuredAtUnix` to decide whether to show "Stand HH:MM". */
crossborderMeasuredAtUnix: number | null;
flowsByCountry: Record<string, number>;
// Prices (EUR/MWh; null if upstream unavailable). Picked from the
// 15-min day-ahead slot whose interval contains "now", not the last
// non-null cell of the array.
priceDeEurMwh: number | null;
priceFrEurMwh: number | null;
/** Unix sec of the price-slot start that produced `priceDeEurMwh`. */
priceMeasuredAtUnix: number | null;
// Stale flags surfaced from the CMS' `x-rustycms-external-stale` header
staleStrommix: boolean;
staleCrossborder: boolean;
stalePrice: boolean;
// ---------------------------------------------------------------------
// Historical aggregates (sourced from CMS `_history` endpoint over
// raw upstream snapshots; null = no data yet for this widget).
// ---------------------------------------------------------------------
/** Today's residual-load curve. One entry per upstream slot (typically
* 15 min). `loadMw - weatherRenewableMw` = the gap conventional plants
* + imports must fill. `null` when history backend is empty / disabled. */
todayHourly: ResidualLoadPoint[] | null;
/** Negative-price hours year-to-date (DE-LU, day-ahead). Counted by
* iterating history snapshots and summing 15-min slots whose price
* was below 0. */
negPriceHoursYtd: number | null;
/** Cross-border net balance YTD in GWh (positive = net export from DE,
* negative = net import). */
netCrossborderGwhYtd: number | null;
/** Per-country YTD balance, same sign convention as `netFlowGw`. */
crossborderByCountryGwhYtd: Record<string, number> | null;
/** Cross-border balance for *today only*, GWh per country and total.
* Same sign convention. `null` when crossborder history is empty. */
crossborderTodayGwh: { total: number; perCountry: Record<string, number> } | null;
/** Most recent contiguous run of `(wind+solar) / load < 10%`. `null`
* when no such run exists in retained history. `endedHoursAgo: 0`
* means the run is still ongoing (last slot is within the threshold). */
lastDunkelflaute: DunkelflautePhase | null;
/** Top 5 longest Dunkelflaute phases in retained history, newest first
* if equal length. Empty array when history exists but no qualifying
* phase found. `null` when history is unavailable. */
worstDunkelflauten: DunkelflautePhase[] | null;
};
/** One upstream slot in the residual-load series. */
export type ResidualLoadPoint = {
/** Unix seconds at the start of the slot. */
unix: number;
/** Total electrical load on the German grid (MW). */
loadMw: number;
/** Wind onshore + wind offshore + solar (MW). */
weatherRenewableMw: number;
};
/** A contiguous low-renewables run, computed from history snapshots. */
export type DunkelflautePhase = {
/** Unix sec of the first slot below the threshold. */
startUnix: number;
/** Run duration in hours (slot count × slot length). */
durationH: number;
/** Lowest Wind+Solar share observed during the run, percentage of load. */
minPct: number;
/** Hours since the run ended (0 = ongoing). */
endedHoursAgo: number;
};