Skip to content

Commit e1b0b43

Browse files
author
jacquesbach
committed
Introduce gridstack.js
1 parent 837a338 commit e1b0b43

6 files changed

Lines changed: 355 additions & 246 deletions

File tree

database.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,11 @@ 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+
7983
conn.commit()
8084
conn.close()
8185
print("Datenbank erfolgreich initialisiert.")

routes.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1049,3 +1049,39 @@ def shap_summary():
10491049
]
10501050

10511051
return jsonify(sorted(result, key=lambda x: x["mean_abs_shap"], reverse=True))
1052+
1053+
@api_bp.route('/api/layout', methods=['GET'])
1054+
def get_layout():
1055+
# Jeder darf das Layout sehen (auch nicht angemeldete)
1056+
conn = sqlite3.connect('solar_data.db')
1057+
c = conn.cursor()
1058+
c.execute("SELECT value FROM user_settings WHERE key = 'dashboard_layout'")
1059+
row = c.fetchone()
1060+
conn.close()
1061+
1062+
if row:
1063+
return jsonify({"layout": json.loads(row[0])}), 200
1064+
return jsonify({"layout": None}), 404
1065+
1066+
@api_bp.route('/api/layout', methods=['POST'])
1067+
def save_layout():
1068+
data = request.json
1069+
layout_json = data.get('layout')
1070+
password = data.get('pw') # Wir senden das PW zur Sicherheit mit
1071+
1072+
# Berechtigung prüfen (wie in deinem /api/auth)
1073+
if password != ADMIN_PASS:
1074+
return jsonify({"error": "Nicht autorisiert"}), 403
1075+
1076+
if not layout_json:
1077+
return jsonify({"error": "Kein Layout gesendet"}), 400
1078+
1079+
conn = sqlite3.connect('solar_data.db')
1080+
c = conn.cursor()
1081+
# Speichern oder Überschreiben
1082+
c.execute("INSERT OR REPLACE INTO user_settings (key, value) VALUES (?, ?)",
1083+
('dashboard_layout', json.dumps(layout_json)))
1084+
conn.commit()
1085+
conn.close()
1086+
1087+
return jsonify({"status": "gespeichert"}), 200

static/css/style.css

