Add header quick links with CRUD management and icon enhancements

- Add LinkConfig model and links field to settings
- Add CRUD API endpoints for links (list/create/update/delete)
- Add Links management tab in WebUI with add/edit/delete dialogs
- Add live icon preview in Link and Script dialog forms
- Show MDI icons inline in Quick Actions cards, Scripts table, Links table
- Add broadcast_links_changed WebSocket event for live updates
- Add EN/RU translations for all links management strings

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-27 14:42:18 +03:00
parent 6f6a4e4aec
commit 99dbbb1019
11 changed files with 886 additions and 5 deletions

View File

@@ -5,7 +5,8 @@ from .browser import router as browser_router
from .callbacks import router as callbacks_router
from .display import router as display_router
from .health import router as health_router
from .links import router as links_router
from .media import router as media_router
from .scripts import router as scripts_router
__all__ = ["audio_router", "browser_router", "callbacks_router", "display_router", "health_router", "media_router", "scripts_router"]
__all__ = ["audio_router", "browser_router", "callbacks_router", "display_router", "health_router", "links_router", "media_router", "scripts_router"]

View File

@@ -0,0 +1,185 @@
"""Header quick links management API endpoints."""
import logging
import re
from typing import Any
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, Field
from ..auth import verify_token
from ..config import LinkConfig, settings
from ..config_manager import config_manager
from ..services.websocket_manager import ws_manager
router = APIRouter(prefix="/api/links", tags=["links"])
logger = logging.getLogger(__name__)
class LinkInfo(BaseModel):
"""Information about a configured link."""
name: str
url: str
icon: str
label: str
class LinkCreateRequest(BaseModel):
"""Request model for creating or updating a link."""
url: str = Field(..., description="URL to open", min_length=1)
icon: str = Field(default="mdi:link", description="MDI icon name (e.g., 'mdi:led-strip-variant')")
label: str = Field(default="", description="Tooltip text")
def _validate_link_name(name: str) -> None:
"""Validate link name.
Args:
name: Link name to validate.
Raises:
HTTPException: If name is invalid.
"""
if not re.match(r'^[a-zA-Z0-9_]+$', name):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Link name must contain only letters, numbers, and underscores",
)
if len(name) > 64:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Link name must be 64 characters or less",
)
@router.get("/list")
async def list_links(_: str = Depends(verify_token)) -> list[LinkInfo]:
"""List all configured links.
Returns:
List of configured links.
"""
return [
LinkInfo(
name=name,
url=config.url,
icon=config.icon,
label=config.label,
)
for name, config in settings.links.items()
]
@router.post("/create/{link_name}")
async def create_link(
link_name: str,
request: LinkCreateRequest,
_: str = Depends(verify_token),
) -> dict[str, Any]:
"""Create a new link.
Args:
link_name: Link name (alphanumeric and underscores only).
request: Link configuration.
Returns:
Success response with link name.
"""
_validate_link_name(link_name)
if link_name in settings.links:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Link '{link_name}' already exists. Use PUT /api/links/update/{link_name} to update it.",
)
link_config = LinkConfig(**request.model_dump())
try:
config_manager.add_link(link_name, link_config)
except Exception as e:
logger.error(f"Failed to add link '{link_name}': {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to add link: {str(e)}",
)
await ws_manager.broadcast_links_changed()
logger.info(f"Link '{link_name}' created successfully")
return {"success": True, "link": link_name}
@router.put("/update/{link_name}")
async def update_link(
link_name: str,
request: LinkCreateRequest,
_: str = Depends(verify_token),
) -> dict[str, Any]:
"""Update an existing link.
Args:
link_name: Link name.
request: Updated link configuration.
Returns:
Success response with link name.
"""
_validate_link_name(link_name)
if link_name not in settings.links:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Link '{link_name}' not found. Use POST /api/links/create/{link_name} to create it.",
)
link_config = LinkConfig(**request.model_dump())
try:
config_manager.update_link(link_name, link_config)
except Exception as e:
logger.error(f"Failed to update link '{link_name}': {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to update link: {str(e)}",
)
await ws_manager.broadcast_links_changed()
logger.info(f"Link '{link_name}' updated successfully")
return {"success": True, "link": link_name}
@router.delete("/delete/{link_name}")
async def delete_link(
link_name: str,
_: str = Depends(verify_token),
) -> dict[str, Any]:
"""Delete a link.
Args:
link_name: Link name.
Returns:
Success response with link name.
"""
_validate_link_name(link_name)
if link_name not in settings.links:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Link '{link_name}' not found",
)
try:
config_manager.delete_link(link_name)
except Exception as e:
logger.error(f"Failed to delete link '{link_name}': {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to delete link: {str(e)}",
)
await ws_manager.broadcast_links_changed()
logger.info(f"Link '{link_name}' deleted successfully")
return {"success": True, "link": link_name}