feat: asset-based image/video sources, notification sounds, UI improvements
Lint & Test / test (push) Has been cancelled

- Replace URL-based image_source/url fields with image_asset_id/video_asset_id
  on StaticImagePictureSource and VideoCaptureSource (clean break, no migration)
- Resolve asset IDs to file paths at runtime via AssetStore.get_file_path()
- Add EntitySelect asset pickers for image/video in stream editor modal
- Add notification sound configuration (global sound + per-app overrides)
- Unify per-app color and sound overrides into single "Per-App Overrides" section
- Persist notification history between server restarts
- Add asset management system (upload, edit, delete, soft-delete)
- Replace emoji buttons with SVG icons throughout UI
- Various backend improvements: SQLite stores, auth, backup, MQTT, webhooks
This commit is contained in:
2026-03-26 20:40:25 +03:00
parent c0853ce184
commit e2e1107df7
100 changed files with 2935 additions and 992 deletions
@@ -120,8 +120,8 @@ async def start_processing(
msg = msg.replace(t.id, f"'{t.name}'")
raise HTTPException(status_code=409, detail=msg)
except Exception as e:
logger.error(f"Failed to start processing: {e}")
raise HTTPException(status_code=500, detail=str(e))
logger.error("Failed to start processing: %s", e, exc_info=True)
raise HTTPException(status_code=500, detail="Internal server error")
@router.post("/api/v1/output-targets/{target_id}/stop", tags=["Processing"])
@@ -140,8 +140,8 @@ async def stop_processing(
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
except Exception as e:
logger.error(f"Failed to stop processing: {e}")
raise HTTPException(status_code=500, detail=str(e))
logger.error("Failed to stop processing: %s", e, exc_info=True)
raise HTTPException(status_code=500, detail="Internal server error")
# ===== STATE & METRICS ENDPOINTS =====
@@ -160,8 +160,8 @@ async def get_target_state(
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
except Exception as e:
logger.error(f"Failed to get target state: {e}")
raise HTTPException(status_code=500, detail=str(e))
logger.error("Failed to get target state: %s", e, exc_info=True)
raise HTTPException(status_code=500, detail="Internal server error")
@router.get("/api/v1/output-targets/{target_id}/metrics", response_model=TargetMetricsResponse, tags=["Metrics"])
@@ -178,8 +178,8 @@ async def get_target_metrics(
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
except Exception as e:
logger.error(f"Failed to get target metrics: {e}")
raise HTTPException(status_code=500, detail=str(e))
logger.error("Failed to get target metrics: %s", e, exc_info=True)
raise HTTPException(status_code=500, detail="Internal server error")
# ===== STATE CHANGE EVENT STREAM =====
@@ -268,8 +268,8 @@ async def start_target_overlay(
except RuntimeError as e:
raise HTTPException(status_code=409, detail=str(e))
except Exception as e:
logger.error(f"Failed to start overlay: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=str(e))
logger.error("Failed to start overlay: %s", e, exc_info=True)
raise HTTPException(status_code=500, detail="Internal server error")
@router.post("/api/v1/output-targets/{target_id}/overlay/stop", tags=["Visualization"])
@@ -286,8 +286,8 @@ async def stop_target_overlay(
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
except Exception as e:
logger.error(f"Failed to stop overlay: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=str(e))
logger.error("Failed to stop overlay: %s", e, exc_info=True)
raise HTTPException(status_code=500, detail="Internal server error")
@router.get("/api/v1/output-targets/{target_id}/overlay/status", tags=["Visualization"])