Lines changed: 20 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -157,32 +157,29 @@
157157
box-shadow 0.2s ease,
158158
transform 0.15s ease;
159159
}
160-
161-
.main-grid {
162-
display: grid;
163-
grid-template-columns: 1fr 1fr 1fr;
164-
gap: 20px;
165-
row-gap: 25px;
166-
margin-bottom: 25px;
160+
161+
.grid-stack-item-content {
162+
overflow-x: hidden;
163+
overflow-y: auto;
167164
}
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;
165+
166+
.edit-mode .grid-stack-item-content {
167+
cursor: grab;
168+
border: 1px dashed var(--accent);
169+
}
170+
171+
.edit-mode .grid-stack-item-content:active {
172+
cursor: grabbing;
184173
}
185174

175+
:where(.card, .chart-card) + :where(.card, .chart-card) {
176+
margin-top: 0;
177+
}
178+
179+
.chart-card {
180+
height: 100%; /* Ersetzt die starren 400px */
181+
}
182+
186183
.card {
187184
background: var(--card-bg);
188185
border-radius: 24px;
@@ -295,10 +292,6 @@
295292
margin-top: 25px;
296293
}
297294

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

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

395381
.top-controls {
396382
flex-direction: column;

static/js/layout.js

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
let dashboardGrid;
2+
3+
// 1. Grid initialisieren
4+
function initGridstack() {
5+
const isAdmin = sessionStorage.getItem('admin_pw') !== null;
6+
7+
dashboardGrid = GridStack.init({
8+
cellHeight: 110,
9+
margin: 20,
10+
animate: true,
11+
staticGrid: !isAdmin, // Wenn nicht Admin, dann gesperrt (kein Drag/Resize)
12+
disableOneColumnMode: false,
13+
oneColumnModeDomSort: true
14+
});
15+
16+
// Falls das PW schon im Speicher war (nach Refresh), UI direkt anpassen
17+
if (isAdmin) {
18+
currentPw = sessionStorage.getItem('admin_pw');
19+
document.getElementById('unlockBtn').style.display = 'none';
20+
document.getElementById('adminArea').style.display = 'flex';
21+
// Hier ggf. loadTariffs() aufrufen, falls script.js schon geladen ist
22+
}
23+
24+
dashboardGrid.on('change', function(event, items) {
25+
saveLayout();
26+
});
27+
}
28+
29+
// 3. Layout speichern (Auth-Check + DB vs. LocalStorage)
30+
async function saveLayout() {
31+
if (!dashboardGrid) return;
32+
const layoutData = dashboardGrid.save();
33+
34+
// Wir schauen nach, ob ein Passwort im SessionStorage liegt
35+
// (Das müsstest du in deiner unlockAdmin() Funktion dort speichern)
36+
const storedPw = sessionStorage.getItem('admin_pw');
37+
38+
if (storedPw) {
39+
try {
40+
const response = await fetch('/api/layout', {
41+
method: 'POST',
42+
headers: { 'Content-Type': 'application/json' },
43+
body: JSON.stringify({
44+
layout: layoutData,
45+
pw: storedPw
46+
})
47+
});
48+
49+
if (response.ok) {
50+
console.log("Layout in DB gespeichert.");
51+
return;
52+
}
53+
} catch (e) { console.error("DB Save failed", e); }
54+
}
55+
56+
// Fallback: LocalStorage für Gäste
57+
localStorage.setItem('balkonkraftwerk_layout', JSON.stringify(layoutData));
58+
console.log("Layout lokal im Browser gespeichert.");
59+
}
60+
61+
// 4. Layout beim Starten laden
62+
async function loadLayout() {
63+
let savedLayout = null;
64+
65+
try {
66+
// Zuerst versuchen, aus der Datenbank zu laden
67+
const authResponse = await fetch('/api/auth');
68+
if (authResponse.ok) {
69+
const layoutResponse = await fetch('/api/layout');
70+
if (layoutResponse.ok) {
71+
const data = await layoutResponse.json();
72+
if (data.layout) savedLayout = data.layout;
73+
}
74+
} else {
75+
throw new Error("Nicht angemeldet");
76+
}
77+
} catch (error) {
78+
// Fallback: Aus dem LocalStorage laden
79+
const localData = localStorage.getItem('balkonkraftwerk_layout');
80+
if (localData) {
81+
savedLayout = JSON.parse(localData);
82+
}
83+
}
84+
85+
// Wenn ein Layout gefunden wurde, anwenden
86+
if (savedLayout && dashboardGrid) {
87+
dashboardGrid.load(savedLayout);
88+
}
89+
}
90+
91+
// Beim Laden der Seite ausführen (füge das zu deinen anderen Init-Funktionen hinzu)
92+
document.addEventListener("DOMContentLoaded", async () => {
93+
initGridstack();
94+
await loadLayout();
95+
96+
// Nach dem Laden des Layouts kann es helfen, Chart.js einen Resize-Befehl zu geben
97+
window.dispatchEvent(new Event('resize'));
98+
});

static/js/script.js

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -25,27 +25,34 @@ async function checkLoadingStatus() {
2525
}
2626
} catch (e) {}
2727
}
28+
let currentPw = null;
2829

2930
async function unlockAdmin() {
30-
const pw = prompt("Passwort zur Anpassung des Stromtarifs:");
31-
if (!pw) return;
32-
const res = await fetch('/api/auth', {
33-
method: 'POST',
34-
headers: {
35-
'Content-Type': 'application/json'
36-
},
37-
body: JSON.stringify({
38-
pw: pw
39-
})
40-
});
41-
if (res.ok) {
42-
currentPw = pw;
43-
document.getElementById('unlockBtn').style.display = 'none';
44-
document.getElementById('adminArea').style.display = 'flex';
45-
loadTariffs();
46-
} else {
47-
alert("Falsches Passwort!");
48-
}
31+
const pw = prompt("Passwort zur Anpassung des Stromtarifs & Layouts:");
32+
if (!pw) return;
33+
34+
const res = await fetch('/api/auth', {
35+
method: 'POST',
36+
headers: { 'Content-Type': 'application/json' },
37+
body: JSON.stringify({ pw: pw })
38+
});
39+
40+
if (res.ok) {
41+
currentPw = pw;
42+
sessionStorage.setItem('admin_pw', pw);
43+
44+
document.getElementById('unlockBtn').style.display = 'none';
45+
document.getElementById('adminArea').style.display = 'flex';
46+
47+
if (dashboardGrid) {
48+
dashboardGrid.enable();
49+
document.getElementById('dashboard-grid').classList.add('edit-mode');
50+
}
51+
52+
loadTariffs();
53+
} else {
54+
alert("Falsches Passwort!");
55+
}
4956
}
5057

5158
function closeAdmin() {

0 commit comments

Comments
 (0)