diff --git a/haos-addon/config.yaml b/haos-addon/config.yaml
index d87fb8c..ee223de 100644
--- a/haos-addon/config.yaml
+++ b/haos-addon/config.yaml
@@ -1,5 +1,5 @@
name: ShineBridge
-version: "1.9.5"
+version: "1.9.6"
slug: shinebridge
description: Energiemanagement für Growatt/Goodwe-Wechselrichter — PV-Überschussladen, Wallbox-EMS, Tarifvergleich, Zigbee2MQTT-Überschussgeräte
url: https://gitea.bitfire.work/retr0/shinebridge
diff --git a/haos-addon/src/web/index.html b/haos-addon/src/web/index.html
index 6efa598..c540942 100644
--- a/haos-addon/src/web/index.html
+++ b/haos-addon/src/web/index.html
@@ -736,6 +736,122 @@ function renderEnergy(inverters, aggregates, period, spotData) {
// ── Finance Tab ───────────────────────────────────────────────
+// Erzeugt plausible, aber frei erfundene Tagesdaten fürs Beispiel-Diagramm im
+// Leerzustand (Erstinstallation). Deterministisch (kein echter Zufall), damit
+// die Vorschau bei jedem Laden gleich aussieht statt bei jedem Reload zu springen.
+function exampleFinanceDays(n) {
+ const out = [];
+ const today = new Date();
+ for (let i = n - 1; i >= 0; i--) {
+ const d = new Date(today); d.setDate(d.getDate() - i);
+ const wave = 4.2 + 2.6 * Math.sin(i * 0.6) + (i % 3 === 0 ? 1.4 : 0);
+ const fixed = Math.max(1.5, wave);
+ const spot = Math.max(0.8, fixed * (0.68 + 0.1 * Math.cos(i * 0.9)));
+ out.push({ date: d.toISOString().slice(0, 10), fixed_eur: +fixed.toFixed(2), spot_eur: +spot.toFixed(2) });
+ }
+ return out;
+}
+
+// Baut das Kosten-Balkendiagramm (echte oder Beispiel-Daten). Aggregiert ab
+// 14 Wochen (98 Tagen) angesammelter Historie automatisch von Tages- auf
+// Wochenbalken; zeigt davon je nach verfügbarer Breite die letzten 10–28
+// Perioden (rollierendes Fenster — ältere Balken wandern rechts raus, sobald
+// neue dazukommen).
+function buildCostChart(days, el, isExample) {
+ const C = isExample
+ ? { fixed: '#5b6472', spot: '#6b6155', grid: '#262b33' }
+ : { fixed: '#58a6ff', spot: '#f0883e', grid: '#30363d' };
+
+ const rawN = days.length;
+ const bucket = rawN >= 98 ? 7 : 1;
+ const allPeriods = bucket === 1 ? days.map(d => ({
+ fixed_eur: d.fixed_eur, spot_eur: d.spot_eur, label: d.date.slice(5),
+ })) : (() => {
+ const out = [];
+ for (let i = 0; i < days.length; i += bucket) {
+ const chunk = days.slice(i, i + bucket);
+ const fixed = chunk.reduce((s, d) => s + (d.fixed_eur || 0), 0);
+ const spotVals = chunk.map(d => d.spot_eur).filter(v => v != null);
+ const spot = spotVals.length ? spotVals.reduce((a, b) => a + b, 0) : null;
+ out.push({ fixed_eur: fixed, spot_eur: spot, label: chunk[0].date.slice(5) });
+ }
+ return out;
+ })();
+
+ // Container-Breite messen und die SVG in echten Pixeln in genau dieser Breite
+ // zeichnen (keine viewBox-Skalierung) — Höhe bleibt dabei fix, unabhängig von
+ // der Breite. Auf breiten Screens werden mehr Perioden gezeigt statt dieselben
+ // künstlich zu verbreitern.
+ const H = 240, PL = 40, PR = 8, PT = 14, PB = 46;
+ const measuredW = el.clientWidth || 380;
+ const W = Math.max(300, measuredW);
+ const cW = W - PL - PR;
+ const IDEAL_SLOT = 24; // px/Periode, entspricht dem Slot, der auf Handy-Breite gut aussah
+ const maxPeriods = Math.min(28, Math.max(10, Math.round(cW / IDEAL_SLOT)));
+ const periods = allPeriods.slice(-maxPeriods);
+
+ const n = periods.length;
+ const cH = H - PT - PB;
+ const slot = cW / n;
+ const barW = Math.max(3, Math.min(26, slot * 0.36));
+ const barGap = Math.max(1.5, slot * 0.08);
+ const rBar = Math.min(2.5, barW / 2);
+
+ const allVals = periods.flatMap(d => [d.fixed_eur, d.spot_eur].filter(v => v != null));
+ const maxVal = Math.max(...allVals, 0.01);
+
+ const yTicks = 4;
+ let gridLines = '', yLabels = '';
+ for (let i = 0; i <= yTicks; i++) {
+ const v = maxVal * i / yTicks;
+ const y = PT + cH - (cH * i / yTicks);
+ gridLines += ``;
+ yLabels += `${fEur(v)}`;
+ }
+
+ const gradId = isExample ? 'Ex' : '';
+ let bars = '', xLabels = '';
+ periods.forEach((d, i) => {
+ const cx = PL + i * slot + slot / 2;
+ const hFixed = Math.max(0, cH * d.fixed_eur / maxVal);
+ bars += ``;
+ if (d.spot_eur != null) {
+ const hSpot = Math.max(0, cH * d.spot_eur / maxVal);
+ bars += ``;
+ }
+ const ly = H - PB + 12;
+ xLabels += `${d.label}`;
+ });
+
+ const defs = `
+
+
+
+
+
+
+
+
+ `;
+
+ const bucketLabel = bucket === 1 ? 'Tag' : 'Woche';
+ const rangeNote = allPeriods.length > n ? ` · letzte ${n}` : '';
+ const legend = `
+ Festpreis
+ Spot (hypothetisch)
+
`;
+
+ return `
+
+
Kosten je ${bucketLabel}${rangeNote}
+
+ ${legend}
+ ${isExample ? `
BEISPIEL
` : ''}
+
`;
+}
+
async function loadFinance() {
const el = document.getElementById("finance-content");
let data;
@@ -746,10 +862,14 @@ async function loadFinance() {
const { days, fixed_total_eur, spot_total_eur, savings_eur, period_start, spot_days, total_days } = data;
if (!days || days.length === 0) {
- el.innerHTML = `
-
📊
-
Noch keine Daten
-
Der Tracker sammelt ab morgen täglich Daten.
Abrechnungsjahr ab ${period_start}.
+ const exampleChart = buildCostChart(exampleFinanceDays(14), el, true);
+ el.innerHTML = `
+
+
📊
+
Noch keine echten Daten
+
Der Tracker sammelt ab morgen täglich Daten.
Abrechnungsjahr ab ${period_start}. Unten siehst du, wie die Ansicht dann aussieht.
+
+ ${exampleChart}
`;
return;
}
@@ -796,99 +916,7 @@ async function loadFinance() {
${spotCard}${savCard}
`;
- // ── SVG-Balkendiagramm ────────────────────────────────────────
- // Bei langen Abrechnungsjahren nicht jeden einzelnen Tag zeigen — statt 112
- // Tagesbalken lieber Wochenbalken.
- const rawN = days.length;
- const bucket = rawN > 180 ? 14 : rawN > 21 ? 7 : 1;
- const allPeriods = bucket === 1 ? days.map(d => ({
- fixed_eur: d.fixed_eur, spot_eur: d.spot_eur, label: d.date.slice(5),
- })) : (() => {
- const out = [];
- for (let i = 0; i < days.length; i += bucket) {
- const chunk = days.slice(i, i + bucket);
- const fixed = chunk.reduce((s, d) => s + (d.fixed_eur || 0), 0);
- const spotVals = chunk.map(d => d.spot_eur).filter(v => v != null);
- const spot = spotVals.length ? spotVals.reduce((a, b) => a + b, 0) : null;
- out.push({ fixed_eur: fixed, spot_eur: spot, label: chunk[0].date.slice(5) });
- }
- return out;
- })();
-
- // Container-Breite messen und die SVG in echten Pixeln in genau dieser Breite
- // zeichnen (keine viewBox-Skalierung mehr) — Höhe bleibt dabei fix, unabhängig
- // von der Breite. Statt auf breiten Screens dieselben 14 Balken künstlich
- // aufzublasen, wird stattdessen die Anzahl gezeigter Perioden an die Breite
- // angepasst — mehr Platz zeigt mehr Wochen, bei einer angenehmen Zielbreite
- // pro Balkenpaar (~46px), nicht überbreite Einzelbalken.
- const H = 240, PL = 40, PR = 8, PT = 14, PB = 46;
- const measuredW = el.clientWidth || 380;
- const W = Math.max(300, measuredW);
- const cW = W - PL - PR;
- const IDEAL_SLOT = 24; // px/Periode, entspricht dem Slot, der auf Handy-Breite gut aussah
- const maxPeriods = Math.min(28, Math.max(10, Math.round(cW / IDEAL_SLOT)));
- const periods = allPeriods.slice(-maxPeriods);
-
- const n = periods.length;
- const cH = H - PT - PB;
- const slot = cW / n;
- const barW = Math.max(3, Math.min(26, slot * 0.36));
- const barGap = Math.max(1.5, slot * 0.08);
- const rBar = Math.min(2.5, barW / 2);
-
- const allVals = periods.flatMap(d => [d.fixed_eur, d.spot_eur].filter(v => v != null));
- const maxVal = Math.max(...allVals, 0.01);
-
- const yTicks = 4;
- let gridLines = '', yLabels = '';
- for (let i = 0; i <= yTicks; i++) {
- const v = maxVal * i / yTicks;
- const y = PT + cH - (cH * i / yTicks);
- gridLines += ``;
- yLabels += `${fEur(v)}`;
- }
-
- // Labels schräg statt horizontal — braucht bei 14 Balken pro Label deutlich
- // weniger Breite, dadurch bleiben trotz Platzmangel alle 14 lesbar.
- let bars = '', xLabels = '';
- periods.forEach((d, i) => {
- const cx = PL + i * slot + slot / 2;
- const hFixed = Math.max(0, cH * d.fixed_eur / maxVal);
- bars += ``;
- if (d.spot_eur != null) {
- const hSpot = Math.max(0, cH * d.spot_eur / maxVal);
- bars += ``;
- }
- const ly = H - PB + 12;
- xLabels += `${d.label}`;
- });
-
- const defs = `
-
-
-
-
-
-
-
-
- `;
-
- const bucketLabel = bucket === 1 ? 'Tag' : bucket === 7 ? 'Woche' : '2 Wochen';
- const rangeNote = allPeriods.length > n ? ` · letzte ${n}` : '';
- const legend = `
- Festpreis
- Spot (hypothetisch)
-
`;
-
- const chartSection = `
-
-
Kosten je ${bucketLabel}${rangeNote}
-
- ${legend}
-
`;
+ const chartSection = buildCostChart(days, el, false);
el.innerHTML = `${empfehlung}${cards}${chartSection}
`;
} catch(e) { el.innerHTML = 'Fehler beim Laden
'; console.error('loadFinance:', e); }