Compare commits
1 Commits
d7c5994e56
...
a0af855846
| Author | SHA1 | Date | |
|---|---|---|---|
| a0af855846 |
@@ -8,7 +8,7 @@ from typing import Optional
|
||||
|
||||
import yaml
|
||||
|
||||
from .config import ScriptConfig, settings
|
||||
from .config import CallbackConfig, ScriptConfig, settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -171,6 +171,114 @@ class ConfigManager:
|
||||
|
||||
logger.info(f"Script '{name}' deleted from config")
|
||||
|
||||
def add_callback(self, name: str, config: CallbackConfig) -> None:
|
||||
"""Add a new callback to config.
|
||||
|
||||
Args:
|
||||
name: Callback name (must be unique).
|
||||
config: Callback configuration.
|
||||
|
||||
Raises:
|
||||
ValueError: If callback already exists.
|
||||
IOError: If config file cannot be written.
|
||||
"""
|
||||
with self._lock:
|
||||
# Read YAML
|
||||
if not self._config_path.exists():
|
||||
data = {}
|
||||
else:
|
||||
with open(self._config_path, "r", encoding="utf-8") as f:
|
||||
data = yaml.safe_load(f) or {}
|
||||
|
||||
# Check if callback already exists
|
||||
if "callbacks" in data and name in data["callbacks"]:
|
||||
raise ValueError(f"Callback '{name}' already exists")
|
||||
|
||||
# Add callback
|
||||
if "callbacks" not in data:
|
||||
data["callbacks"] = {}
|
||||
data["callbacks"][name] = config.model_dump(exclude_none=True)
|
||||
|
||||
# Write YAML
|
||||
self._config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(self._config_path, "w", encoding="utf-8") as f:
|
||||
yaml.dump(data, f, default_flow_style=False, sort_keys=False)
|
||||
|
||||
# Update in-memory settings
|
||||
settings.callbacks[name] = config
|
||||
|
||||
logger.info(f"Callback '{name}' added to config")
|
||||
|
||||
def update_callback(self, name: str, config: CallbackConfig) -> None:
|
||||
"""Update an existing callback.
|
||||
|
||||
Args:
|
||||
name: Callback name.
|
||||
config: New callback configuration.
|
||||
|
||||
Raises:
|
||||
ValueError: If callback does not exist.
|
||||
IOError: If config file cannot be written.
|
||||
"""
|
||||
with self._lock:
|
||||
# Read YAML
|
||||
if not self._config_path.exists():
|
||||
raise ValueError(f"Config file not found: {self._config_path}")
|
||||
|
||||
with open(self._config_path, "r", encoding="utf-8") as f:
|
||||
data = yaml.safe_load(f) or {}
|
||||
|
||||
# Check if callback exists
|
||||
if "callbacks" not in data or name not in data["callbacks"]:
|
||||
raise ValueError(f"Callback '{name}' does not exist")
|
||||
|
||||
# Update callback
|
||||
data["callbacks"][name] = config.model_dump(exclude_none=True)
|
||||
|
||||
# Write YAML
|
||||
with open(self._config_path, "w", encoding="utf-8") as f:
|
||||
yaml.dump(data, f, default_flow_style=False, sort_keys=False)
|
||||
|
||||
# Update in-memory settings
|
||||
settings.callbacks[name] = config
|
||||
|
||||
logger.info(f"Callback '{name}' updated in config")
|
||||
|
||||
def delete_callback(self, name: str) -> None:
|
||||
"""Delete a callback from config.
|
||||
|
||||
Args:
|
||||
name: Callback name.
|
||||
|
||||
Raises:
|
||||
ValueError: If callback does not exist.
|
||||
IOError: If config file cannot be written.
|
||||
"""
|
||||
with self._lock:
|
||||
# Read YAML
|
||||
if not self._config_path.exists():
|
||||
raise ValueError(f"Config file not found: {self._config_path}")
|
||||
|
||||
with open(self._config_path, "r", encoding="utf-8") as f:
|
||||
data = yaml.safe_load(f) or {}
|
||||
|
||||
# Check if callback exists
|
||||
if "callbacks" not in data or name not in data["callbacks"]:
|
||||
raise ValueError(f"Callback '{name}' does not exist")
|
||||
|
||||
# Delete callback
|
||||
del data["callbacks"][name]
|
||||
|
||||
# Write YAML
|
||||
with open(self._config_path, "w", encoding="utf-8") as f:
|
||||
yaml.dump(data, f, default_flow_style=False, sort_keys=False)
|
||||
|
||||
# Update in-memory settings
|
||||
if name in settings.callbacks:
|
||||
del settings.callbacks[name]
|
||||
|
||||
logger.info(f"Callback '{name}' deleted from config")
|
||||
|
||||
|
||||
# Global config manager instance
|
||||
config_manager = ConfigManager()
|
||||
|
||||
@@ -15,7 +15,7 @@ from fastapi.staticfiles import StaticFiles
|
||||
from . import __version__
|
||||
from .auth import get_token_label, token_label_var
|
||||
from .config import settings, generate_default_config, get_config_dir
|
||||
from .routes import audio_router, health_router, media_router, scripts_router
|
||||
from .routes import audio_router, callbacks_router, health_router, media_router, scripts_router
|
||||
from .services import get_media_controller
|
||||
from .services.websocket_manager import ws_manager
|
||||
|
||||
@@ -110,6 +110,7 @@ def create_app() -> FastAPI:
|
||||
|
||||
# Register routers
|
||||
app.include_router(audio_router)
|
||||
app.include_router(callbacks_router)
|
||||
app.include_router(health_router)
|
||||
app.include_router(media_router)
|
||||
app.include_router(scripts_router)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"""API route modules."""
|
||||
|
||||
from .audio import router as audio_router
|
||||
from .callbacks import router as callbacks_router
|
||||
from .health import router as health_router
|
||||
from .media import router as media_router
|
||||
from .scripts import router as scripts_router
|
||||
|
||||
__all__ = ["audio_router", "health_router", "media_router", "scripts_router"]
|
||||
__all__ = ["audio_router", "callbacks_router", "health_router", "media_router", "scripts_router"]
|
||||
|
||||
214
media_server/routes/callbacks.py
Normal file
214
media_server/routes/callbacks.py
Normal file
@@ -0,0 +1,214 @@
|
||||
"""Callback 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 CallbackConfig, settings
|
||||
from ..config_manager import config_manager
|
||||
|
||||
router = APIRouter(prefix="/api/callbacks", tags=["callbacks"])
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CallbackInfo(BaseModel):
|
||||
"""Information about a configured callback."""
|
||||
|
||||
name: str
|
||||
command: str
|
||||
timeout: int
|
||||
working_dir: str | None = None
|
||||
shell: bool
|
||||
|
||||
|
||||
class CallbackCreateRequest(BaseModel):
|
||||
"""Request model for creating or updating a callback."""
|
||||
|
||||
command: str = Field(..., description="Command to execute", min_length=1)
|
||||
timeout: int = Field(default=30, description="Execution timeout in seconds", ge=1, le=300)
|
||||
working_dir: str | None = Field(default=None, description="Working directory")
|
||||
shell: bool = Field(default=True, description="Run command in shell")
|
||||
|
||||
|
||||
def _validate_callback_name(name: str) -> None:
|
||||
"""Validate callback name.
|
||||
|
||||
Args:
|
||||
name: Callback name to validate.
|
||||
|
||||
Raises:
|
||||
HTTPException: If name is invalid.
|
||||
"""
|
||||
# All available callback events
|
||||
valid_names = {
|
||||
"on_play",
|
||||
"on_pause",
|
||||
"on_stop",
|
||||
"on_next",
|
||||
"on_previous",
|
||||
"on_volume",
|
||||
"on_mute",
|
||||
"on_seek",
|
||||
"on_turn_on",
|
||||
"on_turn_off",
|
||||
"on_toggle",
|
||||
}
|
||||
|
||||
if name not in valid_names:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Callback name must be one of: {', '.join(sorted(valid_names))}",
|
||||
)
|
||||
|
||||
|
||||
@router.get("/list")
|
||||
async def list_callbacks(_: str = Depends(verify_token)) -> list[CallbackInfo]:
|
||||
"""List all configured callbacks.
|
||||
|
||||
Returns:
|
||||
List of configured callbacks.
|
||||
"""
|
||||
return [
|
||||
CallbackInfo(
|
||||
name=name,
|
||||
command=config.command,
|
||||
timeout=config.timeout,
|
||||
working_dir=config.working_dir,
|
||||
shell=config.shell,
|
||||
)
|
||||
for name, config in settings.callbacks.items()
|
||||
]
|
||||
|
||||
|
||||
@router.post("/create/{callback_name}")
|
||||
async def create_callback(
|
||||
callback_name: str,
|
||||
request: CallbackCreateRequest,
|
||||
_: str = Depends(verify_token),
|
||||
) -> dict[str, Any]:
|
||||
"""Create a new callback.
|
||||
|
||||
Args:
|
||||
callback_name: Callback event name (on_turn_on, on_turn_off, on_toggle).
|
||||
request: Callback configuration.
|
||||
|
||||
Returns:
|
||||
Success response with callback name.
|
||||
|
||||
Raises:
|
||||
HTTPException: If callback already exists or name is invalid.
|
||||
"""
|
||||
# Validate name
|
||||
_validate_callback_name(callback_name)
|
||||
|
||||
# Check if callback already exists
|
||||
if callback_name in settings.callbacks:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Callback '{callback_name}' already exists. Use PUT /api/callbacks/update/{callback_name} to update it.",
|
||||
)
|
||||
|
||||
# Create callback config
|
||||
callback_config = CallbackConfig(**request.model_dump())
|
||||
|
||||
# Add to config file and in-memory
|
||||
try:
|
||||
config_manager.add_callback(callback_name, callback_config)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to add callback '{callback_name}': {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to add callback: {str(e)}",
|
||||
)
|
||||
|
||||
logger.info(f"Callback '{callback_name}' created successfully")
|
||||
return {"success": True, "callback": callback_name}
|
||||
|
||||
|
||||
@router.put("/update/{callback_name}")
|
||||
async def update_callback(
|
||||
callback_name: str,
|
||||
request: CallbackCreateRequest,
|
||||
_: str = Depends(verify_token),
|
||||
) -> dict[str, Any]:
|
||||
"""Update an existing callback.
|
||||
|
||||
Args:
|
||||
callback_name: Callback event name.
|
||||
request: Updated callback configuration.
|
||||
|
||||
Returns:
|
||||
Success response with callback name.
|
||||
|
||||
Raises:
|
||||
HTTPException: If callback does not exist.
|
||||
"""
|
||||
# Validate 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 POST /api/callbacks/create/{callback_name} to create it.",
|
||||
)
|
||||
|
||||
# Create updated callback config
|
||||
callback_config = CallbackConfig(**request.model_dump())
|
||||
|
||||
# Update config file and in-memory
|
||||
try:
|
||||
config_manager.update_callback(callback_name, callback_config)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update callback '{callback_name}': {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to update callback: {str(e)}",
|
||||
)
|
||||
|
||||
logger.info(f"Callback '{callback_name}' updated successfully")
|
||||
return {"success": True, "callback": callback_name}
|
||||
|
||||
|
||||
@router.delete("/delete/{callback_name}")
|
||||
async def delete_callback(
|
||||
callback_name: str,
|
||||
_: str = Depends(verify_token),
|
||||
) -> dict[str, Any]:
|
||||
"""Delete a callback.
|
||||
|
||||
Args:
|
||||
callback_name: Callback event name.
|
||||
|
||||
Returns:
|
||||
Success response with callback name.
|
||||
|
||||
Raises:
|
||||
HTTPException: If callback does not exist.
|
||||
"""
|
||||
# Validate 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",
|
||||
)
|
||||
|
||||
# Delete from config file and in-memory
|
||||
try:
|
||||
config_manager.delete_callback(callback_name)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to delete callback '{callback_name}': {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to delete callback: {str(e)}",
|
||||
)
|
||||
|
||||
logger.info(f"Callback '{callback_name}' deleted successfully")
|
||||
return {"success": True, "callback": callback_name}
|
||||
@@ -18,6 +18,19 @@
|
||||
--error: #e74c3c;
|
||||
}
|
||||
|
||||
:root[data-theme="light"] {
|
||||
--bg-primary: #ffffff;
|
||||
--bg-secondary: #f5f5f5;
|
||||
--bg-tertiary: #e8e8e8;
|
||||
--text-primary: #1a1a1a;
|
||||
--text-secondary: #4a4a4a;
|
||||
--text-muted: #888888;
|
||||
--accent: #1db954;
|
||||
--accent-hover: #1ed760;
|
||||
--border: #d0d0d0;
|
||||
--error: #e74c3c;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@@ -71,6 +84,30 @@
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
.theme-toggle {
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 0.5rem;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.theme-toggle:hover {
|
||||
background: var(--border);
|
||||
}
|
||||
|
||||
.theme-toggle svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
fill: var(--text-primary);
|
||||
}
|
||||
|
||||
.player-container {
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 12px;
|
||||
@@ -352,7 +389,7 @@
|
||||
}
|
||||
|
||||
.add-script-btn {
|
||||
padding: 0.5rem 1rem;
|
||||
padding: 0.5rem 1.5rem;
|
||||
border-radius: 6px;
|
||||
background: var(--accent);
|
||||
border: none;
|
||||
@@ -361,6 +398,7 @@
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
transition: background 0.2s;
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.add-script-btn:hover {
|
||||
@@ -459,7 +497,8 @@
|
||||
}
|
||||
|
||||
.dialog-body input,
|
||||
.dialog-body textarea {
|
||||
.dialog-body textarea,
|
||||
.dialog-body select {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
@@ -478,7 +517,8 @@
|
||||
}
|
||||
|
||||
.dialog-body input:focus,
|
||||
.dialog-body textarea:focus {
|
||||
.dialog-body textarea:focus,
|
||||
.dialog-body select:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
@@ -723,9 +763,19 @@
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>Media Server</h1>
|
||||
<div class="status-indicator">
|
||||
<span class="status-dot" id="status-dot"></span>
|
||||
<span id="status-text">Disconnected</span>
|
||||
<div style="display: flex; align-items: center; gap: 1rem;">
|
||||
<button class="theme-toggle" onclick="toggleTheme()" title="Toggle theme" id="theme-toggle">
|
||||
<svg id="theme-icon-sun" viewBox="0 0 24 24" style="display: none;">
|
||||
<path d="M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zM2 13h2c.55 0 1-.45 1-1s-.45-1-1-1H2c-.55 0-1 .45-1 1s.45 1 1 1zm18 0h2c.55 0 1-.45 1-1s-.45-1-1-1h-2c-.55 0-1 .45-1 1s.45 1 1 1zM11 2v2c0 .55.45 1 1 1s1-.45 1-1V2c0-.55-.45-1-1-1s-1 .45-1 1zm0 18v2c0 .55.45 1 1 1s1-.45 1-1v-2c0-.55-.45-1-1-1s-1 .45-1 1zM5.99 4.58c-.39-.39-1.03-.39-1.41 0-.39.39-.39 1.03 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0s.39-1.03 0-1.41L5.99 4.58zm12.37 12.37c-.39-.39-1.03-.39-1.41 0-.39.39-.39 1.03 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0 .39-.39.39-1.03 0-1.41l-1.06-1.06zm1.06-10.96c.39-.39.39-1.03 0-1.41-.39-.39-1.03-.39-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0l1.06-1.06zM7.05 18.36c.39-.39.39-1.03 0-1.41-.39-.39-1.03-.39-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0l1.06-1.06z"/>
|
||||
</svg>
|
||||
<svg id="theme-icon-moon" viewBox="0 0 24 24">
|
||||
<path d="M9 2c-1.05 0-2.05.16-3 .46 4.06 1.27 7 5.06 7 9.54 0 4.48-2.94 8.27-7 9.54.95.3 1.95.46 3 .46 5.52 0 10-4.48 10-10S14.52 2 9 2z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="status-indicator">
|
||||
<span class="status-dot" id="status-dot"></span>
|
||||
<span id="status-text">Disconnected</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -801,7 +851,7 @@
|
||||
<div class="script-management">
|
||||
<div class="script-management-header">
|
||||
<h2>Script Management</h2>
|
||||
<button class="add-script-btn" onclick="showAddScriptDialog()">+ Add Script</button>
|
||||
<button class="add-script-btn" onclick="showAddScriptDialog()">+ Add</button>
|
||||
</div>
|
||||
<table class="scripts-table">
|
||||
<thead>
|
||||
@@ -820,6 +870,32 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Callback Management Section -->
|
||||
<div class="script-management">
|
||||
<div class="script-management-header">
|
||||
<h2>Callback Management</h2>
|
||||
<button class="add-script-btn" onclick="showAddCallbackDialog()">+ Add</button>
|
||||
</div>
|
||||
<p style="color: var(--text-secondary); font-size: 0.875rem; margin-bottom: 1rem;">
|
||||
Callbacks are scripts triggered automatically by media control events (play, pause, volume, etc.)
|
||||
</p>
|
||||
<table class="scripts-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Event</th>
|
||||
<th>Command</th>
|
||||
<th>Timeout</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="callbacksTableBody">
|
||||
<tr>
|
||||
<td colspan="4" class="empty-state">No callbacks configured. Click "Add Callback" to create one.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add/Edit Script Dialog -->
|
||||
@@ -870,10 +946,87 @@
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<!-- Add/Edit Callback Dialog -->
|
||||
<dialog id="callbackDialog">
|
||||
<div class="dialog-header">
|
||||
<h3 id="callbackDialogTitle">Add Callback</h3>
|
||||
</div>
|
||||
<form id="callbackForm" onsubmit="saveCallback(event)">
|
||||
<div class="dialog-body">
|
||||
<input type="hidden" id="callbackIsEdit">
|
||||
|
||||
<label>
|
||||
Event *
|
||||
<select id="callbackName" required>
|
||||
<option value="">Select event...</option>
|
||||
<option value="on_play">on_play - After play succeeds</option>
|
||||
<option value="on_pause">on_pause - After pause succeeds</option>
|
||||
<option value="on_stop">on_stop - After stop succeeds</option>
|
||||
<option value="on_next">on_next - After next track succeeds</option>
|
||||
<option value="on_previous">on_previous - After previous track succeeds</option>
|
||||
<option value="on_volume">on_volume - After volume change</option>
|
||||
<option value="on_mute">on_mute - After mute toggle</option>
|
||||
<option value="on_seek">on_seek - After seek succeeds</option>
|
||||
<option value="on_turn_on">on_turn_on - Callback-only action</option>
|
||||
<option value="on_turn_off">on_turn_off - Callback-only action</option>
|
||||
<option value="on_toggle">on_toggle - Callback-only action</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Command *
|
||||
<input type="text" id="callbackCommand" required placeholder="e.g., shutdown /s /t 0">
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Timeout (seconds)
|
||||
<input type="number" id="callbackTimeout" value="30" min="1" max="300">
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Working Directory
|
||||
<input type="text" id="callbackWorkingDir" placeholder="Optional">
|
||||
</label>
|
||||
</div>
|
||||
<div class="dialog-footer">
|
||||
<button type="button" class="btn-secondary" onclick="closeCallbackDialog()">Cancel</button>
|
||||
<button type="submit" class="btn-primary">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<!-- Toast Notification -->
|
||||
<div class="toast" id="toast"></div>
|
||||
|
||||
<script>
|
||||
// Theme management
|
||||
function initTheme() {
|
||||
const savedTheme = localStorage.getItem('theme') || 'dark';
|
||||
setTheme(savedTheme);
|
||||
}
|
||||
|
||||
function setTheme(theme) {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
localStorage.setItem('theme', theme);
|
||||
|
||||
const sunIcon = document.getElementById('theme-icon-sun');
|
||||
const moonIcon = document.getElementById('theme-icon-moon');
|
||||
|
||||
if (theme === 'light') {
|
||||
sunIcon.style.display = 'none';
|
||||
moonIcon.style.display = 'block';
|
||||
} else {
|
||||
sunIcon.style.display = 'block';
|
||||
moonIcon.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function toggleTheme() {
|
||||
const currentTheme = document.documentElement.getAttribute('data-theme') || 'dark';
|
||||
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
||||
setTheme(newTheme);
|
||||
}
|
||||
|
||||
let ws = null;
|
||||
let reconnectTimeout = null;
|
||||
let currentState = 'idle';
|
||||
@@ -889,11 +1042,15 @@
|
||||
|
||||
// Initialize on page load
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
// Initialize theme
|
||||
initTheme();
|
||||
|
||||
const token = localStorage.getItem('media_server_token');
|
||||
if (token) {
|
||||
connectWebSocket(token);
|
||||
loadScripts();
|
||||
loadScriptsTable();
|
||||
loadCallbacksTable();
|
||||
} else {
|
||||
showAuthForm();
|
||||
}
|
||||
@@ -980,6 +1137,7 @@
|
||||
hideAuthForm();
|
||||
loadScripts();
|
||||
loadScriptsTable();
|
||||
loadCallbacksTable();
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
@@ -1515,6 +1673,178 @@
|
||||
showToast('Error deleting script', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Callback Management Functions
|
||||
|
||||
async function loadCallbacksTable() {
|
||||
const token = localStorage.getItem('media_server_token');
|
||||
const tbody = document.getElementById('callbacksTableBody');
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/callbacks/list', {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch callbacks');
|
||||
}
|
||||
|
||||
const callbacksList = await response.json();
|
||||
|
||||
if (callbacksList.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="4" class="empty-state">No callbacks configured. Click "Add Callback" to create one.</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = callbacksList.map(callback => `
|
||||
<tr>
|
||||
<td><code>${callback.name}</code></td>
|
||||
<td style="max-width: 400px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"
|
||||
title="${escapeHtml(callback.command)}">${escapeHtml(callback.command)}</td>
|
||||
<td>${callback.timeout}s</td>
|
||||
<td>
|
||||
<button class="action-btn" onclick="showEditCallbackDialog('${callback.name}')">Edit</button>
|
||||
<button class="action-btn delete" onclick="deleteCallbackConfirm('${callback.name}')">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
} catch (error) {
|
||||
console.error('Error loading callbacks:', error);
|
||||
tbody.innerHTML = '<tr><td colspan="4" class="empty-state" style="color: var(--error);">Failed to load callbacks</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
function showAddCallbackDialog() {
|
||||
const dialog = document.getElementById('callbackDialog');
|
||||
const form = document.getElementById('callbackForm');
|
||||
const title = document.getElementById('callbackDialogTitle');
|
||||
|
||||
// Reset form
|
||||
form.reset();
|
||||
document.getElementById('callbackIsEdit').value = 'false';
|
||||
document.getElementById('callbackName').disabled = false;
|
||||
title.textContent = 'Add Callback';
|
||||
|
||||
dialog.showModal();
|
||||
}
|
||||
|
||||
async function showEditCallbackDialog(callbackName) {
|
||||
const token = localStorage.getItem('media_server_token');
|
||||
const dialog = document.getElementById('callbackDialog');
|
||||
const title = document.getElementById('callbackDialogTitle');
|
||||
|
||||
try {
|
||||
// Fetch current callback details
|
||||
const response = await fetch('/api/callbacks/list', {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch callback details');
|
||||
}
|
||||
|
||||
const callbacksList = await response.json();
|
||||
const callback = callbacksList.find(c => c.name === callbackName);
|
||||
|
||||
if (!callback) {
|
||||
showToast('Callback not found', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Populate form
|
||||
document.getElementById('callbackIsEdit').value = 'true';
|
||||
document.getElementById('callbackName').value = callbackName;
|
||||
document.getElementById('callbackName').disabled = true; // Can't change event name
|
||||
document.getElementById('callbackCommand').value = callback.command;
|
||||
document.getElementById('callbackTimeout').value = callback.timeout;
|
||||
document.getElementById('callbackWorkingDir').value = callback.working_dir || '';
|
||||
|
||||
title.textContent = 'Edit Callback';
|
||||
dialog.showModal();
|
||||
} catch (error) {
|
||||
console.error('Error loading callback for edit:', error);
|
||||
showToast('Failed to load callback details', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function closeCallbackDialog() {
|
||||
const dialog = document.getElementById('callbackDialog');
|
||||
dialog.close();
|
||||
}
|
||||
|
||||
async function saveCallback(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const token = localStorage.getItem('media_server_token');
|
||||
const isEdit = document.getElementById('callbackIsEdit').value === 'true';
|
||||
const callbackName = document.getElementById('callbackName').value;
|
||||
|
||||
const data = {
|
||||
command: document.getElementById('callbackCommand').value,
|
||||
timeout: parseInt(document.getElementById('callbackTimeout').value) || 30,
|
||||
working_dir: document.getElementById('callbackWorkingDir').value || null,
|
||||
shell: true
|
||||
};
|
||||
|
||||
const endpoint = isEdit ?
|
||||
`/api/callbacks/update/${callbackName}` :
|
||||
`/api/callbacks/create/${callbackName}`;
|
||||
|
||||
const method = isEdit ? 'PUT' : 'POST';
|
||||
|
||||
try {
|
||||
const response = await fetch(endpoint, {
|
||||
method,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok && result.success) {
|
||||
showToast(`Callback ${isEdit ? 'updated' : 'created'} successfully`, 'success');
|
||||
closeCallbackDialog();
|
||||
loadCallbacksTable();
|
||||
} else {
|
||||
showToast(result.detail || `Failed to ${isEdit ? 'update' : 'create'} callback`, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving callback:', error);
|
||||
showToast(`Error ${isEdit ? 'updating' : 'creating'} callback`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteCallbackConfirm(callbackName) {
|
||||
if (!confirm(`Are you sure you want to delete the callback "${callbackName}"?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const token = localStorage.getItem('media_server_token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/callbacks/delete/${callbackName}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok && result.success) {
|
||||
showToast('Callback deleted successfully', 'success');
|
||||
loadCallbacksTable();
|
||||
} else {
|
||||
showToast(result.detail || 'Failed to delete callback', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting callback:', error);
|
||||
showToast('Error deleting callback', 'error');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user