From 4635caca98753035b765462c20e02a29a0bf28ab Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Fri, 6 Feb 2026 17:20:51 +0300 Subject: [PATCH] Update media-server: Add execution timing and improve script/callback execution UI Backend improvements: - Add execution_time tracking for script execution - Add execution_time tracking for callback execution - Add /api/callbacks/execute/{callback_name} endpoint for debugging callbacks Frontend improvements: - Fix duration display showing N/A for fast scripts (0 is falsy in JS) - Increase duration precision to 3 decimal places (0.001s) - Always show output section with "(no output)" message when empty - Improve output formatting with italic gray text for empty output Documentation: - Add localization section to README - Document available languages (English, Russian) - Add guide for contributing new translations Co-Authored-By: Claude Sonnet 4.5 --- README.md | 33 ++++ media_server/routes/callbacks.py | 125 ++++++++++++ media_server/routes/scripts.py | 12 +- media_server/static/index.html | 329 ++++++++++++++++++++++++++++++- 4 files changed, 490 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 1dbc26f..1159b1d 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ The media server includes a built-in web interface for controlling and monitorin - **Token authentication** - Saved in browser localStorage - **Responsive design** - Works on desktop and mobile - **Dark theme** - Easy on the eyes +- **Multi-language support** - English and Russian locales with automatic detection ### Accessing the Web UI @@ -56,6 +57,38 @@ To access the Web UI from other devices on your network: **Security Note:** For remote access over the internet, use a reverse proxy with HTTPS (nginx, Caddy) to encrypt traffic. +### Localization + +The Web UI supports multiple languages with automatic browser locale detection: + +**Available Languages:** + +- **English (en)** - Default +- **Русский (ru)** - Russian + +The interface automatically detects your browser language on first visit. You can manually switch languages using the dropdown in the top-right corner of the Web UI. + +**Contributing New Locales:** + +We welcome translations for additional languages! To contribute a new locale: + +1. Copy `media_server/static/locales/en.json` to a new file named with your language code (e.g., `de.json` for German) +2. Translate all strings to your language, keeping the same JSON structure +3. Add your language to the `supportedLocales` object in `media_server/static/index.html`: + + ```javascript + const supportedLocales = { + 'en': 'English', + 'ru': 'Русский', + 'de': 'Deutsch' // Add your language here + }; + ``` + +4. Test the translation by switching to your language in the Web UI +5. Submit a pull request with your changes + +See [CLAUDE.md](CLAUDE.md#internationalization-i18n) for detailed translation guidelines. + ## Requirements - Python 3.10+ diff --git a/media_server/routes/callbacks.py b/media_server/routes/callbacks.py index 87aec18..3855e82 100644 --- a/media_server/routes/callbacks.py +++ b/media_server/routes/callbacks.py @@ -1,7 +1,10 @@ """Callback management API endpoints.""" +import asyncio import logging import re +import subprocess +import time from typing import Any from fastapi import APIRouter, Depends, HTTPException, status @@ -34,6 +37,18 @@ class CallbackCreateRequest(BaseModel): shell: bool = Field(default=True, description="Run command in shell") +class CallbackExecuteResponse(BaseModel): + """Response model for callback execution.""" + + success: bool + callback: str + exit_code: int | None = None + stdout: str = "" + stderr: str = "" + error: str | None = None + execution_time: float | None = None + + def _validate_callback_name(name: str) -> None: """Validate callback name. @@ -84,6 +99,116 @@ async def list_callbacks(_: str = Depends(verify_token)) -> list[CallbackInfo]: ] +@router.post("/execute/{callback_name}") +async def execute_callback( + callback_name: str, + _: str = Depends(verify_token), +) -> CallbackExecuteResponse: + """Execute a callback for debugging purposes. + + Args: + callback_name: Name of the callback to execute + + Returns: + Execution result including stdout, stderr, and exit code + """ + # Validate callback name + _validate_callback_name(callback_name) + + # Check if callback exists + if callback_name not in settings.callbacks: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Callback '{callback_name}' not found. Use /api/callbacks/list to see configured callbacks.", + ) + + callback_config = settings.callbacks[callback_name] + + logger.info(f"Executing callback for debugging: {callback_name}") + + try: + # Execute in thread pool to not block + loop = asyncio.get_event_loop() + result = await loop.run_in_executor( + None, + lambda: _run_callback( + command=callback_config.command, + timeout=callback_config.timeout, + shell=callback_config.shell, + working_dir=callback_config.working_dir, + ), + ) + + return CallbackExecuteResponse( + success=result["exit_code"] == 0, + callback=callback_name, + exit_code=result["exit_code"], + stdout=result["stdout"], + stderr=result["stderr"], + execution_time=result.get("execution_time"), + ) + + except Exception as e: + logger.error(f"Callback execution error: {e}") + return CallbackExecuteResponse( + success=False, + callback=callback_name, + error=str(e), + ) + + +def _run_callback( + command: str, + timeout: int, + shell: bool, + working_dir: str | None, +) -> dict[str, Any]: + """Run a callback synchronously. + + Args: + command: Command to execute + timeout: Timeout in seconds + shell: Whether to run in shell + working_dir: Working directory + + Returns: + Dict with exit_code, stdout, stderr, execution_time + """ + start_time = time.time() + try: + result = subprocess.run( + command, + shell=shell, + cwd=working_dir, + capture_output=True, + text=True, + timeout=timeout, + ) + execution_time = time.time() - start_time + return { + "exit_code": result.returncode, + "stdout": result.stdout[:10000], # Limit output size + "stderr": result.stderr[:10000], + "execution_time": execution_time, + } + except subprocess.TimeoutExpired: + execution_time = time.time() - start_time + return { + "exit_code": -1, + "stdout": "", + "stderr": f"Callback timed out after {timeout} seconds", + "execution_time": execution_time, + } + except Exception as e: + execution_time = time.time() - start_time + return { + "exit_code": -1, + "stdout": "", + "stderr": str(e), + "execution_time": execution_time, + } + + @router.post("/create/{callback_name}") async def create_callback( callback_name: str, diff --git a/media_server/routes/scripts.py b/media_server/routes/scripts.py index 5025f78..631fd38 100644 --- a/media_server/routes/scripts.py +++ b/media_server/routes/scripts.py @@ -4,6 +4,7 @@ import asyncio import logging import re import subprocess +import time from typing import Any from fastapi import APIRouter, Depends, HTTPException, status @@ -33,6 +34,7 @@ class ScriptExecuteResponse(BaseModel): stdout: str = "" stderr: str = "" error: str | None = None + execution_time: float | None = None class ScriptInfo(BaseModel): @@ -117,6 +119,7 @@ async def execute_script( exit_code=result["exit_code"], stdout=result["stdout"], stderr=result["stderr"], + execution_time=result.get("execution_time"), ) except Exception as e: @@ -143,8 +146,9 @@ def _run_script( working_dir: Working directory Returns: - Dict with exit_code, stdout, stderr + Dict with exit_code, stdout, stderr, execution_time """ + start_time = time.time() try: result = subprocess.run( command, @@ -154,22 +158,28 @@ def _run_script( text=True, timeout=timeout, ) + execution_time = time.time() - start_time return { "exit_code": result.returncode, "stdout": result.stdout[:10000], # Limit output size "stderr": result.stderr[:10000], + "execution_time": execution_time, } except subprocess.TimeoutExpired: + execution_time = time.time() - start_time return { "exit_code": -1, "stdout": "", "stderr": f"Script timed out after {timeout} seconds", + "execution_time": execution_time, } except Exception as e: + execution_time = time.time() - start_time return { "exit_code": -1, "stdout": "", "stderr": str(e), + "execution_time": execution_time, } diff --git a/media_server/static/index.html b/media_server/static/index.html index bccb8cd..8afcbe4 100644 --- a/media_server/static/index.html +++ b/media_server/static/index.html @@ -4,6 +4,7 @@ Media Server +