From e1b0b43c9b132e16275085d782a7f5815e233414 Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Sat, 14 Mar 2026 11:17:11 +0100 Subject: [PATCH 01/71] Introduce gridstack.js --- database.py | 6 +- routes.py | 36 +++++ static/css/style.css | 54 +++---- static/js/layout.js | 98 ++++++++++++ static/js/script.js | 45 +++--- templates/index.html | 362 ++++++++++++++++++++----------------------- 6 files changed, 355 insertions(+), 246 deletions(-) create mode 100644 static/js/layout.js diff --git a/database.py b/database.py index 61cbdda..b3bda0e 100644 --- a/database.py +++ b/database.py @@ -75,7 +75,11 @@ def init_db(): c.execute("SELECT COUNT(*) FROM prices") if c.fetchone()[0] == 0: c.execute("INSERT INTO prices (valid_from, price) VALUES ('2026-01-01', 0.329)") - + + # 5. User Settings Tabelle + c.execute('''CREATE TABLE IF NOT EXISTS user_settings + (key TEXT PRIMARY KEY, value TEXT)''') + conn.commit() conn.close() print("Datenbank erfolgreich initialisiert.") diff --git a/routes.py b/routes.py index c2edb38..5b7bba5 100644 --- a/routes.py +++ b/routes.py @@ -1049,3 +1049,39 @@ def shap_summary(): ] return jsonify(sorted(result, key=lambda x: x["mean_abs_shap"], reverse=True)) + +@api_bp.route('/api/layout', methods=['GET']) +def get_layout(): + # Jeder darf das Layout sehen (auch nicht angemeldete) + conn = sqlite3.connect('solar_data.db') + c = conn.cursor() + c.execute("SELECT value FROM user_settings WHERE key = 'dashboard_layout'") + row = c.fetchone() + conn.close() + + if row: + return jsonify({"layout": json.loads(row[0])}), 200 + return jsonify({"layout": None}), 404 + +@api_bp.route('/api/layout', methods=['POST']) +def save_layout(): + data = request.json + layout_json = data.get('layout') + password = data.get('pw') # Wir senden das PW zur Sicherheit mit + + # Berechtigung prüfen (wie in deinem /api/auth) + if password != ADMIN_PASS: + return jsonify({"error": "Nicht autorisiert"}), 403 + + if not layout_json: + return jsonify({"error": "Kein Layout gesendet"}), 400 + + conn = sqlite3.connect('solar_data.db') + c = conn.cursor() + # Speichern oder Überschreiben + c.execute("INSERT OR REPLACE INTO user_settings (key, value) VALUES (?, ?)", + ('dashboard_layout', json.dumps(layout_json))) + conn.commit() + conn.close() + + return jsonify({"status": "gespeichert"}), 200 \ No newline at end of file diff --git a/static/css/style.css b/static/css/style.css index d6afeb4..e6ae533 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -157,32 +157,29 @@ box-shadow 0.2s ease, transform 0.15s ease; } - - .main-grid { - display: grid; - grid-template-columns: 1fr 1fr 1fr; - gap: 20px; - row-gap: 25px; - margin-bottom: 25px; + + .grid-stack-item-content { + overflow-x: hidden; + overflow-y: auto; } - - /* Reihe 1 */ - .main-grid > *:nth-child(1) { grid-column: span 1; } - .main-grid > *:nth-child(2) { grid-column: span 2; } - - /* Reihe 2 & 3: Volle Breite */ - .main-grid > *:nth-child(3), - .main-grid > *:nth-child(4) { grid-column: span 3; } - - /* Reihe 4: 2fr zu 1fr */ - .main-grid > *:nth-child(5) { grid-column: span 2; } - .main-grid > *:nth-child(6) { grid-column: span 1; } - - /* AB Reihe 5 (Element 7 und alle folgenden): Volle Breite */ - .main-grid > *:nth-child(n + 7) { - grid-column: span 3; + + .edit-mode .grid-stack-item-content { + cursor: grab; + border: 1px dashed var(--accent); + } + + .edit-mode .grid-stack-item-content:active { + cursor: grabbing; } + :where(.card, .chart-card) + :where(.card, .chart-card) { + margin-top: 0; + } + + .chart-card { + height: 100%; /* Ersetzt die starren 400px */ + } + .card { background: var(--card-bg); border-radius: 24px; @@ -295,10 +292,6 @@ margin-top: 25px; } - .main-grid > :where(.card, .chart-card) { - margin-top: 0; - } - .card-peak { background: linear-gradient(135deg, #4b79ff 0%, #6dd5fa 100%); box-shadow: 0 15px 35px rgba(75, 121, 255, 0.25); @@ -384,13 +377,6 @@ } @media (max-width: 900px) { - .main-grid { - grid-template-columns: 1fr; - } - - .main-grid > * { - grid-column: span 1 !important; - } .top-controls { flex-direction: column; diff --git a/static/js/layout.js b/static/js/layout.js new file mode 100644 index 0000000..25ccc09 --- /dev/null +++ b/static/js/layout.js @@ -0,0 +1,98 @@ +let dashboardGrid; + +// 1. Grid initialisieren +function initGridstack() { + const isAdmin = sessionStorage.getItem('admin_pw') !== null; + + dashboardGrid = GridStack.init({ + cellHeight: 110, + margin: 20, + animate: true, + staticGrid: !isAdmin, // Wenn nicht Admin, dann gesperrt (kein Drag/Resize) + disableOneColumnMode: false, + oneColumnModeDomSort: true + }); + + // Falls das PW schon im Speicher war (nach Refresh), UI direkt anpassen + if (isAdmin) { + currentPw = sessionStorage.getItem('admin_pw'); + document.getElementById('unlockBtn').style.display = 'none'; + document.getElementById('adminArea').style.display = 'flex'; + // Hier ggf. loadTariffs() aufrufen, falls script.js schon geladen ist + } + + dashboardGrid.on('change', function(event, items) { + saveLayout(); + }); +} + +// 3. Layout speichern (Auth-Check + DB vs. LocalStorage) +async function saveLayout() { + if (!dashboardGrid) return; + const layoutData = dashboardGrid.save(); + + // Wir schauen nach, ob ein Passwort im SessionStorage liegt + // (Das müsstest du in deiner unlockAdmin() Funktion dort speichern) + const storedPw = sessionStorage.getItem('admin_pw'); + + if (storedPw) { + try { + const response = await fetch('/api/layout', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + layout: layoutData, + pw: storedPw + }) + }); + + if (response.ok) { + console.log("Layout in DB gespeichert."); + return; + } + } catch (e) { console.error("DB Save failed", e); } + } + + // Fallback: LocalStorage für Gäste + localStorage.setItem('balkonkraftwerk_layout', JSON.stringify(layoutData)); + console.log("Layout lokal im Browser gespeichert."); +} + +// 4. Layout beim Starten laden +async function loadLayout() { + let savedLayout = null; + + try { + // Zuerst versuchen, aus der Datenbank zu laden + const authResponse = await fetch('/api/auth'); + if (authResponse.ok) { + const layoutResponse = await fetch('/api/layout'); + if (layoutResponse.ok) { + const data = await layoutResponse.json(); + if (data.layout) savedLayout = data.layout; + } + } else { + throw new Error("Nicht angemeldet"); + } + } catch (error) { + // Fallback: Aus dem LocalStorage laden + const localData = localStorage.getItem('balkonkraftwerk_layout'); + if (localData) { + savedLayout = JSON.parse(localData); + } + } + + // Wenn ein Layout gefunden wurde, anwenden + if (savedLayout && dashboardGrid) { + dashboardGrid.load(savedLayout); + } +} + +// Beim Laden der Seite ausführen (füge das zu deinen anderen Init-Funktionen hinzu) +document.addEventListener("DOMContentLoaded", async () => { + initGridstack(); + await loadLayout(); + + // Nach dem Laden des Layouts kann es helfen, Chart.js einen Resize-Befehl zu geben + window.dispatchEvent(new Event('resize')); +}); \ No newline at end of file diff --git a/static/js/script.js b/static/js/script.js index d76de3e..4cdf4e8 100644 --- a/static/js/script.js +++ b/static/js/script.js @@ -25,27 +25,34 @@ async function checkLoadingStatus() { } } catch (e) {} } +let currentPw = null; async function unlockAdmin() { - const pw = prompt("Passwort zur Anpassung des Stromtarifs:"); - if (!pw) return; - const res = await fetch('/api/auth', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - pw: pw - }) - }); - if (res.ok) { - currentPw = pw; - document.getElementById('unlockBtn').style.display = 'none'; - document.getElementById('adminArea').style.display = 'flex'; - loadTariffs(); - } else { - alert("Falsches Passwort!"); - } + const pw = prompt("Passwort zur Anpassung des Stromtarifs & Layouts:"); + if (!pw) return; + + const res = await fetch('/api/auth', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ pw: pw }) + }); + + if (res.ok) { + currentPw = pw; + sessionStorage.setItem('admin_pw', pw); + + document.getElementById('unlockBtn').style.display = 'none'; + document.getElementById('adminArea').style.display = 'flex'; + + if (dashboardGrid) { + dashboardGrid.enable(); + document.getElementById('dashboard-grid').classList.add('edit-mode'); + } + + loadTariffs(); + } else { + alert("Falsches Passwort!"); + } } function closeAdmin() { diff --git a/templates/index.html b/templates/index.html index 6d17c01..786d455 100644 --- a/templates/index.html +++ b/templates/index.html @@ -5,7 +5,9 @@ Balkonkraftwerk Analytics + +
@@ -81,230 +83,205 @@

-
-
-
-
Daten werden geladen...
-
-
Gesamtertrag
-
0.00 kWh -
-
-
Ersparnis in Euro
-
0.00 €
-
-
-
-
- Live -
-
-
-
Leistung
-
0 W + +
+ +
+
+
+ -
-
-
Stromstärke
-
0.000 mA -
-
-
-
Spannung
-
0 V +
Gesamtertrag
+
0.00 kWh
+
+
Ersparnis in Euro
+
0.00 €
-
- -
- Verlauf & Ersparnis - Balken: kWh | Linie: € -
-
- + +
+
+
+
+ Live +
+
+
Leistung
0 W
+
Stromstärke
0.000 mA
+
Spannung
0 V
+
+
-
- -
- Panel-Vergleich (DC) - Stacked: kWh -
-
- + +
+
+
+ +
+ Verlauf & Ersparnis + Balken: kWh | Linie: € +
+
+ +
+
-
- -
-
Tagesverlauf & Dominanz der Panels (DC)
- -
-
-
- Panel 1 -
- Panel 2 + +
+
+
+ +
+ Panel-Vergleich (DC) + Stacked: kWh +
+
+ +
+
-
-
- Live Verteilung nach Panels (DC) -
-
- -
-
-
-
Panel 1
-
0%
-
-
-
Panel 2
-
0%
+ +
+
+
+ +
+
Tagesverlauf & Dominanz der Panels (DC)
+ +
+
+
+ Panel 1 +
+ Panel 2 +
-
-
Leistungs-Rekorde (AC)
-
- 0.0 - W -
-
-
-
Heute
-
0.0 W
-
-
-
All-Time
-
0.0 W
+ +
+
+
+
+ Live Verteilung nach Panels (DC) +
+
+ +
+
+
Panel 1
0%
+
Panel 2
0%
+
-
-
ROI Fortschritt
-
0,00 €
-
Gesamt erzeugt: 0 kWh
-
-
+ +
+
+
+
Leistungs-Rekorde (AC)
+
0.0 W
+
+
Heute
0.0 W
+
All-Time
0.0 W
+
+
-
0%
-
-
-
-
Jahres-Heatmap
- -
-
- -
- Wenig -
- Viel + +
+
+
+
ROI Fortschritt
+
0,00 €
+
Gesamt erzeugt: 0 kWh
+
+
0%
+
+
- - - -
-
Prognose
-

ML-basierte 7-Tage Prognose auf Basis von Ertrag, Bewölkung, Sonnenhöhe und Wettervorhersage.

-
-
-
7-Tage-Prognose
-
- kWh -
-
-
-
Ersparnis
-
- + +
+
+
+
+
Jahres-Heatmap
+
-
-
-
MAE
-
- kWh +
+
+ Wenig +
+ Viel
- - From 10b8c509a6c4e792a8efdcab82f3c7cb7f75d763 Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Sat, 14 Mar 2026 12:36:07 +0100 Subject: [PATCH 05/71] Change deploy process --- .github/workflows/deploy-staging.yml | 5 +++-- .github/workflows/deploy.yml | 5 +++-- static/js/script.js | 1 - templates/index.html | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml index 87e733b..c253b80 100644 --- a/.github/workflows/deploy-staging.yml +++ b/.github/workflows/deploy-staging.yml @@ -16,6 +16,7 @@ jobs: key: ${{ secrets.SSH_PRIVATE_KEY }} script: | cd /home/ubuntu/balkonkraftwerk-staging - git pull origin dev + git fetch origin dev + git reset --hard origin/dev sudo systemctl restart balkonkraftwerk-staging - echo "Staging Update erfolgreich. Test-App läuft auf Port 5001!" \ No newline at end of file + echo "Staging hart auf Stand von 'dev' gesetzt. Port 5001 läuft!" \ No newline at end of file diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 240c733..6c2cb33 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -16,6 +16,7 @@ jobs: key: ${{ secrets.SSH_PRIVATE_KEY }} script: | cd /home/ubuntu/ - git pull origin main + # Verhindert Divergenz-Fehler durch striktes Fast-Forward + git pull origin main --ff-only sudo systemctl restart balkonkraftwerk - echo "Update erfolgreich. App wurde neu gestartet!" \ No newline at end of file + echo "Produktiv-Update via Fast-Forward erfolgreich. App läuft!" \ No newline at end of file diff --git a/static/js/script.js b/static/js/script.js index 4cdf4e8..ab7fc0f 100644 --- a/static/js/script.js +++ b/static/js/script.js @@ -25,7 +25,6 @@ async function checkLoadingStatus() { } } catch (e) {} } -let currentPw = null; async function unlockAdmin() { const pw = prompt("Passwort zur Anpassung des Stromtarifs & Layouts:"); diff --git a/templates/index.html b/templates/index.html index 11cf402..786d455 100644 --- a/templates/index.html +++ b/templates/index.html @@ -288,7 +288,7 @@

- Source on GitHub! + Source on GitHub

