Skip to content

Commit b0bc6ae

Browse files
authored
Merge pull request #9 from jacquesbach/dev
Introduce gridstack.js
2 parents 5224d67 + 3459fad commit b0bc6ae

9 files changed

Lines changed: 816 additions & 436 deletions

File tree

.github/workflows/deploy-staging.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ jobs:
1616
key: ${{ secrets.SSH_PRIVATE_KEY }}
1717
script: |
1818
cd /home/ubuntu/balkonkraftwerk-staging
19-
git pull origin dev
19+
git fetch origin dev
20+
git reset --hard origin/dev
2021
sudo systemctl restart balkonkraftwerk-staging
21-
echo "Staging Update erfolgreich. Test-App läuft auf Port 5001!"
22+
echo "Staging hart auf Stand von 'dev' gesetzt. Port 5001 läuft!"

.github/workflows/deploy.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ jobs:
1616
key: ${{ secrets.SSH_PRIVATE_KEY }}
1717
script: |
1818
cd /home/ubuntu/
19-
git pull origin main
19+
# Verhindert Divergenz-Fehler durch striktes Fast-Forward
20+
git pull origin main --ff-only
2021
sudo systemctl restart balkonkraftwerk
21-
echo "Update erfolgreich. App wurde neu gestartet!"
22+
echo "Produktiv-Update via Fast-Forward erfolgreich. App läuft!"

database.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,15 @@ def init_db():
7575
c.execute("SELECT COUNT(*) FROM prices")
7676
if c.fetchone()[0] == 0:
7777
c.execute("INSERT INTO prices (valid_from, price) VALUES ('2026-01-01', 0.329)")
78-
78+
79+
# 5. User Settings Tabelle
80+
c.execute('''CREATE TABLE IF NOT EXISTS user_settings
81+
(key TEXT PRIMARY KEY, value TEXT)''')
82+
c.execute('''INSERT OR IGNORE INTO user_settings
83+
(key, value) VALUES ('removed_cards', '[]')''')
84+
c.execute('''INSERT OR IGNORE INTO user_settings
85+
(key, value) VALUES ('dashboard_layout', NULL)''')
86+
7987
conn.commit()
8088
conn.close()
8189
print("Datenbank erfolgreich initialisiert.")

ml_logic.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,8 @@ def build_training_data():
6060
def train_model():
6161
X, y = build_training_data()
6262
# Da wir mehr Features haben, sollten wir mind. 10-15 Tage haben für ein erstes Training
63-
if len(X) < 8: #15!!!
64-
print(f"⚠️ Nicht genug Trainingsdaten ({len(X)}/8).") #15!!!
63+
if len(X) < 15:
64+
print(f"⚠️ Nicht genug Trainingsdaten ({len(X)}/15).")
6565
return None
6666

6767
# Feature-Liste erweitert

routes.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import sqlite3
33
import datetime
44
import requests
5+
import json
56
import time
67
import math
78
import numpy as np
@@ -1049,3 +1050,51 @@ def shap_summary():
10491050
]
10501051

10511052
return jsonify(sorted(result, key=lambda x: x["mean_abs_shap"], reverse=True))
1053+
1054+
@api_bp.route('/api/layout', methods=['GET'])
1055+
def get_layout():
1056+
conn = get_db_connection()
1057+
c = conn.cursor()
1058+
c.execute("SELECT value FROM user_settings WHERE key = 'dashboard_layout'")
1059+
row = c.fetchone()
1060+
conn.close()
1061+
if row:
1062+
return jsonify({"layout": json.loads(row[0])}), 200
1063+
return jsonify({"layout": None}), 200
1064+
1065+
@api_bp.route('/api/layout', methods=['POST'])
1066+
def save_layout():
1067+
try:
1068+
data = request.get_json()
1069+
if not data:
1070+
return jsonify({"error": "Ungültiges JSON"}), 400
1071+
1072+
layout_json = data.get('layout')
1073+
password = data.get('pw')
1074+
1075+
if password != ADMIN_PASS:
1076+
return jsonify({"error": "Nicht autorisiert"}), 403
1077+
1078+
conn = get_db_connection()
1079+
c = conn.cursor()
1080+
1081+
if layout_json == "RESET":
1082+
c.execute("DELETE FROM user_settings WHERE key = 'dashboard_layout'")
1083+
print("Layout wurde zurückgesetzt.")
1084+
elif layout_json is not None:
1085+
# WICHTIG: Wir konvertieren das Objekt explizit in einen String für die DB
1086+
layout_string = json.dumps(layout_json)
1087+
c.execute("INSERT OR REPLACE INTO user_settings (key, value) VALUES (?, ?)",
1088+
('dashboard_layout', layout_string))
1089+
print("Layout erfolgreich gespeichert.")
1090+
else:
1091+
conn.close()
1092+
return jsonify({"error": "Kein Layout-Inhalt empfangen"}), 400
1093+
1094+
conn.commit()
1095+
conn.close()
1096+
return jsonify({"status": "gespeichert"}), 200
1097+
1098+
except Exception as e:
1099+
print(f"Server-Fehler: {str(e)}")
1100+
return jsonify({"error": "Interner Server Fehler", "details": str(e)}), 500

