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) <noreply@anthropic.com>
This commit is contained in:
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user