Compare commits

...

3 Commits

Author SHA1 Message Date
admin 8873ffe9d3 Crash endgueltig ausschliessen: keine Element-Destrukturierung mehr
Summenzeile (VerbrauchCharts) und Balken-Einfaerbung (BarChart) greifen die
Werte jetzt per Index mit Array.isArray-Guard ab, statt Elemente per [x,y] zu
destrukturieren. Damit kann "(destructured parameter) is not iterable" auch bei
unerwarteten Datenelementen nicht mehr auftreten.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 16:03:03 +02:00
admin d711c14b59 Fix: Darstellung robust gegen unerwartete Datenelemente
TypeError "(destructured parameter) is not iterable" bei der Summenberechnung
(data.reduce mit [, v]) konnte die Seite crashen, wenn data ein Element
enthielt, das kein [number, number]-Tupel ist.

Loesung: Daten beim Laden in fetchData validieren (nur valide Tupel werden
uebernommen). Damit sind sowohl die Summenzeile als auch BarChart robust.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 15:53:14 +02:00
admin a3a8743998 Navigations-Pfeile an allen Datumseingaben
Links/Rechts-Pfeile (zurueck/vorwaerts) neben jeder Datumsauswahl fuer
schnelle Navigation. Schrittweite je nach Auswahl: Tag (24h, 7d), Monat
(31d), Jahr (365d). Nach dem Klick wird direkt neu geladen.

- Vorwaerts-Pfeil wird deaktiviert, sobald der aktuelle Zeitraum erreicht
  ist (keine Navigation in die Zukunft)
- bei leerem 24h-Feld (rollierend) wird ab heute gerechnet

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 15:47:04 +02:00
2 changed files with 77 additions and 6 deletions
+5 -3
View File
@@ -91,9 +91,11 @@ export default function BarChart({
// markierte Balken (z.B. Ladetage) bekommen eine eigene Farbe // markierte Balken (z.B. Ladetage) bekommen eine eigene Farbe
data: data:
highlight && highlight.size highlight && highlight.size
? data.map(([x, y]) => ? data.map((p) => {
highlight.has(x) ? { x, y, color: highlightColor } : { x, y } const x = Array.isArray(p) ? p[0] : 0;
) const y = Array.isArray(p) ? p[1] : 0;
return highlight.has(x) ? { x, y, color: highlightColor } : { x, y };
})
: data, : data,
color, color,
}, },
+72 -3
View File
@@ -22,6 +22,22 @@ function thisMonthStr(): string {
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`; return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`;
} }
function fmtDate(d: Date): string {
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
}
function addDays(dateStr: string, delta: number): string {
const d = dateStr ? new Date(`${dateStr}T00:00:00`) : new Date();
d.setDate(d.getDate() + delta);
return fmtDate(d);
}
function addMonths(monthStr: string, delta: number): string {
const [y, m] = (monthStr || thisMonthStr()).split('-').map(Number);
const d = new Date(y, m - 1 + delta, 1);
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`;
}
const CURRENT_YEAR = new Date().getFullYear(); const CURRENT_YEAR = new Date().getFullYear();
const YEARS = Array.from({ length: 11 }, (_, i) => CURRENT_YEAR - i); const YEARS = Array.from({ length: 11 }, (_, i) => CURRENT_YEAR - i);
@@ -94,7 +110,18 @@ function ChartCard({ range, every, title, color, selector }: ChartCardProps) {
if (!res.ok) throw new Error(`HTTP ${res.status}`); if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json(); const json = await res.json();
if (json.success) { if (json.success) {
setData(json.data); // nur valide [number, number]-Tupel uebernehmen (robust gegen
// unerwartete Elemente, damit die Darstellung nie crasht)
const clean: VerbrauchPoint[] = Array.isArray(json.data)
? (json.data as unknown[]).filter(
(p): p is VerbrauchPoint =>
Array.isArray(p) &&
p.length >= 2 &&
typeof p[0] === 'number' &&
typeof p[1] === 'number'
)
: [];
setData(clean);
} else { } else {
setError(json.error || 'Fehler beim Laden der Daten.'); setError(json.error || 'Fehler beim Laden der Daten.');
} }
@@ -118,6 +145,25 @@ function ChartCard({ range, every, title, color, selector }: ChartCardProps) {
fetchLaden(value); fetchLaden(value);
}; };
// Navigation per Pfeil: einen Tag/Monat/Jahr zurueck (-1) oder vor (+1)
const step = (delta: number) => {
let nv: string;
if (selector === 'month') nv = addMonths(value, delta);
else if (selector === 'year') nv = String(Number(value || CURRENT_YEAR) + delta);
else nv = addDays(value, delta); // date24 + date
setValue(nv);
fetchData(nv);
fetchLaden(nv);
};
// Vorwaerts nicht in die Zukunft
const canForward =
selector === 'month'
? (value || thisMonthStr()) < thisMonthStr()
: selector === 'year'
? Number(value || CURRENT_YEAR) < CURRENT_YEAR
: (value || todayStr()) < todayStr();
// Jahres-Chart: Achse immer ueber das komplette Kalenderjahr -> alle 12 Monate // Jahres-Chart: Achse immer ueber das komplette Kalenderjahr -> alle 12 Monate
// sichtbar, Restmonate bleiben ohne Balken. // sichtbar, Restmonate bleiben ohne Balken.
let xMin: number | undefined; let xMin: number | undefined;
@@ -131,8 +177,8 @@ function ChartCard({ range, every, title, color, selector }: ChartCardProps) {
} }
} }
// Gesamtverbrauch des angezeigten Zeitraums // Gesamtverbrauch des angezeigten Zeitraums (robust ohne Element-Destrukturierung)
const total = data.reduce((sum, [, v]) => sum + v, 0); const total = data.reduce((sum, p) => sum + (Array.isArray(p) ? Number(p[1]) || 0 : 0), 0);
const totalLabel = range === '365d' ? `Gesamtverbrauch ${value}` : 'Gesamtverbrauch'; const totalLabel = range === '365d' ? `Gesamtverbrauch ${value}` : 'Gesamtverbrauch';
return ( return (
@@ -147,6 +193,17 @@ function ChartCard({ range, every, title, color, selector }: ChartCardProps) {
{selector === 'year' && 'Jahr:'} {selector === 'year' && 'Jahr:'}
</label> </label>
{/* Pfeil zurueck */}
<button
type="button"
onClick={() => step(-1)}
aria-label="zurück"
title="zurück"
className="bg-[#85B7D7] hover:bg-[#6a9fc5] px-2.5 py-1.5 text-sm rounded shadow-sm transition-colors"
>
</button>
{selector === 'year' ? ( {selector === 'year' ? (
<select <select
value={value} value={value}
@@ -167,6 +224,18 @@ function ChartCard({ range, every, title, color, selector }: ChartCardProps) {
className="border border-gray-400 rounded px-3 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-gray-500" className="border border-gray-400 rounded px-3 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-gray-500"
/> />
)} )}
{/* Pfeil vorwaerts (nicht in die Zukunft) */}
<button
type="button"
onClick={() => step(1)}
disabled={!canForward}
aria-label="vorwärts"
title="vorwärts"
className="bg-[#85B7D7] hover:bg-[#6a9fc5] px-2.5 py-1.5 text-sm rounded shadow-sm transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
>
</button>
</div> </div>
<button <button