Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
837a338
Merge pull request #8 from jacquesbach/main
jacquesbach Mar 13, 2026
e1b0b43
Introduce gridstack.js
Mar 14, 2026
41db4db
Test commit
Mar 14, 2026
a138f19
Test commit 2
Mar 14, 2026
363034f
Test commit 3
Mar 14, 2026
10b8c50
Change deploy process
Mar 14, 2026
baa5ec8
Gridstack Layout changes
Mar 14, 2026
b033560
Richtige Lade-Reihenfolge
Mar 14, 2026
3c6489a
Layout Reset Button
Mar 14, 2026
059efdf
margin left & right gridstack
Mar 14, 2026
9807a44
Gridstack editable only for admin
Mar 14, 2026
8108c79
Change container style
Mar 14, 2026
932ff81
Change layout.js
Mar 14, 2026
90b2d84
Change saveLayout function
Mar 14, 2026
ad7e4af
Change /api/layout endpoint
Mar 14, 2026
fc4787a
Import json in routes.py
Mar 14, 2026
6e416c3
Add minimum size to "card-total"
Mar 14, 2026
3187dad
Add minimum size to "card-live"
Mar 14, 2026
e7b440d
Add minimum sizes to cards
Mar 14, 2026
39e3387
Change .grid-stack-item-content style
Mar 14, 2026
c9ded83
Change Forecast Card Styles
Mar 15, 2026
796faee
Change Forecast Card Styles
Mar 15, 2026
d2274d1
Undo forecast card styling
Mar 16, 2026
78399b3
Introduce Auto Card Resize
Mar 16, 2026
4140ba9
Undo auto resize
Mar 16, 2026
0ee798d
Change overflow in gridstack card item content style
Mar 16, 2026
3abdb73
.grid-stack-item-content Styles
Mar 17, 2026
36b491d
forecast card tests
Mar 17, 2026
ae986fa
Forecast chart style
Mar 17, 2026
a8cb23f
.grid-stack Styles
Mar 17, 2026
4592e1e
Forecast Data Validation & UI
Mar 17, 2026
90c2bf2
Style Changes
Mar 17, 2026
b73c583
Administration Button
Mar 17, 2026
287b0da
Administration changes
Mar 17, 2026
2037d1a
Administration Layout
Mar 17, 2026
bcc46b6
Auto Resize for Forecast Card
Mar 17, 2026
f66eec3
Card Resize changes
Mar 17, 2026
cad9403
JS fix
Mar 17, 2026
52dbd71
Global Card Resize
Mar 17, 2026
885b438
Undo Auto Resize
Mar 17, 2026
bb42306
Split Forecast Card
Mar 17, 2026
5d27c63
Test
Mar 17, 2026
9c543af
Grid changes
Mar 17, 2026
29266f1
Card Names
Mar 17, 2026
f3dda72
ShowShapDetails automatic today
Mar 17, 2026
cad3316
Rename card shap details
Mar 17, 2026
f73281a
Changes
Mar 17, 2026
3b63d58
Highlight active forecast point
Mar 18, 2026
8fe97e5
Forecast Chart Highlight active point
Mar 18, 2026
2e188ef
Fix
Mar 18, 2026
950e7b5
Forecast Chart active point style
Mar 18, 2026
0d0a8f2
Remove static height vom Charts
Mar 18, 2026
5e2d551
Change gridstack cell height
Mar 18, 2026
ff34166
Change grid-stack-item sizes
Mar 18, 2026
6e174ef
Remove season strength vom Forecast
Mar 18, 2026
6ae4ce8
Test
Mar 18, 2026
b2c1c83
Prevent duplicate hourly-heatmap initialization
Mar 18, 2026
b02a797
Resize Hourly Heatmap
Mar 18, 2026
2c5e212
Forecast Chart Active Point Architecture
Mar 18, 2026
f57f8e1
Load Forecast refactoring
Mar 18, 2026
eac860a
Change hover style for activeIndex
Mar 18, 2026
7231724
Style changes
Mar 18, 2026
4e7d797
Small changes
Mar 18, 2026
b03866c
Changes
Mar 18, 2026
3f7e537
Add/Remove Card Architecture
Mar 18, 2026
c9c1e6d
Changes
Mar 18, 2026
7f47e84
Database changes
Mar 18, 2026
6a105c4
Changes
Mar 19, 2026
353cb17
Fixes
Mar 19, 2026
63b4ad2
SeasonEffect display none
Mar 19, 2026
dd15302
Remove Delete Architecture
Mar 19, 2026
3459fad
Fixes
Mar 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/workflows/deploy-staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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!"
echo "Staging hart auf Stand von 'dev' gesetzt. Port 5001 läuft!"
5 changes: 3 additions & 2 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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!"
echo "Produktiv-Update via Fast-Forward erfolgreich. App läuft!"
10 changes: 9 additions & 1 deletion database.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,15 @@ 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)''')
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()
print("Datenbank erfolgreich initialisiert.")
Expand Down
4 changes: 2 additions & 2 deletions ml_logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
49 changes: 49 additions & 0 deletions routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import sqlite3
import datetime
import requests
import json
import time
import math
import numpy as np
Expand Down Expand Up @@ -1049,3 +1050,51 @@ 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():
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}), 200

@api_bp.route('/api/layout', methods=['POST'])
def save_layout():
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

conn.commit()
conn.close()
return jsonify({"status": "gespeichert"}), 200

except Exception as e:
print(f"Server-Fehler: {str(e)}")
return jsonify({"error": "Interner Server Fehler", "details": str(e)}), 500
66 changes: 32 additions & 34 deletions static/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -158,39 +158,48 @@
transform 0.15s ease;
}

.main-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 20px;
row-gap: 25px;
margin-bottom: 25px;
.grid-stack {
margin-left: -20px;
margin-right: -20px;
background: transparent !important;
box-shadow: 0 !important;
border: 0 !important;
border-radius: 0 !important;
}

.grid-stack-item-content {
xoverflow-x: hidden;
xoverflow-y: auto;
overflow: visible !important;
background: transparent !important;
box-shadow: none !important;
}

/* 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;
padding: 25px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.03);
display: flex;
flex-direction: column;
justify-content: center;
justify-content: start;
}

.card-gradient {
Expand Down Expand Up @@ -295,10 +304,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);
Expand Down Expand Up @@ -384,13 +389,6 @@
}

@media (max-width: 900px) {
.main-grid {
grid-template-columns: 1fr;
}

.main-grid > * {
grid-column: span 1 !important;
}

.top-controls {
flex-direction: column;
Expand Down
133 changes: 133 additions & 0 deletions static/js/layout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
let dashboardGrid;
let currentPw = "";

// 1. Grid initialisieren
function initGridstack() {
dashboardGrid = GridStack.init({
cellHeight: 50,
margin: 20,
animate: true,
staticGrid: true,
disableOneColumnMode: false,
oneColumnModeDomSort: true
});

dashboardGrid.on('change', function(event, items) {
saveLayout();
});
}

// 2. Layout speichern (Nur in DB und nur wenn PW da ist)
async function saveLayout() {
if (!dashboardGrid || !currentPw || currentPw === "") {
return;
}

const layoutData = dashboardGrid.save();

try {
const response = await fetch('/api/layout', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
layout: layoutData,
pw: currentPw
})
});

if (!response.ok) {
const errorText = await response.text();
console.error("Speichern fehlgeschlagen:", errorText);
}
} catch (e) {
console.error("Netzwerkfehler beim Speichern:", e);
}
}

// 3. Layout beim Starten laden
async function loadLayout() {
let savedLayout = null;
try {
const response = await fetch('/api/layout');
if (response.ok) {
const data = await response.json();
if (data && data.layout) savedLayout = data.layout;
}
} catch (e) { console.error("DB Load failed", e); }

// Wenn ein Layout auf dem Server existiert, anwenden
if (savedLayout && dashboardGrid) {
dashboardGrid.removeAll();
dashboardGrid.load(savedLayout);
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 für ALLE Nutzer auf den Standard zurücksetzen?")) return;

if (!currentPw) return alert("Bitte erst als Admin einloggen!");

const res = await fetch('/api/layout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
layout: "RESET", // Signalwort für das Python-Backend
pw: currentPw
})
});

if (res.ok) {
location.reload(); // Seite neu laden, um Standard-HTML zu zeigen
}
}

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);

});
Loading