From baa5ec83de8cf1a5257732aa96a1b86e7395db06 Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Sat, 14 Mar 2026 12:55:56 +0100 Subject: [PATCH 06/71] Gridstack Layout changes --- routes.py | 8 +++----- static/js/layout.js | 25 ++++++++++++++----------- static/js/script.js | 9 ++++++--- 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/routes.py b/routes.py index 5b7bba5..65c7f43 100644 --- a/routes.py +++ b/routes.py @@ -1052,16 +1052,14 @@ def shap_summary(): @api_bp.route('/api/layout', methods=['GET']) def get_layout(): - # Jeder darf das Layout sehen (auch nicht angemeldete) - conn = sqlite3.connect('solar_data.db') + conn = get_db_connection() c = conn.cursor() c.execute("SELECT value FROM user_settings WHERE key = 'dashboard_layout'") row = c.fetchone() conn.close() - if row: return jsonify({"layout": json.loads(row[0])}), 200 - return jsonify({"layout": None}), 404 + return jsonify({"layout": None}), 200 @api_bp.route('/api/layout', methods=['POST']) def save_layout(): @@ -1076,7 +1074,7 @@ def save_layout(): if not layout_json: return jsonify({"error": "Kein Layout gesendet"}), 400 - conn = sqlite3.connect('solar_data.db') + conn = get_db_connection() c = conn.cursor() # Speichern oder Überschreiben c.execute("INSERT OR REPLACE INTO user_settings (key, value) VALUES (?, ?)", diff --git a/static/js/layout.js b/static/js/layout.js index 25ccc09..baee026 100644 --- a/static/js/layout.js +++ b/static/js/layout.js @@ -1,4 +1,5 @@ let dashboardGrid; +let currentPw = sessionStorage.getItem('admin_pw') || ""; // 1. Grid initialisieren function initGridstack() { @@ -63,26 +64,28 @@ async function loadLayout() { let savedLayout = null; try { - // Zuerst versuchen, aus der Datenbank zu laden - const authResponse = await fetch('/api/auth'); - if (authResponse.ok) { - const layoutResponse = await fetch('/api/layout'); - if (layoutResponse.ok) { - const data = await layoutResponse.json(); - if (data.layout) savedLayout = data.layout; + // Wir versuchen es direkt beim API-Endpunkt + const response = await fetch('/api/layout'); + if (response.ok) { + const data = await response.json(); + if (data && data.layout) { + savedLayout = data.layout; + console.log("Layout aus DB geladen"); } - } else { - throw new Error("Nicht angemeldet"); } } catch (error) { - // Fallback: Aus dem LocalStorage laden + console.warn("API Layout nicht verfügbar, versuche LocalStorage..."); + } + + // Wenn DB nicht ging oder leer war, schau im LocalStorage nach + if (!savedLayout) { const localData = localStorage.getItem('balkonkraftwerk_layout'); if (localData) { savedLayout = JSON.parse(localData); + console.log("Layout aus LocalStorage geladen"); } } - // Wenn ein Layout gefunden wurde, anwenden if (savedLayout && dashboardGrid) { dashboardGrid.load(savedLayout); } diff --git a/static/js/script.js b/static/js/script.js index ab7fc0f..9e9dc45 100644 --- a/static/js/script.js +++ b/static/js/script.js @@ -2,7 +2,6 @@ let myChart; let panelChart; let dcDonutChart; let activeDonutIndex = null; // null = Gesamtansicht, 0 = Panel 1, 1 = Panel 2 -let currentPw = ""; const ROI_DEBUG_FORCE_COMPLETE = false; @@ -585,8 +584,12 @@ async function updateROI() { roiCard.classList.remove('roi-complete'); badgeContainer.innerHTML = ''; } - updateROIForecast(); - } catch (e) { + if (typeof updateROIForecast === "function") { + updateROIForecast(); + } else { + console.log("updateROIForecast ist noch nicht implementiert."); + } + } catch (e) { console.error("ROI Fehler", e); } From b0335608a807a86b21d13d6406197ff6db47c1be Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Sat, 14 Mar 2026 13:05:22 +0100 Subject: [PATCH 07/71] Richtige Lade-Reihenfolge --- static/js/layout.js | 71 +++++++++++++++++++++++++++++++++------------ static/js/script.js | 21 +------------- 2 files changed, 53 insertions(+), 39 deletions(-) diff --git a/static/js/layout.js b/static/js/layout.js index baee026..604caad 100644 --- a/static/js/layout.js +++ b/static/js/layout.js @@ -62,40 +62,73 @@ async function saveLayout() { // 4. Layout beim Starten laden async function loadLayout() { let savedLayout = null; - try { - // Wir versuchen es direkt beim API-Endpunkt const response = await fetch('/api/layout'); if (response.ok) { const data = await response.json(); - if (data && data.layout) { - savedLayout = data.layout; - console.log("Layout aus DB geladen"); - } + if (data && data.layout) savedLayout = data.layout; } - } catch (error) { - console.warn("API Layout nicht verfügbar, versuche LocalStorage..."); - } + } catch (e) { console.error("DB Load failed", e); } - // Wenn DB nicht ging oder leer war, schau im LocalStorage nach if (!savedLayout) { const localData = localStorage.getItem('balkonkraftwerk_layout'); - if (localData) { - savedLayout = JSON.parse(localData); - console.log("Layout aus LocalStorage geladen"); - } + if (localData) savedLayout = JSON.parse(localData); } if (savedLayout && dashboardGrid) { + dashboardGrid.removeAll(); dashboardGrid.load(savedLayout); + console.log("Layout sauber neu geladen."); } } -// Beim Laden der Seite ausführen (füge das zu deinen anderen Init-Funktionen hinzu) document.addEventListener("DOMContentLoaded", async () => { + // --- PHASE 1: Das Gerüst aufbauen --- initGridstack(); - await loadLayout(); - - // Nach dem Laden des Layouts kann es helfen, Chart.js einen Resize-Befehl zu geben - window.dispatchEvent(new Event('resize')); + await loadLayout(); // Wartet, bis Boxen aus DB oder LocalStorage da sind + + // --- PHASE 2: Startwerte für Datumsfelder setzen --- + const t = new Date().toISOString().split('T')[0]; + const startInput = document.getElementById('start'); + const endInput = document.getElementById('end'); + + if (startInput && endInput) { + startInput.value = t; + endInput.value = t; + startInput.addEventListener('change', updateQuickButtonsActiveState); + endInput.addEventListener('change', updateQuickButtonsActiveState); + } + + // --- PHASE 3: Daten in die Boxen pumpen --- + // Wir prüfen bei jeder Funktion, ob sie existiert, um Fehler zu vermeiden + try { + if (typeof fetchData === "function") await fetchData(); + if (typeof updateWeather === "function") updateWeather(); + if (typeof updateLive === "function") updateLive(); + if (typeof updatePeaks === "function") updatePeaks(); + if (typeof updateQuickButtonsActiveState === "function") updateQuickButtonsActiveState(); + + // ML-Funktionen + if (typeof loadForecast === "function") loadForecast(); + if (typeof loadGlobalShap === "function") loadGlobalShap(); + if (typeof loadFeatureImportance === "function") loadFeatureImportance(); + + // Heatmaps + if (typeof initHeatmapYears === "function") initHeatmapYears(); + if (typeof initHourlyHeatmap === "function") initHourlyHeatmap(); + + } catch (err) { + console.error("Fehler beim initialen Daten-Load:", err); + } + + // --- PHASE 4: Intervalle für Updates starten --- + setInterval(() => { if (typeof updateLive === "function") updateLive(); }, 5000); + setInterval(() => { if (typeof fetchData === "function") fetchData(); }, 60000); + setInterval(() => { if (typeof updatePeaks === "function") updatePeaks(); }, 60000); + setInterval(() => { if (typeof checkLoadingStatus === "function") checkLoadingStatus(); }, 500); + + // WICHTIG: Einmal kräftig schütteln, damit Charts ihre Größe im neuen Grid finden + setTimeout(() => { + window.dispatchEvent(new Event('resize')); + }, 200); }); \ No newline at end of file diff --git a/static/js/script.js b/static/js/script.js index 9e9dc45..ecc732a 100644 --- a/static/js/script.js +++ b/static/js/script.js @@ -1396,23 +1396,4 @@ function showShapDetails(point) { } document.getElementById("seasonInterpretation").innerText = text; -} - - -const t = new Date().toISOString().split('T')[0]; -document.getElementById('start').value = t; -document.getElementById('end').value = t; -document.getElementById('start').addEventListener('change', updateQuickButtonsActiveState); -document.getElementById('end').addEventListener('change', updateQuickButtonsActiveState); -fetchData(); -updateWeather(); -updateLive(); -updatePeaks(); -updateQuickButtonsActiveState(); -loadForecast(); -loadGlobalShap(); -loadFeatureImportance(); -setInterval(updateLive, 5000); -setInterval(fetchData, 60000); -setInterval(updatePeaks, 60000); -setInterval(checkLoadingStatus, 500); \ No newline at end of file +} \ No newline at end of file From 3c6489aadea52052054c6240b56013f489744ae3 Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Sat, 14 Mar 2026 14:25:53 +0100 Subject: [PATCH 08/71] Layout Reset Button --- static/js/layout.js | 20 ++++++++++++++++++++ templates/index.html | 1 + 2 files changed, 21 insertions(+) diff --git a/static/js/layout.js b/static/js/layout.js index 604caad..f5844d4 100644 --- a/static/js/layout.js +++ b/static/js/layout.js @@ -80,6 +80,26 @@ async function loadLayout() { dashboardGrid.load(savedLayout); console.log("Layout sauber neu geladen."); } + + +} + +async function resetDatabaseLayout() { + if (!confirm("Möchtest du das Layout wirklich auf Standard zurücksetzen?")) return; + + const storedPw = sessionStorage.getItem('admin_pw'); + if (!storedPw) return alert("Bitte erst einloggen!"); + + // Wir senden ein leeres Array oder null, damit der Server es löscht/leert + await fetch('/api/layout', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + layout: [], // Leeres Layout erzwingen + pw: storedPw + }) + }); + location.reload(); } document.addEventListener("DOMContentLoaded", async () => { diff --git a/templates/index.html b/templates/index.html index 786d455..7ce64cd 100644 --- a/templates/index.html +++ b/templates/index.html @@ -81,6 +81,7 @@