static/css/style.css

Lines changed: 32 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -158,39 +158,48 @@
158158
transform 0.15s ease;
159159
}
160160

161-
.main-grid {
162-
display: grid;
163-
grid-template-columns: 1fr 1fr 1fr;
164-
gap: 20px;
165-
row-gap: 25px;
166-
margin-bottom: 25px;
161+
.grid-stack {
162+
margin-left: -20px;
163+
margin-right: -20px;
164+
background: transparent !important;
165+
box-shadow: 0 !important;
166+
border: 0 !important;
167+
border-radius: 0 !important;
168+
}
169+
170+
.grid-stack-item-content {
171+
xoverflow-x: hidden;
172+
xoverflow-y: auto;
173+
overflow: visible !important;
174+
background: transparent !important;
175+
box-shadow: none !important;
167176
}
168-
169-
/* Reihe 1 */
170-
.main-grid > *:nth-child(1) { grid-column: span 1; }
171-
.main-grid > *:nth-child(2) { grid-column: span 2; }
172-
173-
/* Reihe 2 & 3: Volle Breite */
174-
.main-grid > *:nth-child(3),
175-
.main-grid > *:nth-child(4) { grid-column: span 3; }
176-
177-
/* Reihe 4: 2fr zu 1fr */
178-
.main-grid > *:nth-child(5) { grid-column: span 2; }
179-
.main-grid > *:nth-child(6) { grid-column: span 1; }
180-
181-
/* AB Reihe 5 (Element 7 und alle folgenden): Volle Breite */
182-
.main-grid > *:nth-child(n + 7) {
183-
grid-column: span 3;
177+
178+
.edit-mode .grid-stack-item-content {
179+
cursor: grab;
180+
border: 1px dashed var(--accent);
181+
}
182+
183+
.edit-mode .grid-stack-item-content:active {
184+
cursor: grabbing;
184185
}
185186

187+
:where(.card, .chart-card) + :where(.card, .chart-card) {
188+
margin-top: 0;
189+
}
190+
191+
.chart-card {
192+
height: 100%; /* Ersetzt die starren 400px */
193+
}
194+
186195
.card {
187196
background: var(--card-bg);
188197
border-radius: 24px;
189198
padding: 25px;
190199
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.03);
191200
display: flex;
192201
flex-direction: column;
193-
justify-content: center;
202+
justify-content: start;
194203
}
195204

196205
.card-gradient {
@@ -295,10 +304,6 @@
295304
margin-top: 25px;
296305
}
297306

298-
.main-grid > :where(.card, .chart-card) {
299-
margin-top: 0;
300-
}
301-
302307
.card-peak {
303308
background: linear-gradient(135deg, #4b79ff 0%, #6dd5fa 100%);
304309
box-shadow: 0 15px 35px rgba(75, 121, 255, 0.25);
@@ -384,13 +389,6 @@
384389
}
385390

386391
@media (max-width: 900px) {
387-
.main-grid {
388-
grid-template-columns: 1fr;
389-
}
390-
391-
.main-grid > * {
392-
grid-column: span 1 !important;
393-
}
394392

395393
.top-controls {
396394
flex-direction: column;

static/js/layout.js

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
let dashboardGrid;
2+
let currentPw = "";
3+
4+
// 1. Grid initialisieren
5+
function initGridstack() {
6+
dashboardGrid = GridStack.init({
7+
cellHeight: 50,
8+
margin: 20,
9+
animate: true,
10+
staticGrid: true,
11+
disableOneColumnMode: false,
12+
oneColumnModeDomSort: true
13+
});
14+
15+
dashboardGrid.on('change', function(event, items) {
16+
saveLayout();
17+
});
18+
}
19+
20+
// 2. Layout speichern (Nur in DB und nur wenn PW da ist)
21+
async function saveLayout() {
22+
if (!dashboardGrid || !currentPw || currentPw === "") {
23+
return;
24+
}
25+
26+
const layoutData = dashboardGrid.save();
27+
28+
try {
29+
const response = await fetch('/api/layout', {
30+
method: 'POST',
31+
headers: {
32+
'Content-Type': 'application/json'
33+
},
34+
body: JSON.stringify({
35+
layout: layoutData,
36+
pw: currentPw
37+
})
38+
});
39+
40+
if (!response.ok) {
41+
const errorText = await response.text();
42+
console.error("Speichern fehlgeschlagen:", errorText);
43+
}
44+
} catch (e) {
45+
console.error("Netzwerkfehler beim Speichern:", e);
46+
}
47+
}
48+
49+
// 3. Layout beim Starten laden
50+
async function loadLayout() {
51+
let savedLayout = null;
52+
try {
53+
const response = await fetch('/api/layout');
54+
if (response.ok) {
55+
const data = await response.json();
56+
if (data && data.layout) savedLayout = data.layout;
57+
}
58+
} catch (e) { console.error("DB Load failed", e); }
59+
60+
// Wenn ein Layout auf dem Server existiert, anwenden
61+
if (savedLayout && dashboardGrid) {
62+
dashboardGrid.removeAll();
63+
dashboardGrid.load(savedLayout);
64+
console.log("Globales Layout erfolgreich geladen.");
65+
}
66+
}
67+
68+
// 4. Reset-Funktion für den Button im Admin-Bereich
69+
async function resetDatabaseLayout() {
70+
if (!confirm("Möchtest du das Layout für ALLE Nutzer auf den Standard zurücksetzen?")) return;
71+
72+
if (!currentPw) return alert("Bitte erst als Admin einloggen!");
73+
74+
const res = await fetch('/api/layout', {
75+
method: 'POST',
76+
headers: { 'Content-Type': 'application/json' },
77+
body: JSON.stringify({
78+
layout: "RESET", // Signalwort für das Python-Backend
79+
pw: currentPw
80+
})
81+
});
82+
83+
if (res.ok) {
84+
location.reload(); // Seite neu laden, um Standard-HTML zu zeigen
85+
}
86+
}
87+
88+
document.addEventListener("DOMContentLoaded", async () => {
89+
// --- PHASE 1: Das Gerüst aufbauen ---
90+
initGridstack();
91+
await loadLayout(); // Wartet, bis Boxen aus DB oder LocalStorage da sind
92+
93+
// --- PHASE 2: Startwerte für Datumsfelder setzen ---
94+
const t = new Date().toISOString().split('T')[0];
95+
const startInput = document.getElementById('start');
96+
const endInput = document.getElementById('end');
97+
98+
if (startInput && endInput) {
99+
startInput.value = t;
100+
endInput.value = t;
101+
startInput.addEventListener('change', updateQuickButtonsActiveState);
102+
endInput.addEventListener('change', updateQuickButtonsActiveState);
103+
}
104+
105+
// --- PHASE 3: Daten in die Boxen pumpen ---
106+
// Wir prüfen bei jeder Funktion, ob sie existiert, um Fehler zu vermeiden
107+
try {
108+
if (typeof fetchData === "function") await fetchData();
109+
if (typeof updateWeather === "function") updateWeather();
110+
if (typeof updateLive === "function") updateLive();
111+
if (typeof updatePeaks === "function") updatePeaks();
112+
if (typeof updateQuickButtonsActiveState === "function") updateQuickButtonsActiveState();
113+
114+
// ML-Funktionen
115+
if (typeof loadForecast === "function") loadForecast();
116+
if (typeof loadGlobalShap === "function") loadGlobalShap();
117+
if (typeof loadFeatureImportance === "function") loadFeatureImportance();
118+
119+
// Heatmaps
120+
if (typeof initHeatmapYears === "function") initHeatmapYears();
121+
if (typeof initHourlyHeatmap === "function") initHourlyHeatmap();
122+
123+
} catch (err) {
124+
console.error("Fehler beim initialen Daten-Load:", err);
125+
}
126+
127+
// --- PHASE 4: Intervalle für Updates starten ---
128+
setInterval(() => { if (typeof updateLive === "function") updateLive(); }, 5000);
129+
setInterval(() => { if (typeof fetchData === "function") fetchData(); }, 60000);
130+
setInterval(() => { if (typeof updatePeaks === "function") updatePeaks(); }, 60000);
131+
setInterval(() => { if (typeof checkLoadingStatus === "function") checkLoadingStatus(); }, 500);
132+
133+
});

0 commit comments

Comments
 (0)