feat: donation banner, About tab, settings UI improvements
Lint & Test / test (push) Has been cancelled
Lint & Test / test (push) Has been cancelled
- Dismissible donation/open-source banner after 3+ sessions (30-day snooze) - New About tab in Settings: version, repo link, license info - Centralize project URLs (REPO_URL, DONATE_URL) in __init__.py, served via /health - Center settings tab bar, reduce tab padding for 6-tab fit - External URL save button: icon button instead of full-width text button - Remove redundant settings footer close button - Footer "Source Code" link replaced with "About" opening settings - i18n keys for en/ru/zh
This commit is contained in:
@@ -13,7 +13,7 @@ from typing import Optional
|
||||
import psutil
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
|
||||
from wled_controller import __version__
|
||||
from wled_controller import __version__, REPO_URL, DONATE_URL
|
||||
from wled_controller.api.auth import AuthRequired, is_auth_enabled
|
||||
from wled_controller.api.dependencies import (
|
||||
get_audio_source_store,
|
||||
@@ -54,7 +54,11 @@ logger = get_logger(__name__)
|
||||
psutil.cpu_percent(interval=None)
|
||||
|
||||
# GPU monitoring (initialized once in utils.gpu, shared with metrics_history)
|
||||
from wled_controller.utils.gpu import nvml_available as _nvml_available, nvml as _nvml, nvml_handle as _nvml_handle # noqa: E402
|
||||
from wled_controller.utils.gpu import ( # noqa: E402
|
||||
nvml_available as _nvml_available,
|
||||
nvml as _nvml,
|
||||
nvml_handle as _nvml_handle,
|
||||
)
|
||||
|
||||
|
||||
def _get_cpu_name() -> str | None:
|
||||
@@ -77,9 +81,7 @@ def _get_cpu_name() -> str | None:
|
||||
return line.split(":")[1].strip()
|
||||
elif platform.system() == "Darwin":
|
||||
return (
|
||||
subprocess.check_output(
|
||||
["sysctl", "-n", "machdep.cpu.brand_string"]
|
||||
)
|
||||
subprocess.check_output(["sysctl", "-n", "machdep.cpu.brand_string"])
|
||||
.decode()
|
||||
.strip()
|
||||
)
|
||||
@@ -107,6 +109,8 @@ async def health_check():
|
||||
version=__version__,
|
||||
demo_mode=get_config().demo,
|
||||
auth_required=is_auth_enabled(),
|
||||
repo_url=REPO_URL,
|
||||
donate_url=DONATE_URL,
|
||||
)
|
||||
|
||||
|
||||
@@ -131,12 +135,22 @@ async def list_all_tags(_: AuthRequired):
|
||||
"""Get all tags used across all entities."""
|
||||
all_tags: set[str] = set()
|
||||
from wled_controller.api.dependencies import get_asset_store
|
||||
|
||||
store_getters = [
|
||||
get_device_store, get_output_target_store, get_color_strip_store,
|
||||
get_picture_source_store, get_audio_source_store, get_value_source_store,
|
||||
get_sync_clock_store, get_automation_store, get_scene_preset_store,
|
||||
get_template_store, get_audio_template_store, get_pp_template_store,
|
||||
get_pattern_template_store, get_asset_store,
|
||||
get_device_store,
|
||||
get_output_target_store,
|
||||
get_color_strip_store,
|
||||
get_picture_source_store,
|
||||
get_audio_source_store,
|
||||
get_value_source_store,
|
||||
get_sync_clock_store,
|
||||
get_automation_store,
|
||||
get_scene_preset_store,
|
||||
get_template_store,
|
||||
get_audio_template_store,
|
||||
get_pp_template_store,
|
||||
get_pattern_template_store,
|
||||
get_asset_store,
|
||||
]
|
||||
for getter in store_getters:
|
||||
try:
|
||||
@@ -209,15 +223,11 @@ async def get_displays(
|
||||
except EntityNotFoundError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error("Failed to get displays: %s", e, exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="Internal server error"
|
||||
)
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
|
||||
@router.get("/api/v1/system/processes", response_model=ProcessListResponse, tags=["Config"])
|
||||
@@ -235,10 +245,7 @@ async def get_running_processes(_: AuthRequired):
|
||||
return ProcessListResponse(processes=sorted_procs, count=len(sorted_procs))
|
||||
except Exception as e:
|
||||
logger.error("Failed to get processes: %s", e, exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="Internal server error"
|
||||
)
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
|
||||
@router.get(
|
||||
@@ -260,9 +267,7 @@ def get_system_performance(_: AuthRequired):
|
||||
try:
|
||||
util = _nvml.nvmlDeviceGetUtilizationRates(_nvml_handle)
|
||||
mem_info = _nvml.nvmlDeviceGetMemoryInfo(_nvml_handle)
|
||||
temp = _nvml.nvmlDeviceGetTemperature(
|
||||
_nvml_handle, _nvml.NVML_TEMPERATURE_GPU
|
||||
)
|
||||
temp = _nvml.nvmlDeviceGetTemperature(_nvml_handle, _nvml.NVML_TEMPERATURE_GPU)
|
||||
gpu = GpuInfo(
|
||||
name=_nvml.nvmlDeviceGetName(_nvml_handle),
|
||||
utilization=float(util.gpu),
|
||||
|
||||
@@ -13,7 +13,11 @@ class HealthResponse(BaseModel):
|
||||
timestamp: datetime = Field(description="Current server time")
|
||||
version: str = Field(description="Application version")
|
||||
demo_mode: bool = Field(default=False, description="Whether demo mode is active")
|
||||
auth_required: bool = Field(default=True, description="Whether API key authentication is required")
|
||||
auth_required: bool = Field(
|
||||
default=True, description="Whether API key authentication is required"
|
||||
)
|
||||
repo_url: str = Field(default="", description="Source code repository URL")
|
||||
donate_url: str = Field(default="", description="Donation page URL")
|
||||
|
||||
|
||||
class VersionResponse(BaseModel):
|
||||
@@ -84,6 +88,7 @@ class RestoreResponse(BaseModel):
|
||||
|
||||
# ─── Auto-backup schemas ──────────────────────────────────────
|
||||
|
||||
|
||||
class AutoBackupSettings(BaseModel):
|
||||
"""Settings for automatic backup."""
|
||||
|
||||
@@ -119,6 +124,7 @@ class BackupListResponse(BaseModel):
|
||||
|
||||
# ─── MQTT schemas ──────────────────────────────────────────────
|
||||
|
||||
|
||||
class MQTTSettingsResponse(BaseModel):
|
||||
"""MQTT broker settings response (password is masked)."""
|
||||
|
||||
@@ -138,17 +144,22 @@ class MQTTSettingsRequest(BaseModel):
|
||||
broker_host: str = Field(description="MQTT broker hostname or IP")
|
||||
broker_port: int = Field(ge=1, le=65535, description="MQTT broker port")
|
||||
username: str = Field(default="", description="MQTT username (empty = anonymous)")
|
||||
password: str = Field(default="", description="MQTT password (empty = keep existing if omitted)")
|
||||
password: str = Field(
|
||||
default="", description="MQTT password (empty = keep existing if omitted)"
|
||||
)
|
||||
client_id: str = Field(default="ledgrab", description="MQTT client ID")
|
||||
base_topic: str = Field(default="ledgrab", description="Base topic prefix")
|
||||
|
||||
|
||||
# ─── External URL schema ───────────────────────────────────────
|
||||
|
||||
|
||||
class ExternalUrlResponse(BaseModel):
|
||||
"""External URL setting response."""
|
||||
|
||||
external_url: str = Field(description="External base URL (e.g. https://myserver.example.com:8080). Empty = use auto-detected URL.")
|
||||
external_url: str = Field(
|
||||
description="External base URL (e.g. https://myserver.example.com:8080). Empty = use auto-detected URL."
|
||||
)
|
||||
|
||||
|
||||
class ExternalUrlRequest(BaseModel):
|
||||
@@ -159,10 +170,13 @@ class ExternalUrlRequest(BaseModel):
|
||||
|
||||
# ─── Log level schemas ─────────────────────────────────────────
|
||||
|
||||
|
||||
class LogLevelResponse(BaseModel):
|
||||
"""Current log level response."""
|
||||
|
||||
level: str = Field(description="Current effective log level name (e.g. DEBUG, INFO, WARNING, ERROR, CRITICAL)")
|
||||
level: str = Field(
|
||||
description="Current effective log level name (e.g. DEBUG, INFO, WARNING, ERROR, CRITICAL)"
|
||||
)
|
||||
|
||||
|
||||
class LogLevelRequest(BaseModel):
|
||||
|
||||
Reference in New Issue
Block a user