From 1f674b132c7e3593d89387bcad33740161a919be Mon Sep 17 00:00:00 2001 From: Christian Mueller Date: Fri, 24 Apr 2026 13:01:28 +0200 Subject: [PATCH] feat: auto WLED backup at 02:05, grouped by device with collapsible view - Auto WLED config backup daily at 02:05 (keeps last 14 per device) - Backup page: WLED configs grouped by hostname in accordion - Each device shows backup count and last backup date - Collapsible sections to keep page compact - Readable date format (dd.mm.yyyy HH:MM:SS) for all backups - Scrollable containers for long lists Co-Authored-By: Claude Opus 4.6 (1M context) --- app.py | 69 +++++++++++++++-- templates/backup.html | 176 ++++++++++++++++++++++++------------------ 2 files changed, 164 insertions(+), 81 deletions(-) diff --git a/app.py b/app.py index 547fff2..91c08fd 100644 --- a/app.py +++ b/app.py @@ -48,6 +48,7 @@ async def lifespan(application: FastAPI): bg_scheduler.add_job(scheduler.poll_status, "interval", seconds=interval, id="poll") bg_scheduler.add_job(scheduler.check_devices, "interval", seconds=240, id="device_check") bg_scheduler.add_job(auto_backup_db, "cron", hour=2, minute=0, id="auto_backup") + bg_scheduler.add_job(auto_backup_wled, "cron", hour=2, minute=5, id="auto_backup_wled") # Initial device check (nicht blockierend) bg_scheduler.add_job(scheduler.check_devices, "date", id="device_check_init") bg_scheduler.start() @@ -77,6 +78,36 @@ def auto_backup_db(): logger.info("Auto-Backup erstellt: %s", dst) +def auto_backup_wled(): + """Backup all WLED configs. Keeps last 14 per device.""" + from wled_client import get_wled_config + conn = database.get_db() + devices = conn.execute("SELECT id, hostname FROM wled_devices").fetchall() + conn.close() + + ts = datetime.now().strftime("%Y%m%d_%H%M%S") + count = 0 + for dev in devices: + config = get_wled_config(dev["hostname"]) + if config: + safe_name = dev["hostname"].replace(".", "_") + path = os.path.join(WLED_BACKUP_DIR, f"{safe_name}_{ts}.json") + with open(path, "w") as f: + json.dump(config, f, indent=2) + count += 1 + + # Keep only last 14 per device + prefix = safe_name + "_" + device_backups = sorted( + [f for f in os.listdir(WLED_BACKUP_DIR) if f.startswith(prefix)], + reverse=True, + ) + for old in device_backups[14:]: + os.remove(os.path.join(WLED_BACKUP_DIR, old)) + + logger.info("WLED Auto-Backup: %d Geraete gesichert", count) + + app = FastAPI(title="Busylight Manager", lifespan=lifespan) app.mount("/static", StaticFiles(directory="static"), name="static") templates = Jinja2Templates(directory="templates") @@ -512,23 +543,51 @@ async def backup_page(request: Request): if f.startswith("busylight_") and f.endswith(".db"): path = os.path.join(BACKUP_DIR, f) size = os.path.getsize(path) / 1024 - db_backups.append({"name": f, "size": f"{size:.0f} KB"}) + # Parse date from filename: busylight_YYYYMMDD_HHMMSS.db + try: + ts = f.replace("busylight_", "").replace(".db", "") + dt = datetime.strptime(ts, "%Y%m%d_%H%M%S") + date_display = dt.strftime("%d.%m.%Y %H:%M:%S") + except ValueError: + date_display = f + db_backups.append({"name": f, "date": date_display, "size": f"{size:.0f} KB"}) - # WLED config backups - wled_backups = [] + # WLED config backups grouped by device + wled_by_device = {} if os.path.exists(WLED_BACKUP_DIR): for f in sorted(os.listdir(WLED_BACKUP_DIR), reverse=True): if f.endswith(".json"): path = os.path.join(WLED_BACKUP_DIR, f) size = os.path.getsize(path) / 1024 - wled_backups.append({"name": f, "size": f"{size:.1f} KB"}) + # Extract device name and timestamp from filename + # Format: hostname_parts_YYYYMMDD_HHMMSS.json + parts = f.rsplit("_", 2) + if len(parts) >= 3: + device_key = parts[0] + date_str = parts[1] + time_str = parts[2].replace(".json", "") + try: + dt = datetime.strptime(date_str + time_str, "%Y%m%d%H%M%S") + date_display = dt.strftime("%d.%m.%Y %H:%M:%S") + except ValueError: + date_display = f"{date_str} {time_str}" + else: + device_key = f.replace(".json", "") + date_display = "-" + + hostname = device_key.replace("_", ".") + if hostname not in wled_by_device: + wled_by_device[hostname] = [] + wled_by_device[hostname].append({ + "name": f, "size": f"{size:.1f} KB", "date": date_display, + }) conn = database.get_db() devices = conn.execute("SELECT id, hostname, name FROM wled_devices ORDER BY name, hostname").fetchall() conn.close() return templates.TemplateResponse(*_ctx(request, "backup", - db_backups=db_backups, wled_backups=wled_backups, devices=devices)) + db_backups=db_backups, wled_by_device=wled_by_device, devices=devices)) @app.get("/api/backup/db/download") diff --git a/templates/backup.html b/templates/backup.html index ce3178a..84f993c 100644 --- a/templates/backup.html +++ b/templates/backup.html @@ -10,14 +10,12 @@ {% endif %} - {% if request.query_params.get('wled_backed_up') %}
{{ request.query_params.get('wled_backed_up') }} Tuerschild-Config(s) gesichert.
{% endif %} - {% if request.query_params.get('restored') == '1' %}
WLED-Config erfolgreich wiederhergestellt. @@ -48,11 +46,11 @@
-
- - +
+
+ - + @@ -60,15 +58,15 @@ {% for b in db_backups %} - - + + - - - {% endif %} - -
DateiDatum Groesse Aktionen
{{ b.name }}{{ b.size }}{{ b.date }}{{ b.size }} - +
-
@@ -106,89 +104,115 @@
-
+
-
- - - - - - - - - - {% for b in wled_backups %} - - - - - - -
DateiGroesseAktionen
{{ b.name }}{{ b.size }} - - - - -
- -
-
+ + + + + + + + + {% for b in backups %} + + + + + + + - + + {% endfor %} + +
DatumGroesseAktionen
{{ b.date }}{{ b.size }} + + + + + + + +
- {% endfor %} - {% if not wled_backups %} -
Noch keine Config-Backups vorhanden
+
+
+ {% endfor %} + + {% if not wled_by_device %} +
+ Noch keine Config-Backups vorhanden +
+ {% endif %} +