+
From 059efdf5108d935b92af2b12674fbea8c8c9a190 Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Sat, 14 Mar 2026 21:13:00 +0100 Subject: [PATCH 09/71] margin left & right gridstack --- static/css/style.css | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/static/css/style.css b/static/css/style.css index e6ae533..76a7dc0 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -19,6 +19,7 @@ max-width: 1400px; margin: 0 auto; padding: 20px; + overflow-x: hidden; } .header { @@ -157,7 +158,12 @@ box-shadow 0.2s ease, transform 0.15s ease; } - + + .grid-stack { + margin-left: -20px; + margin-right: -20px; + } + .grid-stack-item-content { overflow-x: hidden; overflow-y: auto; From 9807a44b83a71b0a9a875528a3856db4f6616ae6 Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Sat, 14 Mar 2026 21:43:26 +0100 Subject: [PATCH 10/71] Gridstack editable only for admin --- routes.py | 21 ++++--- static/js/layout.js | 139 ++++++++++---------------------------------- static/js/script.js | 17 ++++-- 3 files changed, 57 insertions(+), 120 deletions(-) diff --git a/routes.py b/routes.py index 65c7f43..d49b120 100644 --- a/routes.py +++ b/routes.py @@ -1065,20 +1065,25 @@ def get_layout(): def save_layout(): data = request.json layout_json = data.get('layout') - password = data.get('pw') # Wir senden das PW zur Sicherheit mit + password = data.get('pw') - # Berechtigung prüfen (wie in deinem /api/auth) + # Berechtigung streng prüfen if password != ADMIN_PASS: return jsonify({"error": "Nicht autorisiert"}), 403 - if not layout_json: - return jsonify({"error": "Kein Layout gesendet"}), 400 - conn = get_db_connection() c = conn.cursor() - # Speichern oder Überschreiben - c.execute("INSERT OR REPLACE INTO user_settings (key, value) VALUES (?, ?)", - ('dashboard_layout', json.dumps(layout_json))) + + # Wenn das Signal "RESET" kommt, löschen wir das gespeicherte Layout + if layout_json == "RESET": + c.execute("DELETE FROM user_settings WHERE key = 'dashboard_layout'") + elif layout_json is not None: + c.execute("INSERT OR REPLACE INTO user_settings (key, value) VALUES (?, ?)", + ('dashboard_layout', json.dumps(layout_json))) + else: + conn.close() + return jsonify({"error": "Kein Layout gesendet"}), 400 + conn.commit() conn.close() diff --git a/static/js/layout.js b/static/js/layout.js index f5844d4..cb68f51 100644 --- a/static/js/layout.js +++ b/static/js/layout.js @@ -1,65 +1,45 @@ let dashboardGrid; -let currentPw = sessionStorage.getItem('admin_pw') || ""; +let currentPw = ""; // 1. Grid initialisieren function initGridstack() { - const isAdmin = sessionStorage.getItem('admin_pw') !== null; - dashboardGrid = GridStack.init({ cellHeight: 110, margin: 20, animate: true, - staticGrid: !isAdmin, // Wenn nicht Admin, dann gesperrt (kein Drag/Resize) + staticGrid: true, disableOneColumnMode: false, oneColumnModeDomSort: true }); - // Falls das PW schon im Speicher war (nach Refresh), UI direkt anpassen - if (isAdmin) { - currentPw = sessionStorage.getItem('admin_pw'); - document.getElementById('unlockBtn').style.display = 'none'; - document.getElementById('adminArea').style.display = 'flex'; - // Hier ggf. loadTariffs() aufrufen, falls script.js schon geladen ist - } - dashboardGrid.on('change', function(event, items) { saveLayout(); }); } -// 3. Layout speichern (Auth-Check + DB vs. LocalStorage) +// 2. Layout speichern (Nur in DB und nur wenn PW da ist) async function saveLayout() { - if (!dashboardGrid) return; + if (!dashboardGrid || !currentPw) return; + const layoutData = dashboardGrid.save(); - // Wir schauen nach, ob ein Passwort im SessionStorage liegt - // (Das müsstest du in deiner unlockAdmin() Funktion dort speichern) - const storedPw = sessionStorage.getItem('admin_pw'); - - if (storedPw) { - try { - const response = await fetch('/api/layout', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - layout: layoutData, - pw: storedPw - }) - }); - - if (response.ok) { - console.log("Layout in DB gespeichert."); - return; - } - } catch (e) { console.error("DB Save failed", e); } - } - - // Fallback: LocalStorage für Gäste - localStorage.setItem('balkonkraftwerk_layout', JSON.stringify(layoutData)); - console.log("Layout lokal im Browser gespeichert."); + try { + const response = await fetch('/api/layout', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + layout: layoutData, + pw: currentPw + }) + }); + + if (response.ok) { + console.log("Layout in DB für alle Nutzer aktualisiert."); + } + } catch (e) { console.error("DB Save failed", e); } } -// 4. Layout beim Starten laden +// 3. Layout beim Starten laden async function loadLayout() { let savedLayout = null; try { @@ -70,85 +50,30 @@ async function loadLayout() { } } catch (e) { console.error("DB Load failed", e); } - if (!savedLayout) { - const localData = localStorage.getItem('balkonkraftwerk_layout'); - if (localData) savedLayout = JSON.parse(localData); - } - + // Wenn ein Layout auf dem Server existiert, anwenden if (savedLayout && dashboardGrid) { dashboardGrid.removeAll(); dashboardGrid.load(savedLayout); - console.log("Layout sauber neu geladen."); + console.log("Globales Layout erfolgreich geladen."); } - - } +// 4. Reset-Funktion für den Button im Admin-Bereich async function resetDatabaseLayout() { - if (!confirm("Möchtest du das Layout wirklich auf Standard zurücksetzen?")) return; + if (!confirm("Möchtest du das Layout für ALLE Nutzer auf den Standard zurücksetzen?")) return; - const storedPw = sessionStorage.getItem('admin_pw'); - if (!storedPw) return alert("Bitte erst einloggen!"); + if (!currentPw) return alert("Bitte erst als Admin einloggen!"); - // Wir senden ein leeres Array oder null, damit der Server es löscht/leert - await fetch('/api/layout', { + const res = await fetch('/api/layout', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - layout: [], // Leeres Layout erzwingen - pw: storedPw + layout: "RESET", // Signalwort für das Python-Backend + pw: currentPw }) }); - location.reload(); -} - -document.addEventListener("DOMContentLoaded", async () => { - // --- PHASE 1: Das Gerüst aufbauen --- - initGridstack(); - await loadLayout(); // Wartet, bis Boxen aus DB oder LocalStorage da sind - - // --- PHASE 2: Startwerte für Datumsfelder setzen --- - const t = new Date().toISOString().split('T')[0]; - const startInput = document.getElementById('start'); - const endInput = document.getElementById('end'); - - if (startInput && endInput) { - startInput.value = t; - endInput.value = t; - startInput.addEventListener('change', updateQuickButtonsActiveState); - endInput.addEventListener('change', updateQuickButtonsActiveState); - } - - // --- PHASE 3: Daten in die Boxen pumpen --- - // Wir prüfen bei jeder Funktion, ob sie existiert, um Fehler zu vermeiden - try { - if (typeof fetchData === "function") await fetchData(); - if (typeof updateWeather === "function") updateWeather(); - if (typeof updateLive === "function") updateLive(); - if (typeof updatePeaks === "function") updatePeaks(); - if (typeof updateQuickButtonsActiveState === "function") updateQuickButtonsActiveState(); - - // ML-Funktionen - if (typeof loadForecast === "function") loadForecast(); - if (typeof loadGlobalShap === "function") loadGlobalShap(); - if (typeof loadFeatureImportance === "function") loadFeatureImportance(); - - // Heatmaps - if (typeof initHeatmapYears === "function") initHeatmapYears(); - if (typeof initHourlyHeatmap === "function") initHourlyHeatmap(); - - } catch (err) { - console.error("Fehler beim initialen Daten-Load:", err); + + if (res.ok) { + location.reload(); // Seite neu laden, um Standard-HTML zu zeigen } - - // --- PHASE 4: Intervalle für Updates starten --- - setInterval(() => { if (typeof updateLive === "function") updateLive(); }, 5000); - setInterval(() => { if (typeof fetchData === "function") fetchData(); }, 60000); - setInterval(() => { if (typeof updatePeaks === "function") updatePeaks(); }, 60000); - setInterval(() => { if (typeof checkLoadingStatus === "function") checkLoadingStatus(); }, 500); - - // WICHTIG: Einmal kräftig schütteln, damit Charts ihre Größe im neuen Grid finden - setTimeout(() => { - window.dispatchEvent(new Event('resize')); - }, 200); -}); \ No newline at end of file +} \ No newline at end of file diff --git a/static/js/script.js b/static/js/script.js index ecc732a..4163059 100644 --- a/static/js/script.js +++ b/static/js/script.js @@ -36,14 +36,14 @@ async function unlockAdmin() { }); if (res.ok) { - currentPw = pw; - sessionStorage.setItem('admin_pw', pw); + currentPw = pw; // PW nur in den flüchtigen RAM laden document.getElementById('unlockBtn').style.display = 'none'; document.getElementById('adminArea').style.display = 'flex'; if (dashboardGrid) { - dashboardGrid.enable(); + // Grid bearbeitbar machen + dashboardGrid.setStatic(false); document.getElementById('dashboard-grid').classList.add('edit-mode'); } @@ -54,8 +54,15 @@ async function unlockAdmin() { } function closeAdmin() { - document.getElementById('adminArea').style.display = 'none'; - document.getElementById('unlockBtn').style.display = 'block'; + currentPw = ""; + document.getElementById('adminArea').style.display = 'none'; + document.getElementById('unlockBtn').style.display = 'block'; + + if (dashboardGrid) { + // Grid wieder für alle sperren + dashboardGrid.setStatic(true); + document.getElementById('dashboard-grid').classList.remove('edit-mode'); + } } async function loadTariffs() { From 8108c79ef25b96963ba9489b301b29e49d4882de Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Sat, 14 Mar 2026 21:49:47 +0100 Subject: [PATCH 11/71] Change container style --- static/css/style.css | 1 - static/js/script.js | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/static/css/style.css b/static/css/style.css index 76a7dc0..d8fb161 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -19,7 +19,6 @@ max-width: 1400px; margin: 0 auto; padding: 20px; - overflow-x: hidden; } .header { diff --git a/static/js/script.js b/static/js/script.js index 4163059..c22a591 100644 --- a/static/js/script.js +++ b/static/js/script.js @@ -36,7 +36,7 @@ async function unlockAdmin() { }); if (res.ok) { - currentPw = pw; // PW nur in den flüchtigen RAM laden + currentPw = pw; document.getElementById('unlockBtn').style.display = 'none'; document.getElementById('adminArea').style.display = 'flex'; From 932ff81955a6b14437c37e027e0dc5bc5391dd15 Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Sat, 14 Mar 2026 21:52:49 +0100 Subject: [PATCH 12/71] Change layout.js --- static/js/layout.js | 53 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/static/js/layout.js b/static/js/layout.js index cb68f51..5dde9ca 100644 --- a/static/js/layout.js +++ b/static/js/layout.js @@ -76,4 +76,55 @@ async function resetDatabaseLayout() { if (res.ok) { location.reload(); // Seite neu laden, um Standard-HTML zu zeigen } -} \ No newline at end of file +} + +document.addEventListener("DOMContentLoaded", async () => { + // --- PHASE 1: Das Gerüst aufbauen --- + initGridstack(); + await loadLayout(); // Wartet, bis Boxen aus DB oder LocalStorage da sind + + // --- PHASE 2: Startwerte für Datumsfelder setzen --- + const t = new Date().toISOString().split('T')[0]; + const startInput = document.getElementById('start'); + const endInput = document.getElementById('end'); + + if (startInput && endInput) { + startInput.value = t; + endInput.value = t; + startInput.addEventListener('change', updateQuickButtonsActiveState); + endInput.addEventListener('change', updateQuickButtonsActiveState); + } + + // --- PHASE 3: Daten in die Boxen pumpen --- + // Wir prüfen bei jeder Funktion, ob sie existiert, um Fehler zu vermeiden + try { + if (typeof fetchData === "function") await fetchData(); + if (typeof updateWeather === "function") updateWeather(); + if (typeof updateLive === "function") updateLive(); + if (typeof updatePeaks === "function") updatePeaks(); + if (typeof updateQuickButtonsActiveState === "function") updateQuickButtonsActiveState(); + + // ML-Funktionen + if (typeof loadForecast === "function") loadForecast(); + if (typeof loadGlobalShap === "function") loadGlobalShap(); + if (typeof loadFeatureImportance === "function") loadFeatureImportance(); + + // Heatmaps + if (typeof initHeatmapYears === "function") initHeatmapYears(); + if (typeof initHourlyHeatmap === "function") initHourlyHeatmap(); + + } catch (err) { + console.error("Fehler beim initialen Daten-Load:", err); + } + + // --- PHASE 4: Intervalle für Updates starten --- + setInterval(() => { if (typeof updateLive === "function") updateLive(); }, 5000); + setInterval(() => { if (typeof fetchData === "function") fetchData(); }, 60000); + setInterval(() => { if (typeof updatePeaks === "function") updatePeaks(); }, 60000); + setInterval(() => { if (typeof checkLoadingStatus === "function") checkLoadingStatus(); }, 500); + + // WICHTIG: Einmal kräftig schütteln, damit Charts ihre Größe im neuen Grid finden + setTimeout(() => { + window.dispatchEvent(new Event('resize')); + }, 200); +}); \ No newline at end of file From 90b2d8471edf6b0b8ca74f7f5d4ffd7e928ba746 Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Sat, 14 Mar 2026 22:57:03 +0100 Subject: [PATCH 13/71] Change saveLayout function --- static/js/layout.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/static/js/layout.js b/static/js/layout.js index 5dde9ca..8f411d4 100644 --- a/static/js/layout.js +++ b/static/js/layout.js @@ -19,24 +19,31 @@ function initGridstack() { // 2. Layout speichern (Nur in DB und nur wenn PW da ist) async function saveLayout() { - if (!dashboardGrid || !currentPw) return; + if (!dashboardGrid || !currentPw || currentPw === "") { + return; + } const layoutData = dashboardGrid.save(); try { const response = await fetch('/api/layout', { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { + 'Content-Type': 'application/json' + }, body: JSON.stringify({ layout: layoutData, pw: currentPw }) }); - if (response.ok) { - console.log("Layout in DB für alle Nutzer aktualisiert."); + if (!response.ok) { + const errorText = await response.text(); + console.error("Speichern fehlgeschlagen:", errorText); } - } catch (e) { console.error("DB Save failed", e); } + } catch (e) { + console.error("Netzwerkfehler beim Speichern:", e); + } } // 3. Layout beim Starten laden From ad7e4af87d22791cbc83ef1ead9798d6cf422fbd Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Sat, 14 Mar 2026 23:00:16 +0100 Subject: [PATCH 14/71] Change /api/layout endpoint --- routes.py | 53 +++++++++++++++++++++++++++++++---------------------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/routes.py b/routes.py index d49b120..8c3e115 100644 --- a/routes.py +++ b/routes.py @@ -1063,28 +1063,37 @@ def get_layout(): @api_bp.route('/api/layout', methods=['POST']) def save_layout(): - data = request.json - layout_json = data.get('layout') - password = data.get('pw') - - # Berechtigung streng prüfen - if password != ADMIN_PASS: - return jsonify({"error": "Nicht autorisiert"}), 403 - - conn = get_db_connection() - c = conn.cursor() + try: + data = request.get_json() + if not data: + return jsonify({"error": "Ungültiges JSON"}), 400 + + layout_json = data.get('layout') + password = data.get('pw') + + if password != ADMIN_PASS: + return jsonify({"error": "Nicht autorisiert"}), 403 + + conn = get_db_connection() + c = conn.cursor() + + if layout_json == "RESET": + c.execute("DELETE FROM user_settings WHERE key = 'dashboard_layout'") + print("Layout wurde zurückgesetzt.") + elif layout_json is not None: + # WICHTIG: Wir konvertieren das Objekt explizit in einen String für die DB + layout_string = json.dumps(layout_json) + c.execute("INSERT OR REPLACE INTO user_settings (key, value) VALUES (?, ?)", + ('dashboard_layout', layout_string)) + print("Layout erfolgreich gespeichert.") + else: + conn.close() + return jsonify({"error": "Kein Layout-Inhalt empfangen"}), 400 - # Wenn das Signal "RESET" kommt, löschen wir das gespeicherte Layout - if layout_json == "RESET": - c.execute("DELETE FROM user_settings WHERE key = 'dashboard_layout'") - elif layout_json is not None: - c.execute("INSERT OR REPLACE INTO user_settings (key, value) VALUES (?, ?)", - ('dashboard_layout', json.dumps(layout_json))) - else: + conn.commit() conn.close() - return jsonify({"error": "Kein Layout gesendet"}), 400 + return jsonify({"status": "gespeichert"}), 200 - conn.commit() - conn.close() - - return jsonify({"status": "gespeichert"}), 200 \ No newline at end of file + except Exception as e: + print(f"Server-Fehler: {str(e)}") + return jsonify({"error": "Interner Server Fehler", "details": str(e)}), 500 \ No newline at end of file From fc4787aca2200a9c21f92b78a44c3c7441ec80f1 Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Sat, 14 Mar 2026 23:07:20 +0100 Subject: [PATCH 15/71] Import json in routes.py --- routes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/routes.py b/routes.py index 8c3e115..f7253ba 100644 --- a/routes.py +++ b/routes.py @@ -2,6 +2,7 @@ import sqlite3 import datetime import requests +import json import time import math import numpy as np From 6e416c3c3c8f8ce8892923f26b1bbd08745e0202 Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Sat, 14 Mar 2026 23:16:47 +0100 Subject: [PATCH 16/71] Add minimum size to "card-total" --- templates/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/index.html b/templates/index.html index 7ce64cd..3f3693e 100644 --- a/templates/index.html +++ b/templates/index.html @@ -87,7 +87,7 @@

-
+
-
+
From e7b440de40f10c8e2cef2881309f036a51ab63a1 Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Sat, 14 Mar 2026 23:22:23 +0100 Subject: [PATCH 18/71] Add minimum sizes to cards --- templates/index.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/templates/index.html b/templates/index.html index c0d3fd3..93d0e73 100644 --- a/templates/index.html +++ b/templates/index.html @@ -103,7 +103,7 @@

-
+
@@ -118,7 +118,7 @@

-
+
-
+
-
+
Leistungs-Rekorde (AC)
From 39e3387350ddfca42aac2abfb4e69a0b56c447f4 Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Sat, 14 Mar 2026 23:32:33 +0100 Subject: [PATCH 19/71] Change .grid-stack-item-content style --- static/css/style.css | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/static/css/style.css b/static/css/style.css index d8fb161..f0ce7bb 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -165,7 +165,10 @@ .grid-stack-item-content { overflow-x: hidden; - overflow-y: auto; + overflow-y: auto; + overflow: visible !important; + background: transparent !important; + box-shadow: none !important; } .edit-mode .grid-stack-item-content { From c9ded83d9089033a7e33034183bafb05c4ebe469 Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Sun, 15 Mar 2026 08:48:52 +0100 Subject: [PATCH 20/71] Change Forecast Card Styles --- static/css/style.css | 6 ++++-- static/js/layout.js | 26 ++++++++++++++++++++++++++ static/js/script.js | 21 +++++++++++++-------- 3 files changed, 43 insertions(+), 10 deletions(-) diff --git a/static/css/style.css b/static/css/style.css index f0ce7bb..e58b349 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -195,7 +195,9 @@ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.03); display: flex; flex-direction: column; - justify-content: center; + justify-content: flex-start; + height: 100%; + box-sizing: border-box; } .card-gradient { @@ -203,7 +205,7 @@ color: white; box-shadow: 0 15px 35px rgba(255, 75, 114, 0.25); } - + .card-gradient .label { color: rgba(255, 255, 255, 0.85); font-size: 0.9em; diff --git a/static/js/layout.js b/static/js/layout.js index 8f411d4..1569381 100644 --- a/static/js/layout.js +++ b/static/js/layout.js @@ -85,6 +85,32 @@ async function resetDatabaseLayout() { } } +function resizeForecastCardToFit() { + if (!window.dashboardGrid) return; + + const gridItem = document.getElementById('card-forecast'); + const innerCard = gridItem.querySelector('.card'); + + if (!gridItem || !innerCard) return; + + // 1. Wir erlauben der Karte kurz, ihre natürliche, volle Höhe anzunehmen + innerCard.style.height = 'auto'; + const requiredHeightPx = innerCard.scrollHeight; + + // 2. Zurücksetzen für das Flex-Layout + innerCard.style.height = '100%'; + + // 3. Höhe einer einzelnen Gridstack-Zelle plus Margin abrufen + const cellHeight = dashboardGrid.getCellHeight(); + const margin = dashboardGrid.getOpts().margin || 20; + + // 4. Ausrechnen, wie viele "Blöcke" (h) wir brauchen, aufgerundet + const newH = Math.ceil((requiredHeightPx + margin) / (cellHeight + margin)); + + // 5. Gridstack anweisen, die neue Höhe anzuwenden + dashboardGrid.update(gridItem, { h: newH }); +} + document.addEventListener("DOMContentLoaded", async () => { // --- PHASE 1: Das Gerüst aufbauen --- initGridstack(); diff --git a/static/js/script.js b/static/js/script.js index c22a591..db4df40 100644 --- a/static/js/script.js +++ b/static/js/script.js @@ -1153,16 +1153,19 @@ function showShapDetails(point) { const container = document.getElementById("shapForcePlot"); // ===== ResizeObserver nur einmal registrieren ===== - if (!container._resizeObserverAttached) { - + const cardItem = document.getElementById("card-forecast"); + if (!cardItem._resizeObserverAttached) { const observer = new ResizeObserver(() => { - if (window._lastShapPoint) { - showShapDetails(window._lastShapPoint); - } + // Wir nutzen einen winzigen Delay, damit Gridstack erst fertig zeichnet + clearTimeout(window._shapResizeTimer); + window._shapResizeTimer = setTimeout(() => { + if (window._lastShapPoint) { + showShapDetails(window._lastShapPoint); + } + }, 50); }); - - observer.observe(container); - container._resizeObserverAttached = true; + observer.observe(cardItem); + cardItem._resizeObserverAttached = true; } container.innerHTML = ""; @@ -1170,6 +1173,7 @@ function showShapDetails(point) { container.style.display = "flex"; container.style.alignItems = "center"; container.style.justifyContent = "center"; + container.style.width = "100%"; container.style.height = "100px"; container.style.overflow = "visible"; @@ -1403,4 +1407,5 @@ function showShapDetails(point) { } document.getElementById("seasonInterpretation").innerText = text; + resizeForecastCardToFit(); } \ No newline at end of file From 796faeec9c3397620c6cecbb06005adcc93eb918 Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Sun, 15 Mar 2026 09:00:24 +0100 Subject: [PATCH 21/71] Change Forecast Card Styles --- static/css/style.css | 4 +++- static/js/layout.js | 23 ++++++++++++++--------- static/js/script.js | 8 ++++++++ 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/static/css/style.css b/static/css/style.css index e58b349..2e25724 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -198,6 +198,8 @@ justify-content: flex-start; height: 100%; box-sizing: border-box; + overflow: hidden; /* Verhindert das Auslaufen des Inhalts während der Animation */ + transition: height 0.3s ease; /* Macht das Aufklappen geschmeidig */ } .card-gradient { @@ -205,7 +207,7 @@ color: white; box-shadow: 0 15px 35px rgba(255, 75, 114, 0.25); } - + .card-gradient .label { color: rgba(255, 255, 255, 0.85); font-size: 0.9em; diff --git a/static/js/layout.js b/static/js/layout.js index 1569381..74c2362 100644 --- a/static/js/layout.js +++ b/static/js/layout.js @@ -93,22 +93,27 @@ function resizeForecastCardToFit() { if (!gridItem || !innerCard) return; - // 1. Wir erlauben der Karte kurz, ihre natürliche, volle Höhe anzunehmen + // 1. Temporär feste Höhen lösen innerCard.style.height = 'auto'; + innerCard.style.minHeight = 'auto'; + + // 2. Die tatsächliche Höhe messen + // Wir nehmen scrollHeight, um den Inhalt außerhalb des sichtbaren Bereichs zu erfassen const requiredHeightPx = innerCard.scrollHeight; - - // 2. Zurücksetzen für das Flex-Layout - innerCard.style.height = '100%'; - // 3. Höhe einer einzelnen Gridstack-Zelle plus Margin abrufen - const cellHeight = dashboardGrid.getCellHeight(); + // 3. Gridstack Zellen-Metriken + const cellHeight = dashboardGrid.getCellHeight(); const margin = dashboardGrid.getOpts().margin || 20; - // 4. Ausrechnen, wie viele "Blöcke" (h) wir brauchen, aufgerundet - const newH = Math.ceil((requiredHeightPx + margin) / (cellHeight + margin)); + // 4. Berechnung der benötigten Rows (h) + // Wir addieren einen kleinen Puffer (10px) für Padding-Unterschiede + const newH = Math.ceil((requiredHeightPx + 10) / (cellHeight + margin)); - // 5. Gridstack anweisen, die neue Höhe anzuwenden + // 5. Update ausführen dashboardGrid.update(gridItem, { h: newH }); + + // 6. Zurück auf 100%, damit die Card-Klasse das Gridstack-Item wieder voll ausfüllt + innerCard.style.height = '100%'; } document.addEventListener("DOMContentLoaded", async () => { diff --git a/static/js/script.js b/static/js/script.js index db4df40..0917f29 100644 --- a/static/js/script.js +++ b/static/js/script.js @@ -1047,6 +1047,14 @@ async function loadForecast() { } } }); + // 1. Sicherstellen, dass die Details am Anfang ausgeblendet sind + document.getElementById("shapDetailCard").style.display = "none"; + + // 2. WICHTIG: Die Karte sofort an den Inhalt (Global SHAP etc.) anpassen + // Ein kleines Delay hilft, damit das Chart fertig gerendert ist + setTimeout(() => { + resizeForecastCardToFit(); + }, 200); } async function loadFeatureImportance() { From d2274d1b70591897c75c54dd82dc0babbd37d481 Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Mon, 16 Mar 2026 21:50:27 +0100 Subject: [PATCH 22/71] Undo forecast card styling --- static/css/style.css | 11 ++++++----- static/js/layout.js | 31 ------------------------------- static/js/script.js | 29 ++++++++--------------------- 3 files changed, 14 insertions(+), 57 deletions(-) diff --git a/static/css/style.css b/static/css/style.css index 2e25724..8efad81 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -195,13 +195,14 @@ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.03); display: flex; flex-direction: column; - justify-content: flex-start; - height: 100%; - box-sizing: border-box; - overflow: hidden; /* Verhindert das Auslaufen des Inhalts während der Animation */ - transition: height 0.3s ease; /* Macht das Aufklappen geschmeidig */ + justify-content: center; } + background: var(--card-bg); + + /* WICHTIG: Ändere 'center' auf 'flex-start' */ + + .card-gradient { background: var(--gradient); color: white; diff --git a/static/js/layout.js b/static/js/layout.js index 74c2362..8f411d4 100644 --- a/static/js/layout.js +++ b/static/js/layout.js @@ -85,37 +85,6 @@ async function resetDatabaseLayout() { } } -function resizeForecastCardToFit() { - if (!window.dashboardGrid) return; - - const gridItem = document.getElementById('card-forecast'); - const innerCard = gridItem.querySelector('.card'); - - if (!gridItem || !innerCard) return; - - // 1. Temporär feste Höhen lösen - innerCard.style.height = 'auto'; - innerCard.style.minHeight = 'auto'; - - // 2. Die tatsächliche Höhe messen - // Wir nehmen scrollHeight, um den Inhalt außerhalb des sichtbaren Bereichs zu erfassen - const requiredHeightPx = innerCard.scrollHeight; - - // 3. Gridstack Zellen-Metriken - const cellHeight = dashboardGrid.getCellHeight(); - const margin = dashboardGrid.getOpts().margin || 20; - - // 4. Berechnung der benötigten Rows (h) - // Wir addieren einen kleinen Puffer (10px) für Padding-Unterschiede - const newH = Math.ceil((requiredHeightPx + 10) / (cellHeight + margin)); - - // 5. Update ausführen - dashboardGrid.update(gridItem, { h: newH }); - - // 6. Zurück auf 100%, damit die Card-Klasse das Gridstack-Item wieder voll ausfüllt - innerCard.style.height = '100%'; -} - document.addEventListener("DOMContentLoaded", async () => { // --- PHASE 1: Das Gerüst aufbauen --- initGridstack(); diff --git a/static/js/script.js b/static/js/script.js index 0917f29..c22a591 100644 --- a/static/js/script.js +++ b/static/js/script.js @@ -1047,14 +1047,6 @@ async function loadForecast() { } } }); - // 1. Sicherstellen, dass die Details am Anfang ausgeblendet sind - document.getElementById("shapDetailCard").style.display = "none"; - - // 2. WICHTIG: Die Karte sofort an den Inhalt (Global SHAP etc.) anpassen - // Ein kleines Delay hilft, damit das Chart fertig gerendert ist - setTimeout(() => { - resizeForecastCardToFit(); - }, 200); } async function loadFeatureImportance() { @@ -1161,19 +1153,16 @@ function showShapDetails(point) { const container = document.getElementById("shapForcePlot"); // ===== ResizeObserver nur einmal registrieren ===== - const cardItem = document.getElementById("card-forecast"); - if (!cardItem._resizeObserverAttached) { + if (!container._resizeObserverAttached) { + const observer = new ResizeObserver(() => { - // Wir nutzen einen winzigen Delay, damit Gridstack erst fertig zeichnet - clearTimeout(window._shapResizeTimer); - window._shapResizeTimer = setTimeout(() => { - if (window._lastShapPoint) { - showShapDetails(window._lastShapPoint); - } - }, 50); + if (window._lastShapPoint) { + showShapDetails(window._lastShapPoint); + } }); - observer.observe(cardItem); - cardItem._resizeObserverAttached = true; + + observer.observe(container); + container._resizeObserverAttached = true; } container.innerHTML = ""; @@ -1181,7 +1170,6 @@ function showShapDetails(point) { container.style.display = "flex"; container.style.alignItems = "center"; container.style.justifyContent = "center"; - container.style.width = "100%"; container.style.height = "100px"; container.style.overflow = "visible"; @@ -1415,5 +1403,4 @@ function showShapDetails(point) { } document.getElementById("seasonInterpretation").innerText = text; - resizeForecastCardToFit(); } \ No newline at end of file From 78399b395586fc33e592d924a4b43fff1edcf377 Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Mon, 16 Mar 2026 22:30:04 +0100 Subject: [PATCH 23/71] Introduce Auto Card Resize --- static/css/style.css | 19 +++++++-------- static/js/layout.js | 58 ++++++++++++++++++++++++++++++++++++++++++++ static/js/script.js | 14 ----------- 3 files changed, 67 insertions(+), 24 deletions(-) diff --git a/static/css/style.css b/static/css/style.css index 8efad81..82376c2 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -164,13 +164,12 @@ } .grid-stack-item-content { - overflow-x: hidden; - overflow-y: auto; - overflow: visible !important; + height: 100%; + overflow: hidden; background: transparent !important; box-shadow: none !important; } - + .edit-mode .grid-stack-item-content { cursor: grab; border: 1px dashed var(--accent); @@ -196,19 +195,19 @@ display: flex; flex-direction: column; justify-content: center; + height: 100%; + } + + .card canvas { + width: 100% !important; } - - background: var(--card-bg); - - /* WICHTIG: Ändere 'center' auf 'flex-start' */ - .card-gradient { background: var(--gradient); color: white; box-shadow: 0 15px 35px rgba(255, 75, 114, 0.25); } - + .card-gradient .label { color: rgba(255, 255, 255, 0.85); font-size: 0.9em; diff --git a/static/js/layout.js b/static/js/layout.js index 8f411d4..eb669a7 100644 --- a/static/js/layout.js +++ b/static/js/layout.js @@ -15,6 +15,16 @@ function initGridstack() { dashboardGrid.on('change', function(event, items) { saveLayout(); }); + + dashboardGrid.on('resizestop', function(event, el) { + const charts = el.querySelectorAll("canvas"); + charts.forEach(canvas => { + const chart = Chart.getChart(canvas); + if (chart) { + chart.resize(); + } + }); + }); } // 2. Layout speichern (Nur in DB und nur wenn PW da ist) @@ -85,10 +95,58 @@ async function resetDatabaseLayout() { } } +function resizeGridItemToContent(gridItemId) { + + const el = document.getElementById(gridItemId); + if (!el || !dashboardGrid) return; + + const content = el.querySelector(".card"); + + const contentHeight = content.scrollHeight; + + const cellHeight = dashboardGrid.getCellHeight(); + const margin = dashboardGrid.opts.margin; + + const newHeight = Math.ceil((contentHeight + margin) / cellHeight); + + dashboardGrid.update(el, { h: newHeight }); +} + +function initAutoCardResize() { + + const cards = document.querySelectorAll(".grid-stack-item"); + + const observer = new ResizeObserver(entries => { + + entries.forEach(entry => { + + const gridItem = entry.target.closest(".grid-stack-item"); + + if (!gridItem) return; + + const id = gridItem.id; + + resizeGridItemToContent(id); + + }); + + }); + + cards.forEach(card => { + + const content = card.querySelector(".card"); + + if (content) observer.observe(content); + + }); + +} + document.addEventListener("DOMContentLoaded", async () => { // --- PHASE 1: Das Gerüst aufbauen --- initGridstack(); await loadLayout(); // Wartet, bis Boxen aus DB oder LocalStorage da sind + initAutoCardResize(); // --- PHASE 2: Startwerte für Datumsfelder setzen --- const t = new Date().toISOString().split('T')[0]; diff --git a/static/js/script.js b/static/js/script.js index c22a591..43a7119 100644 --- a/static/js/script.js +++ b/static/js/script.js @@ -1151,20 +1151,6 @@ function showShapDetails(point) { }; const container = document.getElementById("shapForcePlot"); - - // ===== ResizeObserver nur einmal registrieren ===== - if (!container._resizeObserverAttached) { - - const observer = new ResizeObserver(() => { - if (window._lastShapPoint) { - showShapDetails(window._lastShapPoint); - } - }); - - observer.observe(container); - container._resizeObserverAttached = true; - } - container.innerHTML = ""; container.style.position = "relative"; container.style.display = "flex"; From 4140ba98cc1d1fec7af24043a53da365640ddfae Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Mon, 16 Mar 2026 22:37:34 +0100 Subject: [PATCH 24/71] Undo auto resize --- static/css/style.css | 16 +++++------- static/js/layout.js | 58 -------------------------------------------- static/js/script.js | 14 +++++++++++ 3 files changed, 20 insertions(+), 68 deletions(-) diff --git a/static/css/style.css b/static/css/style.css index 82376c2..4aba25a 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -162,14 +162,15 @@ margin-left: -20px; margin-right: -20px; } - + .grid-stack-item-content { - height: 100%; - overflow: hidden; + overflow-x: hidden; + overflow-y: auto; + overflow: visible !important; background: transparent !important; box-shadow: none !important; } - + .edit-mode .grid-stack-item-content { cursor: grab; border: 1px dashed var(--accent); @@ -195,11 +196,6 @@ display: flex; flex-direction: column; justify-content: center; - height: 100%; - } - - .card canvas { - width: 100% !important; } .card-gradient { @@ -207,7 +203,7 @@ color: white; box-shadow: 0 15px 35px rgba(255, 75, 114, 0.25); } - + .card-gradient .label { color: rgba(255, 255, 255, 0.85); font-size: 0.9em; diff --git a/static/js/layout.js b/static/js/layout.js index eb669a7..8f411d4 100644 --- a/static/js/layout.js +++ b/static/js/layout.js @@ -15,16 +15,6 @@ function initGridstack() { dashboardGrid.on('change', function(event, items) { saveLayout(); }); - - dashboardGrid.on('resizestop', function(event, el) { - const charts = el.querySelectorAll("canvas"); - charts.forEach(canvas => { - const chart = Chart.getChart(canvas); - if (chart) { - chart.resize(); - } - }); - }); } // 2. Layout speichern (Nur in DB und nur wenn PW da ist) @@ -95,58 +85,10 @@ async function resetDatabaseLayout() { } } -function resizeGridItemToContent(gridItemId) { - - const el = document.getElementById(gridItemId); - if (!el || !dashboardGrid) return; - - const content = el.querySelector(".card"); - - const contentHeight = content.scrollHeight; - - const cellHeight = dashboardGrid.getCellHeight(); - const margin = dashboardGrid.opts.margin; - - const newHeight = Math.ceil((contentHeight + margin) / cellHeight); - - dashboardGrid.update(el, { h: newHeight }); -} - -function initAutoCardResize() { - - const cards = document.querySelectorAll(".grid-stack-item"); - - const observer = new ResizeObserver(entries => { - - entries.forEach(entry => { - - const gridItem = entry.target.closest(".grid-stack-item"); - - if (!gridItem) return; - - const id = gridItem.id; - - resizeGridItemToContent(id); - - }); - - }); - - cards.forEach(card => { - - const content = card.querySelector(".card"); - - if (content) observer.observe(content); - - }); - -} - document.addEventListener("DOMContentLoaded", async () => { // --- PHASE 1: Das Gerüst aufbauen --- initGridstack(); await loadLayout(); // Wartet, bis Boxen aus DB oder LocalStorage da sind - initAutoCardResize(); // --- PHASE 2: Startwerte für Datumsfelder setzen --- const t = new Date().toISOString().split('T')[0]; diff --git a/static/js/script.js b/static/js/script.js index 43a7119..c22a591 100644 --- a/static/js/script.js +++ b/static/js/script.js @@ -1151,6 +1151,20 @@ function showShapDetails(point) { }; const container = document.getElementById("shapForcePlot"); + + // ===== ResizeObserver nur einmal registrieren ===== + if (!container._resizeObserverAttached) { + + const observer = new ResizeObserver(() => { + if (window._lastShapPoint) { + showShapDetails(window._lastShapPoint); + } + }); + + observer.observe(container); + container._resizeObserverAttached = true; + } + container.innerHTML = ""; container.style.position = "relative"; container.style.display = "flex"; From 0ee798d22e5e8f99c658f1984dfed02aa719a0b3 Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Mon, 16 Mar 2026 22:53:00 +0100 Subject: [PATCH 25/71] Change overflow in gridstack card item content style --- static/css/style.css | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/static/css/style.css b/static/css/style.css index 4aba25a..1d7b9f3 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -162,11 +162,12 @@ margin-left: -20px; margin-right: -20px; } - + .grid-stack-item-content { - overflow-x: hidden; - overflow-y: auto; - overflow: visible !important; + xoverflow-x: hidden; + xoverflow-y: auto; + xoverflow: visible !important; + overflow: hidden; background: transparent !important; box-shadow: none !important; } From 3abdb7322ba58129d31bc315f89d6a8ea33d6da7 Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Tue, 17 Mar 2026 08:36:33 +0100 Subject: [PATCH 26/71] .grid-stack-item-content Styles --- static/css/style.css | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/static/css/style.css b/static/css/style.css index 1d7b9f3..939d5d0 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -164,9 +164,9 @@ } .grid-stack-item-content { - xoverflow-x: hidden; - xoverflow-y: auto; - xoverflow: visible !important; + overflow-x: hidden; + overflow-y: auto; + overflow: visible !important; overflow: hidden; background: transparent !important; box-shadow: none !important; From 36b491df6b9c2457868c0e503eb64c1402a739be Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Tue, 17 Mar 2026 08:39:51 +0100 Subject: [PATCH 27/71] forecast card tests --- templates/index.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/templates/index.html b/templates/index.html index 93d0e73..e5c33d3 100644 --- a/templates/index.html +++ b/templates/index.html @@ -232,8 +232,8 @@

-
-
+
Prognose

ML-basierte 7-Tage Prognose auf Basis von Ertrag, Bewölkung, Sonnenhöhe und Wettervorhersage.

@@ -280,8 +280,8 @@

-
-
+
From ae986fa69b87c48cec934bc61991415aa6df67ba Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Tue, 17 Mar 2026 08:42:08 +0100 Subject: [PATCH 28/71] Forecast chart style --- templates/index.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/templates/index.html b/templates/index.html index e5c33d3..93d0e73 100644 --- a/templates/index.html +++ b/templates/index.html @@ -232,8 +232,8 @@

- +
+
Prognose

ML-basierte 7-Tage Prognose auf Basis von Ertrag, Bewölkung, Sonnenhöhe und Wettervorhersage.

@@ -280,8 +280,8 @@

- +
+
From a8cb23f76070dee7ecfc104d1b57051078bab00a Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Tue, 17 Mar 2026 08:47:32 +0100 Subject: [PATCH 29/71] .grid-stack Styles --- static/css/style.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/static/css/style.css b/static/css/style.css index 939d5d0..06e714a 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -161,6 +161,10 @@ .grid-stack { margin-left: -20px; margin-right: -20px; + background: transparent !important; + box-shadow: none !important; + border: 0 !important; + border-radius: 0 !important; } .grid-stack-item-content { From 4592e1e36b02a70a9c65186b0d53362ccb005d4c Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Tue, 17 Mar 2026 10:29:40 +0100 Subject: [PATCH 30/71] Forecast Data Validation & UI --- static/css/style.css | 2 +- static/js/script.js | 103 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 1 deletion(-) diff --git a/static/css/style.css b/static/css/style.css index 06e714a..2d9f482 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -162,7 +162,7 @@ margin-left: -20px; margin-right: -20px; background: transparent !important; - box-shadow: none !important; + box-shadow: 0 !important; border: 0 !important; border-radius: 0 !important; } diff --git a/static/js/script.js b/static/js/script.js index c22a591..444e141 100644 --- a/static/js/script.js +++ b/static/js/script.js @@ -868,6 +868,93 @@ function prettyFeatureName(key) { return featureNames[key] || key; } +// ========================= +// 🔒 VALIDATION + ERROR UI +// ========================= + +function validateForecastData(forecast) { + + if (!Array.isArray(forecast) || forecast.length === 0) { + return "Keine Forecast-Daten vorhanden"; + } + + for (const f of forecast) { + + if (f.kwh_pred == null || isNaN(f.kwh_pred)) { + return "Ungültige Prognosewerte (kwh_pred fehlt)"; + } + + if (f.kwh_lower == null || f.kwh_upper == null) { + return "Unsicherheitsband unvollständig"; + } + + if (!f.date) { + return "Datum fehlt in Forecast"; + } + + } + + return null; +} + +function showForecastError(message) { + + const canvas = document.getElementById("forecastChart"); + const container = canvas.parentElement; + + container.innerHTML = ` +
+ ⚠️ Forecast Fehler:
+ ${message} +
+ `; +} + +function validateShapData(point) { + + if (!point) return "Kein Datenpunkt vorhanden"; + + if (!point.shap || typeof point.shap !== "object") { + return "SHAP-Werte fehlen"; + } + + if (Object.keys(point.shap).length === 0) { + return "SHAP-Daten sind leer"; + } + + if (point.kwh_pred == null) { + return "Prognosewert fehlt"; + } + + return null; +} + +function showShapError(message) { + + const card = document.getElementById("shapDetailCard"); + + card.style.display = "block"; + + card.innerHTML = ` +
+ ⚠️ SHAP Fehler:
+ ${message} +
+ `; +} + async function loadForecast() { const response = await fetch("/api/forecast"); @@ -883,6 +970,14 @@ async function loadForecast() { return; } + const validationError = validateForecastData(forecast); + + if (validationError) { + console.error("Forecast Validation Error:", validationError, forecast); + showForecastError(validationError); + return; + } + const totalKwh = forecast.reduce((sum, d) => sum + d.kwh_pred, 0); const totalEur = forecast.reduce((sum, d) => sum + d.eur_pred, 0); @@ -1125,6 +1220,14 @@ async function loadGlobalShap() { function showShapDetails(point) { + const validationError = validateShapData(point); + + if (validationError) { + console.error("SHAP Validation Error:", validationError, point); + showShapError(validationError); + return; + } + // ===== Punkt global merken für ResizeObserver ===== window._lastShapPoint = point; From 90c2bf274460cf0216a33511d4214607d0ca56a7 Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Tue, 17 Mar 2026 10:37:24 +0100 Subject: [PATCH 31/71] Style Changes --- static/css/style.css | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/static/css/style.css b/static/css/style.css index 2d9f482..5bf737e 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -170,8 +170,6 @@ .grid-stack-item-content { overflow-x: hidden; overflow-y: auto; - overflow: visible !important; - overflow: hidden; background: transparent !important; box-shadow: none !important; } @@ -200,7 +198,7 @@ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.03); display: flex; flex-direction: column; - justify-content: center; + justify-content: start; } .card-gradient { From b73c583a490077656200d75a03a3720d6ff9ee6c Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Tue, 17 Mar 2026 10:38:56 +0100 Subject: [PATCH 32/71] Administration Button --- templates/index.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/index.html b/templates/index.html index 93d0e73..84d49a0 100644 --- a/templates/index.html +++ b/templates/index.html @@ -35,7 +35,7 @@

- +
- Deine Tarife + Deine Stromtarife
+
-
From 287b0dab1b6d2b1e692149c0a28bf9df7ab4b1fd Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Tue, 17 Mar 2026 10:44:36 +0100 Subject: [PATCH 33/71] Administration changes --- static/js/script.js | 4 ++-- templates/index.html | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/static/js/script.js b/static/js/script.js index 444e141..323f688 100644 --- a/static/js/script.js +++ b/static/js/script.js @@ -26,7 +26,7 @@ async function checkLoadingStatus() { } async function unlockAdmin() { - const pw = prompt("Passwort zur Anpassung des Stromtarifs & Layouts:"); + const pw = prompt("Passwort zur Administration:"); if (!pw) return; const res = await fetch('/api/auth', { @@ -1221,7 +1221,7 @@ async function loadGlobalShap() { function showShapDetails(point) { const validationError = validateShapData(point); - + if (validationError) { console.error("SHAP Validation Error:", validationError, point); showShapError(validationError); diff --git a/templates/index.html b/templates/index.html index 84d49a0..5855cd8 100644 --- a/templates/index.html +++ b/templates/index.html @@ -80,6 +80,16 @@

+
+ Grid-Layout anpassen +
From 2037d1ab1e3626e6b633b11fe66309e4f6e6fb36 Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Tue, 17 Mar 2026 10:46:52 +0100 Subject: [PATCH 34/71] Administration Layout --- templates/index.html | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/templates/index.html b/templates/index.html index 5855cd8..fcfeb39 100644 --- a/templates/index.html +++ b/templates/index.html @@ -81,12 +81,13 @@

Grid-Layout anpassen
From bcc46b6fad14061214d9f84cf17dde3b1e50c8be Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Tue, 17 Mar 2026 11:04:47 +0100 Subject: [PATCH 35/71] Auto Resize for Forecast Card --- static/css/style.css | 5 +++-- static/js/layout.js | 31 +++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/static/css/style.css b/static/css/style.css index 5bf737e..0a39d1a 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -168,8 +168,9 @@ } .grid-stack-item-content { - overflow-x: hidden; - overflow-y: auto; + xoverflow-x: hidden; + xoverflow-y: auto; + overflow: visible !important; background: transparent !important; box-shadow: none !important; } diff --git a/static/js/layout.js b/static/js/layout.js index 8f411d4..e49187a 100644 --- a/static/js/layout.js +++ b/static/js/layout.js @@ -85,10 +85,41 @@ async function resetDatabaseLayout() { } } +function initAutoResizeForCard(cardId) { + + const gridItem = document.getElementById(cardId); + const content = gridItem.querySelector(".card"); + + if (!gridItem || !content) return; + + let resizeTimeout; + + const observer = new ResizeObserver(() => { + + // kleines Debounce (wichtig wegen Chart.js Render-Zyklen) + clearTimeout(resizeTimeout); + + resizeTimeout = setTimeout(() => { + + const newHeightPx = content.scrollHeight; + + const cellHeight = dashboardGrid.getCellHeight(); + const newGridHeight = Math.ceil(newHeightPx / cellHeight); + + dashboardGrid.update(gridItem, { h: newGridHeight }); + + }, 80); // Sweet Spot + + }); + + observer.observe(content); +} + document.addEventListener("DOMContentLoaded", async () => { // --- PHASE 1: Das Gerüst aufbauen --- initGridstack(); await loadLayout(); // Wartet, bis Boxen aus DB oder LocalStorage da sind + initAutoResizeForCard("card-forecast"); // --- PHASE 2: Startwerte für Datumsfelder setzen --- const t = new Date().toISOString().split('T')[0]; From f66eec3c85d45ae937948b5df09a0059009cad10 Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Tue, 17 Mar 2026 11:17:58 +0100 Subject: [PATCH 36/71] Card Resize changes --- static/js/layout.js | 13 +++++++++++++ static/js/script.js | 15 +++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/static/js/layout.js b/static/js/layout.js index e49187a..1d14a30 100644 --- a/static/js/layout.js +++ b/static/js/layout.js @@ -115,6 +115,19 @@ function initAutoResizeForCard(cardId) { observer.observe(content); } +function forceGridResize(cardId) { + const gridItem = document.getElementById(cardId); + const content = gridItem.querySelector(".card"); + + if (!gridItem || !content) return; + + const newHeightPx = content.scrollHeight; + const cellHeight = dashboardGrid.getCellHeight(); + const newGridHeight = Math.ceil(newHeightPx / cellHeight); + + dashboardGrid.update(gridItem, { h: newGridHeight }); +} + document.addEventListener("DOMContentLoaded", async () => { // --- PHASE 1: Das Gerüst aufbauen --- initGridstack(); diff --git a/static/js/script.js b/static/js/script.js index 323f688..5736944 100644 --- a/static/js/script.js +++ b/static/js/script.js @@ -1140,6 +1140,11 @@ async function loadForecast() { showShapDetails(forecast[index]); } } + animation: { + onComplete: () => { + forceGridResize("card-forecast"); + } + } } }); } @@ -1171,6 +1176,11 @@ async function loadFeatureImportance() { display: false } } + animation: { + onComplete: () => { + forceGridResize("card-forecast"); + } + } } }); @@ -1209,6 +1219,11 @@ async function loadGlobalShap() { display: false } } + animation: { + onComplete: () => { + forceGridResize("card-forecast"); + } + } } }); From cad9403f45f6fb2fe8e775a408caa68e07280621 Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Tue, 17 Mar 2026 11:19:53 +0100 Subject: [PATCH 37/71] JS fix --- static/js/script.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/static/js/script.js b/static/js/script.js index 5736944..5cacd62 100644 --- a/static/js/script.js +++ b/static/js/script.js @@ -1139,7 +1139,7 @@ async function loadForecast() { const index = points[0].index; showShapDetails(forecast[index]); } - } + }, animation: { onComplete: () => { forceGridResize("card-forecast"); @@ -1175,7 +1175,7 @@ async function loadFeatureImportance() { legend: { display: false } - } + }, animation: { onComplete: () => { forceGridResize("card-forecast"); @@ -1218,7 +1218,7 @@ async function loadGlobalShap() { legend: { display: false } - } + }, animation: { onComplete: () => { forceGridResize("card-forecast"); From 52dbd71c7dc5332e5bc55ab0966830ac06a09b83 Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Tue, 17 Mar 2026 14:33:40 +0100 Subject: [PATCH 38/71] Global Card Resize --- static/js/layout.js | 56 ++++++++++++++++++++++++-------------------- static/js/script.js | 6 ++--- templates/index.html | 2 +- 3 files changed, 35 insertions(+), 29 deletions(-) diff --git a/static/js/layout.js b/static/js/layout.js index 1d14a30..a34dd96 100644 --- a/static/js/layout.js +++ b/static/js/layout.js @@ -85,54 +85,60 @@ async function resetDatabaseLayout() { } } -function initAutoResizeForCard(cardId) { +function initAutoHeightGrid() { - const gridItem = document.getElementById(cardId); - const content = gridItem.querySelector(".card"); + const items = document.querySelectorAll(".grid-stack-item.auto-height"); - if (!gridItem || !content) return; + items.forEach(item => { - let resizeTimeout; + const content = item.querySelector(".grid-stack-item-content"); + const card = content?.querySelector(".card"); - const observer = new ResizeObserver(() => { + if (!content || !card) return; - // kleines Debounce (wichtig wegen Chart.js Render-Zyklen) - clearTimeout(resizeTimeout); + let resizeTimeout; - resizeTimeout = setTimeout(() => { + const resize = () => { - const newHeightPx = content.scrollHeight; + const rect = card.getBoundingClientRect(); + const heightPx = rect.height; const cellHeight = dashboardGrid.getCellHeight(); - const newGridHeight = Math.ceil(newHeightPx / cellHeight); + const newH = Math.ceil(heightPx / cellHeight); - dashboardGrid.update(gridItem, { h: newGridHeight }); + // 🔥 Nur updaten wenn nötig (sehr wichtig!) + if (item.gridstackNode.h !== newH) { + dashboardGrid.update(item, { h: newH }); + } + }; - }, 80); // Sweet Spot + const observer = new ResizeObserver(() => { - }); + clearTimeout(resizeTimeout); - observer.observe(content); -} + resizeTimeout = setTimeout(() => { + resize(); + }, 60); // debounce für Chart.js -function forceGridResize(cardId) { - const gridItem = document.getElementById(cardId); - const content = gridItem.querySelector(".card"); + }); - if (!gridItem || !content) return; + observer.observe(card); - const newHeightPx = content.scrollHeight; - const cellHeight = dashboardGrid.getCellHeight(); - const newGridHeight = Math.ceil(newHeightPx / cellHeight); + // 👉 initial nach Render + setTimeout(resize, 300); - dashboardGrid.update(gridItem, { h: newGridHeight }); + // 👉 fallback (Fonts / async Layout) + requestAnimationFrame(() => { + requestAnimationFrame(resize); + }); + }); } document.addEventListener("DOMContentLoaded", async () => { // --- PHASE 1: Das Gerüst aufbauen --- initGridstack(); await loadLayout(); // Wartet, bis Boxen aus DB oder LocalStorage da sind - initAutoResizeForCard("card-forecast"); + initAutoHeightGrid(); // --- PHASE 2: Startwerte für Datumsfelder setzen --- const t = new Date().toISOString().split('T')[0]; diff --git a/static/js/script.js b/static/js/script.js index 5cacd62..e5b8233 100644 --- a/static/js/script.js +++ b/static/js/script.js @@ -1142,7 +1142,7 @@ async function loadForecast() { }, animation: { onComplete: () => { - forceGridResize("card-forecast"); + window.dispatchEvent(new Event('resize')); } } } @@ -1178,7 +1178,7 @@ async function loadFeatureImportance() { }, animation: { onComplete: () => { - forceGridResize("card-forecast"); + window.dispatchEvent(new Event('resize')); } } } @@ -1221,7 +1221,7 @@ async function loadGlobalShap() { }, animation: { onComplete: () => { - forceGridResize("card-forecast"); + window.dispatchEvent(new Event('resize')); } } } diff --git a/templates/index.html b/templates/index.html index fcfeb39..8276e52 100644 --- a/templates/index.html +++ b/templates/index.html @@ -243,7 +243,7 @@

-
+
Prognose
From 885b4384f92404d7e1160c5aaaac103d24cc39ce Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Tue, 17 Mar 2026 14:42:06 +0100 Subject: [PATCH 39/71] Undo Auto Resize --- static/js/layout.js | 56 +------------------------------------------- static/js/script.js | 15 ------------ templates/index.html | 2 +- 3 files changed, 2 insertions(+), 71 deletions(-) diff --git a/static/js/layout.js b/static/js/layout.js index a34dd96..1d0423c 100644 --- a/static/js/layout.js +++ b/static/js/layout.js @@ -85,60 +85,10 @@ async function resetDatabaseLayout() { } } -function initAutoHeightGrid() { - - const items = document.querySelectorAll(".grid-stack-item.auto-height"); - - items.forEach(item => { - - const content = item.querySelector(".grid-stack-item-content"); - const card = content?.querySelector(".card"); - - if (!content || !card) return; - - let resizeTimeout; - - const resize = () => { - - const rect = card.getBoundingClientRect(); - const heightPx = rect.height; - - const cellHeight = dashboardGrid.getCellHeight(); - const newH = Math.ceil(heightPx / cellHeight); - - // 🔥 Nur updaten wenn nötig (sehr wichtig!) - if (item.gridstackNode.h !== newH) { - dashboardGrid.update(item, { h: newH }); - } - }; - - const observer = new ResizeObserver(() => { - - clearTimeout(resizeTimeout); - - resizeTimeout = setTimeout(() => { - resize(); - }, 60); // debounce für Chart.js - - }); - - observer.observe(card); - - // 👉 initial nach Render - setTimeout(resize, 300); - - // 👉 fallback (Fonts / async Layout) - requestAnimationFrame(() => { - requestAnimationFrame(resize); - }); - }); -} - document.addEventListener("DOMContentLoaded", async () => { // --- PHASE 1: Das Gerüst aufbauen --- initGridstack(); await loadLayout(); // Wartet, bis Boxen aus DB oder LocalStorage da sind - initAutoHeightGrid(); // --- PHASE 2: Startwerte für Datumsfelder setzen --- const t = new Date().toISOString().split('T')[0]; @@ -179,9 +129,5 @@ document.addEventListener("DOMContentLoaded", async () => { setInterval(() => { if (typeof fetchData === "function") fetchData(); }, 60000); setInterval(() => { if (typeof updatePeaks === "function") updatePeaks(); }, 60000); setInterval(() => { if (typeof checkLoadingStatus === "function") checkLoadingStatus(); }, 500); - - // WICHTIG: Einmal kräftig schütteln, damit Charts ihre Größe im neuen Grid finden - setTimeout(() => { - window.dispatchEvent(new Event('resize')); - }, 200); + }); \ No newline at end of file diff --git a/static/js/script.js b/static/js/script.js index e5b8233..096dd0c 100644 --- a/static/js/script.js +++ b/static/js/script.js @@ -1140,11 +1140,6 @@ async function loadForecast() { showShapDetails(forecast[index]); } }, - animation: { - onComplete: () => { - window.dispatchEvent(new Event('resize')); - } - } } }); } @@ -1176,11 +1171,6 @@ async function loadFeatureImportance() { display: false } }, - animation: { - onComplete: () => { - window.dispatchEvent(new Event('resize')); - } - } } }); @@ -1219,11 +1209,6 @@ async function loadGlobalShap() { display: false } }, - animation: { - onComplete: () => { - window.dispatchEvent(new Event('resize')); - } - } } }); diff --git a/templates/index.html b/templates/index.html index 8276e52..fcfeb39 100644 --- a/templates/index.html +++ b/templates/index.html @@ -243,7 +243,7 @@

-
+
Prognose
From bb42306ca41cb6e32f652cfd338a5cb8ed2dd5e2 Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Tue, 17 Mar 2026 15:37:08 +0100 Subject: [PATCH 40/71] Split Forecast Card --- static/js/script.js | 3 --- templates/index.html | 24 ++++++++++++++++++++---- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/static/js/script.js b/static/js/script.js index 096dd0c..f4e94c3 100644 --- a/static/js/script.js +++ b/static/js/script.js @@ -936,11 +936,8 @@ function validateShapData(point) { } function showShapError(message) { - const card = document.getElementById("shapDetailCard"); - card.style.display = "block"; - card.innerHTML = `
- - +
+
+ +
+
+
+
- +
+
+
+
+
+
Global SHAP
+
+
+
+
+
+
Feature Importance
-
From 5d27c63e294b829155f31de58bbb8d6ba240514d Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Tue, 17 Mar 2026 15:38:38 +0100 Subject: [PATCH 41/71] Test --- templates/index.html | 1 + 1 file changed, 1 insertion(+) diff --git a/templates/index.html b/templates/index.html index fbbae85..e402410 100644 --- a/templates/index.html +++ b/templates/index.html @@ -324,3 +324,4 @@

+ \ No newline at end of file From 9c543afa6292f17db6d4889b95bda06ad220a029 Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Tue, 17 Mar 2026 15:44:02 +0100 Subject: [PATCH 42/71] Grid changes --- templates/index.html | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/templates/index.html b/templates/index.html index e402410..f3f29d3 100644 --- a/templates/index.html +++ b/templates/index.html @@ -260,7 +260,7 @@

-
+
-
+
@@ -323,5 +323,4 @@

- - \ No newline at end of file + \ No newline at end of file From 29266f1336b1cee1f3accc6235497f3203c032d0 Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Tue, 17 Mar 2026 15:45:44 +0100 Subject: [PATCH 43/71] Card Names --- templates/index.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/index.html b/templates/index.html index f3f29d3..cef897a 100644 --- a/templates/index.html +++ b/templates/index.html @@ -260,7 +260,7 @@

-
+
-
+
@@ -299,7 +299,7 @@

-
+
From f3dda72a7e55de90dc83c9b7426ca491bbd2542a Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Tue, 17 Mar 2026 16:13:19 +0100 Subject: [PATCH 44/71] ShowShapDetails automatic today --- static/js/script.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/static/js/script.js b/static/js/script.js index f4e94c3..e787644 100644 --- a/static/js/script.js +++ b/static/js/script.js @@ -952,6 +952,14 @@ function showShapError(message) { `; } +function getTodayForecastPoint(forecastData) { + + const todayStr = new Date().toISOString().split("T")[0]; + // → ergibt z.B. "2026-03-17" + + return forecastData.find(d => d.date === todayStr); +} + async function loadForecast() { const response = await fetch("/api/forecast"); @@ -1139,6 +1147,11 @@ async function loadForecast() { }, } }); + const todayPoint = getTodayForecastPoint(data.forecast) || data.forecast[0]; + if (todayPoint) { + showShapDetails(todayPoint); + document.getElementById("card-shap")?.style.display = "block"; + }; } async function loadFeatureImportance() { From cad3316d74b5ff1b136165d40d042094ee8f6deb Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Tue, 17 Mar 2026 16:18:33 +0100 Subject: [PATCH 45/71] Rename card shap details --- static/js/script.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/static/js/script.js b/static/js/script.js index e787644..64f4abf 100644 --- a/static/js/script.js +++ b/static/js/script.js @@ -953,10 +953,7 @@ function showShapError(message) { } function getTodayForecastPoint(forecastData) { - const todayStr = new Date().toISOString().split("T")[0]; - // → ergibt z.B. "2026-03-17" - return forecastData.find(d => d.date === todayStr); } @@ -1150,7 +1147,7 @@ async function loadForecast() { const todayPoint = getTodayForecastPoint(data.forecast) || data.forecast[0]; if (todayPoint) { showShapDetails(todayPoint); - document.getElementById("card-shap")?.style.display = "block"; + document.getElementById("card-shap-details")?.style.display = "block"; }; } From f73281a10b3304f247d873a0ddb9b5b3a9061fd1 Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Tue, 17 Mar 2026 16:20:43 +0100 Subject: [PATCH 46/71] Changes --- static/js/script.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/js/script.js b/static/js/script.js index 64f4abf..4820419 100644 --- a/static/js/script.js +++ b/static/js/script.js @@ -1147,7 +1147,7 @@ async function loadForecast() { const todayPoint = getTodayForecastPoint(data.forecast) || data.forecast[0]; if (todayPoint) { showShapDetails(todayPoint); - document.getElementById("card-shap-details")?.style.display = "block"; + document.getElementById("shapDetailCard").style.display = "block"; }; } From 3b63d585541004057ebb4784138eb69e2de123b2 Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Wed, 18 Mar 2026 08:38:55 +0100 Subject: [PATCH 47/71] Highlight active forecast point --- static/js/script.js | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/static/js/script.js b/static/js/script.js index 4820419..4934ddb 100644 --- a/static/js/script.js +++ b/static/js/script.js @@ -957,6 +957,19 @@ function getTodayForecastPoint(forecastData) { return forecastData.find(d => d.date === todayStr); } +function getForecastIndex(forecastData, targetDate) { + return forecastData.findIndex(d => d.date === targetDate); +} + +function highlightForecastPoint(chart, index) { + if (!chart || index < 0) return; + chart.setActiveElements([{ + datasetIndex: 0, + index: index + }]); + chart.update(); +} + async function loadForecast() { const response = await fetch("/api/forecast"); @@ -1140,15 +1153,18 @@ async function loadForecast() { if (points.length) { const index = points[0].index; showShapDetails(forecast[index]); + highlightForecastPoint(forecastChart, index); + } }, } }); const todayPoint = getTodayForecastPoint(data.forecast) || data.forecast[0]; if (todayPoint) { - showShapDetails(todayPoint); - document.getElementById("shapDetailCard").style.display = "block"; - }; + showShapDetails(todayPoint); + const index = getForecastIndex(data.forecast, todayPoint.date); + highlightForecastPoint(forecastChart, index); + } } async function loadFeatureImportance() { From 8fe97e53d60b90b78f3b220dc030702f357be7b7 Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Wed, 18 Mar 2026 08:54:57 +0100 Subject: [PATCH 48/71] Forecast Chart Highlight active point --- static/js/script.js | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/static/js/script.js b/static/js/script.js index 4934ddb..cea7901 100644 --- a/static/js/script.js +++ b/static/js/script.js @@ -852,6 +852,12 @@ async function loadHourlyHeatmap(month) { // Beim Laden der Seite aufrufen document.addEventListener("DOMContentLoaded", initHourlyHeatmap); +// ========================= +// FORECAST JS +// ========================= + +let activeForecastIndex = 0; // Default = erster Punkt + function prettyFeatureName(key) { const featureNames = { clouds: "Mittlerer Bewölkungsgrad", @@ -868,9 +874,7 @@ function prettyFeatureName(key) { return featureNames[key] || key; } -// ========================= // 🔒 VALIDATION + ERROR UI -// ========================= function validateForecastData(forecast) { @@ -957,12 +961,9 @@ function getTodayForecastPoint(forecastData) { return forecastData.find(d => d.date === todayStr); } -function getForecastIndex(forecastData, targetDate) { - return forecastData.findIndex(d => d.date === targetDate); -} - -function highlightForecastPoint(chart, index) { +function setActivePoint(chart, index) { if (!chart || index < 0) return; + activeForecastIndex = index; chart.setActiveElements([{ datasetIndex: 0, index: index @@ -1153,18 +1154,19 @@ async function loadForecast() { if (points.length) { const index = points[0].index; showShapDetails(forecast[index]); - highlightForecastPoint(forecastChart, index); - + setActivePoint(forecastChart, index); } }, } }); const todayPoint = getTodayForecastPoint(data.forecast) || data.forecast[0]; if (todayPoint) { - showShapDetails(todayPoint); - const index = getForecastIndex(data.forecast, todayPoint.date); - highlightForecastPoint(forecastChart, index); - } + showShapDetails(todayPoint); + document.getElementById("shapDetailCard").style.display = "block"; + setTimeout(() => { + setActivePoint(forecastChart, 0); + }, 100); + }; } async function loadFeatureImportance() { From 2e188efff4ce991172df331fae80e709f8edf5d4 Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Wed, 18 Mar 2026 09:03:44 +0100 Subject: [PATCH 49/71] Fix --- static/js/script.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/static/js/script.js b/static/js/script.js index cea7901..6b0111b 100644 --- a/static/js/script.js +++ b/static/js/script.js @@ -965,7 +965,7 @@ function setActivePoint(chart, index) { if (!chart || index < 0) return; activeForecastIndex = index; chart.setActiveElements([{ - datasetIndex: 0, + datasetIndex: 2, index: index }]); chart.update(); @@ -1154,7 +1154,7 @@ async function loadForecast() { if (points.length) { const index = points[0].index; showShapDetails(forecast[index]); - setActivePoint(forecastChart, index); + setActivePoint(window.forecastChartInstance, index); } }, } @@ -1163,9 +1163,10 @@ async function loadForecast() { if (todayPoint) { showShapDetails(todayPoint); document.getElementById("shapDetailCard").style.display = "block"; + const todayIndex = forecast.findIndex(f => f.date === todayPoint.date); setTimeout(() => { - setActivePoint(forecastChart, 0); - }, 100); + setActivePoint(window.forecastChartInstance, todayIndex); + }, 100); }; } From 950e7b528068f12855ed4f8c1bca8cac7f680c60 Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Wed, 18 Mar 2026 09:19:39 +0100 Subject: [PATCH 50/71] Forecast Chart active point style --- static/js/script.js | 42 +++++++++++++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/static/js/script.js b/static/js/script.js index 6b0111b..21cd304 100644 --- a/static/js/script.js +++ b/static/js/script.js @@ -1051,11 +1051,22 @@ async function loadForecast() { borderColor: '#ef4444', backgroundColor: '#ef4444', tension: 0.3, - pointRadius: 6, + pointRadius: (ctx) => { + return ctx.dataIndex === activeForecastIndex ? 8 : 6; + }, pointHoverRadius: 10, hitRadius: 20, + pointBackgroundColor: (ctx) => { + return ctx.dataIndex === activeForecastIndex + ? '#ffffff' + : '#ef4444'; + }, + pointBorderColor: '#ef4444', + pointBorderWidth: (ctx) => { + return ctx.dataIndex === activeForecastIndex ? 3 : 0; + }, fill: false - } + }, ] }, options: { @@ -1152,11 +1163,26 @@ async function loadForecast() { true ); if (points.length) { - const index = points[0].index; - showShapDetails(forecast[index]); - setActivePoint(window.forecastChartInstance, index); - } + const index = points[0].index; + showShapDetails(forecast[index]); + activeForecastIndex = index; + setActivePoint(chart, index); + } }, + onHover: (event, elements, chart) => { + if (elements.length) { + const index = elements[0].index; + + chart.setActiveElements([{ + datasetIndex: 2, + index: index + }]); + + chart.update(); + } else { + setActivePoint(chart, activeForecastIndex); + } + }, } }); const todayPoint = getTodayForecastPoint(data.forecast) || data.forecast[0]; @@ -1164,9 +1190,7 @@ async function loadForecast() { showShapDetails(todayPoint); document.getElementById("shapDetailCard").style.display = "block"; const todayIndex = forecast.findIndex(f => f.date === todayPoint.date); - setTimeout(() => { - setActivePoint(window.forecastChartInstance, todayIndex); - }, 100); + setActivePoint(window.forecastChartInstance, todayIndex); }; } From 0d0a8f2a012fedf91c99436477f2e2e22bfccfe0 Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Wed, 18 Mar 2026 09:25:51 +0100 Subject: [PATCH 51/71] Remove static height vom Charts --- templates/index.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/index.html b/templates/index.html index cef897a..a3fe3d8 100644 --- a/templates/index.html +++ b/templates/index.html @@ -255,7 +255,7 @@

MAE
- kWh

- +
@@ -294,7 +294,7 @@

Global SHAP
- +

@@ -304,7 +304,7 @@

Feature Importance
- +

From 5e2d5516f0c0410758464f45622a725015722a09 Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Wed, 18 Mar 2026 09:34:00 +0100 Subject: [PATCH 52/71] Change gridstack cell height --- static/js/layout.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/js/layout.js b/static/js/layout.js index 1d0423c..b5ed4d1 100644 --- a/static/js/layout.js +++ b/static/js/layout.js @@ -4,7 +4,7 @@ let currentPw = ""; // 1. Grid initialisieren function initGridstack() { dashboardGrid = GridStack.init({ - cellHeight: 110, + cellHeight: 50, margin: 20, animate: true, staticGrid: true, @@ -129,5 +129,5 @@ document.addEventListener("DOMContentLoaded", async () => { setInterval(() => { if (typeof fetchData === "function") fetchData(); }, 60000); setInterval(() => { if (typeof updatePeaks === "function") updatePeaks(); }, 60000); setInterval(() => { if (typeof checkLoadingStatus === "function") checkLoadingStatus(); }, 500); - + }); \ No newline at end of file From ff341666651b89391a24a030daa3fb80bd4fe39d Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Wed, 18 Mar 2026 09:40:30 +0100 Subject: [PATCH 53/71] Change grid-stack-item sizes --- templates/index.html | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/templates/index.html b/templates/index.html index a3fe3d8..2a21fc1 100644 --- a/templates/index.html +++ b/templates/index.html @@ -98,7 +98,7 @@

-
+
-
+
@@ -129,7 +129,7 @@

-
+
-
+
-
+
Leistungs-Rekorde (AC)
@@ -213,7 +213,7 @@

-
+
ROI Fortschritt
@@ -226,7 +226,7 @@

-
+
@@ -243,7 +243,7 @@

-
+
Prognose
@@ -260,7 +260,7 @@

-
+
-
+
@@ -299,7 +299,7 @@

-
+
From 6e174ef35d50f453a29fde3168370250e1595f84 Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Wed, 18 Mar 2026 09:51:17 +0100 Subject: [PATCH 54/71] Remove season strength vom Forecast --- templates/index.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/templates/index.html b/templates/index.html index 2a21fc1..df43dbb 100644 --- a/templates/index.html +++ b/templates/index.html @@ -163,7 +163,7 @@

-
+
-
+
@@ -276,7 +276,7 @@

-
+
From 6ae4ce844bb5aebc41237e0bd9cc4940cee98553 Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Wed, 18 Mar 2026 09:54:03 +0100 Subject: [PATCH 55/71] Test --- templates/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/index.html b/templates/index.html index df43dbb..60a69cf 100644 --- a/templates/index.html +++ b/templates/index.html @@ -162,7 +162,7 @@

- + test
From b2c1c8322e4ea927d1a93656f8417814fe948df0 Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Wed, 18 Mar 2026 10:36:48 +0100 Subject: [PATCH 56/71] Prevent duplicate hourly-heatmap initialization --- static/js/script.js | 4 ++++ templates/index.html | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/static/js/script.js b/static/js/script.js index 21cd304..5ffb99f 100644 --- a/static/js/script.js +++ b/static/js/script.js @@ -732,7 +732,11 @@ document.getElementById("heatmapYearSelect") document.addEventListener("DOMContentLoaded", initHeatmapYears); +let heatmapInitialized = false; + async function initHourlyHeatmap() { + if (heatmapInitialized) return; + heatmapInitialized = true; const res = await fetch("/api/heatmap_hourly"); const data = await res.json(); diff --git a/templates/index.html b/templates/index.html index 60a69cf..e1d6cf4 100644 --- a/templates/index.html +++ b/templates/index.html @@ -162,7 +162,7 @@

- test +
From b02a797fabad62800644c1c6df212b9618e776d0 Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Wed, 18 Mar 2026 10:40:18 +0100 Subject: [PATCH 57/71] Resize Hourly Heatmap --- templates/index.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/index.html b/templates/index.html index e1d6cf4..1c68655 100644 --- a/templates/index.html +++ b/templates/index.html @@ -162,8 +162,8 @@

- -
+ +
-
+
From 2c5e2127840c117479130049a597d978badf4274 Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Wed, 18 Mar 2026 10:55:41 +0100 Subject: [PATCH 58/71] Forecast Chart Active Point Architecture --- static/js/script.js | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/static/js/script.js b/static/js/script.js index 5ffb99f..1bc5bc9 100644 --- a/static/js/script.js +++ b/static/js/script.js @@ -972,6 +972,7 @@ function setActivePoint(chart, index) { datasetIndex: 2, index: index }]); + chart.data.datasets[2].data = [...chart.data.datasets[2].data]; chart.update(); } @@ -1159,30 +1160,26 @@ async function loadForecast() { } }, onClick: (event, elements, chart) => { - const points = chart.getElementsAtEventForMode( - event, - 'index', { - intersect: false - }, - true - ); - if (points.length) { + const points = chart.getElementsAtEventForMode( + event, + 'index', + { intersect: false }, + true + ); + if (points.length) { const index = points[0].index; showShapDetails(forecast[index]); - activeForecastIndex = index; setActivePoint(chart, index); } - }, + }, onHover: (event, elements, chart) => { if (elements.length) { const index = elements[0].index; - chart.setActiveElements([{ datasetIndex: 2, index: index }]); - - chart.update(); + chart.update('none'); } else { setActivePoint(chart, activeForecastIndex); } From f57f8e1e68d7dce9a1a09f08f0d1e5a9505c52c4 Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Wed, 18 Mar 2026 11:24:06 +0100 Subject: [PATCH 59/71] Load Forecast refactoring --- static/js/script.js | 439 ++++++++++++++++++++++---------------------- 1 file changed, 223 insertions(+), 216 deletions(-) diff --git a/static/js/script.js b/static/js/script.js index 1bc5bc9..a9cd672 100644 --- a/static/js/script.js +++ b/static/js/script.js @@ -860,7 +860,11 @@ document.addEventListener("DOMContentLoaded", initHourlyHeatmap); // FORECAST JS // ========================= -let activeForecastIndex = 0; // Default = erster Punkt +const appState = { + forecast: [], + activeIndex: 0, + chart: null +}; function prettyFeatureName(key) { const featureNames = { @@ -965,235 +969,238 @@ function getTodayForecastPoint(forecastData) { return forecastData.find(d => d.date === todayStr); } -function setActivePoint(chart, index) { - if (!chart || index < 0) return; - activeForecastIndex = index; - chart.setActiveElements([{ +function setActiveIndex(index) { + if (!appState.chart || index < 0) return; + appState.activeIndex = index; + appState.chart.setActiveElements([{ datasetIndex: 2, index: index }]); - chart.data.datasets[2].data = [...chart.data.datasets[2].data]; - chart.update(); + appState.chart.update(); + showShapDetails(appState.forecast[index]); } async function loadForecast() { - const response = await fetch("/api/forecast"); - const data = await response.json(); - - console.log("Forecast API:", data); - - const forecast = data.forecast || []; - const mae = data.mae || 0; - - if (forecast.length === 0) { - console.warn("Keine Forecast Daten vorhanden"); - return; - } - - const validationError = validateForecastData(forecast); - - if (validationError) { - console.error("Forecast Validation Error:", validationError, forecast); - showForecastError(validationError); + const response = await fetch("/api/forecast"); + const data = await response.json(); + + console.log("Forecast API:", data); + + const forecast = data.forecast || []; + const mae = data.mae || 0; + + if (forecast.length === 0) { + console.warn("Keine Forecast Daten vorhanden"); return; } - - const totalKwh = forecast.reduce((sum, d) => sum + d.kwh_pred, 0); - const totalEur = forecast.reduce((sum, d) => sum + d.eur_pred, 0); - - document.getElementById('forecast-total-kwh').innerHTML = `${totalKwh.toFixed(2)}kWh`; - - document.getElementById("forecast-total-eur").innerHTML = `${totalEur.toFixed(2)}`; - - document.getElementById("forecast-mae").innerHTML = `${mae.toFixed(2)}kWh`; - - - // ========================= - // Forecast Chart mit Unsicherheitsband - // ========================= - - const labels = forecast.map(f => { - const parts = f.date.split("-"); - return `${parts[2]}.${parts[1]}.${parts[0]}`; - }); - const values = forecast.map(f => f.kwh_pred); - - const lower = forecast.map(f => Number(f.kwh_lower)); - const upper = forecast.map(f => Number(f.kwh_upper)); - - const ctx = document.getElementById("forecastChart"); - - if (window.forecastChartInstance) { - window.forecastChartInstance.destroy(); - } - - window.forecastChartInstance = new Chart(ctx, { - type: 'line', - data: { - labels: labels, - datasets: [ - { - label: '90% Quantil', - data: upper, - borderColor: 'transparent', - pointRadius: 0, - fill: false, - tooltipHidden: true - }, - { - label: 'Unsicherheitsband (80%)', - data: lower, - borderColor: 'transparent', - borderWidth: 0, - pointRadius: 0, - fill: '-1', - backgroundColor: 'rgba(239,68,68,0.18)', + + const validationError = validateForecastData(forecast); + + if (validationError) { + console.error("Forecast Validation Error:", validationError, forecast); + showForecastError(validationError); + return; + } + + appState.forecast = forecast; + + const totalKwh = forecast.reduce((sum, d) => sum + d.kwh_pred, 0); + const totalEur = forecast.reduce((sum, d) => sum + d.eur_pred, 0); + + document.getElementById('forecast-total-kwh').innerHTML = `${totalKwh.toFixed(2)}kWh`; + document.getElementById("forecast-total-eur").innerHTML = `${totalEur.toFixed(2)}`; + document.getElementById("forecast-mae").innerHTML = `${mae.toFixed(2)}kWh`; + + const labels = forecast.map(f => { + const parts = f.date.split("-"); + return `${parts[2]}.${parts[1]}.${parts[0]}`; + }); + const values = forecast.map(f => f.kwh_pred); + + const lower = forecast.map(f => Number(f.kwh_lower)); + const upper = forecast.map(f => Number(f.kwh_upper)); + + const ctx = document.getElementById("forecastChart"); + + if (window.forecastChartInstance) { + window.forecastChartInstance.destroy(); + } + + window.forecastChartInstance = new Chart(ctx, { + type: 'line', + data: { + labels: labels, + datasets: [ + { + label: '90% Quantil', + data: upper, + borderColor: 'transparent', + pointRadius: 0, + fill: false, + tooltipHidden: true + }, + { + label: 'Unsicherheitsband (80%)', + data: lower, + borderColor: 'transparent', + borderWidth: 0, + pointRadius: 0, + fill: '-1', + backgroundColor: 'rgba(239,68,68,0.18)', + }, + { + label: 'Prognose', + data: values, + borderColor: '#ef4444', + backgroundColor: '#ef4444', + tension: 0.3, + + pointRadius: (ctx) => { + return ctx.dataIndex === appState.activeIndex ? 8 : 6; + }, + pointHoverRadius: 10, + hitRadius: 20, + pointBackgroundColor: (ctx) => { + return ctx.dataIndex === appState.activeIndex + ? '#ffffff' + : '#ef4444'; + }, + pointBorderColor: '#ef4444', + pointBorderWidth: (ctx) => { + return ctx.dataIndex === appState.activeIndex ? 3 : 0; + }, + + fill: false + }, + ] + }, + options: { + responsive: true, + interaction: { + mode: 'index', + intersect: false }, - { - label: 'Prognose', - data: values, - borderColor: '#ef4444', - backgroundColor: '#ef4444', - tension: 0.3, - pointRadius: (ctx) => { - return ctx.dataIndex === activeForecastIndex ? 8 : 6; - }, - pointHoverRadius: 10, - hitRadius: 20, - pointBackgroundColor: (ctx) => { - return ctx.dataIndex === activeForecastIndex - ? '#ffffff' - : '#ef4444'; - }, - pointBorderColor: '#ef4444', - pointBorderWidth: (ctx) => { - return ctx.dataIndex === activeForecastIndex ? 3 : 0; + plugins: { + legend: { + display: false }, - fill: false + tooltip: { + enabled: false, + external: function(context) { + let tooltipEl = document.getElementById('forecast-chart-tooltip'); + + if (!tooltipEl) { + tooltipEl = document.createElement('div'); + tooltipEl.id = 'forecast-chart-tooltip'; + tooltipEl.classList.add('heatmap-tooltip'); + Object.assign(tooltipEl.style, { + opacity: 1, + pointerEvents: 'none', + position: 'absolute', + transition: 'opacity 0.15s ease', + bottom: 'auto', + left: '0px', + top: '0px', + transform: 'translate(-50%, -110%)', + whiteSpace: 'nowrap', + zIndex: '100' + }); + document.body.appendChild(tooltipEl); + } + + const tooltipModel = context.tooltip; + if (tooltipModel.opacity === 0) { + tooltipEl.style.opacity = 0; + return; + } + + if (tooltipModel.body) { + const index = tooltipModel.dataPoints[0].dataIndex; + const f = forecast[index]; + + tooltipEl.innerHTML = ` +
+ ${labels[index]} +
+ +
+ + Prognose: ${f.kwh_pred.toFixed(3)} kWh +
+ +
+
+ + Max (90% Quantil): ${Number(f.kwh_upper).toFixed(2)} kWh +
+
+ + Min (10% Quantil): ${Number(f.kwh_lower).toFixed(2)} kWh +
+
+ +
+ 80% Wahrscheinlichkeits-Intervall +
+ `; + } + + const position = context.chart.canvas.getBoundingClientRect(); + tooltipEl.style.opacity = 1; + tooltipEl.style.left = position.left + window.pageXOffset + tooltipModel.caretX + 'px'; + tooltipEl.style.top = position.top + window.pageYOffset + tooltipModel.caretY + 'px'; + } + } }, - ] - }, - options: { - responsive: true, - interaction: { - mode: 'index', - intersect: false - }, - plugins: { - legend: { - display: false - }, - tooltip: { - enabled: false, // Standard deaktivieren - external: function(context) { - let tooltipEl = document.getElementById('forecast-chart-tooltip'); - - if (!tooltipEl) { - tooltipEl = document.createElement('div'); - tooltipEl.id = 'forecast-chart-tooltip'; - // Wir fügen deine CSS-Klasse hinzu! - tooltipEl.classList.add('heatmap-tooltip'); - // Überschreiben einiger Werte für die Chart-Positionierung - Object.assign(tooltipEl.style, { - opacity: 1, - pointerEvents: 'none', - position: 'absolute', - transition: 'opacity 0.15s ease', - bottom: 'auto', // Reset von deinem CSS - left: '0px', - top: '0px', - transform: 'translate(-50%, -110%)', // Zentriert über dem Punkt - whiteSpace: 'nowrap', - zIndex: '100' - }); - document.body.appendChild(tooltipEl); - } - - const tooltipModel = context.tooltip; - if (tooltipModel.opacity === 0) { - tooltipEl.style.opacity = 0; - return; - } - - if (tooltipModel.body) { - const index = tooltipModel.dataPoints[0].dataIndex; - const f = forecast[index]; - - tooltipEl.innerHTML = ` -
- ${labels[index]} -
- -
- - Prognose: ${f.kwh_pred.toFixed(3)} kWh -
- -
-
- - Max (90% Quantil): ${Number(f.kwh_upper).toFixed(2)} kWh -
-
- - Min (10% Quantil): ${Number(f.kwh_lower).toFixed(2)} kWh -
-
- -
- 80% Wahrscheinlichkeits-Intervall -
- `; - } - - const position = context.chart.canvas.getBoundingClientRect(); - tooltipEl.style.opacity = 1; - tooltipEl.style.left = position.left + window.pageXOffset + tooltipModel.caretX + 'px'; - tooltipEl.style.top = position.top + window.pageYOffset + tooltipModel.caretY + 'px'; - } - } - }, - scales: { - y: { - beginAtZero: true - } - }, - onClick: (event, elements, chart) => { - const points = chart.getElementsAtEventForMode( - event, - 'index', - { intersect: false }, - true - ); - if (points.length) { - const index = points[0].index; - showShapDetails(forecast[index]); - setActivePoint(chart, index); + scales: { + y: { + beginAtZero: true } }, - onHover: (event, elements, chart) => { - if (elements.length) { - const index = elements[0].index; - chart.setActiveElements([{ - datasetIndex: 2, - index: index - }]); - chart.update('none'); - } else { - setActivePoint(chart, activeForecastIndex); - } - }, - } - }); - const todayPoint = getTodayForecastPoint(data.forecast) || data.forecast[0]; - if (todayPoint) { - showShapDetails(todayPoint); - document.getElementById("shapDetailCard").style.display = "block"; - const todayIndex = forecast.findIndex(f => f.date === todayPoint.date); - setActivePoint(window.forecastChartInstance, todayIndex); - }; -} + + onClick: (event, elements, chart) => { + const points = chart.getElementsAtEventForMode( + event, + 'index', + { intersect: false }, + true + ); + if (points.length) { + setActiveIndex(points[0].index); + } + }, + + onHover: (event, elements, chart) => { + if (elements.length) { + chart.setActiveElements([{ + datasetIndex: 2, + index: elements[0].index + }]); + chart.update('none'); + } else { + chart.setActiveElements([{ + datasetIndex: 2, + index: appState.activeIndex + }]); + chart.update('none'); + } + }, + } + }); + + appState.chart = window.forecastChartInstance; + + const todayPoint = getTodayForecastPoint(forecast) || forecast[0]; + + if (todayPoint) { + const todayIndex = forecast.findIndex(f => f.date === todayPoint.date); + + document.getElementById("shapDetailCard").style.display = "block"; + + setActiveIndex(todayIndex); + } + } async function loadFeatureImportance() { From eac860ad24f5d06b848b309af9883873ae1a5f25 Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Wed, 18 Mar 2026 12:19:18 +0100 Subject: [PATCH 60/71] Change hover style for activeIndex --- static/js/script.js | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/static/js/script.js b/static/js/script.js index a9cd672..7e08eca 100644 --- a/static/js/script.js +++ b/static/js/script.js @@ -1171,21 +1171,24 @@ async function loadForecast() { } }, - onHover: (event, elements, chart) => { - if (elements.length) { - chart.setActiveElements([{ - datasetIndex: 2, - index: elements[0].index - }]); - chart.update('none'); - } else { - chart.setActiveElements([{ - datasetIndex: 2, - index: appState.activeIndex - }]); - chart.update('none'); - } - }, + onHover: (event, elements, chart) => { + if (elements.length) { + const hoverIndex = elements[0].index; + if (hoverIndex !== appState.activeIndex) { + chart.setActiveElements([{ + datasetIndex: 2, + index: hoverIndex + }]); + chart.update('none'); + } + } else { + chart.setActiveElements([{ + datasetIndex: 2, + index: appState.activeIndex + }]); + chart.update('none'); + } + } } }); From 723172411fc6d8f58cc468a9738f87f2d961f3e8 Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Wed, 18 Mar 2026 13:41:19 +0100 Subject: [PATCH 61/71] Style changes --- static/js/script.js | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/static/js/script.js b/static/js/script.js index 7e08eca..f26447a 100644 --- a/static/js/script.js +++ b/static/js/script.js @@ -1055,22 +1055,20 @@ async function loadForecast() { borderColor: '#ef4444', backgroundColor: '#ef4444', tension: 0.3, - pointRadius: (ctx) => { return ctx.dataIndex === appState.activeIndex ? 8 : 6; }, pointHoverRadius: 10, hitRadius: 20, pointBackgroundColor: (ctx) => { - return ctx.dataIndex === appState.activeIndex - ? '#ffffff' - : '#ef4444'; - }, - pointBorderColor: '#ef4444', - pointBorderWidth: (ctx) => { - return ctx.dataIndex === appState.activeIndex ? 3 : 0; - }, - + if (ctx.dataIndex === appState.activeIndex) return '#ffffff'; + return '#ef4444'; + }, + pointBorderWidth: (ctx) => { + if (ctx.dataIndex === appState.activeIndex) return 3; + return 0; + }, + pointBorderColor: '#ef4444', fill: false }, ] From 4e7d79792d9db97e1c0b815eb62bb8d56436c8bc Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Wed, 18 Mar 2026 13:52:41 +0100 Subject: [PATCH 62/71] Small changes --- static/js/script.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/static/js/script.js b/static/js/script.js index f26447a..5f44c32 100644 --- a/static/js/script.js +++ b/static/js/script.js @@ -1058,15 +1058,15 @@ async function loadForecast() { pointRadius: (ctx) => { return ctx.dataIndex === appState.activeIndex ? 8 : 6; }, - pointHoverRadius: 10, hitRadius: 20, - pointBackgroundColor: (ctx) => { - if (ctx.dataIndex === appState.activeIndex) return '#ffffff'; - return '#ef4444'; + pointHoverRadius: (ctx) => { + return ctx.dataIndex === appState.activeIndex ? 8 : 10; }, - pointBorderWidth: (ctx) => { - if (ctx.dataIndex === appState.activeIndex) return 3; - return 0; + pointHoverBackgroundColor: (ctx) => { + return ctx.dataIndex === appState.activeIndex ? '#ffffff' : '#ffffff'; + }, + pointHoverBorderWidth: (ctx) => { + return ctx.dataIndex === appState.activeIndex ? 3 : 1; }, pointBorderColor: '#ef4444', fill: false From b03866c887b952ad46bd07c455c348d7c60326d6 Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Wed, 18 Mar 2026 14:07:04 +0100 Subject: [PATCH 63/71] Changes --- static/js/script.js | 55 ++++++++++++++++++++++++++++++++------------- 1 file changed, 40 insertions(+), 15 deletions(-) diff --git a/static/js/script.js b/static/js/script.js index 5f44c32..4047ebf 100644 --- a/static/js/script.js +++ b/static/js/script.js @@ -1050,27 +1050,52 @@ async function loadForecast() { backgroundColor: 'rgba(239,68,68,0.18)', }, { - label: 'Prognose', - data: values, - borderColor: '#ef4444', - backgroundColor: '#ef4444', - tension: 0.3, - pointRadius: (ctx) => { - return ctx.dataIndex === appState.activeIndex ? 8 : 6; - }, - hitRadius: 20, - pointHoverRadius: (ctx) => { + label: 'Prognose', + data: values, + borderColor: '#ef4444', + backgroundColor: '#ef4444', + tension: 0.3, + + // 🔴 STANDARD vs AKTIV + pointRadius: (ctx) => { + return ctx.dataIndex === appState.activeIndex ? 8 : 6; + }, + + pointBackgroundColor: (ctx) => { + return ctx.dataIndex === appState.activeIndex + ? '#ffffff' // aktiv → weiß innen + : '#ef4444'; // normal → rot + }, + + pointBorderColor: '#ef4444', + + pointBorderWidth: (ctx) => { + return ctx.dataIndex === appState.activeIndex ? 3 : 0; + }, + + hitRadius: 20, + + // 🟡 HOVER LOGIK (WICHTIG!) + pointHoverRadius: (ctx) => { return ctx.dataIndex === appState.activeIndex ? 8 : 10; }, + pointHoverBackgroundColor: (ctx) => { - return ctx.dataIndex === appState.activeIndex ? '#ffffff' : '#ffffff'; + return ctx.dataIndex === appState.activeIndex + ? '#ffffff' // aktiv → KEINE Änderung + : '#ef4444'; // normal → bleibt rot }, + + pointHoverBorderColor: '#ef4444', + pointHoverBorderWidth: (ctx) => { - return ctx.dataIndex === appState.activeIndex ? 3 : 1; + return ctx.dataIndex === appState.activeIndex + ? 3 // aktiv → KEINE Änderung + : 0; // normal → kein Rand beim Hover }, - pointBorderColor: '#ef4444', - fill: false - }, + + fill: false + }, ] }, options: { From 3f7e537fe576412ef9ca4559a03eb6c9ae3d6ac6 Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Wed, 18 Mar 2026 15:58:03 +0100 Subject: [PATCH 64/71] Add/Remove Card Architecture --- ml_logic.py | 4 +- routes.py | 38 ++++++++++++- static/js/layout.js | 124 +++++++++++++++++++++++++++++++++++++++++++ static/js/script.js | 9 +++- templates/index.html | 12 +++++ 5 files changed, 182 insertions(+), 5 deletions(-) diff --git a/ml_logic.py b/ml_logic.py index f6d3a87..1d48364 100644 --- a/ml_logic.py +++ b/ml_logic.py @@ -60,8 +60,8 @@ def build_training_data(): def train_model(): X, y = build_training_data() # Da wir mehr Features haben, sollten wir mind. 10-15 Tage haben für ein erstes Training - if len(X) < 8: #15!!! - print(f"⚠️ Nicht genug Trainingsdaten ({len(X)}/8).") #15!!! + if len(X) < 15: + print(f"⚠️ Nicht genug Trainingsdaten ({len(X)}/15).") return None # Feature-Liste erweitert diff --git a/routes.py b/routes.py index f7253ba..07fe284 100644 --- a/routes.py +++ b/routes.py @@ -1097,4 +1097,40 @@ def save_layout(): except Exception as e: print(f"Server-Fehler: {str(e)}") - return jsonify({"error": "Interner Server Fehler", "details": str(e)}), 500 \ No newline at end of file + return jsonify({"error": "Interner Server Fehler", "details": str(e)}), 500 + +@api_bp.route('/api/cards', methods=['GET']) +def get_cards(): + conn = get_db_connection() + c = conn.cursor() + c.execute("SELECT value FROM user_settings WHERE key = 'removed_cards'") + row = c.fetchone() + conn.close() + + if row: + return jsonify({"removed": json.loads(row[0])}), 200 + return jsonify({"removed": []}), 200 + + +@api_bp.route('/api/cards', methods=['POST']) +def save_cards(): + data = request.get_json() + password = data.get('pw') + + if password != ADMIN_PASS: + return jsonify({"error": "Nicht autorisiert"}), 403 + + removed = data.get('removed', []) + + conn = get_db_connection() + c = conn.cursor() + + c.execute( + "INSERT OR REPLACE INTO user_settings (key, value) VALUES (?, ?)", + ('removed_cards', json.dumps(removed)) + ) + + conn.commit() + conn.close() + + return jsonify({"status": "ok"}), 200 \ No newline at end of file diff --git a/static/js/layout.js b/static/js/layout.js index b5ed4d1..818eb7d 100644 --- a/static/js/layout.js +++ b/static/js/layout.js @@ -17,6 +17,36 @@ function initGridstack() { }); } +function injectDeleteButtons() { + document.querySelectorAll('.grid-stack-item').forEach(item => { + + // schon vorhanden? -> skip + if (item.querySelector('.card-delete-btn')) return; + + const btn = document.createElement('div'); + btn.className = 'card-delete-btn'; + btn.innerHTML = '🗑️'; + + Object.assign(btn.style, { + position: 'absolute', + top: '8px', + right: '8px', + cursor: 'pointer', + fontSize: '14px', + opacity: '0.7', + display: currentPw ? 'block' : 'none', + zIndex: 20 + }); + + btn.onclick = (e) => { + e.stopPropagation(); + removeCard(item.id); + }; + + item.appendChild(btn); + }); +} + // 2. Layout speichern (Nur in DB und nur wenn PW da ist) async function saveLayout() { if (!dashboardGrid || !currentPw || currentPw === "") { @@ -85,10 +115,104 @@ async function resetDatabaseLayout() { } } +let removedCards = []; + +function removeCard(cardId) { + if (!currentPw) return alert("Nur im Admin-Modus möglich"); + + const el = document.getElementById(cardId); + if (!el) return; + + dashboardGrid.removeWidget(el); + + if (!removedCards.includes(cardId)) { + removedCards.push(cardId); + } + + saveRemovedCards(); + updateAdminCardList(); +} + +function restoreCard(cardId) { + if (!currentPw) return; + + const el = document.getElementById(cardId); + + if (el) { + dashboardGrid.addWidget(el); + } + + removedCards = removedCards.filter(id => id !== cardId); + + saveRemovedCards(); + updateAdminCardList(); +} + +async function loadRemovedCards() { + const res = await fetch('/api/cards'); + const data = await res.json(); + + removedCards = data.removed || []; + + removedCards.forEach(id => { + const el = document.getElementById(id); + if (el) dashboardGrid.removeWidget(el); + }); + + updateAdminCardList(); +} + +async function saveRemovedCards() { + if (!currentPw) return; + + await fetch('/api/cards', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + removed: removedCards, + pw: currentPw + }) + }); +} + +function updateAdminCardList() { + const container = document.getElementById('cardManagerList'); + if (!container) return; + + container.innerHTML = ''; + + document.querySelectorAll('.grid-stack-item').forEach(el => { + const id = el.id; + const isRemoved = removedCards.includes(id); + + const row = document.createElement('div'); + + row.style.display = 'flex'; + row.style.justifyContent = 'space-between'; + row.style.marginBottom = '6px'; + + row.innerHTML = ` + ${id} + + `; + + container.appendChild(row); + }); +} + +function toggleDeleteButtons(show) { + document.querySelectorAll('.card-delete-btn').forEach(btn => { + btn.style.display = show ? 'block' : 'none'; + }); +} + document.addEventListener("DOMContentLoaded", async () => { // --- PHASE 1: Das Gerüst aufbauen --- initGridstack(); await loadLayout(); // Wartet, bis Boxen aus DB oder LocalStorage da sind + await loadRemovedCards(); // --- PHASE 2: Startwerte für Datumsfelder setzen --- const t = new Date().toISOString().split('T')[0]; diff --git a/static/js/script.js b/static/js/script.js index 4047ebf..f690b58 100644 --- a/static/js/script.js +++ b/static/js/script.js @@ -41,6 +41,9 @@ async function unlockAdmin() { document.getElementById('unlockBtn').style.display = 'none'; document.getElementById('adminArea').style.display = 'flex'; + injectDeleteButtons(); + toggleDeleteButtons(true); + if (dashboardGrid) { // Grid bearbeitbar machen dashboardGrid.setStatic(false); @@ -48,6 +51,7 @@ async function unlockAdmin() { } loadTariffs(); + updateAdminCardList(); } else { alert("Falsches Passwort!"); } @@ -57,9 +61,10 @@ function closeAdmin() { currentPw = ""; document.getElementById('adminArea').style.display = 'none'; document.getElementById('unlockBtn').style.display = 'block'; - + + toggleDeleteButtons(false); + if (dashboardGrid) { - // Grid wieder für alle sperren dashboardGrid.setStatic(true); document.getElementById('dashboard-grid').classList.remove('edit-mode'); } diff --git a/templates/index.html b/templates/index.html index 1c68655..40f36ab 100644 --- a/templates/index.html +++ b/templates/index.html @@ -89,6 +89,18 @@

margin-bottom: 5px; margin-top: 2.5em; "> + Karten verwalten +

+
+
Grid-Layout anpassen
From c9c1e6df3e22ccc496a93fa3f66cdcf841054d9c Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Wed, 18 Mar 2026 16:13:24 +0100 Subject: [PATCH 65/71] Changes --- static/js/layout.js | 37 +++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/static/js/layout.js b/static/js/layout.js index 818eb7d..e3a4bd5 100644 --- a/static/js/layout.js +++ b/static/js/layout.js @@ -118,12 +118,12 @@ async function resetDatabaseLayout() { let removedCards = []; function removeCard(cardId) { - if (!currentPw) return alert("Nur im Admin-Modus möglich"); + if (!currentPw) return; const el = document.getElementById(cardId); if (!el) return; - dashboardGrid.removeWidget(el); + el.style.display = 'none'; if (!removedCards.includes(cardId)) { removedCards.push(cardId); @@ -134,13 +134,10 @@ function removeCard(cardId) { } function restoreCard(cardId) { - if (!currentPw) return; - const el = document.getElementById(cardId); + if (!el) return; - if (el) { - dashboardGrid.addWidget(el); - } + el.style.display = ''; removedCards = removedCards.filter(id => id !== cardId); @@ -156,7 +153,7 @@ async function loadRemovedCards() { removedCards.forEach(id => { const el = document.getElementById(id); - if (el) dashboardGrid.removeWidget(el); + if (el) el.style.display = 'none'; }); updateAdminCardList(); @@ -175,25 +172,41 @@ async function saveRemovedCards() { }); } +const ALL_CARDS = [ + "card-total", + "card-live", + "card-main-chart", + "card-panel-chart", + "card-hourly-heatmap", + "card-donut-chart", + "card-records", + "card-roi", + "card-yearly-heatmap", + "card-forecast", + "card-shap-details", + "card-global-shap", + "card-feature-importance" +]; + function updateAdminCardList() { const container = document.getElementById('cardManagerList'); if (!container) return; container.innerHTML = ''; - document.querySelectorAll('.grid-stack-item').forEach(el => { - const id = el.id; + ALL_CARDS.forEach(id => { const isRemoved = removedCards.includes(id); const row = document.createElement('div'); - row.style.display = 'flex'; row.style.justifyContent = 'space-between'; row.style.marginBottom = '6px'; row.innerHTML = ` ${id} - `; From 7f47e8480a81a8ce34913ac9e00df29309ad6715 Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Wed, 18 Mar 2026 16:44:32 +0100 Subject: [PATCH 66/71] Database changes --- database.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/database.py b/database.py index b3bda0e..4272f1c 100644 --- a/database.py +++ b/database.py @@ -78,7 +78,11 @@ def init_db(): # 5. User Settings Tabelle c.execute('''CREATE TABLE IF NOT EXISTS user_settings - (key TEXT PRIMARY KEY, value TEXT)''') + (key TEXT PRIMARY KEY, value TEXT)''') + c.execute('''INSERT OR IGNORE INTO user_settings + (key, value) VALUES ('removed_cards', '[]')''') + c.execute('''INSERT OR IGNORE INTO user_settings + (key, value) VALUES ('dashboard_layout', NULL)''') conn.commit() conn.close() From 6a105c40fe35f43f8039952f6c6c750adc5479a0 Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Thu, 19 Mar 2026 10:26:01 +0100 Subject: [PATCH 67/71] Changes --- static/js/layout.js | 7 ++++++- static/js/script.js | 11 ++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/static/js/layout.js b/static/js/layout.js index e3a4bd5..f53e30d 100644 --- a/static/js/layout.js +++ b/static/js/layout.js @@ -20,7 +20,11 @@ function initGridstack() { function injectDeleteButtons() { document.querySelectorAll('.grid-stack-item').forEach(item => { - // schon vorhanden? -> skip + if (!item.id) { + console.warn("Grid Item ohne ID:", item); + return; + } + if (item.querySelector('.card-delete-btn')) return; const btn = document.createElement('div'); @@ -226,6 +230,7 @@ document.addEventListener("DOMContentLoaded", async () => { initGridstack(); await loadLayout(); // Wartet, bis Boxen aus DB oder LocalStorage da sind await loadRemovedCards(); + injectDeleteButtons(); // --- PHASE 2: Startwerte für Datumsfelder setzen --- const t = new Date().toISOString().split('T')[0]; diff --git a/static/js/script.js b/static/js/script.js index f690b58..58c81a6 100644 --- a/static/js/script.js +++ b/static/js/script.js @@ -40,15 +40,16 @@ async function unlockAdmin() { document.getElementById('unlockBtn').style.display = 'none'; document.getElementById('adminArea').style.display = 'flex'; - - injectDeleteButtons(); - toggleDeleteButtons(true); - + if (dashboardGrid) { - // Grid bearbeitbar machen dashboardGrid.setStatic(false); document.getElementById('dashboard-grid').classList.add('edit-mode'); } + + setTimeout(() => { + injectDeleteButtons(); + toggleDeleteButtons(true); + }, 100); loadTariffs(); updateAdminCardList(); From 353cb17ebd2aac984ec8b9ecede1e6c961a6ec66 Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Thu, 19 Mar 2026 10:49:54 +0100 Subject: [PATCH 68/71] Fixes --- static/js/layout.js | 7 ++----- templates/index.html | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/static/js/layout.js b/static/js/layout.js index f53e30d..b7ddb0e 100644 --- a/static/js/layout.js +++ b/static/js/layout.js @@ -20,10 +20,7 @@ function initGridstack() { function injectDeleteButtons() { document.querySelectorAll('.grid-stack-item').forEach(item => { - if (!item.id) { - console.warn("Grid Item ohne ID:", item); - return; - } + if (!item.id || !ALL_CARDS.includes(item.id)) return; if (item.querySelector('.card-delete-btn')) return; @@ -46,7 +43,7 @@ function injectDeleteButtons() { e.stopPropagation(); removeCard(item.id); }; - + item.appendChild(btn); }); } diff --git a/templates/index.html b/templates/index.html index 40f36ab..071a58a 100644 --- a/templates/index.html +++ b/templates/index.html @@ -103,7 +103,7 @@

"> Grid-Layout anpassen

- +
From 63b4ad2b86c9b3927a29a6a3b5111bc6facd5077 Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Thu, 19 Mar 2026 10:58:42 +0100 Subject: [PATCH 69/71] SeasonEffect display none --- templates/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/index.html b/templates/index.html index 071a58a..d628c13 100644 --- a/templates/index.html +++ b/templates/index.html @@ -288,7 +288,7 @@

- +
From dd15302941bd1e13d9934f13fffcbf67ce94b3e8 Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Thu, 19 Mar 2026 11:10:08 +0100 Subject: [PATCH 70/71] Remove Delete Architecture --- routes.py | 36 ------------ static/js/layout.js | 139 -------------------------------------------- static/js/script.js | 14 ++--- 3 files changed, 4 insertions(+), 185 deletions(-) diff --git a/routes.py b/routes.py index 07fe284..1dc99c4 100644 --- a/routes.py +++ b/routes.py @@ -1098,39 +1098,3 @@ def save_layout(): except Exception as e: print(f"Server-Fehler: {str(e)}") return jsonify({"error": "Interner Server Fehler", "details": str(e)}), 500 - -@api_bp.route('/api/cards', methods=['GET']) -def get_cards(): - conn = get_db_connection() - c = conn.cursor() - c.execute("SELECT value FROM user_settings WHERE key = 'removed_cards'") - row = c.fetchone() - conn.close() - - if row: - return jsonify({"removed": json.loads(row[0])}), 200 - return jsonify({"removed": []}), 200 - - -@api_bp.route('/api/cards', methods=['POST']) -def save_cards(): - data = request.get_json() - password = data.get('pw') - - if password != ADMIN_PASS: - return jsonify({"error": "Nicht autorisiert"}), 403 - - removed = data.get('removed', []) - - conn = get_db_connection() - c = conn.cursor() - - c.execute( - "INSERT OR REPLACE INTO user_settings (key, value) VALUES (?, ?)", - ('removed_cards', json.dumps(removed)) - ) - - conn.commit() - conn.close() - - return jsonify({"status": "ok"}), 200 \ No newline at end of file diff --git a/static/js/layout.js b/static/js/layout.js index b7ddb0e..b5ed4d1 100644 --- a/static/js/layout.js +++ b/static/js/layout.js @@ -17,37 +17,6 @@ function initGridstack() { }); } -function injectDeleteButtons() { - document.querySelectorAll('.grid-stack-item').forEach(item => { - - if (!item.id || !ALL_CARDS.includes(item.id)) return; - - if (item.querySelector('.card-delete-btn')) return; - - const btn = document.createElement('div'); - btn.className = 'card-delete-btn'; - btn.innerHTML = '🗑️'; - - Object.assign(btn.style, { - position: 'absolute', - top: '8px', - right: '8px', - cursor: 'pointer', - fontSize: '14px', - opacity: '0.7', - display: currentPw ? 'block' : 'none', - zIndex: 20 - }); - - btn.onclick = (e) => { - e.stopPropagation(); - removeCard(item.id); - }; - - item.appendChild(btn); - }); -} - // 2. Layout speichern (Nur in DB und nur wenn PW da ist) async function saveLayout() { if (!dashboardGrid || !currentPw || currentPw === "") { @@ -116,118 +85,10 @@ async function resetDatabaseLayout() { } } -let removedCards = []; - -function removeCard(cardId) { - if (!currentPw) return; - - const el = document.getElementById(cardId); - if (!el) return; - - el.style.display = 'none'; - - if (!removedCards.includes(cardId)) { - removedCards.push(cardId); - } - - saveRemovedCards(); - updateAdminCardList(); -} - -function restoreCard(cardId) { - const el = document.getElementById(cardId); - if (!el) return; - - el.style.display = ''; - - removedCards = removedCards.filter(id => id !== cardId); - - saveRemovedCards(); - updateAdminCardList(); -} - -async function loadRemovedCards() { - const res = await fetch('/api/cards'); - const data = await res.json(); - - removedCards = data.removed || []; - - removedCards.forEach(id => { - const el = document.getElementById(id); - if (el) el.style.display = 'none'; - }); - - updateAdminCardList(); -} - -async function saveRemovedCards() { - if (!currentPw) return; - - await fetch('/api/cards', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - removed: removedCards, - pw: currentPw - }) - }); -} - -const ALL_CARDS = [ - "card-total", - "card-live", - "card-main-chart", - "card-panel-chart", - "card-hourly-heatmap", - "card-donut-chart", - "card-records", - "card-roi", - "card-yearly-heatmap", - "card-forecast", - "card-shap-details", - "card-global-shap", - "card-feature-importance" -]; - -function updateAdminCardList() { - const container = document.getElementById('cardManagerList'); - if (!container) return; - - container.innerHTML = ''; - - ALL_CARDS.forEach(id => { - const isRemoved = removedCards.includes(id); - - const row = document.createElement('div'); - row.style.display = 'flex'; - row.style.justifyContent = 'space-between'; - row.style.marginBottom = '6px'; - - row.innerHTML = ` - ${id} - - `; - - container.appendChild(row); - }); -} - -function toggleDeleteButtons(show) { - document.querySelectorAll('.card-delete-btn').forEach(btn => { - btn.style.display = show ? 'block' : 'none'; - }); -} - document.addEventListener("DOMContentLoaded", async () => { // --- PHASE 1: Das Gerüst aufbauen --- initGridstack(); await loadLayout(); // Wartet, bis Boxen aus DB oder LocalStorage da sind - await loadRemovedCards(); - injectDeleteButtons(); // --- PHASE 2: Startwerte für Datumsfelder setzen --- const t = new Date().toISOString().split('T')[0]; diff --git a/static/js/script.js b/static/js/script.js index 58c81a6..4047ebf 100644 --- a/static/js/script.js +++ b/static/js/script.js @@ -40,19 +40,14 @@ async function unlockAdmin() { document.getElementById('unlockBtn').style.display = 'none'; document.getElementById('adminArea').style.display = 'flex'; - + if (dashboardGrid) { + // Grid bearbeitbar machen dashboardGrid.setStatic(false); document.getElementById('dashboard-grid').classList.add('edit-mode'); } - - setTimeout(() => { - injectDeleteButtons(); - toggleDeleteButtons(true); - }, 100); loadTariffs(); - updateAdminCardList(); } else { alert("Falsches Passwort!"); } @@ -62,10 +57,9 @@ function closeAdmin() { currentPw = ""; document.getElementById('adminArea').style.display = 'none'; document.getElementById('unlockBtn').style.display = 'block'; - - toggleDeleteButtons(false); - + if (dashboardGrid) { + // Grid wieder für alle sperren dashboardGrid.setStatic(true); document.getElementById('dashboard-grid').classList.remove('edit-mode'); } From 3459fad62a18568452f316936cb31740ff55ab6b Mon Sep 17 00:00:00 2001 From: jacquesbach Date: Thu, 19 Mar 2026 11:14:27 +0100 Subject: [PATCH 71/71] Fixes --- templates/index.html | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/templates/index.html b/templates/index.html index d628c13..2909428 100644 --- a/templates/index.html +++ b/templates/index.html @@ -89,18 +89,6 @@

margin-bottom: 5px; margin-top: 2.5em; "> - Karten verwalten -

-
-
Grid-Layout anpassen